So I have come across a very interesting behavior of python name mangling. Consider the following code
class C:
def __init__(self):
self.__c = 1
@staticmethod
def change(instance):
print(dir(instance))
print(instance.__c)
print(instance._C__c)
Here I create a private field __c and expect to have direct access to it from within class and access via _C__c from outside of the class. So, if we pass an instance of C to C.change either 2nd or 3rd print should fail.
Lets check:
>>> c = C()
>>> dir(c)
['_C__c', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'change']
>>> C.change(c)
['_C__c', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'change']
1
1
First, for debug we print all available members of c with dir(c)
. Then we call C.change passing it the variable c
.
Hmm, that is unexpected, no errors.
So, first print in change
shows us all the available entries of the instance
object. Here we see that field __c
is available as _C__c
. That seems ok, since we access not through self, but through another variable.
Having such output from 'dir' I expect print(instance.__c)
to fail with AttributeError.
However, unexpectedly, it works just fine!
This really confuses me, since I do not understand, why is __c
accessible and if it is so by design, then why is it not listed in dir
output?
Whenever you write __c
inside a class, it will be textually replaced by _<classname>__c
. It's not dynamically performed, it's done at the parsing stage. Hence, the interpreter won't ever see __c
, only _<classname>__c
. That's why only _C__c
appears in dir(instance)
.
Quoting the docs:
[...] Private names are transformed to a longer form before code is generated for them. The transformation inserts the class name, with leading underscores removed and a single underscore inserted, in front of the name. For example, the identifier
__spam
occurring in a class namedHam
will be transformed to_Ham__spam
. This transformation is independent of the syntactical context in which the identifier is used. [...]
For that reason, it only applies to dotted attribute access (x.y
), not to dynamic access via (get|set)attr
:
>>> class Foo:
... def __init__(self):
... setattr(self, '__x', 'test')
...
>>> Foo().__x
'test'