O que é Name Mangling e Como Ele Funciona?

Em uma lição anterior, você aprendeu sobre prefixar atributos com um único underscore e um duplo underscore. Para lembrar a diferença entre eles, um único underscore é uma convenção que significa que o atributo é destinado ao uso interno na classe e não deve ser acessado diretamente de fora da classe. Duplo underscore, por outro lado, impede que esse atributo seja acessado diretamente de fora da classe. Aqui está um exemplo que demonstra como os dois funcionam:
class Example:
    def __init__(self):
        self._internal = 'I can be accessed from outside the class, but should not'
        self.__private = 'You cannot access me directly from outside the class'

obj = Example()

print(obj._internal) # I can be accessed from outside the class, but should not
print(obj.__private)  # AttributeError: 'Example' object has no attribute '__private'
Prefixar um atributo com dois underscores aciona o processo de name mangling do Python, no qual o Python renomeia internamente o atributo adicionando um underscore e o nome da classe como prefixo, transformando __attribute em _ClassName__attribute. Para ver isso em ação, você cria uma instância da classe e usa o atributo especial __dict__ dessa instância, que é um dicionário contendo os atributos do objeto:
class Example:
    def __init__(self, internal, private):
        self._internal = internal
        self.__private = private

example1 = Example(
    'I can be accessed from outside the class, but should not',
    'I cannot be accessed directly from outside the class'
)

print(example1.__dict__)
O resultado seria:
{
  '_internal': 'I can be accessed from outside the class, but should not',
  '_Example__private': 'I cannot be accessed directly from outside the class'
}
Como você pode ver, o atributo __private é armazenado como _Example__private. Isso significa que você ainda pode acessar esse atributo fora da classe dessa maneira:
class Example:
    def __init__(self, internal, private):
        self._internal = internal
        self.__private = private

example1 = Example(
    'I can be accessed from outside the class, but should not',
    'I cannot be accessed directly from outside the class'
)
example2 = Example(
    'I should not be accessed from outside the class',
    'But I can be accessed from outside the class with name mangling'
)

print(example1._Example__private) # I cannot be accessed directly from outside the class
print(example2._Example__private) # But I can be accessed from outside the class with name mangling
Então, por que o Python faz name mangling? O principal objetivo do name mangling é evitar a sobrescrição acidental de atributos e métodos quando você usa herança. Aqui está um exemplo que deixa isso claro:
class Parent:
    def __init__(self):
        self.__data = 'Parent data'

class Child(Parent):
    def __init__(self):
        super().__init__()
        self.__data = 'Child data'

c = Child()
print(c.__dict__) # {'_Parent__data': 'Parent data', '_Child__data': 'Child data'}
Você pode ver que tanto a classe Parent quanto a Child que herda dela têm seus próprios atributos _class__data separados. Isso é possível graças ao name mangling. Caso contrário, a Child teria sobrescrito os dados da Parent por acidente. Aqui está o que teria acontecido sem permitir que o Python fizesse o name mangling, ou seja, se você não prefixar os atributos em ambas as classes com duplo underscore:
class Parent:
   def __init__(self):
       self.data = 'Parent data'

class Child(Parent):
   def __init__(self):
       super().__init__()
       self.data = 'Child data'

c = Child()
print(c.__dict__)  # {'data': 'Child data'}
Então, qual você deve usar para prefixar atributos entre underscore simples (_) e underscore duplo (__)? Depende. Se um atributo é destinado apenas para uso interno dentro da classe, use underscore simples. Mas se você estiver trabalhando com uma classe que será herdada, você deve usar um duplo underscore para que o atributo do pai não seja sobrescrito.
Este módulo não possui perguntas. Marque como concluído.