Python 伪不可变对象字段
Python pseudo-immutable object field
我目前需要部分创建一个 Python 对象并能够更新它一段时间。虽然,一旦我将对象用作字典键,我一定无法更新它。
当然还有将字段标记为私有的解决方案,这主要是对程序员的警告,我实际上会采用该解决方案。
但我偶然发现了另一种解决方案,我想知道这是否是个好主意,或者它是否会完全出错。这是:
class Foo():
def __init__(self, bar):
self._bar = bar
self._has_been_hashed = False
def __hash__(self):
self._has_been_hashed = True
return self._bar.__hash__()
def __eq__(self, other):
return self._bar == other._bar
def __copy__(self):
return Foo(self._bar)
def set_bar(self, bar):
if self.has_been_hashed:
raise FooIsNowImmutable
else:
self._bar = bar
一些测试证明它可以按预期工作,我不能再使用 set_bar 一旦我将我的对象用作字典键。
你怎么看?这是个好主意吗?它会反对我吗?有没有更简单的方法?这在某种程度上是一种不好的做法吗?
这样做有点脆弱,因为您永远不知道什么时候可以将某些东西用作字典键,或者什么时候它的 hash
可能会因为其他原因被调用。对象不应该 "know" 是否被用作字典键。仅仅因为其他地方的一些其他代码将对象放入字典中,而拥有可能引发异常的代码将会令人困惑。
遵循 "explicit is better than implicit" 的 Python 哲学,给你的对象一个名为 .finalize()
或 .lock()
或其他东西的方法会更安全,这将设置一个指示对象不可变的标志。您还可以反转引发异常的逻辑,以便 __hash__
在对象尚未锁定时引发异常(而不是在对象已被散列时引发异常)。
当您准备好使对象不可变时,您可以调用 .lock()
。当你完成了你需要做的任何改变时,明确地将它设置为不可变更有意义,而不是隐含地假设一旦你在字典中使用它,你就完成了改变它。
你可以这样做,但我不确定我是否会推荐它。为什么字典里需要它?
它需要更多地了解对象的状态...想想文件对象。你会把一个放在字典里吗?很多功能都得打开才能使用,一旦关闭就不能用了。用户必须在周围的代码中了解对象所处的状态。
对于文件,这是有道理的——毕竟,您通常不会在程序的大部分中保持文件打开,或者如果您这样做,它们有非常明确的初始化和关闭代码;类似的东西必须对你的对象有意义。特别是如果您有一些采用该对象的 API,但期望一个不可变的版本,而其他一些采用相同的对象,但期望对其进行更改...
我以前用过lock方法,对于复杂的、只读的对象,你想初始化一次,然后确保没有人乱动,它很有效。例如。你从磁盘加载一份(比如,英语)字典的副本......当你填充它时它必须是可变的,但你不希望任何人不小心修改它,所以锁定它是一个好主意。不过,我只会在它是一次性锁的情况下使用它——你正在锁定和解锁的东西似乎是灾难的根源。
恕我直言,如果您只想创建一个可以在可散列位置使用的版本,则有两种解决方案。首先是在将其放入字典时显式创建一个不可变副本 - tuple
和 frozenset
是此类行为的示例......如果你想将 list
放入dict
,你不能,但你可以先从它创建一个 tuple
,然后可以对其进行哈希处理。创建一个对象的 frozen
版本,然后通过查看对象类型就可以很清楚地知道它是可变的还是不可变的,因此很容易看到它被错误使用的情况。
其次,如果您确实希望它是可散列的,但需要它是可变的...这实际上是合法的,但实现方式略有不同。它可以追溯到散列的想法......散列既用于优化查找,也用于平等。
首先是确保您可以取回对象...您将某些内容放入字典中,然后将其散列为值 4 - 放入插槽 4。然后对其进行修改。然后你再去查找它,现在它散列为 9 - 插槽 9 中没有任何东西,或者更糟的是,一个不同的对象,你被打破了。
其次是相等 - 对于集合之类的东西,我需要知道我的对象是否已经在那里。我可以散列,但是如果您对散列有所了解,您仍然需要检查相等性以检查散列冲突。
这并不排除支持 __hash__
和 是可变的,但这是不寻常的。您需要为您的项目决定是什么使它相同,即使它是可变的。然后你需要做的是给每个对象一个唯一的 id。从技术上讲,您可以使用 id(self)
,但像 uuid
模块这样的东西可能是更好的选择。 UUID4(或者从技术上讲,UUID4 的哈希值)决定了哈希值和相等性;包含相同 UUID4 的两个对象应该是完全相同的对象;具有完全相同数据但不同 UUID4 的两个对象将是不同的对象。
我目前需要部分创建一个 Python 对象并能够更新它一段时间。虽然,一旦我将对象用作字典键,我一定无法更新它。
当然还有将字段标记为私有的解决方案,这主要是对程序员的警告,我实际上会采用该解决方案。
但我偶然发现了另一种解决方案,我想知道这是否是个好主意,或者它是否会完全出错。这是:
class Foo():
def __init__(self, bar):
self._bar = bar
self._has_been_hashed = False
def __hash__(self):
self._has_been_hashed = True
return self._bar.__hash__()
def __eq__(self, other):
return self._bar == other._bar
def __copy__(self):
return Foo(self._bar)
def set_bar(self, bar):
if self.has_been_hashed:
raise FooIsNowImmutable
else:
self._bar = bar
一些测试证明它可以按预期工作,我不能再使用 set_bar 一旦我将我的对象用作字典键。
你怎么看?这是个好主意吗?它会反对我吗?有没有更简单的方法?这在某种程度上是一种不好的做法吗?
这样做有点脆弱,因为您永远不知道什么时候可以将某些东西用作字典键,或者什么时候它的 hash
可能会因为其他原因被调用。对象不应该 "know" 是否被用作字典键。仅仅因为其他地方的一些其他代码将对象放入字典中,而拥有可能引发异常的代码将会令人困惑。
遵循 "explicit is better than implicit" 的 Python 哲学,给你的对象一个名为 .finalize()
或 .lock()
或其他东西的方法会更安全,这将设置一个指示对象不可变的标志。您还可以反转引发异常的逻辑,以便 __hash__
在对象尚未锁定时引发异常(而不是在对象已被散列时引发异常)。
当您准备好使对象不可变时,您可以调用 .lock()
。当你完成了你需要做的任何改变时,明确地将它设置为不可变更有意义,而不是隐含地假设一旦你在字典中使用它,你就完成了改变它。
你可以这样做,但我不确定我是否会推荐它。为什么字典里需要它?
它需要更多地了解对象的状态...想想文件对象。你会把一个放在字典里吗?很多功能都得打开才能使用,一旦关闭就不能用了。用户必须在周围的代码中了解对象所处的状态。
对于文件,这是有道理的——毕竟,您通常不会在程序的大部分中保持文件打开,或者如果您这样做,它们有非常明确的初始化和关闭代码;类似的东西必须对你的对象有意义。特别是如果您有一些采用该对象的 API,但期望一个不可变的版本,而其他一些采用相同的对象,但期望对其进行更改...
我以前用过lock方法,对于复杂的、只读的对象,你想初始化一次,然后确保没有人乱动,它很有效。例如。你从磁盘加载一份(比如,英语)字典的副本......当你填充它时它必须是可变的,但你不希望任何人不小心修改它,所以锁定它是一个好主意。不过,我只会在它是一次性锁的情况下使用它——你正在锁定和解锁的东西似乎是灾难的根源。
恕我直言,如果您只想创建一个可以在可散列位置使用的版本,则有两种解决方案。首先是在将其放入字典时显式创建一个不可变副本 - tuple
和 frozenset
是此类行为的示例......如果你想将 list
放入dict
,你不能,但你可以先从它创建一个 tuple
,然后可以对其进行哈希处理。创建一个对象的 frozen
版本,然后通过查看对象类型就可以很清楚地知道它是可变的还是不可变的,因此很容易看到它被错误使用的情况。
其次,如果您确实希望它是可散列的,但需要它是可变的...这实际上是合法的,但实现方式略有不同。它可以追溯到散列的想法......散列既用于优化查找,也用于平等。
首先是确保您可以取回对象...您将某些内容放入字典中,然后将其散列为值 4 - 放入插槽 4。然后对其进行修改。然后你再去查找它,现在它散列为 9 - 插槽 9 中没有任何东西,或者更糟的是,一个不同的对象,你被打破了。
其次是相等 - 对于集合之类的东西,我需要知道我的对象是否已经在那里。我可以散列,但是如果您对散列有所了解,您仍然需要检查相等性以检查散列冲突。
这并不排除支持 __hash__
和 是可变的,但这是不寻常的。您需要为您的项目决定是什么使它相同,即使它是可变的。然后你需要做的是给每个对象一个唯一的 id。从技术上讲,您可以使用 id(self)
,但像 uuid
模块这样的东西可能是更好的选择。 UUID4(或者从技术上讲,UUID4 的哈希值)决定了哈希值和相等性;包含相同 UUID4 的两个对象应该是完全相同的对象;具有完全相同数据但不同 UUID4 的两个对象将是不同的对象。