Require for feedback: Explicit private attribute (2nd)

I spend about one year to make private_attribute implemented in c++. The new project is private_attribute_cpp.

According to last discussion and actual experience, I changed some rule for private_attribute:

  • Subclases cannot visit parent classes’ private attributes.
  • If one subclass declared one private name that on parent class had declared, for instance, visiting this name attribute from parent or child classes would be independent (you might get different values with same attribute name from different classes).

Example

(Below is one of my colleague’s experience. I just show the smallest reproduction)

One day he found the module named module, which had declared that there was a class named Class . He found that it can solve his work, but he needed more. So he writed this code to implement some extra functions:

# script.py

# many other imports
from module import Class

# many other codes


class MyClass(Class):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._value = 0

    def public_method(self, *args, **kwargs):
        super().public_method(*args, **kwargs)
        self._value += 1

    @property
    def value(self) -> int:
        return self._value


# many other codes

def main():
    # many other codes
    x = MyClass()
    x.public_method()
    print(x.value)  # 1
    x.public_method()
    print(x.value)  # 2
    # many other codes


if __name__ == "__main__":
    main()

The IDE read the pyi file:

# module.pyi

# some imports

# many other functions and classes

class Class:
    def __init__(self, *args, **kwargs) -> None: ...

    def public_method(self, *args, **kwargs) -> None: ...

# many other functions and classes

Finally the IDE didn’t find any problem in this code. Both mypy and pyright didn’t too. So he ran this script…

Traceback (most recent call last):
  File "C:\Users\hh180\OneDrive\Desktop\fake_projects\fake_project\script.py", line 34, in <module>
    main()
  File "C:\Users\hh180\OneDrive\Desktop\fake_projects\fake_project\script.py", line 26, in main
    x.public_method()
  File "C:\Users\hh180\OneDrive\Desktop\fake_projects\fake_project\script.py", line 14, in public_method
    self._value += 1
TypeError: can only concatenate str (not "int") to str

“WTF! Why those tools didn’t find this problem and where is the problem?” He fell into confusion. He spent many hours checking the code he had written but found nothing too. The IDE hinted that self._value is just int but some unknown factors turned it into str.

Finally he spent serval time reading the source code of module.py and found the reason:

# module.py

# many other imports

# many other codes

class Class:
    def __init__(self, *args, **kwargs):
        # many other init code
        self._value = ""

    def public_method(self, *args, **kwargs):
        # many other code
        self._value = "some_string"
        # many other code

# many other codes

The reason for this mistake was that he had named this private attribute name same with the parent class private attribute name.

However, no module author will declare which private names he used in documents, so this problem is difficult to prevent. What’s worse is that the author may change these names in next update without any notifications.

Now I will show how private_attribute_cpp solves this problems.

Assume that now I doesn’t know this question, I also use the _value as the name of the private attribute’s name. But I use this module:

# script_use_private_attribute.py

# many other imports
from module import Class
from private_attribute import PrivateAttrType

# many other codes


class MyClass(Class, metaclass=PrivateAttrType):
    __private_attrs__ = ("_value",)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._value = 0

    def public_method(self, *args, **kwargs):
        super().public_method(*args, **kwargs)
        self._value += 1

    @property
    def value(self) -> int:
        return self._value


# many other codes

def main():
    # many other codes
    x = MyClass()
    x.public_method()
    print(x.value)  # 1
    x.public_method()
    print(x.value)  # 2
    # many other codes


if __name__ == "__main__":
    main()

When I run this script:

Traceback (most recent call last):
  File "C:\Users\hh180\OneDrive\Desktop\fake_projects\fake_project\script_use_private_attribute.py", line 37, in <module>
    main()
  File "C:\Users\hh180\OneDrive\Desktop\fake_projects\fake_project\script_use_private_attribute.py", line 28, in main
    x = MyClass()
  File "C:\Users\hh180\OneDrive\Desktop\fake_projects\fake_project\script_use_private_attribute.py", line 12, in __init__
    super().__init__(*args, **kwargs)
  File "C:\Users\hh180\OneDrive\Desktop\fake_projects\fake_project\module.py", line 8, in __init__
    self._value = ""
AttributeError: private attribute

Now I can quickly know that there is a name comflict and I need to change the name without checking any other codes.

More features

Assume that this module author noticed this module and updated:

# many other imports
from private_attribute import PrivateAttrBase
# many other codes

class Class(PrivateAttrBase):
    __private_attrs__ = ("_value",)
    def __init__(self, *args, **kwargs):
        # many other init code
        self._value = ""

    def public_method(self, *args, **kwargs):
        # many other code
        self._value = "some_string"
        # many other code

# many other codes

The first original script.py will report the question:

Traceback (most recent call last):
  File "C:\Users\hh180\OneDrive\Desktop\fake_projects\fake_project_2\script.py", line 34, in <module>
    main()
  File "C:\Users\hh180\OneDrive\Desktop\fake_projects\fake_project_2\script.py", line 25, in main
    x = MyClass()
  File "C:\Users\hh180\OneDrive\Desktop\fake_projects\fake_project_2\script.py", line 10, in __init__
    self._value = 0
AttributeError: private attribute

This traceback can also point the name comflict problem straightly.

However, the script_use_private_attribute.py will run successfully. This is magically. Now we add the way to debug what the parent class will visit:

# many other imports
from private_attribute import PrivateAttrBase
# many other codes

class Class(PrivateAttrBase):
    __private_attrs__ = ("_value",)
    def __init__(self, *args, **kwargs):
        # many other init code
        self._value = ""

    def public_method(self, *args, **kwargs):
        # many other code
        self._value = "some_string"
        # many other code

    def debug_value(self):
        print("In Class, the value is:", self._value)

# many other codes

In main we add this method:

def main():
    # many other codes
    x = MyClass()
    x.public_method()
    print(x.value)  # 1
    x.debug_value()
    x.public_method()
    print(x.value)  # 2
    x.debug_value()
    # many other codes

The output is:

1
In Class, the value is: some_string
2
In Class, the value is: some_string

As you see, for private attributes, the parent class and child class have different namespaces, which can solve this problem.

Why double-underline cannot solve this problem thoroughly

Python has this feature to solve this problem. However, if two classes or more in one interitance chain have same name, it will still due to name comflict. In fact, the author may not document some inheritance relationships because he think that this is not important for users, which cause the problem more difficult to be covered.

Why double-underline cannot solve this problem thoroughly

Do you mean private name mangling? 6. Expressions — Python 3.11.15 documentation

They should solve your use-case, yes, can you explain why they do not work for you with an example?

EDIT: ah, two classes with the same name in the inheritance chain, right.

You can use it to find this problem:

# module1.py

class _Helper:
    def __init__(self):
        self.__value = 1
    def get_something(self):
        self.__value += 1

class Class1(_Helper):
    ...
# module2.py
class _Helper:
    def __init__(self):
        self.__value = "1"
    def get_somethingelse(self):
        self.__value += "1"

class Class2(_Helper):
    ...
from module1 import Class1
from module2 import Class2

class MyClass(Class1, Class2):
    ...

a = MyClass()
a.get_something()
a.get_somethingelse()

Result:

Traceback (most recent call last):
  File "C:\Users\hh180\OneDrive\Desktop\fake_projects\interfance\script.py", line 9, in <module>
    a.get_somethingelse()
  File "C:\Users\hh180\OneDrive\Desktop\fake_projects\interfance\module2.py", line 5, in get_somethingelse
    self.__value += "1"
TypeError: unsupported operand type(s) for +=: 'int' and 'str'

Two authors may not declare that there is a class named _Helper in their modules.

What happens if you run something like:

inst_1 = MyClass()
inst_2 = MyClass()

print(dir(inst_1))
print(dir(inst_2))

You will get:

['_Helper__value', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__firstlineno__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__static_attributes__', '__str__', '__subclasshook__', '__weakref__', 'get_something', 'get_somethingelse']
['_Helper__value', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__firstlineno__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__static_attributes__', '__str__', '__subclasshook__', '__weakref__', 'get_something', 'get_somethingelse']

As you see, they are same, and the attribute to __value only appear once in both list

Sorry, I meant something like:

class MyClass(PrivateAttrBase):
    __private_attrs__ = ("_value")
...

inst_1 = MyClass()
inst_2 = MyClass()

print(dir(inst_1))
print(dir(inst_2))

The _value won’t be in dir because it doesn’t in object’s __dict__.