1

I have two classes : Child class which inherits Ancestor class. My goal is to add id attribute for each instance without override

Code example:

class Ancestor(tensorflow.Module):
    _id = 0

    def __init__(self, some_list=):
        super(Ancestor, self).__init__()
        self.id = Ancestor._id + 1
        Ancestor._id += 1


class Child(Ancestor):
    _id = 0

    def __init__(self):
        self.id = Child._id + 1
        Child._id += 1
        some_list = [char + self.id for char in ["a", "b", "c"] ]
        super(Child, self).__init__(some_list)


a1 = Ancestor()
a2 = Ancestor()
c1 = Child()
c2 = Child()



print(a1.id, a2.id, c1.id, c2.id)
>>> 1 2 3 4

I want this setup to print: 1 2 1 2

How can this be achieved?

Edited "some_list" in Child constructor simply is there to emphasize that Child must receive it's id, prior to calling Ancestor's super method

Natan
  • 19
  • 3
  • 2
    Just get rid of the `super()...` line in Child. Things would be more complicated if you actually needed something from the inherited `__init__()`, but that's not the case here. – jasonharper Mar 17 '20 at 16:16
  • You can also run `super()...` first and then override the `id` property with `self.id=...` in the `Child.__init__()` – hurlenko Mar 17 '20 at 16:19

2 Answers2

4

Python 3 users: see the bottom for a better solution using __init_subclass__.


Don't hard-code the class name. Use type(self) to get access to the appropriate class for each instance.

class Ancestor(object):
    _id = 0

    def __init__(self):
        type(self)._id += 1
        self.id = self._id


class Child(Ancestor):
    _id = 0

    def __init__(self):
        super(Child, self).__init__()
        # Actual class-specific stuff here.
        # If there is none, you don't even need to override __init__


a1 = Ancestor()
a2 = Ancestor()
c1 = Child()
c2 = Child()

assert (a1.id, a2.id, c1.id, c2.id) == (1,2,1,2)

Or perhaps cleaner, make the attribute a generator, rather than an value to be updated. Note that since the count instance maintains its own state, there is no need to assign anything back to the class attribute, and you can thus access _id via the instance rather than its type.

from itertools import count

class Ancestor(object):
    _id = count(1)

    def __init__(self, **kwargs):
        super(Ancestor, self).__init__(**kwargs)
        self.id = next(self._id)


class Child(Ancestor):
    _id = count(1)

Python 3

You can use __init_subclass__ to ensure that every descendent of Ancestor has its own ID generator without having to add it explicitly in the class definition.

from itertools import count


class CountedInstance:
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls._id = count(1)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.id = next(self._id)


class Ancestor(CountedInstance):
    pass

class Child(Ancestor):
    pass
chepner
  • 497,756
  • 71
  • 530
  • 681
  • Thanks! The setup I provided have 2 inaccuracies in it: 1. The Ancestor class itself inherits from class: Ancestor(SomeClass) 2. Child __init__ creates some variables, which depends on its own id and pass these variables to Ancestor class via super. This in fact mean that Child’s id must be calculated prior to calling super – Natan Mar 18 '20 at 07:47
  • #1 shouldn't be a problem. You can either add the necessary code to `SomeClass.__init__` or `SomeClass.__init_subclass__` instead, or have `Ancestor` inherit from both `SomeClass` and `CountedInstance`. #2 sounds like it might be a design issue; can you provide a more accurate example in the question? – chepner Mar 18 '20 at 11:48
  • OK, just make sure you call `super().__init__` before you define `some_list`. – chepner Mar 22 '20 at 19:09
  • That won't work for me since "some_list", which is passed to the Ancestor is dependent on Child id so it must be calculated prior to calling super().__init__ – Natan Mar 23 '20 at 06:45
  • `self._id` resolves to `Child._id`, even when resolved inside `Ancester.__init__` or `CountedInstance.__init__`, because `self` is an instance of `Child`. – chepner Mar 23 '20 at 12:57
2

Use type(self) instead of the name of the class.

class Ancestor(object):
    _id = 0
    def __init__(self):
        type(self)._id += 1
        self.id = self._id

class Child(Ancestor):
    _id = 0

a1 = Ancestor()
a2 = Ancestor()
c1 = Child()
c2 = Child()

print(a1.id, a2.id, c1.id, c2.id)  # -> 1 2 1 2

Note you could use self.__class__ to accomplish the same thing, but it can be overwritten, so type(self) is better.

wjandrea
  • 28,235
  • 9
  • 60
  • 81
  • I'm not sure if `type` does much more than return the value of its argument's `__class__` attribute anyway; overwriting the value changes the return value of `type`. Myself, I just don't like using dunder attributes explicitly when I can help it. – chepner Mar 17 '20 at 17:57
  • @chepner Good point about dunder attributes. But are you sure overwriting `__class__` changed the return value of `type`? I couldn't reproduce it, and I even found [some discussion about why they need to be different](https://stackoverflow.com/questions/1060499/#comment94370801_1060537), citing Guido himself. – wjandrea Mar 17 '20 at 20:34
  • Really? Starting with two bare-bones classes `A` and `B`, I define `a = A()`. Then `type(a)` returns `A`, but after `a.__class__ = B`, `type(a)` returns `B`. This holds in Python 2.7, Python 3.7, and Python 3.8. – chepner Mar 17 '20 at 20:41
  • @chepner Oh, huh! That actually changes `a`'s class! Once you do that, try for example `A.someattr = 'AAA'; B.someattr = 'BBB'`. Then `a.someattr == 'BBB'`. Or `type(a)()`, returns a `B` object. The difference is, I'm talking about [overriding `__class__` in the class body](https://stackoverflow.com/a/10633356/4518341). – wjandrea Mar 17 '20 at 20:54
  • Hm. Is `__class__` handled specially by `__getattribute__` somewhere along the line? It doesn't really seem to be either a class attribute or an instance attribute otherwise. – chepner Mar 17 '20 at 21:04