Наследование переменных класса в python

Пытаясь понять oop в python, я столкнулся с этой ситуацией, которая меня озадачивает, и я не смог найти удовлетворительного объяснения... Я создавал класс Countable, у которого есть атрибут счетчика, который подсчитывает, сколько экземпляров класса имеют был инициализирован. Я хочу, чтобы этот счетчик увеличивался также при инициализации подкласса (или подподкласса) данного класса. Вот моя реализация:

class Countable(object):
    counter = 0
    def __new__(cls, *args, **kwargs):
        cls.increment_counter()
        count(cls)
        return object.__new__(cls, *args, **kwargs)

    @classmethod
    def increment_counter(cls):
        cls.counter += 1
        if cls.__base__ is not object:
            cls.__base__.increment_counter()

где count(cls) для отладки, а потом запишу.

Теперь давайте создадим несколько подклассов:

class A(Countable):
    def __init__(self, a='a'):
        self.a = a

class B(Countable):
    def __init__(self, b='b'):
        self.b = b

class B2(B):
    def __init__(self, b2='b2'):
        self.b2 = b2

def count(cls):
    print('@{:<5}  Countables: {}  As: {}  Bs: {}  B2s: {}'
          ''.format(cls.__name__, Countable.counter, A.counter, B.counter, B2.counter))

когда я запускаю код, подобный следующему:

a = A()
a = A()
a = A()
b = B()
b = B()
a = A()
b2 = B2()
b2 = B2()

Я получаю следующий вывод, который мне кажется странным:

@A      Countables:  1  As: 1  Bs: 1  B2s: 1
@A      Countables:  2  As: 2  Bs: 2  B2s: 2
@A      Countables:  3  As: 3  Bs: 3  B2s: 3
@B      Countables:  4  As: 3  Bs: 4  B2s: 4
@B      Countables:  5  As: 3  Bs: 5  B2s: 5
@A      Countables:  6  As: 4  Bs: 5  B2s: 5
@B2     Countables:  7  As: 4  Bs: 6  B2s: 6
@B2     Countables:  8  As: 4  Bs: 7  B2s: 7

Почему вначале счетчики A и B увеличиваются, несмотря на то, что я вызываю только A()? И почему после первого вызова B() он ведет себя так, как ожидалось?

Я уже выяснил, что для того, чтобы вести себя так, как я хочу, достаточно добавить counter = 0 в каждый подкласс, но я не смог найти объяснения, почему он так себя ведет.... Спасибо!


Я добавил несколько отладочных отпечатков и для простоты ограничил создание классов двумя. Это довольно странно:

>>> a = A()
<class '__main__.A'> incrementing
increment parent of <class '__main__.A'> as well
<class '__main__.Countable'> incrementing
@A      Counters: 1  As: 1  Bs: 1  B2s: 1
>>> B.counter
1
>>> B.counter is A.counter
True
>>> b = B()
<class '__main__.B'> incrementing
increment parent of <class '__main__.B'> as well
<class '__main__.Countable'> incrementing
@B      Counters: 2  As: 1  Bs: 2  B2s: 2
>>> B.counter is A.counter
False

Почему, когда B() еще не инициализирован, он указывает на ту же переменную, что и A.counter, но после создания одного объекта это другой?


person Pietro Tortella    schedule 15.09.2017    source источник
comment
Я не могу воспроизвести ваш вывод. Мой вывод для B2s всегда такой же, как Bs.   -  person Aran-Fey    schedule 15.09.2017
comment
Я отредактировал ваш вопрос с упрощенным примером проблемы. Это интересный вопрос, надеюсь, кто-то может пролить свет на процесс   -  person Chen A.    schedule 15.09.2017
comment
@ Rawing, вы правы, я вставил вывод другого примера ... теперь я это исправляю!   -  person Pietro Tortella    schedule 15.09.2017
comment
Знаете ли вы, что в python есть __subclasses__, который даст вам подклассы класса? stackoverflow.com/a/3862957/7432   -  person Bryan Oakley    schedule 15.09.2017


Ответы (1)


Проблема с вашим кодом заключается в том, что подклассы Countable не имеют собственного атрибута counter. Они просто наследуют его от Countable, поэтому, когда изменяется counter Countable, похоже, что counter дочернего класса также изменяется.

Минимальный пример:

class Countable:
    counter = 0

class A(Countable):
    pass # A does not have its own counter, it shares Countable's counter

print(Countable.counter) # 0
print(A.counter) # 0

Countable.counter += 1

print(Countable.counter) # 1
print(A.counter) # 1

Если бы у A был собственный атрибут counter, все работало бы как положено:

class Countable:
    counter = 0

class A(Countable):
    counter = 0 # A has its own counter now

print(Countable.counter) # 0
print(A.counter) # 0

Countable.counter += 1

print(Countable.counter) # 1
print(A.counter) # 0

Но если все эти классы используют один и тот же counter, почему мы видим в выводе разные числа? Это потому, что вы на самом деле добавляете атрибут counter в дочерний класс позже, с помощью этого кода:

cls.counter += 1

Это эквивалентно cls.counter = cls.counter + 1. Однако важно понимать, что означает cls.counter. В cls.counter + 1 у cls еще нет собственного атрибута counter, так что это фактически дает вам counter родительского класса. Затем это значение увеличивается, и cls.counter = ... добавляет атрибут counter к дочернему классу, которого до сих пор не существовало. По сути, это эквивалентно написанию cls.counter = cls.__base__.counter + 1. Вы можете увидеть это в действии здесь:

class Countable:
    counter = 0

class A(Countable):
    pass

# Does A have its own counter attribute?
print('counter' in A.__dict__) # False

A.counter += 1

# Does A have its own counter attribute now?
print('counter' in A.__dict__) # True

Итак, каково решение этой проблемы? Вам нужен метакласс . Это дает вам возможность присвоить каждому подклассу Countable собственный атрибут counter при его создании:

class CountableMeta(type):
    def __init__(cls, name, bases, attrs):
        cls.counter = 0  # each class gets its own counter

class Countable:
    __metaclass__ = CountableMeta

# in python 3 Countable would be defined like this:
#
# class Countable(metaclass=CountableMeta):
#    pass

class A(Countable):
    pass

print(Countable.counter) # 0
print(A.counter) # 0

Countable.counter += 1

print(Countable.counter) # 1
print(A.counter) # 0
person Aran-Fey    schedule 15.09.2017
comment
Я бы просто добавил, что в Python3.6+ также можно использовать __init_subclass__() hook для той же цели (добавление атрибута counter к каждому подклассу). - person plamut; 15.09.2017
comment
Или (в Python 2.7.x+ и 3.x) используйте декоратор класса. - person bruno desthuilliers; 15.09.2017
comment
однако после завершения создания первого объекта (a = A()) я получаю id(Countable.counter) == id(A.counter). Почему это происходит, если присваивание создает новую переменную класса для класса A?? - person blue_note; 15.09.2017
comment
Целые числа @blue_note неизменяемы, поэтому нет особого смысла проверять их идентификатор. Важно не то, что оба класса используют один и тот же экземпляр int, а то, имеет ли A свой собственный атрибут, который затеняет Countable. Если бы целые числа были изменяемыми, то оба класса, использующие один и тот же экземпляр int, были бы проблемой, но это не так. См. также этот вопрос для получения дополнительной информации о сравнении идентификаторов целых чисел. - person Aran-Fey; 15.09.2017
comment
да, но вы имеете в виду значения int стажеров Python ?? Я, хотя это произошло только для строк. - person blue_note; 15.09.2017
comment
@blue_note Да, он (CPython) делает это для целых чисел от -5 до 256. - person Aran-Fey; 15.09.2017