Python: Понимание классовых и экземплярных переменных
Я столкнулся с неправильным пониманием классовых и экземплярных переменных в Python. Вот пример кода:
class Animal(object):
energy = 10
skills = []
def work(self):
print 'Я что-то делаю'
self.energy -= 1
def new_skill(self, skill):
self.skills.append(skill)
if __name__ == '__main__':
a1 = Animal()
a2 = Animal()
a1.work()
print(a1.energy) # результат: 9
print(a2.energy) # результат: 10
a1.new_skill('гав')
a2.new_skill('спать')
print(a1.skills) # результат: ['гав', 'спать']
print(a2.skills) # результат: ['гав', 'спать']
Я думал, что переменные energy
и skills
являются классовыми, так как я объявил их вне любых методов. Я изменяю их значения внутри методов, используя self
, возможно, это неверно? Однако результаты показывают, что переменная energy
принимает разные значения для каждого объекта (как экземплярная переменная), а skills
, наоборот, кажется, общая для всех экземпляров (как классовая переменная). Похоже, я упустил что-то важное... Как правильно работать с классовыми и экземплярными переменными?
5 ответ(ов)
Секрет здесь кроется в понимании того, что делает выражение self.energy -= 1
. На самом деле это две операции: первая — получение значения self.energy - 1
, а вторая — присвоение этого значения обратно в self.energy
.
Тем не менее, путаница возникает из-за того, что ссылки интерпретируются по-разному с обеих сторон присваивания. Когда Python пытается получить self.energy
, он ищет этот атрибут в экземпляре, не находит его и обращается к атрибуту класса. Однако при присвоении self.energy
он всегда будет присваивать значение атрибуту экземпляра, даже если этого атрибута ранее не существовало.
Вы сталкиваетесь с проблемами инициализации, связанными с изменяемостью.
Во-первых, решение. skills
и energy
являются атрибутами класса. Хорошей практикой является рассматривать их как «только для чтения», в качестве начальных значений для атрибутов экземпляра. Классический способ построить ваш класс выглядит так:
class Animal(object):
energy = 10
skills = []
def __init__(self, en=energy, sk=None):
self.energy = en
self.skills = [] if sk is None else sk
Таким образом, каждый экземпляр будет иметь свои собственные атрибуты, и все ваши проблемы исчезнут.
Во-вторых, что происходит в этом коде? Почему skills
общие, в то время как energy
принадлежит каждому экземпляру?
Оператор -=
является тонким. Это оператор для модификации на месте, если возможно. Разница в том, что тип list
является изменяемым, поэтому модификация на месте часто происходит. Например:
In [6]:
b = []
print(b, id(b))
b += ['strong']
print(b, id(b))
[] 201781512
['strong'] 201781512
Таким образом, a1.skills
и a2.skills
ссылаются на один и тот же список, который также доступен как Animal.skills
. Но energy
— это неизменяемый тип int
, поэтому модификация невозможна. В этом случае создается новый объект int
, и каждый экземпляр управляет своей собственной копией переменной energy
:
In [7]:
a = 10
print(a, id(a))
a -= 1
print(a, id(a))
10 1360251232
9 1360251200
Таким образом, для исправления проблемы с skills
рекомендуем использовать инициализацию в конструкторе, чтобы у каждого экземпляра был свой собственный список навыков.
При первоначальном создании оба атрибута являются одним и тем же объектом:
>>> a1 = Animal()
>>> a2 = Animal()
>>> a1.energy is a2.energy
True
>>> a1.skills is a2.skills
True
>>> a1 is a2
False
Когда вы присваиваете значение атрибуту класса, он становится локальным для экземпляра:
>>> id(a1.energy)
31346816
>>> id(a2.energy)
31346816
>>> a1.work()
I do something
>>> id(a1.energy)
31346840 # id изменился, так как атрибут стал локальным для экземпляра
>>> id(a2.energy)
31346816
Метод new_skill()
не присваивает новое значение массиву skills
, а вместо этого добавляет
элемент, что изменяет список на месте.
Если вы вручную добавите навык, список skills
станет локальным для экземпляра:
>>> id(a1.skills)
140668681481032
>>> a1.skills = ['sit', 'jump']
>>> id(a1.skills)
140668681617704
>>> id(a2.skills)
140668681481032
>>> a1.skills
['sit', 'jump']
>>> a2.skills
['bark', 'sleep']
Наконец, если вы удалите атрибут экземпляра a1.skills
, ссылка вернется к атрибуту класса:
>>> a1.skills
['sit', 'jump']
>>> del a1.skills
>>> a1.skills
['bark', 'sleep']
>>> id(a1.skills)
140668681481032
Таким образом, важно понимать, что атрибуты класса и атрибуты экземпляра ведут себя по-разному в зависимости от того, как вы к ним обращаетесь и модифицируете их.
Вы правы в том, что для доступа к переменным класса лучше использовать сам класс, а не экземпляр (self). Это важно, чтобы четко понимать, что вы обращаетесь к переменным, общим для всех экземпляров, а не к данным конкретного экземпляра.
В вашем примере переменная energy
и список skills
являются атрибутами класса Animal
. Если вы будете изменять их с помощью self
, то это будет не совсем корректно, так как может привести к путанице с экземплярными переменными (если они у вас существуют).
Вот как правильно следует использовать класс для доступа к его переменным:
class Animal(object):
energy = 10
skills = []
def work(self):
print('I do something')
# Доступ к переменной класса через имя класса
Animal.energy -= 1
def new_skill(self, skill):
# Доступ к переменной класса через имя класса
Animal.skills.append(skill)
Таким образом, при использовании Animal.energy
и Animal.skills
, вы явно указываете, что работаете с атрибутами класса, а не с экземплярами. Это улучшает читаемость кода и помогает избежать потенциальных ошибок, связанных с переопределением переменных в экземплярах класса.
В вашем коде, когда вы вызываете a1.work()
, создаётся экземплярная переменная для объекта a1
с именем energy
. Затем, когда интерпретатор доходит до print a1.energy
, он обращается к экземплярной переменной объекта a1
.
Когда интерпретатор доходит до print a2.energy
, он обращается к переменной класса, и поскольку вы не изменили значение переменной класса, выводится 10.
Таким образом, a1.energy
и a2.energy
могут ссылаться на разные значения, если для a1
была создана экземплярная переменная energy
, а для a2
остаётся значение переменной класса.
Как изменить порядок столбцов в DataFrame?
'pip' не распознан как командa внутреннего или внешнего формата
Почему statistics.mean() работает так медленно?
Преобразование строки даты JSON в datetime в Python
Есть ли разница между поднятием экземпляра класса Exception и самого класса Exception?