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

#python-3.x #list #pass-by-reference #function-call #empty-list

#python-3.x #Список #передача по ссылке #вызов функции #пустой список

Вопрос:

Для 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 = []
 

Спасибо.

Комментарии:

1. Короче говоря, вы спрашиваете, как распределяются локальные и глобальные переменные. Суть в том, что когда вы выполняете новое назначение, например _in = list() , _in будет локальным (только в функции), вы создаете новый _in объект. Если вы используете _in.clear() , изменение будет глобальным и приведет к пустому списку. Аналогично, _dest1 это новое назначение (только локальное) и _dest1.extend([val for val in _src[0:len(_src):2]]) является глобальным вариантом.

2. Да, и в случае 1 и 2 вы изменяете то, что находится внутри списка, и это будет распространяться.

3. @Thymen: На самом деле речь идет не о локальном и глобальном (это термины области действия, которые в данном случае вводят в заблуждение; list полученный into _in может не быть глобальным, даже если он есть в примере кода). _in сам по себе всегда является локальным в этом коде, но иногда он использует псевдоним объекта, предоставленного вызывающим объектом (при вводе функции это всегда верно), но он может быть возвращен к другому объекту (который может быть или не быть вновь созданным; _in = some_global присвоил бы ему псевдоним тому же объекту, что и глобальный, но _in остаетсялокальный). Реальная проблема заключается в «мутации» и «повторной привязке» /»переназначении», не связанных с областью действия.

4. «Я думал, что python3 работает при передаче по ссылке с переменными списка?» Нет . Python никогда не вызывается по ссылке и не вызывается по значению. Кроме того, стратегия оценки никогда не зависит от типа объекта. То, что вы видите здесь, это то, что ваши функции либо изменяют объект списка, например _in.append , или _in[i] = x , или они не изменяют объект списка, _in = list() . Это просто присвоение , и присвоение никогда не изменяется. Проверьте: nedbatchelder.com/text/names.html

Ответ №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()
 

Комментарии:

1. Благодарю вас за исчерпывающий ответ. Многому научился из ваших примеров!

2. @rbewoor: Рад, что смог помочь. Если на ваш вопрос дан полный ответ, не могли бы вы, пожалуйста, установить флажок под стрелками голосования, чтобы принять ответ? Не спешите (если кто-то другой даст лучший ответ, пожалуйста, дайте им чек), но это хороший способ указать, что ваша проблема полностью решена.

3. Готово, сэр! Спасибо, что рассказали мне о флажке.