【问题标题】:Proper way to return private object in Python在 Python 中返回私有对象的正确方法
【发布时间】:2017-07-06 23:58:11
【问题描述】:

我是 Python 新手(目前为止很喜欢它),但在 C++ 和 C# 等 OO 语言方面拥有多年经验,并认为自己是一名强大的 OO 设计师。

我的理解是 Python 并不严格强制执行私有对象属性,但按照惯例,人们期望如果您使用下划线命名属性,他们将知道不要在类外访问它。好吧,够公平的。

我的问题:如果一个对象包含“私有”对象并且我将它返回给调用者,我是否应该制作一个副本以便他们不会搞砸?还是 Python 会自动复制?

# My Channel class has a dictionary of capabilities  
class Channel(object):
    def __init__(self):
        self._capabilities = dict()

如果我执行以下操作,调用者是否可以通过弄乱返回的字典来弄乱我的功能?

@property
def capabilities(self):
    return self._capabilities

或者我应该这样做并返回一份副本以保护自己?

@property
def capabilities(self):
    # I'm assuming that this creates a new copy of the dictionary
    return dict(self._capabilities)

我猜 Python 返回一个引用,以便调用者确实可以弄乱我的私人字典(或列表,或其他),所以我最好先复制一份。

【问题讨论】:

  • 你可以使用@capabilities.getter来指定一个getter函数。您可以做的是在该方法中引发异常,因此人们知道最好不要尝试阅读它。
  • 这完全取决于。这是语义。如果您需要他们能够修改对象,则返回一个副本。 Python 对所有可变对象使用引用,因此它将返回与 C++ 中的 shared_ptr 有效相似的内容(即使在 None 上也会调用 Py_IncrRef)。通常,_method 表示类型是“受保护的”,它被认为是内部的和实现相关的,但没有严格执行。 __method 表示直接类之外的任何调用者都有名称修改,因此它最类似于“私有”。这些是指导方针。
  • "或者 Python 会自动复制吗?" - 与 C++ 不同,Python 几乎从不进行隐式复制。
  • 如果内部状态取决于该对象的完整性,但它应该是“只读的”,那么您可能需要一个不带 setter 的 getter 来返回副本。如果您担心实现细节(例如,设置一个值会更新依赖于实现的数据库记录),您可能希望返回没有任何副本的现有对象,但仍然建议其他人不要使用 _method 触及实现.
  • Python 通过引用传递所有内容(不是真的,而是使用 C# 术语...),但有些结构是可变的,有些则不是。您无需为非可变结构制作副本,因为外部没有任何东西可以改变您的内部属性指向的状态。但是,对于可变结构,如果您希望您的类的用户会修改它们,您将需要创建副本(甚至是深层副本)。

标签: python class properties


【解决方案1】:

如果您使用的是 Python 3.3 及更高版本,标准库中有一个类 types.MappingProxyType。它的构造函数接受一个字典并返回一个只读视图。如果您返回此类对象,而不是字典的副本,则返回的 MappingProxyType 会在客户端代码尝试更改它时引发异常。

您还可以通过从collections.abc.Mapping 继承并实现三个特殊方法:__getitem__, __iter__, and __len__,使您的类模拟不可变映射。然后客户端代码可以访问_capabilities 中的任何项目,但不能修改它。客户端甚至可以遍历整个集合。

但是 Python 哲学(“我们都是成年人”)说,也许最好还是返回字典并相信用户的代码不会弄乱它。试图让 Python 模拟 C++ 不一定是最好的方法。正如您所指出的,Python 实际上并没有阻止客户端使用以下划线开头的变量。

【讨论】:

  • “相信用户的代码不会弄乱它”——我认为这更像是对用户的一种体贴姿态,需要一些正式的过程来改变全局状态。我觉得它有点像 django 对 settings.py 文件的处理——你可以整天阅读设置,但它不是用来写的,因为哇你会弄得一团糟。
  • 感谢您的回复。我想我最喜欢 Paul 的 MappintProxyType 方法。我的大部分问题都是关于 Python 哲学的。我见过许多开发人员切换到一种新语言,并尝试像使用另一种语言那样做事。这很丑陋。我试图不成为那种人,并进入 Python 的心态。
【解决方案2】:

我睡在上面,意识到我可以写一个测试并找出我自己的答案。当我返回一个对象(我的测试中的字典)时,我得到一个对实际私有对象的引用。如果我在返回的内容中添加一个条目,那么它会在原始对象的字典中添加一个条目。

因此,如果我想防止这种情况发生,那么我需要创建一个副本并返回。

    @property
    def capabilities(self):
        # I'm assuming that this creates a new copy of the dictionary
        return dict(self._capabilities)

我认为我最初的部分问题是这种方法是否是 Python 的常见模式。它在 C# 中,我打算将其作为一般做法。

【讨论】:

  • 请记住,您可能需要深拷贝(而不是浅拷贝)才能得到完全保护。
  • 这不是 Python 的常见模式,因为通常我们希望我们代码的用户是成年人,而不是在他们不应该进行更改的地方进行更改。大多数情况下,只需在文档中注明就足够了,即如果您需要修改此结构,则将其视为只读。 Python 虽然很慢,但我们不想浪费周期试图让用户远离伤害自己。毕竟,你可以让他们更难,但如果他们真的想弄乱你的代码的内部结构,没有什么能阻止他们,那么为什么首先要麻烦呢?
【解决方案3】:

是的,以下划线开头的属性被视为私有属性。您可以访问或修改它们,但您不应该这样做。

但是,当您创建公共属性时,您实际上授予用户可以修改其返回的内容的权限。 Python 总是返回引用,问题只是引用是可变的还是不可变的,字典和列表是可变的,因此它们可以更改内容,而其他类型(如数字和字符串)是不可变的,因此它们“可以安全返回”。

与其考虑如何返回副本,不如考虑_capabilities 的哪些“属性”和“方法”是用户感兴趣的。例如,如果您只想要“has_capability”和“value_of_capability”,您可以简单地为此创建函数:

class Channel(object):
    def __init__(self):
        self._capabilities = dict()

    def has_capability(self, capability):
        return capability in self._capabilities

    def value_of_capability(self, capability):
        return self._capabilities[capability]

对于其他应该支持的操作也是如此。隐藏一个属性然后“公开”它是没有意义的(无论是作为参考还是作为副本)。副本的问题在于它很慢并且可能会导致意外,因为您可以修改它,但更改不会传播回来。这不是很直观。

【讨论】:

    猜你喜欢
    • 2020-01-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-20
    相关资源
    最近更新 更多