#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. Готово, сэр! Спасибо, что рассказали мне о флажке.