0

I am trying to create two types of stacks in python (LIFO technology). One of which (Stack master class) is a boilerplate stack class and the other (CountingStack class) inherits from the master class, but also has a method to count pop() calls.

However, when instantiating an object of CountingStack class, it doesn't seem to inherit the "__stk" attribute in the master class. This list is the actual container itself which acts as the stack.

The error I get is:

Traceback (most recent call last):
  File "main.py", line 31, in <module>
    stk.pop()
  File "main.py", line 24, in pop
    self.__stk.pop()
AttributeError: 'CountingStack' object has no attribute '_CountingStack__stk'

And my script is below:

class Stack:
    def __init__(self):
        self.__stk = []

    def push(self, val):
        self.__stk.append(val)

    def pop(self):
        val = self.__stk[-1]
        del self.__stk[-1]
        return val


class CountingStack(Stack):
    def __init__(self):
        Stack.__init__(self)
        self.__pop_counter__ = 0
        self.__push_counter__ = 0

    def get_counter(self):
        return self.__pop_counter__

    def pop(self):
        self.__stk.pop()
        self.__pop_counter__ += 1
    

stk = CountingStack()
for i in range(100):
    stk.push(i)
    stk.pop()
print(stk.get_counter())

I am honestly not sure why the script is looking for an attribute called "_CountingStack__stk" other than it being a generated attribute of the subclass as a result of inheritance.

Thanks in advance!

Arsen A
  • 23
  • 4
  • Don't use `__`-prefixed names unless you have a reason to: they are hidden from child classes *by design*. – chepner Jan 13 '21 at 21:11
  • The entire point of leading-double-underscore attributes is to get the behavior you're seeing. If you don't want that behavior, don't use leading double underscores. (In this case, you should probably be delegating to `super().pop()` instead of accessing `self.__stk` directly, though.) – user2357112 Jan 13 '21 at 21:11
  • @user2357112supportsMonica - That works, thank you! But now I'm not sure why it works. How does super().pop() automatically know to pop elements from __stk in the masterclass? What if I had two containers? Will it pop both? – Arsen A Jan 13 '21 at 21:16
  • `super().pop()` calls the superclass `pop` method (or the next `pop` method in the MRO, for multiple inheritance cases), and the superclass `pop` method uses the superclass attribute (because name mangling works lexically, based on what class a method appears in in the source code). – user2357112 Jan 13 '21 at 21:27

1 Answers1

1

Names prefixed with __ undergo name mangling precisely so that they are not visible to or shadowed by attributes in a child class. Unless you have a good reason to use them, just use a _-prefixed name instead to indicate a private attribute.

Further, do no invent your own dunder (__name__) names; they are reserved for use by the Python implementation.

Finally, CountingStack.pop has to explicitly return the value returns by self._stk.pop.

class Stack:
    def __init__(self):
        self._stk = []

    def push(self, val):
        self._stk.append(val)

    def pop(self):
        val = self._stk[-1]
        del self._stk[-1]
        return val


class CountingStack(Stack):
    def __init__(self):
        Stack.__init__(self)
        self._pop_counter = 0
        self._push_counter = 0

    def get_counter(self):
        return self._pop_counter

    def pop(self):
        self._pop_counter += 1
        return self._stk.pop()

Better yet, though, CountingStack.pop should be implemented in terms of Stack.pop, which should use list.pop instead of using del. Likewise for CountingStack.push (which I assume you want given the definition of _push_counter).

class Stack:
    def __init__(self):
        self._stk = []

    def push(self, val):
        self._stk.append(val)

    def pop(self):
        return self._stk.pop()


class CountingStack(Stack):
    def __init__(self):
        super().__init__()
        self._pop_counter = 0
        self._push_counter = 0

    def get_counter(self):
        return self._pop_counter

    def pop(self):
        self._pop_counter += 1
        return super().pop()

    def push(self, val):
        self._push_counter += 1
        super().push(val)
chepner
  • 497,756
  • 71
  • 530
  • 681
  • "Unless you have a good reason to use them, just use a `_`-prefixed name instead to indicate a private attribute." - on the other hand, `_`-prefixed names effectively end up becoming part of your interface to subclasses whether you want them to be or not. If someone tries to write a `class Sub(Super)` with an `_x` attribute when `Super` already uses that name, they'll get weird bugs until they discover the clash, with no documented hint that using that name would be problematic. – user2357112 Jan 13 '21 at 21:23
  • There's a decent argument to be made that if a class is designed to be subclassed, non-public attributes should default to `__` unless subclasses are intended to interact with them. – user2357112 Jan 13 '21 at 21:24
  • That's rarely the case, though my subsequent suggestion to use `super` means the child doesn't, in fact, need access to the attributes, and `__` names could be used again. But one rarely sees such names in practice. – chepner Jan 13 '21 at 21:25