Ловушка добавления объектов класса в список в python

#python #list

Вопрос:

Мне что-то непонятно при добавлении объекта в список python. Поскольку исходный код слишком сложен и слишком длинен, чтобы публиковать его здесь, я разработал минимальный пример, чтобы показать эффект. Чтобы объяснить это, мне удалось переработать код, чтобы он функционировал должным образом и давал правильный результат. Но это потребовало от меня много времени для отладки. Причина, по которой это происходит, мне до сих пор не ясна. Поэтому я очень рад, если вы сможете объяснить мне, почему это происходит.

Ладно, пошли.

Вот рабочая версия кода. В конце вы можете видеть, что он выдает результат и работает так, как ожидалось:

 import numpy as np

class foo():
    def __init__(self):
        self.x = np.random.random()
    
    def move(self, value):
        self.x  = value
        
p  = []
p2 = []

for _ in range(10):
    
    z = foo()
    z.move(4)
    p2.append(z)
    
p = p2
    
for i in range(10):
    print("Print x: {}".format(p[i].x))
 

Запустив код, я получаю следующий вывод (который по прошествии времени отличается, так как я вызываю функцию генератора случайных чисел). Но это работает!:

 Print x: 4.18111236900313
Print x: 4.399997493230335
Print x: 4.594324823463079
Print x: 4.8205743285019045
Print x: 4.125578603746895
Print x: 4.430324972670274
Print x: 4.135255992051397
Print x: 4.9479568256336295
Print x: 4.789025666744436
Print x: 4.261426793909385
 

Сейчас я покажу вам первую версию кода, которая вызвала у меня много головной боли и много гнева. Пожалуйста, обратите внимание, что я немного изменил второй цикл for, потому что нашел его более логичным для себя. Я вызываю метод move() напрямую и append() в одной строке, а не в двух.
Вот версия:

 import numpy as np

class foo():
    def __init__(self):
        self.x = np.random.random()
    
    def move(self, value):
        self.x  = value
        
p  = []
p2 = []

for _ in range(10):
    
    z = foo()
    p2.append(z.move(4))    # Here is the only change to the code!
    
p = p2

    
for i in range(10):
    print("Print x: {}".format(p[i].x))
 

Но, запустив этот код, я получаю следующую ошибку:

 AttributeError: 'NoneType' object has no attribute 'x'
 

и затем я обнаруживаю, что списки p и p2 не имеют элементов в качестве элементов.
Почему это?
Я очень рад понять, почему…

Редактировать

Как было предложено ниже, я понял, что move() метод ничего не возвращает.

Поэтому я изменил код следующим образом:

 import numpy as np

class foo():
    def __init__(self):
        self.x = np.random.random()
    
    def move(self, value):
        
        output    = foo()
        output.x  = value
        
        return output      # <= Here the change
        
p  = []
p2 = []

for _ in range(10):
    
    z = foo()
    p2.append(z.move(4))
    
p = p2

    
for i in range(10):
    print("Print x: {}".format(p[i].x))
 

И это работает!
Еще одна важная вещь, которую мы узнали сегодня…

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

1. foo.move не возвращается foo ; он возвращается None (неявно, потому что нет return оператора.

Ответ №1:

Создание копии объекта в первую очередь помогает контролировать ситуацию (неизменяемые простые объекты данных более полезны для удобства обслуживания, чем изменяемые объекты).

 def move_immutable(self, value):
    c = copy.copy(self)  # add also "import copy" to the beginning of the file
    c.x  = value
    return c
 

Если вы действительно хотите, чтобы все оставалось изменяемым, вы могли бы вместо вышеперечисленного сделать это в цикле вместо p2.добавить(z.переместить(4)):

     z = foo()
    z.move(4)
    p2.append(z)
 

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

1. Необходимо import copy импортировать каждый раз при вызове move_immutable ??

2. Весь импорт должен быть в начале файла, но он там для ясности. Я немного отредактирую.

Ответ №2:

Я думаю, что вы действительно можете спросить, почему в приложении хранится то, что возвращает метод, а не объект. Это связано с тем, что при использовании метода в присваивании он немедленно выполняется, и вместо него используется возвращаемое значение.

Вот почему вы можете передавать методы функциям, которые предназначены только для приема возвращаемого ими значения.

Пример:

 def square(x):
    return x*x

def print_variable(variable_to_print):
    print(variable_to_print)

print_variable(square(2))
 

В этом примере print_variable не получает квадрат(2), он получает значение 4.

Вот почему move должен вернуть себя, чтобы обновить x на месте так, как вы его используете.

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

 class foo(x=None):
    def __init__(self):
        self.x = np.random.random()
        if x:
            self.x  = x

    def move(self, value):
        self.x  = value

p  = []
p2 = []

for _ in range(10):
    p2.append(foo(4))

p = p2
 

Ответ №3:

Ваш z.move() метод возвращается None .

Если вы хотите изменить это поведение, измените его на возврат self :

     def move(self, value):
        self.x  = value
        return self
 

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

1. Это «исправляет» проблему, но нарушает довольно разумное соглашение о том, что метод должен либо изменять свой объект, либо возвращать значение, но не то и другое вместе.

2. цитата из «конвенции», пожалуйста?