7

I have this python code. The result is TopTest: attr1=0, attr2=1 for X which is fine but the result is SubTest: attr1=2, attr2=3 for Y which I don't quite understand.

Basically, I have a class attribute, which is a counter, and it runs in the __init__ method. When I launch Y, the counter is set to 2 and only after are the attributes are assigned. I don't understand why it starts at 2. Shouldn't the subclass copy the superclass and the counter restart at 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 Answers5

2

You access and use explicitly TopTest.count, and your subclass will stick to this explicitness. You might want to consider to use type(self).count instead, then each instance will use its own class's variable which can be made a different one in each subclass.

To make your subclass have its own class variable, just add a count = 0 to its definition:

class SubTest(TopTest):
    count = 0
Alfe
  • 56,346
  • 20
  • 107
  • 159
  • 1
    `self.count += 2` would create instance variable, leaving the class variable intact – then0rTh Jun 21 '17 at 15:12
  • Right, my mistake. I'm gonna fix it. – Alfe Jun 21 '17 at 15:20
  • 1
    _I know i only criticize, but:_ To avoid using the double underscores, you _could_ use `count = [0]`, but _should_ use `type(self)`. 1-element list is just ugly workaround for a problem that already has a solution. – then0rTh Jun 21 '17 at 15:43
  • I like that constructive critique ;-) and removed the reference-stuff again. Using explicitly a class variable is probably more Pythonic than using the poor-man's-references. – Alfe Jun 21 '17 at 15:46
2

It looks like you want to keep a counter for each instance of each subclass of TopTest, but you do not want to repeat yourself by declaring a new count class variable for each subclass. You can achieve this using a Metaclass:

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

The count attribute of your x and y objects should now be independent, and subsequent instances of TopTest and SubTest will increment the count:

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

However, metaclasses can be confusing and should only be used if they are truly necessary. In your particular case it would be much simpler just to re-define the count class attribute for every subclass of TopTest:

class SubTest(TopTest):
    count = 0
Graham
  • 7,431
  • 18
  • 59
  • 84
Billy
  • 5,179
  • 2
  • 27
  • 53
1

You're close - when you look up a property of an object, you're not necessarily looking up a property belonging to the object itself. Rather, lookups follow Python's method resolution order, which... isn't entirely simple. In this case, however, only three steps are performed:

  1. Check if Y has a property named count.
  2. It doesn't, so check if its class SubTest has a property named count.
  3. It doesn't, so check if its parent TopTest has a property named count. It does, so access that.

Simply put, when you access Y.count, you're actually accessing TopTest.count.


There's also the fact that you have a bug in your code - SubTest increments TopTest's count and not its own. The title of your question says "subclass counter", but since you're counting in __init__() I assume you're looking for an instance counter (to count subclasses I'm fairly certain you'd need to use metaclasses). This is a perfect use case for self.__class__, a property which contains an object's class! In order to use it:

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

Using that, SubTest.count will be incremented instead of TopTest.count when you call SubTest().

obskyr
  • 1,380
  • 1
  • 9
  • 25
  • isn't `cls = type(self)` better? – then0rTh Jun 21 '17 at 14:57
  • 1
    @then0rTh You're right, I'm just so used to using `super()`, hahah. `super()` would actually get `TopTest` in this case! I've gone ahead and changed it to `__class__`, which works just as well as `type()` (and also supports old-style classes). – obskyr Jun 21 '17 at 15:05
0

When a new instance of SubTest is created TopTest.__init__() is called - since SubTest inherited TopTest.__init__() - which increments TopTest.count by two.

And since SubTest never defines a class level count variable, when SubTest.count is executed, Python falls back and uses TopTest.count.

This behavior can be fixed by redefining count local to SubTest.

class SubTest(TopTest):
    count = 0
Christian Dean
  • 22,138
  • 7
  • 54
  • 87
0

If you want each class to have it's own class variable implicitly, you can use a metaclass to add in this variable.

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  
Jared Goguen
  • 8,772
  • 2
  • 18
  • 36
  • This code fails in Python 3. [That is because Python 3 changed the way metaclass were specified](https://stackoverflow.com/questions/39013249/metaclass-in-python3-5). You need to do `Parent(metaclass=MetaCount):` instead of `__metaclass__ = MetaCount` inside of `Parent`. – Christian Dean Jun 21 '17 at 15:05
  • @Christian updated, thanks, my machine running 2.7 and I'm too lazy to REPL – Jared Goguen Jun 21 '17 at 16:41