Python类中的属性

2023-08-29 08:27:54 浏览数 (1)

公共与私有

通常,在编程中,当某物是公共的时,你可以访问它并使用它;当它是私有的时,你不能。这就像思考某事与说出某事:当你思考某事时,它是你自己的;但是,无论你大声说出什么,它都不再只属于你,而变得公开。

在Python中情况有所不同。你可能听说过在Python中没有真正的私有。这是什么意思?Python有私有属性和方法吗?

我们在Python类的方法和属性的上下文中使用这两个术语,公共和私有。

当属性是私有的时,你不应该使用它;当方法是私有的时,你不应该调用它。你可能已经注意到我用了“应该”这个词。这是因为,正如我已经提到的,Python中的情况有所不同:当某事是公共的时,你可以访问和使用它;当它是私有的时,你不应该这样做——但这并不意味着你不能。所以,当你在Python中思考某事时,它应该保持属于你自己——但是任何人都可以用相当简单的方法听到它。

正如你所见,Python在隐私方面并不严格。它建议你遵循一些规则,而不是强制你遵循它们。它建议类的用户不要访问私有方法和属性——但用户可以随意这样做,而且更重要的是,他们不需要付出太多的努力。

在本文中,我将用简单的话和简单的例子来解释这些事情。

当你思考某事时,它仍然是你自己的;但是,无论你大声说出什么,它都不再只属于你,而变得公开。

在Python中,当你思考某事时,它应该保持属于你自己——但任何人都可以用相当简单的方法听到它。

“私有”方法和属性

在Python中不存在真正的隐私。Python提供的是伪隐私或准隐私。它有两个级别,我称之为指示隐私和捉迷藏隐私。

指示隐私

你可以指示一个特定的属性是私有的。要做到这一点,只需在属性名前面加上一个下划线。这样做,你表明,建议或推荐该方法/属性应该被视为私有的,意味着它不应该在类的外部使用。

因此,instance.do_it() 是一个常规(公共)方法,而 instance._do_it() 是一个标识为私有的方法。因此,作为类的用户,你被要求不要使用它。它的存在是因为它有一些实现目的,而你与之无关。这不是一个秘密。你可以查看它,没有人会对你隐藏任何东西。但这不是为你准备的。接受你所提供的内容,不要触碰你不能使用的内容。

让我们考虑一个简单的例子:

代码语言:javascript复制
# class_me.py
class Me:
    def __init__(self, name, smile=":-D"):
        self.name = name
        self.smile = smile
        self._thoughts = []
    
    def say(self, what):
        return str(what)
    
    def smile_to(self, whom):
        return f"{self.smile} → {whom}"
    
    def _think(self, what):
        self._thoughts  = [what]

    def _smile_to_myself(self):
        return f"{self.smile} → {self.name}"

(如果你不知道我为什么写 self._thoughts = [what] 而不是 self._thoughts = what,请参阅附录1。)

好的,我们有一个叫做 Me 的类,它代表了你——至少在你创建它时如此。它具有以下属性:

  • .name,一个公共属性 → 你的名字肯定是公开的。
  • .smile,一个公共属性 → 你的微笑可以被外界看到,所以它肯定是公开的。
  • ._thoughts,一个私有属性 → 你的想法肯定是私有的,对吗?

正如你所见,这两个公共属性的名称没有前导下划线,而唯一的私有属性的名称有。

现在让我们来看看可用的方法:

  • .say(),一个公共方法 → 当你说些什么时,人们可以听到你的话,所以你的话是公开的。
  • .smile_to(),一个公共方法 → 当你对某人微笑时,这个人和周围的人都可以看到你在微笑。
  • ._smile_to_myself(),一个私有方法 → 这是一种不同类型的微笑;它是为类的作者(在我们的例子中是你)保留的,在没有人看到的时候才会这样做,所以它是一个私有方法。
  • ._think(),一个私有方法 → 当你想些什么时,这是你的私人想法;如果你想大声说出来,你应该使用公共的 .say() 方法。

让我们来使用这个类。我将为自己创建一个类的实例,所以我将称它为 marcin。你可以为自己创建一个实例。

代码语言:javascript复制
>>> from class_me import Me
>>> marcin = Me(name="Marcin")
>>> marcin # doctest:  ELLIPSIS
<__main__.Me object at 0x...>
>>> marcin.say("What a beautiful day!")
'What a beautiful day!'
>>> marcin.smile_to("Justyna")
':-D → Justyna'

我在上面的代码块中使用了 doctest 来格式化代码。它帮助我确保代码是正确的。你可以从以下文章中了解更多关于这个文档测试框架的信息:

https://towardsdatascience.com/python-documentation-testing-with-doctest-the-easy-way-c024556313ca

如果你想将代码作为doctest复制粘贴并自行运行,可以访问文章末尾的附录2,其中包含按此方式格式化的剩余代码(例如Me类的代码)。

好的,一切看起来都很好。到目前为止,我们甚至没有看私有方法和属性;我们只使用了公共方法。现在是时候了:

代码语言:javascript复制
>>> dir(marcin)  #doctest:  NORMALIZE_WHITESPACE
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', 
'__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', 
'__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', 
'__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', 
'__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 
'__weakref__', '_smile_to_myself', '_think', '_thoughts', 'name', 
'say', 'smile', 'smile_to']

我们看到了什么?实际上,我们看到了一切。我们当然可以看到公共属性.name和.smile,以及公共方法.say()和.smile_to()。但是我们也看到了私有属性._thoughts和私有方法._think()._smile_to_myself()。此外,我们还看到了许多我们没有创建的方法和属性。

请记住,以.name()命名的方法是dunder方法,而不是私有方法。我们以后再讨论这个。

既然我们能够看到私有属性,很可能我们也能够使用它们:

代码语言:javascript复制
>>> marcin._think("My wife is so beautiful!")
>>> marcin._think("But let this be my secret!")

什么都没有发生?那也许没问题?也许我们可以使用私有方法,但无论它们在做什么都对我们隐藏起来?

当然不是。只是._think()方法没有返回任何内容(或者返回None),但它将思想保存到._thoughts属性中,这也是私有的。让我们检查一下你是否能看到我的私人思想:

代码语言:javascript复制
>>> marcin._smile_to_myself()
':-D → Marcin'

是的,你可以。最后一个测试:让我们看看你是否可以看到我自己在笑:

你也可以看到。所以,显然你可以看到私有属性,并且可以使用私有方法,尽管我明确指示了这些属性和方法的名称前加下划线表示它们是私有的,因此我不希望你使用它们。使用私有方法或属性有点像在淋浴时监视我,你可以看到我想隐藏的东西。

然而,有时候出于这个原因或其他原因,你可能想要修改现有类;这可能意味着覆盖私有属性或方法。而Python在这方面的处理方式就非常出色。从理论上讲,这些属性是私有的,所以你不应该使用它们;有时候,使用它们甚至可能破坏一个类。这也是一种保护措施;你知道这些属性是私有的,所以最好不要碰它们。

但是当你知道自己在做什么,当你的目的要求你使用私有属性时,Python可以实现这一点。这为Python开发者提供了许多额外的机会。

使用私有方法或属性有点像在淋浴时监视我,你可以看到我想隐藏的东西。

这为Python开发者提供了许多额外的机会。

有点夸张,用Python你可以做任何你想做的事情。你可以重写内置函数、异常等等。(如果你想了解更多关于重写异常的内容,请阅读这篇更好编程的文章。)你还可以使用私有属性。这是没问题的,假设——就像任何代码的情况一样——你不想对用户的计算机造成任何伤害。

我相信你会同意,这种类型的隐私是脆弱的,因为用户可以像使用公共属性和类一样使用这些私有属性和类。然而,Python提供了一种更严格的隐私方式,我称之为捉迷藏隐私。

捉迷藏隐私

虽然隐私级别只包括指示属性是私有还是公共,但捉迷藏级别更进一步。正如你马上将看到的,它在某种程度上帮助你保护私有属性。

这是否意味着这一次,私有属性和方法将真正隐藏起来,用户将无法使用它们?并非完全如此。正如我所写的,捉迷藏隐私提供了一定程度的保护,但并非完全保护。Python通过一种称为名称修饰的方法来实现这一点。

当你想要使用名称修饰,即捉迷藏隐私时,你需要在私有属性的名称前添加不只一个下划线,而是两个下划线。在我们的Me类中,例如,这将是.__thoughts.__think()。通过名称修饰,私有属性或方法以特定的方式修改,以便更难从类外部访问它们。

让我们看看它是如何工作的。首先,我们将首先修改我们的Me类的名称,将其改为PrivateMe(请参阅附录2中的格式化为doctest的代码):

代码语言:javascript复制
# class_me.py
class PrivateMe:
    def __init__(self, name, smile=":-D"):
        self.name = name
        self.smile = smile
        self.__thoughts = []
    
    def say(self, what):
        return str(what)
    
    def smile_to(self, whom):
        return f"{self.smile} → {whom}"
    
    def __think(self, what):
        self.__thoughts  = [what]

    def __smile_to_myself(self):
        return f"{self.smile} → {self.name}"

首先,让我们创建一个实例——同样,这将是me的一个实例——并使用公共方法:

代码语言:javascript复制
>>> marcin = PrivateMe(name="Marcin")
>>> marcin.say("What a beautiful day!")
'What a beautiful day!'
>>> marcin.smile_to("Justyna")
':-D → Justyna'

目前为止还不错,但这并不奇怪——毕竟,我们使用了公共方法。之前,我们成功地使用了私有方法,比如._smile_to_myself()。这次,我们也尝试一下是否能成功。为了检查这一点,我将尝试使用.__smile_to_myself()方法对自己微笑:

代码语言:javascript复制
>>> marcin.__smile_to_myself()
Traceback (most recent call last):
    ...
AttributeError: 'PrivateMe' object has no attribute '__smile_to_myself'

哈!我们知道PrivateMe类有__smile_to_myself()方法,但我们无法使用它。显然,它是受保护的,就像任何私有方法应该是的。

然而...看起来方法是完全受保护的,尽管不久前我声称在Python中,私有属性并不是完全受保护的。那么,到底发生了什么呢?

我们刚刚经历了名称修饰的工作原理。它隐藏了私有属性,或者更确切地说,隐藏了私有属性的名称。换句话说,它以一种特定的方式改变它们的名称;新名称将遵循以下的_ClassName__attribute 的命名规则:

通过这种方式,你无法使用原始名称访问属性,但可以使用名称修饰后的名称来访问它们。在我们的PrivateMe类中,这将像这样工作:

代码语言:javascript复制
class MyClass:
    __privacy = None     # this becomes ._MyClass__privacy    
    def __hide_me(self): # this becomes ._MyClass__hide_me()
        pass

你可以亲自看到该属性确实存在,只是被重命名了。我们肯定会在dir()函数的输出中看到这一点:

代码语言:javascript复制
>>> marcin._PrivateMe__smile_to_myself()
':-D → Marcin'

我们的私有方法和属性可以使用新的名称访问:

代码语言:javascript复制
>>> dir(marcin) # doctest:  NORMALIZE_WHITESPACE
['_PrivateMe__smile_to_myself', '_PrivateMe__think',
 '_PrivateMe__thoughts', '__class__', '__delattr__',
 '__dict__', '__dir__', '__doc__', '__eq__', '__format__',
 '__ge__', '__getattribute__', '__gt__', '__hash__',
 '__init__', '__init_subclass__', '__le__', '__lt__',
 '__module__', '__ne__', '__new__', '__reduce__',
 '__reduce_ex__', '__repr__', '__setattr__',
 '__sizeof__', '__str__', '__subclasshook__',
 '__weakref__', 'name', 'say', 'smile', 'smile_to']
  • .__smile_to_myself()._PrivateMe__smile_to_myself()
  • .__think()._PrivateMe__think()
  • .__thoughts ._PrivateMe__thoughts

名称修饰²使我们能够实现隐藏和寻找的隐私级别。

还有一件事要记住。当你想通过添加两个前导下划线使属性变为私有时,请不要在名称的末尾添加两个额外的下划线。以这种方式命名的方法称为dunder(双下划线)方法——它们绝对不是私有的;实际上,它们与私有相反。我们将在其他时间讨论它们。要使用名称修饰,你只需要记住这个命名规则:不要使用.name()的约定来命名私有方法,因为这样不起作用。

结论

我们讨论了在Python中面向对象编程的上下文中的隐私概念。在编写类时,有时你可能希望隐藏一些实现细节,你可以通过将类的某些属性和方法设为私有来实现这一点。但它们永远不是真正的私有。

这种方法对我来说听起来并不自然。当我想到私有属性时,我将其想象为一个在类外部看不到和使用的属性。同样,它是一个可以被看到和使用的公共属性。

如果你的想象力以类似的方式工作,你需要戴上改变世界的眼镜,这样你就可以在Python世界中随意移动而不会不时地摔倒。每次使用Python时,你都必须戴上这副眼镜。迟早,它们会帮助你适应Python不同的世界,其中隐私的概念运作方式如此不同。

总结一下,Python无法完全保护类的属性。然而,它提供了两个级别的保护,我称之为指示和捉迷藏隐私。

指示隐私。你可以将属性标记为私有,并相信没有人会在类外部使用该属性。指示方法基于信任:我们相信类的用户不会使用其私有属性。该方法除此之外没有其他保护措施。

指示方法基于信任:我们相信类的用户不会使用其私有属性。该方法除此之外没有其他保护措施。

捉迷藏隐私。这是更高级别的隐私保护,也是Python在类属性隐私方面提供的最多的。在指示隐私的情况下,你可以像使用公共属性一样使用被标记为私有的属性,但在这里不能。你可以获得对私有属性的一定程度的保护。这仍然不是完全保护;私有属性由于更改了名称而被隐藏。你仍然可以找到、访问和使用它们,但至少它们在某种程度上受到了保护。它们并不真正隐藏,因为dir()函数会显示类的所有属性,包括公共和私有属性,但后者的名称已经改变。


感谢阅读本文。我希望在Python类的上下文中,隐私不再成为你的问题。虽然乍一看这个主题可能似乎困难,或者至少很奇怪,但你很快就会习惯Python隐私的奇特世界。请放心,许多Python开发者欣赏Python中这些机制的工作方式。如果你不欣赏,很可能你迟早会加入他们。

至于我个人而言,我不仅不反对Python对待隐私的方式,我甚至很赞赏。我已经多次使用过这种方法,知道它的存在很好,以防万一,等待着我来窥视类的属性和方法。

脚注

¹ 请记住,在Python中,方法是类的属性。因此,每当我提到属性的隐私性时,我指的是包括方法在内的属性的隐私性。

² 名称改编有两个目的:

  • 它提高了类的私有属性和方法的保护级别。
  • 它确保继承自父类的私有属性不会被继承它的类覆盖。因此,当你使用两个前导下划线时,你不必担心该属性在类中被继承类覆盖。

本文讨论的是第一点。第二点超出了本文的范围,我们将在其他时间讨论它。

附录1

这个附录解释了为什么在编写Me类时,我写成了

代码语言:javascript复制
self._thoughts  = [what]

而不是

代码语言:javascript复制
self._thoughts  = what

就地连接运算符 =的工作方式如下所示:

代码语言:javascript复制
>>> x = [1, 2, 3]
>>> y = [4, 5, 6]
>>> x  = y
>>> y
[4, 5, 6]
>>> x
[1, 2, 3, 4, 5, 6]

正如你所见,这个操作将两个列表相加;作为就地操作,它会影响第一个列表,并使第二个列表保持不变。然而,这对于非可迭代对象(如数字,这里是整数)不起作用:

代码语言:javascript复制
>>> x  = 5
Traceback (most recent call last):
    ...
TypeError: 'int' object is not iterable

因此,你可以使用就地连接运算符将另一个可迭代对象(如列表、元组、范围对象和生成器)添加到列表中:

代码语言:javascript复制
>>> x  = (10, 20)
>>> x
[1, 2, 3, 4, 5, 6, 10, 20]
>>> x  = range(3)
>>> x
[1, 2, 3, 4, 5, 6, 10, 20, 0, 1, 2]
>>> x  = (i**2 for i in range(3))
>>> x
[1, 2, 3, 4, 5, 6, 10, 20, 0, 1, 2, 0, 1, 4]

字符串也是可迭代的,所以你也可以将它们添加到列表中:

代码语言:javascript复制
>>> x  = "Cuma"
>>> x
[1, 2, 3, 4, 5, 6, 10, 20, 0, 1, 2, 0, 1, 4, 'C', 'u', 'm', 'a']

正如你所见,"Cuma"字符串被视为其各个字符的可迭代对象,并且是这些字符被添加到x中,而不是整个单词本身。

这就是为什么self._thoughts = what不起作用的原因。如果我们使用它,将会产生以下不希望的效果:

代码语言:javascript复制
>>> marcin._think("I am tired.")
>>> marcin._thoughts
['I', ' ', 'a', 'm', ' ', 't', 'i', 'r', 'e', 'd', '.']
附录2

格式化为doctest的Me类:

代码语言:javascript复制
>>> class Me:
...     def __init__(self, name, smile=":-D"):
...         self.name = name
...         self.smile = smile
...         self._thoughts = []
...     def say(self, what):
...         return str(what)
...     def smile_to(self, whom):
...         return f"{self.smile} → {whom}"
...     def _think(self, what):
...         self._thoughts  = [what]
...     def _smile_to_myself(self):
...         return f"{self.smile} → {self.name}"

用于doctest的PrivateMe类的格式化版本:

代码语言:javascript复制
>>> class PrivateMe:
...     def __init__(self, name, smile=":-D"):
...         self.name = name
...         self.smile = smile
...         self.__thoughts = []
...     def say(self, what):
...         return str(what)
...     def smile_to(self, whom):
...         return f"{self.smile} → {whom}"
...     def __think(self, what):
...         self.__thoughts  = [what]
...     def __smile_to_myself(self):
...         return f"{self.smile} → {self.name}"

0 人点赞