Python中类变量的继承

11

当我试图理解Python中的OOP时,遇到了一个令我困惑的情况,并且我无法找到满意的解释...... 我正在构建一个计数类(Countable class),它有一个计数器属性,用于计算已初始化的类实例数量。 我希望在子类(或子子类)初始化时也增加此计数器。以下是我的实现:

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()的时候,A和B的计数器都会自增呢?并且为什么在第一次调用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相同的变量,但创建单个对象后它却是不同的?


我无法复现你的输出。我的B2s输出总是与Bs相同。 - Aran-Fey
我编辑了你的问题,并提供了一个简化的示例来说明问题。这是一个有趣的问题,希望有人能够解决这个过程。 - Chen A.
@Rawing 你说得对,我粘贴了另一个示例的输出...现在我修复它了! - Pietro Tortella
你是否知道Python有__subclasses__函数,可以返回一个类的所有子类?https://dev59.com/QW865IYBdhLWcg3wWtKz#3862957 - Bryan Oakley
1个回答

17
您的代码问题在于Countable的子类没有自己的counter属性。它们仅从Countable继承,因此当Countablecounter更改时,子类的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

2
我想补充一下,在Python3.6+中,我们也可以使用__init_subclass__() hook来实现相同的目的(为每个子类添加一个counter属性)。 - plamut
或者(在Python 2.7.x+和3.x中)使用类装饰器。 - bruno desthuilliers
然而,在第一个对象创建完成后(a = A()),我得到了 id(Countable.counter) == id(A.counter)。如果赋值为类A创建了一个新的类变量,那么为什么会发生这种情况? - blue_note
@blue_note ints是不可变的,因此检查它们的id没有太多意义。重要的不是两个类是否共享相同的int实例,而是A是否有自己的属性遮盖了Countable的属性。如果ints是可变的,那么两个类共享相同的int实例将是一个问题,但它们不是。有关比较int id的更多信息,请参见此问题。 - Aran-Fey
是的,但是你的意思是Python中整数也会发生这种情况吗?我以为只有字符串才会。 - blue_note
1
@blue_note 是的,CPython 对于介于-5和256之间的整数会这样做。 - Aran-Fey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接