Python 对象属性 - 访问方法论

23 投票
7 回答
44649 浏览
提问于 2025-04-11 09:30

假设我有一个类,里面有一些属性。用什么方式访问这些属性比较好呢?是像 obj.attr 这样直接访问,还是应该写一些获取方法?

编辑:能不能详细讲讲用一个下划线或两个下划线来命名属性的最佳实践?我发现大多数模块里都是用一个下划线。


如果这个问题已经有人问过(我感觉应该有人问过,不过搜索没有找到相关内容),请告诉我一下,我会把这个问题关闭。

7 个回答

8

编辑:你能详细讲讲用一个或两个下划线开头命名属性的最佳实践吗?我在大多数模块中看到都是用一个下划线。

一个下划线在Python中并没有特别的意思,它只是个最佳实践,意思是“嘿,你可能不想随便访问这个,除非你知道自己在干什么”。而两个下划线则会让Python在内部对名字进行处理,这样这个属性只能在定义它的类里面访问。

如果属性的名字前后都有两个下划线,那就表示这是一个特殊的功能,比如__add__,它是在使用+号运算符时被调用的。

想了解更多,可以查看PEP 8,特别是“命名约定”这一部分。

58

关于单下划线和双下划线:它们都表示“私有”的意思。也就是说,大家会知道这个属性(无论是方法、普通数据属性还是其他东西)并不是对象的公共接口的一部分。直接去碰它可能会引发麻烦。

此外,双下划线的属性(而不是单下划线的属性)会被名称重整,这样从子类或其他地方意外访问它的可能性就小了很多。你仍然可以访问它们,但没有那么简单。例如:

>>> class ClassA:
...     def __init__(self):
...         self._single = "Single"
...         self.__double = "Double"
...     def getSingle(self):
...         return self._single
...     def getDouble(self):
...         return self.__double
... 
>>> class ClassB(ClassA):
...     def getSingle_B(self):
...         return self._single
...     def getDouble_B(self):
...         return self.__double
... 
>>> a = ClassA()
>>> b = ClassB()

你现在可以轻松访问 a._singleb._single,并获取由 ClassA 创建的 _single 属性:

>>> a._single, b._single
('Single', 'Single')
>>> a.getSingle(), b.getSingle(), b.getSingle_B()
('Single', 'Single', 'Single')

但是直接尝试访问 ab 实例的 __double 属性是行不通的:

>>> a.__double
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: ClassA instance has no attribute '__double'
>>> b.__double
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: ClassB instance has no attribute '__double'

虽然在 ClassA 中定义的方法可以直接访问它(当在任一实例上调用时):

>>> a.getDouble(), b.getDouble()
('Double', 'Double')

但是在 ClassB 中定义的方法则不能:

>>> b.getDouble_B()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in getDouble_B
AttributeError: ClassB instance has no attribute '_ClassB__double'

在这个错误中,你可以得到一些提示。__double 属性的名称在类内部访问时,会被重整为包含访问它的类的名称。当 ClassA 尝试访问 self.__double 时,它实际上在编译时变成了访问 self._ClassA__doubleClassB 也是如此。(如果 ClassB 中的方法要给 __double 赋值,虽然没有包含在代码中,但它不会触碰到 ClassA__double,而是会创建一个新的属性。)这个属性没有其他保护,所以如果你知道正确的名称,仍然可以直接访问它:

>>> a._ClassA__double, b._ClassA__double
('Double', 'Double')

那么这有什么问题呢?

问题在于,当你想要继承并改变任何处理这个属性的代码时,就会遇到麻烦。你要么必须重新实现所有直接接触这个双下划线属性的代码,要么就得猜测类名并手动重整名称。当这个双下划线属性实际上是一个方法时,问题会更严重:重写这个方法或在子类中调用这个方法意味着必须手动进行名称重整,或者重新实现所有调用这个方法的代码,以避免使用双下划线名称。更不用说用 getattr() 动态访问属性时,你也得手动重整。

另一方面,因为这个属性只是被简单地重写,所以它提供的“保护”非常有限。任何代码仍然可以通过手动重整来访问这个属性,虽然这样会让他们的代码依赖于你的类名,而你如果想重构代码或重命名类(同时保持用户可见的名称,这是 Python 中常见的做法),就可能无意中破坏他们的代码。他们还可以通过将他们的类命名为和你的一样来“欺骗”Python,让 Python 为他们进行名称重整:注意,重整后的属性名称中没有包含模块名。最后,双下划线属性在所有属性列表和所有不注意跳过以单下划线开头的属性的反射形式中仍然是可见的。

所以,如果你使用双下划线名称,尽量少用,因为它们可能会带来很多不便,并且永远不要把它们用于方法或任何子类可能想要重新实现、重写或直接访问的东西。要明白,双下划线的名称重整并没有提供真正的保护。最终,使用单下划线能给你带来同样的效果,并且让你减少(潜在的、未来的)麻烦。使用单下划线。

23

大家普遍接受的做法就是使用简单的属性,像这样:

>>> class MyClass:
...     myAttribute = 0
... 
>>> c = MyClass()
>>> c.myAttribute 
0
>>> c.myAttribute = 1
>>> c.myAttribute
1

如果你发现自己需要写获取器和设置器(getters 和 setters),那么你可以查找一下“python 类属性”。另外,Ryan Tomayko 关于获取器/设置器的文章是个不错的起点(虽然有点长)

撰写回答