2

I have a parent class Animal and child class Dog. I wanted to create 1 instance of each and print the their count. Here is the working code:

class Animal:
  count=0
  def __init__(self):
     Animal.count+=1
  @classmethod
  def getCount(cls):
     return cls.count

class Dog (Animal):
  count=0
  def __init__(self):
    super().__init__()
    Dog.count+=1

 a1=Animal()
 print(Animal.getCount(),Dog.getCount())
 d1=Dog()
 print(Animal.getCount(),Dog.getCount())

It prints:
1 0
2 1

which is correct as there are 2 animals but only 1 of them is dog.

The problem occurs when i create count variable as private __count without changing any other piece of code.

class Animal:
  __count=0
  def __init__(self):
    Animal.__count+=1
  @classmethod
  def getCount(cls):
    return cls.__count

class Dog (Animal):
  __count=0
  def __init__(self):
    super().__init__()
    Dog.__count+=1

a1=Animal()
print(Animal.getCount(),Dog.getCount())
d1=Dog()
print(Animal.getCount(),Dog.getCount())



Now, it prints:
1 1
2 2

Seems like Dog class is only accessing the Animal's __count.
Can you detect the bug in the code?

iro otaku
  • 51
  • 4
  • you use a `@classmethod` so everything that uses that class, which is both of your subclasses, will use it. – Nullman Mar 24 '19 at 14:04
  • 1
    Very closely related: [Python parent class access to child private variables](//stackoverflow.com/q/10807692) – Aran-Fey Mar 24 '19 at 14:32

2 Answers2

4

The short answer

When an attribute is private, like __count, that means it can only be accessed from within the same class. Animal.__count can only be accessed within Animal, and Dog.__count can only be accessed within Dog.

Because getCount is defined in Animal, it only has access to Animal.__count, so that's what it returns.

If you want to access a "private" variable of a subclass, use a single underscore prefix like _count.

Related reading:

Going into detail

Private variables are implemented through a mechanism called name mangling. From the docs:

Since there is a valid use-case for class-private members (namely to avoid name clashes of names with names defined by subclasses), there is limited support for such a mechanism, called name mangling. Any identifier of the form __spam (at least two leading underscores, at most one trailing underscore) is textually replaced with _classname__spam, where classname is the current class name with leading underscore(s) stripped. This mangling is done without regard to the syntactic position of the identifier, as long as it occurs within the definition of a class.

What this means is that your code is translated to this:

class Animal:
    _Animal__count = 0

    def __init__(self):
        Animal._Animal__count += 1

    @classmethod
    def getCount(cls):
        return cls._Animal__count

class Dog(Animal):
    _Dog__count = 0

    def __init__(self):
        super().__init__()
        Dog._Dog__count += 1

If you look at it like this, it's obvious that getCount cannot access Dog's __count variable.

Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
  • 1
    *it can only be accessed from within the same class* - this is not right. Please see my answer. – heemayl Mar 24 '19 at 16:17
  • getCount is defined in Animal but its also inherited in Dog. There is another __count defined in Dog. So shouldn't Dog.getCount() return Dog's __count? – iro otaku Mar 24 '19 at 20:08
  • @irootaku Inheriting a method doesn't make a copy of it. It's defined in `Animal`, so it can't access private `Dog` attributes. For a bit more detail, see [the docs](https://docs.python.org/3/tutorial/classes.html#private-variables). – Aran-Fey Mar 24 '19 at 20:11
2

This is caused by name mangling done by Python when any class level variable starts with at least two underscores, and have at most one underscore at the end.

For example:

class Foo:
    __bar = 10

Now, Foo.__bar can be accessed outside of the Foo class as Foo._Foo__bar.


In your case, you would be better off simply using a single underscore i.e. _count as variable name which indicates the users that the name is meant to be used privately only.


If you want to follow the abtitious route and keep your current structure, you can define the superclass' getCount method to return values based on the class it's called from:

In [1720]: class Animal: 
      ...:     __count=0 
      ...:     def __init__(self): 
      ...:         Animal.__count += 1

      ...:     @classmethod 
      ...:     def getCount(cls):     
      ...:         return cls.__count if cls.__name__ == 'Animal' else getattr(cls, f'_{cls.__name__}__count') 

As an aside, you might want to use snake_case for method names e.g. get_count, and 4 spaces for indenting.

heemayl
  • 39,294
  • 7
  • 70
  • 76