0

Python: Понимание классовых и экземплярных переменных

8

Я столкнулся с неправильным пониманием классовых и экземплярных переменных в 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 ответ(ов)

0

Секрет здесь кроется в понимании того, что делает выражение self.energy -= 1. На самом деле это две операции: первая — получение значения self.energy - 1, а вторая — присвоение этого значения обратно в self.energy.

Тем не менее, путаница возникает из-за того, что ссылки интерпретируются по-разному с обеих сторон присваивания. Когда Python пытается получить self.energy, он ищет этот атрибут в экземпляре, не находит его и обращается к атрибуту класса. Однако при присвоении self.energy он всегда будет присваивать значение атрибуту экземпляра, даже если этого атрибута ранее не существовало.

0

Вы сталкиваетесь с проблемами инициализации, связанными с изменяемостью.

Во-первых, решение. 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 рекомендуем использовать инициализацию в конструкторе, чтобы у каждого экземпляра был свой собственный список навыков.

0

При первоначальном создании оба атрибута являются одним и тем же объектом:

>>> 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

Таким образом, важно понимать, что атрибуты класса и атрибуты экземпляра ведут себя по-разному в зависимости от того, как вы к ним обращаетесь и модифицируете их.

0

Вы правы в том, что для доступа к переменным класса лучше использовать сам класс, а не экземпляр (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, вы явно указываете, что работаете с атрибутами класса, а не с экземплярами. Это улучшает читаемость кода и помогает избежать потенциальных ошибок, связанных с переопределением переменных в экземплярах класса.

0

В вашем коде, когда вы вызываете a1.work(), создаётся экземплярная переменная для объекта a1 с именем energy. Затем, когда интерпретатор доходит до print a1.energy, он обращается к экземплярной переменной объекта a1.

Когда интерпретатор доходит до print a2.energy, он обращается к переменной класса, и поскольку вы не изменили значение переменной класса, выводится 10.

Таким образом, a1.energy и a2.energy могут ссылаться на разные значения, если для a1 была создана экземплярная переменная energy, а для a2 остаётся значение переменной класса.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь