pythonpython-3.xprivatemembername-mangling

Python name mangling allows access both ways


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?


Solution

  • 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 named Ham 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'