名称以两个下划线开头的实例属性被奇怪地重命名

Sha*_*ker 0 python private double-underscore name-mangling

根据我的类的当前实现,当我尝试使用类方法获取私有属性的值时,我将其None作为输出。关于我哪里出错的任何想法?

代码

from abc import ABC, abstractmethod

class Search(ABC):
    @abstractmethod
    def search_products_by_name(self, name):
        print('found', name)


class Catalog(Search):
    def __init__(self):
        self.__product_names = {}
    
    def search_products_by_name(self, name):
        super().search_products_by_name(name)
        return self.__product_names.get(name)


x = Catalog()
x.__product_names = {'x': 1, 'y':2}
print(x.search_products_by_name('x'))
Run Code Online (Sandbox Code Playgroud)

Ale*_*ood 7

这段代码发生了什么?

上面的代码看起来不错,但有些行为可能看起来不寻常。如果我们在交互式控制台中输入:

c = Catalog()
# vars() returns the instance dict of an object,
# showing us the value of all its attributes at this point in time.
vars(c)
Run Code Online (Sandbox Code Playgroud)

那么结果是这样的:

{'_Catalog__product_names': {}}
Run Code Online (Sandbox Code Playgroud)

这很奇怪!在我们的类定义中,我们没有给任何属性命名_Catalog__product_names。我们将一个属性命名为__product_names,但该属性似乎已重命名。

这是怎么回事

这种行为不是错误——它实际上是 python 的一个特性,称为private name mangling。对于您在一个类定义定义,如果属性名称以两个前导下划线开头的所有属性-也确实不能有两个尾随下划线结束-那么该属性将被重新命名这个样子。__foo在类中命名的属性Bar将被重命名_Bar__foo__spam在类中命名的属性Breakfast将被重命名_Breakfast__spam;等等等等。

名称修改仅在您尝试从类外部访问属性时发生。类中的方法仍然可以使用您在__init__.

为什么你会想要这个?

我个人从未发现过此功能的用例,并且对此有些怀疑。它的主要用例适用于您希望方法或属性可以在类内私有访问的情况,但不能通过同名访问类外的函数或从此类继承的其他类

(注意 YouTube 演讲是 2013 年的,演讲中的例子是用 python 2 编写的,所以例子中的一些语法与现代 python 有点不同——print仍然是语句而不是函数等。)

下面是使用类继承时私有名称修改如何工作的说明:

>>> class Foo:
...   def __init__(self):
...     self.__private_attribute = 'No one shall ever know'
...   def baz_foo(self):
...     print(self.__private_attribute)
...     
>>> class Bar(Foo):
...   def baz_bar(self):
...     print(self.__private_attribute)
...     
>>> 
>>> b = Bar()
>>> b.baz_foo()
No one shall ever know
>>> 
>>> b.baz_bar()
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "<string>", line 3, in baz_bar
AttributeError: 'Bar' object has no attribute '_Bar__private_attribute'
>>>
>>> vars(b)
{'_Foo__private_attribute': 'No one shall ever know'}
>>>
>>> b._Foo__private_attribute
'No one shall ever know'
Run Code Online (Sandbox Code Playgroud)

基类Foo中定义的方法能够使用在 中定义的私有名称访问私有属性FooBar然而,子类中定义的方法只能通过使用其损坏的名称来访问私有属性;其他任何事情都会导致异常。

collections.OrderedDict是标准库中类的一个很好的例子,它广泛使用名称修改来确保 的子类OrderedDict不会意外覆盖某些OrderedDict对工作方式很重要的方法OrderedDict

我该如何解决?

这里显而易见的解决方案是重命名您的属性,使其只有一个前导下划线,就像这样。这仍然向外部用户发出一个明确的信号,即这是一个私有属性,不应由类外的函数或类直接修改,但不会导致任何奇怪的名称修改行为:

from abc import ABC, abstractmethod

class Search(ABC):
    @abstractmethod
    def search_products_by_name(self, name):
        print('found', name)


class Catalog(Search):
    def __init__(self):
        self._product_names = {}
    
    def search_products_by_name(self, name):
        super().search_products_by_name(name)
        return self._product_names.get(name)


x = Catalog()
x._product_names = {'x': 1, 'y':2}
print(x.search_products_by_name('x'))
Run Code Online (Sandbox Code Playgroud)

另一种解决方案是使用名称 mangling 滚动,如下所示:

from abc import ABC, abstractmethod

class Search(ABC):
    @abstractmethod
    def search_products_by_name(self, name):
        print('found', name)


class Catalog(Search):
    def __init__(self):
        self.__product_names = {}
    
    def search_products_by_name(self, name):
        super().search_products_by_name(name)
        return self.__product_names.get(name)


x = Catalog()
# we have to use the mangled name when accessing it from outside the class
x._Catalog__product_names = {'x': 1, 'y':2}
print(x.search_products_by_name('x'))
Run Code Online (Sandbox Code Playgroud)

或者——这可能会更好,因为从类外部使用其损坏的名称访问属性有点奇怪——像这样:

from abc import ABC, abstractmethod

class Search(ABC):
    @abstractmethod
    def search_products_by_name(self, name):
        print('found', name)


class Catalog(Search):
    def __init__(self):
        self.__product_names = {}
    
    def search_products_by_name(self, name):
        super().search_products_by_name(name)
        return self.__product_names.get(name)

    def set_product_names(self, product_names):
        # we can still use the private name from within the class
        self.__product_names = product_names


x = Catalog()
x.set_product_names({'x': 1, 'y':2})
print(x.search_products_by_name('x'))
Run Code Online (Sandbox Code Playgroud)