我应该在 Python 中使用名称修改吗?
- 2024-12-05 08:38:00
- admin 原创
- 141
问题描述:
在其他语言中,有助于编写更好代码的一般准则是始终尽可能隐藏所有内容。如果不确定变量应该是私有的还是受保护的,最好选择私有的。
对于 Python 来说也是如此吗?我是否应该首先在所有内容上使用两个前导下划线,然后根据需要使它们不那么隐蔽(只有一个下划线)?
如果约定只使用一个下划线,我也想知道其理由。
这是我在JBernardo 的回答下留下的评论。它解释了我为什么问这个问题,也解释了我为什么想知道 Python 与其他语言的不同之处:
我所学的语言会教你认为所有东西都应该只在需要时公开,不能再公开了。这样做的理由是,这样可以减少依赖性,使代码更容易修改。Python 的做法是反过来的——从公开开始,然后隐藏——这对我来说很奇怪。
解决方案 1:
如果有疑问,请将其保留为“public” - 我的意思是,不要添加任何内容来掩盖属性的名称。如果您有一个具有某些内部值的类,请不要为此烦恼。不要写:
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 新手讨厌它,甚至一些 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 类有许多只获取值的 getter和只设置值的 setter。比如说,你需要七行代码来声明一个属性 - Python 程序员会说这太复杂了。此外,你需要编写大量代码来获取一个公共字段,因为在实践中你可以使用 getter 和 setter 来更改其值。
那么为什么要遵循这种默认私有的策略呢?只需默认将您的属性公开即可。当然,这在 Java 中是有问题的,因为如果您决定向属性添加一些验证,则需要您更改所有内容:
person.age = age;
在你的代码中,比如说,
person.setAge(age);
setAge()
存在:
public void setAge(int age) {
if (age >= 0) {
this.age = age;
} else {
this.age = 0;
}
}
因此在 Java(和其他语言)中,默认还是使用 getter 和 setter,因为编写它们可能很烦人,但如果您遇到我所描述的情况,它们可以为您节省大量时间。
但是,你不需要在 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
,为什么要添加私有字段和 getter 和 setter 呢?
(另请参阅Python 不是 Java和这篇关于使用 getter 和 setter 的危害的文章。)
无论如何,一切都是可见的——试图隐藏会使你的工作复杂化
即使在具有私有属性的语言中,您也可以通过一些反射/自省库来访问它们。人们经常这样做,在框架中或为了解决紧急需求。问题是自省库只是一种复杂的方式,可以完成您可以用公共属性做的事情。
由于 Python 是一种非常动态的语言,因此给你的课程添加这个负担是适得其反的。
问题不在于无法看到 - 而在于需要看到
对于 Pythonista 来说,封装并不是无法看到类的内部结构,而是可以避免查看它。封装是组件的属性,用户可以使用它而不必关心内部细节。如果你可以使用组件而不必担心它的实现,那么它就是封装的(以 Python 程序员的观点)。
现在,如果你写了一个类,你可以使用它而不必考虑实现细节,如果你出于某种原因想要查看类内部,那也没有问题。关键是:你的 API 应该很好,其余的都是细节。
吉多这么说
嗯,这并不有争议:事实上,他是这么说的。(寻找“敞开的和服”。)
这就是文化
是的,确实有一些原因,但不是关键原因。这主要是 Python 编程的文化方面。坦率地说,也可能是另一种情况 - 但事实并非如此。此外,您也可以反过来问:为什么某些语言默认使用私有属性?主要原因与 Python 实践相同:因为这是这些语言的文化,每种选择都有优点和缺点。
既然已经存在这种文化,你最好遵循它。否则,__
当你在 Stack Overflow 上提问时,Python 程序员会告诉你从代码中删除 :)
解决方案 2:
首先——什么是名称混淆?
当您在类定义中使用__any_name
或 时__any_name_
,会调用名称修改,即两个(或更多)前导下划线和最多一个尾随下划线。
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 标准库样式指南,目前内容(节选):
关于的使用存在一些争议
__names
。如果您的类旨在被子类化,并且您具有不想让子类使用的属性,请考虑使用双前导下划线命名它们,并且不使用尾随下划线。
请注意,在混乱的名称中只使用简单的类名,因此如果子类选择相同的类名和属性名,仍然会发生名称冲突。
名称修改可能会使某些用途(例如调试和
__getattr__()
)变得不那么方便。但是,名称修改算法有据可查,并且易于手动执行。并非所有人都喜欢名称混淆。尝试在避免意外名称冲突的需求与高级呼叫者的潜在使用之间取得平衡。
它是如何工作的?
如果在类定义中添加两个下划线(但不以双下划线结尾),则名称将被破坏,并且在对象上会添加一个下划线后跟类名:
>>> 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 新手有时很难理解无法手动访问类定义中定义的名称。这不是反对使用 Python 的充分理由,但如果您面对的是学习型受众,则需要考虑这一点。
一个下划线?
如果约定只使用一个下划线,我也想知道其理由。
当我的意图是让用户不要触碰某个属性时,我倾向于只使用一个下划线,但这是因为在我的心理模型中,子类化者可以访问该名称(他们总是可以访问,因为他们可以很容易地发现混乱的名称)。
如果我正在审查使用前缀的代码__
,我会问他们为什么要调用名称修改,以及他们是否不能用单个下划线做得一样好,记住如果子类为类和类属性选择相同的名称,尽管如此仍会发生名称冲突。
解决方案 3:
我不会说练习能写出更好的代码。可见性修饰符只会分散你对手头任务的注意力,并且会迫使你的界面按照你的意图使用。一般来说,如果程序员没有正确阅读文档,强制可见性可以防止他们把事情搞砸。
一个更好的解决方案是 Python 鼓励的路线:您的类和变量应该有很好的文档记录,并且它们的行为清晰。源代码应该可用。这是编写代码的更具扩展性和可靠性的方法。
我在 Python 中的策略是这样的:
只管写代码,不要假设应该如何保护你的数据。这假设你写代码是为了为你的问题创建理想的接口。
对于可能不会在外部使用且不属于正常“客户端代码”界面的内容,请使用前导下划线。
仅在类内部纯粹为了方便时才使用双下划线,否则如果意外暴露将造成相当大的损害。
最重要的是,每件事都应该清楚。如果别人会用到它,就记录下来。如果你希望它在一年后仍然有用,就记录下来。
附注:在其他语言中,您实际上应该使用protected:您永远不知道您的类以后可能会被继承,也不知道它会用于什么用途。最好只保护那些您确定不能或不应该被外部代码使用的变量。
解决方案 4:
您不应该从私有数据开始,然后在必要时将其公开。相反,您应该从确定对象的接口开始。也就是说,您应该从确定世界可以看到什么(公开的内容)开始,然后确定实现这一点所需的私有内容。
其他语言很难将曾经是公共的变量变为私有的。也就是说,如果我将变量变为私有或受保护的,我会破坏很多代码。但 Python 中的属性并非如此。相反,即使重新排列内部数据,我也可以保持相同的界面。
和 _ 之间的区别在于,python 实际上试图强制执行后者。当然,它并没有真正努力,但确实使它变得困难。使用 _ 只是告诉其他程序员意图是什么,他们可以随意忽略它,后果自负。但忽略这条规则有时是有帮助的。示例包括调试、临时黑客攻击以及使用不打算按照您使用的方式使用的第三方代码。
解决方案 5:
这个问题已经有很多很好的答案了,但我还是想再提一个。这也是对那些一直说双下划线不是私密的(它确实是)的人的部分回应。
如果你看一下 Java/C#,它们都有 private/protected/public。所有这些都是编译时构造。它们仅在编译时强制执行。如果你在 Java/C# 中使用反射,你可以轻松访问私有方法。
现在,每次在 Python 中调用函数时,本质上都是在使用反射。这些代码片段在 Python 中是相同的。
lst = []
lst.append(1)
getattr(lst, 'append')(1)
“点”语法只是后一段代码的语法糖。主要是因为使用 getattr 只有一个函数调用就已经很丑陋了。情况只会变得更糟。
因此,不可能有 Java/C# 版本的私有函数,因为 Python 不会编译代码。Java 和 C# 无法在运行时检查函数是私有函数还是公共函数,因为这些信息已经丢失(并且它不知道该函数是从哪里调用的)。
现在有了这些信息,双下划线的名称修改对于实现“私有性”最有意义。现在,当从“self”实例调用一个函数并注意到它以“__”开头时,它就会立即执行名称修改。这只是更多的语法糖。这种语法糖允许在仅使用反射进行数据成员访问的语言中实现“私有”的等价物。
免责声明:我从未听过 Python 开发中的任何人说过这样的话。缺乏“私有”的真正原因是文化原因,但您还会注意到,大多数脚本/解释语言都没有私有。严格执行的私有性在编译时之外的任何情况下都不实用。
解决方案 6:
首先:为什么要隐藏数据?为什么这如此重要?
很多时候你并不是真的想做某件事,但你还是会去做,因为其他人在做。
如果你真的真的不想让人们使用某些东西,可以在它前面加一个下划线。就是这样... Pythonistas 知道,带有一个下划线的东西并不能保证每次都能正常工作,并且可能会在你不知情的情况下发生变化。
这就是我们的生活方式,我们对此很满意。
使用两个下划线将使你的类很难被子类化,甚至你也不想那样工作。
解决方案 7:
所选的答案很好地解释了属性如何消除对私有属性的需要,但我还要补充一点,模块级别的函数消除了对私有方法的需要。
如果在模块级别将方法转换为函数,则子类将无法重写该方法。将某些功能移至模块级别比尝试使用名称修改来隐藏方法更符合 Python 风格。
解决方案 8:
下面的代码片段将解释所有不同的情况:
两个前导下划线 (__a)
单前导下划线 (_a)
没有下划线 (a)
class Test:
def __init__(self):
self.__a = 'test1'
self._a = 'test2'
self.a = 'test3'
def change_value(self,value):
self.__a = value
return self.__a
打印测试对象的所有有效属性
testObj1 = Test()
valid_attributes = dir(testObj1)
print valid_attributes
['_Test__a', '__doc__', '__init__', '__module__', '_a', 'a',
'change_value']
在这里,您可以看到 __a 的名称已更改为 _Test__a,以防止任何子类覆盖此变量。这个概念在 Python 中称为“名称改编”。您可以像这样访问它:
testObj2 = Test()
print testObj2._Test__a
test1
类似地,对于 _a 来说,该变量只是为了通知开发人员它应该用作该类的内部变量,即使你访问它,python 解释器也不会做任何事情,但这不是一个好的做法。
testObj3 = Test()
print testObj3._a
test2
变量可以从任何地方访问,就像公共类变量一样。
testObj4 = Test()
print testObj4.a
test3
希望答案对你有帮助:)
解决方案 9:
乍一看它应该与其他语言相同(“其他”指的是 Java 或 C++),但事实并非如此。
在 Java 中,你把所有不应该被外部访问的变量都设为私有。同时在 Python 中你无法实现这一点,因为没有“隐私”(正如 Python 原则之一所说 - “我们都是成年人”)。所以双下划线只意味着“伙计们,不要直接使用这个字段”。单下划线也有同样的含义,同时当你必须从考虑的类继承时,这不会造成任何麻烦(这只是双下划线可能造成的问题的一个例子)。
因此,我建议您默认对“私人”成员使用单下划线。
解决方案 10:
“如果不确定一个变量应该是私有的还是受保护的,最好选择私有的。”——是的,在 Python 中也是如此。
这里的一些答案提到了“约定”,但没有提供这些约定的链接。Python 的权威指南PEP 8明确指出:
如果有疑问,请选择非公开;以后将其公开比将公共属性设为非公开更容易。
其他答案已经考虑了 Python 中public 和 private 之间的区别以及名称修改。来自同一链接,
我们在这里不使用术语“私有”,因为在 Python 中没有属性是真正私有的(通常不需要不必要的工作)。
解决方案 11:
Python 名称修改示例程序
class Demo:
__any_name = "__any_name"
__any_other_name_ = "__any_other_name_"
[n for n in dir(Demo) if 'any' in n] # GIVES OUTPUT AS ['_Demo__any_name',
# '_Demo__any_other_name_']