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

Posted

技术标签:

【中文标题】名称以两个下划线开头的实例属性被奇怪地重命名了【英文标题】:Instance attribute that has a name starting with two underscores is weirdly renamed 【发布时间】:2021-10-01 05:30:03 【问题描述】:

当我尝试使用类方法获取私有属性的值时,使用我的类的当前实现,我得到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'))

【问题讨论】:

【参考方案1】:

这段代码发生了什么?

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

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)

那么结果是这样的:

'_Catalog__product_names': 

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

发生了什么

这种行为不是错误——它实际上是 python 的一个特性,称为private name mangling。对于您在类定义中定义的所有属性,如果属性名称以两个前导下划线开头——并且 not 是否以两个尾随下划线结尾——那么该属性将像这样重命名。 Bar 类中名为 __foo 的属性将重命名为 _Bar__fooBreakfast 类中名为 __spam 的属性将重命名为 _Breakfast__spam;等等等等。

仅当您尝试从类外部访问属性时才会发生名称修改。类中的方法仍然可以使用您在 __init__ 中定义的“私有”名称访问该属性。

你为什么想要这个?

我个人从未找到此功能的用例,对此有些怀疑。它的主要用例是您希望方法或属性可以在类中私有访问,但不能通过相同的名称访问到类之外的函数或从该类继承的其他类.

这里的一些用例: What is the benefit of private name mangling?

这是一个很好的YouTube talk,其中包括一些用例 这个功能大约 34 分钟。

(注意 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'

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

collections.OrderedDict 是标准库中一个类的good example,它广泛使用名称修饰来确保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'))

另一种解决方案是使用名称 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'))

或者——这可能更好,因为从类外部使用它的重命名访问属性有点奇怪——就像这样:

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'))

【讨论】:

【参考方案2】:

双下划线的目的是避免名称与子类定义的名称冲突。这不是一种表示某些东西是“私有”的方式,因为 Python 没有阻止访问的概念。

__ 有用的情况是:

class Product:
   discount = 5
   __init__(self, name, price):
      self.name = name
      self.price = price

class Item(Product):
   def discount(self):
      self.price = self.price * 0.9

discount 具有子类方法名的类。如果使用了__discount,则该变量的名称将变为_Product__discount

如果没有子类,使用__没有意义

在您的代码中没有理由使用 ABC,并且可以以更 Python 的方式轻松编写:

class Catalog:
    def __init__(self, products=None):
        self.products = products

    def search_products(self, name):
        return [item for item in self.products if name in item]


x = Catalog()
x.products = ["x": 1, "y": 2, "x": 5]
results = x.search_products(name="x")  
# ['x': 1, 'x': 5]

【讨论】:

请务必注意,Python 的“私有”变量概念与其他语言中的私有变量概念非常不同。然而,“Private name mangling”是通常用来指代此功能的名称,实际上,文档在副标题为 Private Variables 的部分中提到了此功能。 (尽管如此,我还是赞成您的回答,因为您提出的区别很重要。)docs.python.org/3/tutorial/classes.html#private-variables

以上是关于名称以两个下划线开头的实例属性被奇怪地重命名了的主要内容,如果未能解决你的问题,请参考以下文章

python基础 类

Python 的类的下划线命名有什么不同?

5.2访问限制

property,私有变量

以一个下划线开头的PHP函数名称[重复]

使用 find 和 sed 递归地重命名文件