Почему на переменную списка иногда не влияют изменения в функции, поскольку я думал, что python3 работает при передаче по ссылке с переменными списка?

Для python3 мне изначально нужно было извлечь нечетные и четные позиции из списка и назначить их новым спискам, а затем очистить исходный список. Я думал, что на списки повлиял вызов функции через передачу по ссылке. Тестирование некоторых сценариев, это работает когда-то. Может кто-нибудь объяснить, как именно здесь работает python3?

Случай 1: пустой список заполняется строкой, как и ожидалось.

def func1(_in):
    _in.append('abc')

mylist = list()
print(f"Before:\nmylist = {mylist}")
func1(mylist)
print(f"After:\nmylist = {mylist}")

Выходной случай 1:

Before:
mylist = []
After:
mylist = ['abc']

Случай 2: средний элемент списка заменен строкой, как и ожидалось.

def func2(_in):
    _in[1] = 'abc'

mylist = list(range(3))
print(f"Before:\nmylist = {mylist}")
func2(mylist)
print(f"After:\nmylist = {mylist}")

Выходной случай 2:

Before:
mylist = [0, 1, 2]
After:
mylist = [0, 'abc', 2]

Случай 3: почему список не пуст после вызова функции?

def func3(_in):
    _in = list()

mylist = list(range(3))
print(f"Before:\nmylist = {mylist}")
func3(mylist)
print(f"After:\nmylist = {mylist}")

Выходной случай 3:

Before:
mylist = [0, 1, 2]
After:
mylist = [0, 1, 2]

Случай 4: работает точно так, как ожидалось, но обратите внимание, что я вернул все три списка из функции.

def func4_with_ret(_src, _dest1, _dest2):
    _dest1 = [val for val in _src[0:len(_src):2]]
    _dest2 = [val for val in _src[1:len(_src):2]]
    _src = list()
    return _src, _dest1, _dest2

source = list(range(6))
evens, odds = list(), list()
print(f"Before function call:\nsource = {source}\nevens = {evens}\nodds = {odds}")
source, evens, odds = func4_with_ret(source, evens, odds)
print(f"\nAfter function call:\nsource = {source}\nevens = {evens}\nodds = {odds}")

Выходной случай 4:

Before function call:
source = [0, 1, 2, 3, 4, 5]
evens = []
odds = []

After function call:
source = []
evens = [0, 2, 4]
odds = [1, 3, 5]

Случай 5: почему никакое влияние на переменные вне функции, если я не возвращаюсь явно из вызова функции?

def func5_no_ret(_src, _dest1, _dest2):
    _dest1 = [val for val in _src[0:len(_src):2]]
    _dest2 = [val for val in _src[1:len(_src):2]]
    _src = list()

source = list(range(6))
evens, odds = list(), list()
print(f"Before function call:\nsource = {source}\nevens = {evens}\nodds = {odds}")
func5_no_ret(source, evens, odds)
print(f"\nAfter function call:\nsource = {source}\nevens = {evens}\nodds = {odds}")

Выходной случай 5:

Before function call:
source = [0, 1, 2, 3, 4, 5]
evens = []
odds = []

After function call:
source = [0, 1, 2, 3, 4, 5]
evens = []
odds = []

Спасибо.


person rbewoor    schedule 29.01.2021    source источник
comment
Короче говоря, вы спрашиваете, как распределяются локальные и глобальные переменные. Суть в том, что когда вы выполняете новое присваивание, такое как _in = list(), _in будет локальным (только в функции), вы создаете новый объект _in. Если вы используете _in.clear(), изменение будет глобальным и приведет к пустому списку. Точно так же _dest1 — это новое назначение (только локальное), а _dest1.extend([val for val in _src[0:len(_src):2]]) — это глобальный вариант.   -  person Thymen    schedule 29.01.2021
comment
Да, и в случае 1 и 2 вы изменяете то, что находится внутри списка, и это будет распространяться.   -  person Jens Munk    schedule 29.01.2021
comment
@Thymen: На самом деле речь идет не о локальном и глобальном (в данном случае это термины области действия, которые вводят в заблуждение; list, полученное в _in, может не быть глобальным, даже если оно есть в примере кода). _in сам по себе всегда является локальным в этом коде, но иногда он использует псевдоним для объекта, предоставленного вызывающей стороной (когда функция введена, это всегда верно), но он может быть повторно привязан к другому объекту (который может или не может быть вновь созданным; _in = some_global назначит его тому же объекту, что и глобальный, но _in останется локальным). Настоящая проблема заключается в мутации и повторной привязке/переназначении, не связанной с областью действия.   -  person ShadowRanger    schedule 29.01.2021
comment
Я думал, что python3 работает при передаче по ссылке с переменными списка? Нет. Python никогда не вызывается ни по ссылке, ни по значению. Кроме того, стратегия оценки никогда не зависит от типа объекта. Здесь вы видите, что ваши функции либо изменяют объект списка, например. _in.append или _in[i] = x, или они не изменяют объект списка, _in = list(). Это просто присваивание, а присваивание никогда не изменяется. Проверьте: nedbatchelder.com/text/names.html   -  person juanpa.arrivillaga    schedule 29.01.2021


Ответы (1)


Ваша конечная проблема состоит в том, что вы путаете (на месте) мутацию с повторным связыванием (также называемым несколько менее точно переназначением).

Во всех случаях, когда изменение не видно за пределами функции, вы переустанавливаете имя внутри функции. Когда вы делаете:

name = val

неважно, что раньше было в name; это восстановление к val, и ссылка на старый объект отбрасывается. Когда это последняя ссылка, это приводит к очистке объекта; в вашем случае аргумент использованный для псевдонима объекта также связан с именем в вызывающем объекте, но после повторной привязки эта ассоциация псевдонимов теряется.

В сторону для людей C/C++: перепривязка похожа на присвоение переменной указателя, например. int *px = pfoo; (начальное связывание), за которым следует px = pbar; (повторное связывание), где pfoo и pbar сами являются указателями на int. Когда происходит присваивание px = pbar;, не имеет значения, что px раньше указывало на то же самое, что и pfoo, теперь оно указывает на что-то новое, и следующее за ним *px = 1; (мутация, а не повторное связывание) влияет только на то, на что указывает pbar, оставляя цель pfoo не изменилась.

Напротив, мутация не нарушает ассоциации псевдонимов, поэтому:

name[1] = val

перепривязывает себя name[1], но не перепривязывает name; он продолжает ссылаться на тот же объект, что и раньше, он просто изменяет этот объект на месте, оставляя все псевдонимы нетронутыми (таким образом, все имена, использующие псевдонимы одного и того же объекта, видят результат изменения).

В вашем конкретном случае вы можете изменить сломанные функции с повторной привязки на псевдоним, изменив назначение/удаление среза или другие формы мутации на месте, например:

def func3(_in):
    # _in = list()  BAD, rebinds
    _in.clear()     # Good, method mutates in place
    del _in[:]      # Good, equivalent to clear
    _in[:] = list() # Acceptable; needlessly creates empty list, but closest to original
                    # code, and has same effect

def func5_no_ret(_src, _dest1, _dest2):
    # BAD, all rebinding to new lists, not changing contents of original lists
    #_dest1 = [val for val in _src[0:len(_src):2]]
    #_dest2 = [val for val in _src[1:len(_src):2]]
    #_src = list()

    # Acceptable (you should just use multiple return values, not modify caller arguments)
    # this isn't C where multiple returns are a PITA
    _dest1[:] = _src[::2]  # Removed slice components where defaults equivalent
    _dest2[:] = _src[1::2] # and dropped pointless listcomp; if _src might not be a list
                           # list(_src[::2]) is still better than no-op listcomp
    _src.clear()

    # Best (though clearing _src is still weird)
    retval = _src[::2], _src[1::2]
    _src.clear()
    return retval

    # Perhaps overly clever to avoid named temporary:
    try:
        return _src[::2], _src[1::2]
    finally:
        _src.clear()
person ShadowRanger    schedule 29.01.2021
comment
Спасибо за исчерпывающий ответ. Многому научился на ваших примерах! - person rbewoor; 29.01.2021
comment
@rbewoor: Рад, что смог помочь. Если на ваш вопрос дан полный ответ, не могли бы вы установить флажок под стрелками для голосования, чтобы принять ответ? Не торопитесь (если кто-то другой даст лучший ответ, отдайте ему чек), но это хороший способ показать, что ваша проблема полностью решена. - person ShadowRanger; 29.01.2021
comment
Готово, сэр! Спасибо, что рассказали мне о галочке. - person rbewoor; 29.01.2021