Python子类计数器

7
我有这段 Python 代码。对于 X,结果是 TopTest: attr1=0, attr2=1,这很好理解。但对于 Y,结果是 SubTest: attr1=2, attr2=3,我不太明白。
基本上,我有一个类属性,它是一个计数器,并在 __init__ 方法 中运行。当我启动 Y 时,计数器被设置为 2,只有在属性分配后才开始计数。我不明白为什么它从 2 开始。难道子类不应该复制超类并将计数器重置为 0 吗?
class AttrDisplay: 
  def gatherAttrs(self):        
    attrs = []        
    for key in sorted(self.__dict__):            
        attrs.append('%s=%s' % (key, getattr(self, key)))        
    return ', '.join(attrs)
  def __repr__(self):        
    return '[%s: %s]' % (self.__class__.__name__, self.gatherAttrs())

class TopTest(AttrDisplay): 
    count = 0        
    def __init__(self):            
        self.attr1 = TopTest.count            
        self.attr2 = TopTest.count+1            
        TopTest.count += 2

class SubTest(TopTest):
    pass

X, Y = TopTest(), SubTest()         
print(X)                            
print(Y)                         
5个回答

2

您明确访问和使用TopTest.count,而您的子类将坚持这种明确性。您可能希望考虑改用type(self).count,然后每个实例都将使用其自己的类变量,可以在每个子类中设置不同的变量。

要使您的子类具有自己的类变量,请在其定义中添加count = 0

class SubTest(TopTest):
    count = 0

1
self.count += 2 会创建实例变量,而不影响类变量。 - then0rTh
好的,我的错。我会修复它。 - Alfe
1
我知道我只是在批评,但是:为了避免使用双下划线,你可以使用 count = [0],但应该使用 type(self)。一个元素的列表只是一个已经有解决方案的问题的丑陋解决方法。 - then0rTh
我喜欢那个建设性的批评;-)并且再次删除参考资料。明确使用类变量可能比使用穷人手头的引用更符合Pythonic。 - Alfe

2

看起来你想为每个TopTest子类的每个实例保留一个计数器,但你不想通过为每个子类声明一个新的count类变量来重复自己。你可以使用元类来实现这一点:

class TestMeta(type):
    def __new__(cls, name, bases, attrs):
        new_class = super().__new__(cls, name, bases, attrs)
        new_class.count = 0
        return new_class

class TopTest(AttrDisplay, metaclass=TestMeta):
    def __init__(self):
        self.attr1 = self.count
        self.attr2 = self.count + 1
        self.increment_count(2)
    @classmethod
    def increment_count(cls, val):
        cls.count += val

class SubTest(TopTest):
    pass

您的 xy 对象的 count 属性现在应该是独立的,后续的 TopTestSubTest 实例将会增加 count

>>> x, y = TopTest(), SubTest()
>>> x.attr2
1
>>> y.attr2
1
>>> y2 = SubTest()
>>> y2.attr2
3

然而,元类可能会让人感到困惑,只有在确实必要时才应使用。在您的特定情况下,更简单的方法是为 TopTest 的每个子类重新定义 count 类属性:

class SubTest(TopTest):
    count = 0

1
你已经接近了答案 - 当你查找对象的属性时,并不一定是在查找属于该对象本身的属性。相反,查找遵循 Python 的 方法解析顺序,这并不是完全简单的。但在这种情况下,只执行了三个步骤:
  1. 检查 Y 是否有名为 count 的属性。
  2. 它没有,因此检查其类 SubTest 是否有名为 count 的属性。
  3. 它没有,因此检查其父类 TopTest 是否有名为 count 的属性。它有,所以访问那个属性。
简而言之,当你访问 Y.count 时,实际上是在访问 TopTest.count

还有一个事实,你的代码中有一个bug - SubTest增加了TopTest的计数而不是自己的计数。你的问题标题说“子类计数器”,但由于你在__init__()中计数,我假设你正在寻找一个实例计数器(要计算子类,我相当确定你需要使用元类)。这是使用self.__class__的完美用例,它是包含对象类的属性!为了使用它:

def __init__(self):
    self.attr1 = self.__class__.count
    self.attr2 = self.__class__.count + 1            
    self.__class__.count += 2

使用这种方式,当您调用SubTest()时,SubTest.count将会被增加,而不是TopTest.count

cls = type(self) 不是更好吗? - then0rTh
1
@then0rTh 你说得对,我只是太习惯使用 super() 了,哈哈。在这种情况下,super() 实际上会获取 TopTest!我已经改成了 __class__,它与 type() 一样有效(并且还支持旧式类)。 - obskyr

0
当创建SubTest的新实例时,将调用TopTest.__init__(),因为SubTest继承了TopTest.__init__(),这会使得TopTest.count增加两个。
由于SubTest从未定义类级别的count变量,因此在执行SubTest.count时,Python会退回并使用TopTest.count
通过在SubTest中重新定义count来修复此行为。
class SubTest(TopTest):
    count = 0

你可以通过在SubTest内重新定义count来解决这个问题,但是如果你在init中修改TopTest.count,那么SubTest.count将永远为0。 - then0rTh
@then0rTh 对的,我不确定我理解了。那不是原帖作者想要的行为吗? - Christian Dean
我认为 SubTest.count 应该计算 SubTest 的实例,但我可能错了。 - then0rTh

0
如果您希望每个类都隐式地拥有自己的类变量,您可以使用元类来添加此变量。
class MetaCount(type):
    def __new__(cls, name, bases, attrs):
        new_cls = super(MetaCount, cls).__new__(cls, name, bases, attrs)
        new_cls.count = 0
        return new_cls

class Parent(metaclass=MetaCount):
    def __init__(self):
        self.attr1 = self.count            
        self.attr2 = self.count + 1            
        type(self).count += 2 # self.count += 2 creates an *instance* variable

class Child(Parent):
    pass


p, c = Parent(), Child()         
print(p.count) # 2                           
print(c.count) # 2  

这段代码在Python 3中失败了。这是因为Python 3改变了元类的指定方式。你需要在Parent内部使用Parent(metaclass=MetaCount):而不是__metaclass__ = MetaCount。请参考此链接 - Christian Dean
@Christian 已更新,谢谢。我的机器运行的是2.7版本,我太懒了不想用REPL。 - Jared Goguen

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