我应该在Python中使用名称修改吗?

152 投票
11 回答
65720 浏览
提问于 2025-04-17 02:30

在其他编程语言中,有一个通用的原则是尽量让所有东西都保持隐藏。如果你不确定一个变量应该是私有的还是保护的,通常选择私有的会更好。

那么在Python中也是这样吗?我是不是应该一开始就给所有东西加上两个下划线,等到需要的时候再减少到一个下划线呢?

如果大家通常只用一个下划线,我也想知道这样做的原因。

这是我在JBernardo的回答下留下的评论。它解释了我为什么会问这个问题,以及我为什么想知道Python和其他语言的不同之处:

我来自一些编程语言,它们教你要尽量让东西保持公开,只有在需要的时候才公开更多。这样做的理由是可以减少依赖关系,让代码更安全,修改起来也更简单。而Python的做法正好相反——从公开开始,然后逐渐变得隐藏,这让我觉得很奇怪。

11 个回答

17

我不认为多练习就能写出更好的代码。可见性修饰符只会让你分心,反而强迫你的接口按照你想要的方式使用。一般来说,强制可见性可以防止程序员在没有好好阅读文档的情况下搞砸事情。

一个更好的解决方案是Python所提倡的方式:你的类和变量应该有清晰的文档说明,它们的行为也要明确。源代码应该是可以获取的。这种方式写出的代码更容易扩展,也更可靠。

我在Python中的策略是这样的:

  1. 直接写代码,不要假设你的数据应该怎么保护。这意味着你要写出适合你问题的理想接口。
  2. 对于那些可能不会被外部使用的内容,使用一个前导下划线,这些内容不属于正常的“客户端代码”接口。
  3. 双下划线只用于那些在类内部纯粹是为了方便的东西,或者如果意外暴露会造成严重问题的内容。

最重要的是,所有内容的功能应该清晰明了。如果别人会使用它,就要写文档。如果你希望它在一年后仍然有用,也要写文档。

顺便提一下,在其他语言中你其实应该使用protected:你永远不知道你的类将来可能会被继承,或者会被用来做什么。最好只保护那些你确定不能或不应该被外部代码使用的变量。

36

首先 - 什么是名称改编?

名称改编是在你定义一个类时,如果使用了 __任意名称__任意名称_,也就是前面有两个(或更多)下划线,最多一个后面的下划线,就会触发名称改编。

class Demo:
    __any_name = "__any_name"
    __any_other_name_ = "__any_other_name_"

现在来看:

>>> [n for n in dir(Demo) if 'any' in n]
['_Demo__any_name', '_Demo__any_other_name_']
>>> Demo._Demo__any_name
'__any_name'
>>> Demo._Demo__any_other_name_
'__any_other_name_'

不确定时该怎么办?

名称改编的表面用途是为了防止子类使用父类中已经使用的属性。

这样做的一个潜在好处是可以避免子类在重写功能时与父类的属性发生冲突,这样父类的功能就能正常工作。不过,Python文档中的例子并不符合Liskov替换原则,我也想不出有什么例子能让我觉得这很有用。

缺点是,它增加了理解和阅读代码的难度,尤其是在调试时,你会看到源代码中的双下划线名称和调试器中的改编名称,这让人很困惑。

我个人的做法是尽量避免使用。我在一个非常大的代码库上工作,偶尔出现的这种用法总是显得格外突兀,并且似乎没有必要。

你需要了解这个概念,以便在看到时能识别出来。

PEP 8

PEP 8,也就是Python标准库的风格指南,目前的说法是(简化版):

关于使用 __名称 存在一些争议。

如果你的类是打算被子类化的,并且你有一些不希望子类使用的属性,考虑用两个前导下划线而不加后缀下划线来命名它们。

  1. 注意,改编后的名称只使用简单的类名,所以如果子类选择了相同的类名和属性名,仍然可能会发生名称冲突。

  2. 名称改编可能会让某些用法,比如调试和 __getattr__(),变得不太方便。不过,名称改编的算法有很好的文档说明,手动执行也很简单。

  3. 并不是每个人都喜欢名称改编。要努力平衡避免意外名称冲突的需要和高级用户可能的使用。

它是如何工作的?

如果在类定义中前面加两个下划线(没有结尾的双下划线),名称就会被改编,且在对象前面会加上一个下划线和类名:

>>> class Foo(object):
...     __foobar = None
...     _foobaz = None
...     __fooquux__ = None
... 
>>> [name for name in dir(Foo) if 'foo' in name]
['_Foo__foobar', '__fooquux__', '_foobaz']

注意,只有在解析类定义时,名称才会被改编:

>>> Foo.__test = None
>>> Foo.__test
>>> Foo._Foo__test
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'Foo' has no attribute '_Foo__test'

此外,刚接触Python的人有时会对无法手动访问在类定义中看到的名称感到困惑。这并不是反对使用它的强有力理由,但如果你的听众是学习者,这一点是值得考虑的。

一个下划线呢?

如果约定是只使用一个下划线,我也想知道背后的理由。

当我希望用户不要碰某个属性时,我通常只使用一个下划线,但这是因为在我的思维模型中,子类会访问到这个名称(他们总是能看到改编后的名称)。

如果我在审查使用 __ 前缀的代码,我会问他们为什么要使用名称改编,是否可以用一个下划线就能达到同样的效果,记住如果子类选择了相同的类名和属性名,仍然会发生名称冲突。

243

如果不确定,就把它设为“公开”。也就是说,不要给你的属性名字加上什么让人看不懂的东西。如果你有一个类里面有一些内部值,没必要太在意这些。与其写:

class Stack(object):

    def __init__(self):
        self.__storage = [] # Too uptight

    def push(self, value):
        self.__storage.append(value)

不如默认写成这样:

class Stack(object):

    def __init__(self):
        self.storage = [] # No mangling

    def push(self, value):
        self.storage.append(value)

这种做法肯定会引起争议。刚学Python的人可能会讨厌这样做,甚至一些老手也不太喜欢这个默认设置,但这就是默认,所以我建议你遵循这个做法,即使你觉得不太舒服。

如果你真的想向用户传达“别碰这个!”的意思,通常的做法是在变量前加一个下划线。这只是个约定,但大家都明白这个意思,处理这类东西时会更加小心:

class Stack(object):

    def __init__(self):
        self._storage = [] # This is ok, but Pythonistas use it to be relaxed about it

    def push(self, value):
        self._storage.append(value)

这样做也可以避免属性名和变量名之间的冲突:

 class Person(object):
     def __init__(self, name, age):
         self.name = name
         self._age = age if age >= 0 else 0
     
     @property
     def age(self):
         return self._age
     
     @age.setter
     def age(self, age):
         if age >= 0:
             self._age = age
         else:
             self._age  = 0

那么双下划线呢?我们使用双下划线主要是为了避免意外重载方法和与父类属性的命名冲突。如果你写的类会被多次扩展,这个做法会非常有用。

如果你想用双下划线做其他事情,可以,但这并不常见,也不推荐。

编辑:为什么会这样呢?其实,Python的常规风格并不强调把东西设为私有,反而是相反的!这背后有很多原因,大多数都是有争议的……让我们看看其中一些。

Python有属性

如今,大多数面向对象的编程语言采用的是相反的做法:不应该被使用的东西就不应该可见,因此属性应该是私有的。从理论上讲,这样可以让类更易管理,耦合度更低,因为没人会随意更改对象的值。

但事情并没有那么简单。例如,Java类有很多获取器(getters)只负责获取值,还有设置器(setters)只负责设置值。假设你需要七行代码来声明一个属性,而Python程序员会觉得这太复杂了。而且,你需要写很多代码才能获取一个公共字段,因为在实际操作中,你可以通过获取器和设置器来更改它的值。

那么,为什么要遵循这种默认私有的政策呢?不如默认把属性设为公开。当然,这在Java中是个问题,因为如果你决定给属性添加一些验证,你就得把所有的:

person.age = age;

改成,比如:

person.setAge(age);

setAge()变成:

public void setAge(int age) {
    if (age >= 0) {
        this.age = age;
    } else {
        this.age = 0;
    }
}

所以在Java(和其他语言)中,默认还是使用获取器和设置器,因为虽然写起来麻烦,但在我刚才描述的情况下可以节省很多时间。

不过,在Python中你不需要这样做,因为Python有属性。如果你有这个类:

 class Person(object):
     def __init__(self, name, age):
         self.name = name
         self.age = age

……然后你决定验证年龄,你就不需要改动代码中的person.age = age部分。只需添加一个属性(如下所示):

 class Person(object):
     def __init__(self, name, age):
         self.name = name
         self._age = age if age >= 0 else 0
     
     @property
     def age(self):
         return self._age
     
     @age.setter
     def age(self, age):
         if age >= 0:
             self._age = age
         else:
             self._age  = 0

假设你可以这样做,还能继续使用person.age = age,那你为什么还要添加私有字段和获取器、设置器呢?

(另外,看看Python不是Java关于使用获取器和设置器的危害的文章。)

反正一切都是可见的——试图隐藏反而让工作变得复杂

即使在有私有属性的语言中,你也可以通过一些反射或自省库访问它们。而且人们经常这样做,在框架中或者为了应急需求。问题是,自省库只是做了你可以用公共属性做到的事情的复杂化。

由于Python是一种非常动态的语言,把这种负担加到你的类上是得不偿失的。

问题不在于看不见——而在于被要求看见

对于Python程序员来说,封装并不是不能看到类的内部,而是可以选择不去看。如果你能使用一个组件而不关心它的内部细节,那么这个组件就是封装好的(在Python程序员看来)。

现在,如果你写了一个类,你可以在不考虑实现细节的情况下使用它,如果你出于某种原因想要查看类的内部,那也没问题。关键是:你的API应该设计得很好,其他的都是细节。

Guido说过

这并不争议:他确实说过。(找找“open kimono。”)

这就是文化

是的,确实有一些原因,但没有关键的理由。这主要是Python编程文化的一部分。坦白说,情况也可以反过来——但事实并非如此。此外,你也可以反过来问:为什么有些语言默认使用私有属性?原因和Python的做法一样:因为这是这些语言的文化,每种选择都有其优缺点。

既然已经有了这种文化,遵循它是明智的选择。否则,当你在Stack Overflow上提问时,Python程序员会让你把代码中的__去掉,这可就麻烦了 :)

撰写回答