Проблемы с пониманием передаваемых значений и ссылок в Python

#python #oop #reference #pass-by-reference

#python #ооп #ссылка #передача по ссылке

Вопрос:

Проблемы с тем, когда объекты изменены, а когда их нет в Python. Вот мой плохо продуманный пример ниже:

 class person:
    age = 21

class bar:
    def __init__(self, arg1):
        self.foo = arg1
        self.foo.age = 23

def baz(arg1):
    arg1.age = 27

def teh(arg1):
    arg1 = [3,2,1]

Person1 = person()
bar1 = bar(Person1)

print Person1.age
print bar1.foo.age

baz(Person1)

print Person1.age
print bar1.foo.age

meh = [1,2,3]
teh(meh)
print meh
  

Результат таков

 23
23
27
27
[1, 2, 3]
  

Итак, когда мы объявляем Person1, Person1.age равен 21. Ссылка на этот объект передается конструктору класса другого экземпляра класса bar, называемого bar1. Любые изменения, внесенные в эту ссылку, изменят Person1.

Это также тот случай, когда мы передаем Person1 обычной функции, Person1.age теперь равен 27.

Но почему это не работает с переменной «meh»? Конечно, если мы назначим переменную a = meh и изменим a = [6, 6, 6] , то meh также изменится. Я в замешательстве. Есть ли какая-либо литература о том, как все это работает?

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

1. Чтобы получить правильный отступ кода в StackOverflow, вам нужно использовать пробелы вместо таблиц. Также неплохо сделать это в вашем собственном коде, потому что, я полагаю, это то, что большинство людей делают на Python.

Ответ №1:

Я вижу три фундаментальные концепции Python, которые могут пролить некоторый свет на этот вопрос:

1) Во-первых, назначение из изменяемого объекта, подобного в

 self.foo = arg1
  

похоже на копирование указателя (а не значения, на которое указано): self.foo и arg1 являются одним и тем же объектом. Вот почему следующая строка,

 self.foo.age = 23
  

изменяет arg1 (т. Е. Person1 ). Переменные, таким образом, являются разными «именами» или «метками», которые могут указывать на уникальный объект (здесь person объект). Это объясняет, почему baz(Person1) изменяет Person1.age и bar1.foo.age на 27, поскольку Person1 и bar1.foo — это всего лишь два имени для одного и того же person объекта ( Person1 is bar1.foo возвращает True в Python).

2) Вторым важным понятием является понятие присваиваний. В

 def teh(arg1):
    arg1 = [3,2,1]
  

переменная arg1 является локальной, так что код

 meh = [1,2,3]
teh(meh)
  

сначала это делает arg1 = meh , что означает, что arg1 это дополнительное (локальное) имя для списка meh ; но делать arg1 = [3, 2, 1] это все равно, что сказать «Я передумал: arg1 отныне будет именем нового списка, [3, 2, 1]». Здесь важно иметь в виду, что назначения, несмотря на то, что они обозначаются знаком «равно», асимметричны: они присваивают (изменяемому) объекту справа и сбоку дополнительное имя, указанное в левой части (вот почему вы не можете этого сделать [3, 2, 1] = arg1 , поскольку левая сторона должна быть именем [или именами]). Итак, arg1 = meh; arg1 = [3, 2, 1] не может измениться meh .

3) Последний пункт связан с названием вопроса: «передача по значению» и «передача по ссылке» не являются понятиями, которые актуальны в Python. Вместо этого соответствующими понятиями являются «изменяемый объект» и «неизменяемый объект«. Списки изменчивы, а числа — нет, что объясняет то, что вы наблюдаете. Кроме того, ваши объекты Person1 и bar1 изменчивы (вот почему вы можете изменить возраст пользователя). Вы можете найти более подробную информацию об этих понятиях в текстовом руководстве и видеоуроке. В Википедии также есть некоторая (более техническая) информация. Пример иллюстрирует разницу в поведении между изменяемым и неизменяемым:

 x = (4, 2)
y = x  # This is equivalent to copying the value (4, 2), because tuples are immutable
y  = (1, 2, 3)  # This does not change x, because tuples are immutable; this does y = (4, 2)   (1, 2, 3)

x = [4, 2]
y = x  # This is equivalent to copying a pointer to the [4, 2] list
y  = [1, 2, 3]  # This also changes x, because x and y are different names for the same (mutable) object
  

Последняя строка не эквивалентна y = y [1, 2, 3] , потому что это только поместило бы новый объект списка в переменную y вместо изменения списка, на который ссылаются оба y и x .

Три вышеуказанные концепции (переменные как имена [для изменяемых объектов], асимметричное присвоение и изменчивость / неизменяемость) объясняют многие особенности поведения Python.

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

1. «Изменяемый объект» против «неизменяемого объекта» на самом деле здесь не очень уместно; проблема в «мутации» против «назначения». arg1 может использоваться для изменения meh , но его нельзя использовать для присвоения нового значения meh .

2. @ruakh: Это правда, что код в вопросе касается только присвоения и изменчивости; однако в названии говорится о «передаче по значению и ссылке», поэтому я думаю, что в любом случае полезно упомянуть подход Python к модификации (или нет) переменных, передаваемых вызывающей стороной (т. Е. Изменяемых и неизменяемых объектов).).

3. @ruakh: PS: Тем не менее, я обновил свой пост, чтобы подчеркнуть аспект изменчивости и неизменности.

Ответ №2:

Конечно, если мы присвоим переменной a = meh и изменим a = [6, 6, 6], то meh также будет изменен.

Нет, на самом деле, это не:

 >>> meh = [1,2,3]
>>> a = meh
>>> a = [6, 6, 6]
>>> print a
[6, 6, 6]
>>> print meh
[1, 2, 3]
  

Это разница между перезаписью переменной и изменением экземпляра, на который указывает переменная.

Списки, словари, наборы и объекты являются изменяемыми типами. Если вы добавляете, удаляете, устанавливаете, получаете или иным образом изменяете что-то в их экземпляре, это обновляет все, что ссылается на этот экземпляр.

Однако, если вы назначаете совершенно новый экземпляр типа переменной, это изменяет ссылку, хранящуюся в этой переменной, и, следовательно, старый экземпляр, на который ссылается, не изменяется.


 a = [1,2,3] # New instance
a[1] = 4    # Modifying existing instance

b = {'x':1, 'y':2} # New instance
b['x'] = 3         # Modifying existing instance

self.x = [1,2,3] # Modifying existing object instance pointed to by 'self',
                 # and creating new instance of a list to store in 'self.x'

self.x[0] = 5    # Modifying existing list instance pointed to by 'self.x'
  

Ответ №3:

В Python нет передачи по значению или передачи по ссылке, но вместо этого используется передача по объекту — другими словами, объекты передаются непосредственно в функции и привязываются к имени параметра, указанному в определении функции.

Когда вы выполняете spam = «green», вы привязываете имя spam к объекту string «green»; если вы затем выполняете eggs = spam, вы ничего не скопировали, вы не создали ссылочных указателей; вы просто привязали другое имя, eggs, к тому же объекту (в данном случае «зеленый»). Если вы затем привяжете спам к чему-то другому (spam = 3.14159), яйца все равно будут привязаны к «зеленому».

В вашей teh функции вы не изменяете / модифицируете / мутируете переданный объект, вы переназначаете имя arg1 в другой список. Чтобы изменить arg1 , вы хотите это:

 def teh(arg1):
    arg1[:] = [3, 2, 1]
  

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

1. «Передача по имени» (или «вызов по имени») — это совершенно другое дело, см. cs.sfu.ca /~cameron/Teaching/383/PassByName.html и en.wikipedia.org/wiki/Evaluation_strategy#Call_by_name . Модель передачи аргументов в Python вместо этого описывается как «вызов путем совместного использования», а на других языках с такой же передачей аргументов считается вызовом по значению, причем значение всегда является указателем / ссылкой на объект.

2. @delnan: Спасибо, обновлено до ‘call-by-object’. Python не раскрывает основную механику, используемую для реализации вызова по объекту, поэтому использование термина «вызов по значению» вредно.