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.