声明从__init__调用的函数中的变量是否仍然使用密钥共享字典?

Nat*_*aul 7 python dictionary python-3.x python-internals

__init__为了清晰和组织原因,我试图总是在里面声明类属性.最近,我了解到严格遵循这种做法还有额外的非美学好处,这要归功于为Python 3.3添加了PEP 412.具体来说,如果在中定义了所有属性__init__,则对象可以通过共享其键和散列来减少空间.

我的问题是,当在被调用的函数中声明属性时,对象密钥共享是否会发生__init__

这是一个例子:

class Dog:
    def __init__(self):
        self.height = 5
        self.weight = 25

class Cat:
    def __init__(self):
        self.set_shape()

    def set_shape(self):
        self.height = 2
        self.weight = 10
Run Code Online (Sandbox Code Playgroud)

在这种情况下,所有实例都Dog将共享密钥heightweight.实例Cat也分享密钥heightweight(彼此之间,Dog当然不是s).

顺便说一句,你会如何测试?

请注意,Brandon Rhodes在他的词典甚至Mightier谈话中说这是关于密钥共享的:

如果添加的单个键不在原型键集中,则会松开密钥共享

Jim*_*ard 6

当在被调用的函数中声明属性时,会发生对象密钥共享__init__吗?

是的,无论您在何处设置属性,都要求在初始化后都具有相同的密钥集,实例字典使用共享密钥字典实现.提出的两种情况都减少了内存占用.

您可以通过使用sys.getsizeof获取实例字典的大小进行测试,然后将其与从中创建的类似字典进行比较.dict.__sizeof__基于此的实现区别于返回不同的大小:

# on 64bit version of Python 3.6.1
print(sys.getsizeof(vars(c)))
112
print(getsizeof(dict(vars(c))))
240
Run Code Online (Sandbox Code Playgroud)

所以,要找出答案,你需要做的就是比较这些.

至于你的编辑:

"如果添加的单个键不在原型键组中,则会松开键共享"

正确,这是我(目前)发现的破坏共享密钥用法的两件事之一:

  1. 在实例dict中使用非字符串键.这只能以愚蠢的方式进行.(你可以用它做vars(inst).update)
  2. 同一类的两个实例的字典内容有所不同,这可以通过更改实例字典来完成.(添加到其中的单个键不在原型键集中)

    我不确定在添加单个密钥时是否会发生这种情况,这是一个可能会发生变化的实现细节.(附录:见Martijn的评论)

有关此问题的相关讨论,请参阅我在此处所做的问答:为什么Python 3中的__dict__实例如此之小?

这两件事都会导致CPython使用"普通"字典.当然,这是一个不应该依赖的实现细节.您可能会或可能不会在Python的其他实现和/或CPython的未来版本中找到它.

  • @NathanielSaul 如果你愿意的话。要点是,只要它们具有字符串键并且两个实例的字典在内容上没有太大偏差,从哪里添加它们就没有什么区别,您将获得一个共享键字典。 (2认同)

Mar*_*ers 6

我想你指的是PEP的以下段落(在Split-Table词典部分):

调整拆分字典的大小时,它将转换为组合表.如果由于存储实例属性而调整大小,并且只有一个类的实例,那么将立即重新分割该字典.由于大多数OO代码将在__init__方法中设置属性,因此将在创建第二个实例之前设置所有属性,并且不再需要调整大小,因为所有其他实例字典将具有正确的大小.

因此,在创建第二个实例之前,无论添加什么内容,字典键都将保持共享.这样做__init__是实现这一目标的最合理方法.

但这并不意味着属性设置为不共享稍后的时间; 它们仍然可以在实例之间共享 ; 只要你不引起任何词典的组合.因此,在创建第二个实例后,只有在发生以下任何情况时才会停止共享密钥:

  • 新属性会导致字典调整大小
  • 新属性不是字符串属性(字典是针对常见的all-keys-are-strings情况高度优化的).
  • 属性以不同的顺序插入; 例如a.foo = None先设置,然后第二个实例先b设置b.bar = None,这里b有一个不兼容的插入顺序,因为共享字典有foo第一个.
  • 属性已删除.即使对于一个实例,也会导致共享.如果您关心共享词典,请不要删除属性.

因此,当您有两个实例(以及两个共享密钥的字典)时,只要您不触发任何上述情况,密钥就不会被重新拆分,您的实例将继续共享密钥.

这也意味着,委托设置属性称为辅助方法, __init__不会影响上述情况下,这些属性仍然会创建第二个实例之前设置.毕竟__init__在第二种方法返回之前还无法返回.

换句话说,您不必过于担心设置属性的位置.在__init__方法中设置它们可以让您更轻松地避免组合场景,但是在创建第二个实例之前设置的任何属性都保证是共享键的一部分.

至于如何测试这个:用sys.getsizeof()函数查看内存大小; 如果在更大的对象中创建映射的副本,则共享__dict____dict__表:

import sys

def shared(instance):
    return sys.getsizeof(vars(instance)) < sys.getsizeof(dict(vars(instance)))
Run Code Online (Sandbox Code Playgroud)

快速演示:

>>> class Foo:
...     pass
...
>>> a, b = Foo(), Foo()  # two instances
>>> shared(a), shared(b)  # they both share the keys
(True, True)
>>> a.bar = 'baz'  # adding a single key
>>> shared(a), shared(b)  # no change, the keys are still shared!
(True, True)
>>> a.spam, a.ham, a.monty, a.eric = (
...     'eggs', 'eggs and spam', 'python',
...     'idle')  # more keys still
>>> shared(a), shared(b)  # no change, the keys are still shared!
(True, True)
>>> a.holy, a.bunny, a.life = (
...     'grail', 'of caerbannog',
...     'of brian')  # more keys, resize time
>>> shared(a), shared(b)  # oops, we killed it
(False, False)
Run Code Online (Sandbox Code Playgroud)

仅当达到阈值时(对于具有8个备用插槽的空字典,在添加第6个键时才会进行调整大小),字典是否松散了共享属性.

字典在大约2/3满时调整大小,调整大小通常会使表大小加倍.因此,下一个调整大小将在添加第11个键时发生,然后在22,然后是43,等等.因此对于大型实例字典,您有更多的喘息空间.

  • @SB:3.10 和 [特别是 3.11](https://github.com/faster-cpython/ideas/issues/72) 发生了很多变化,并且 `shared()` 函数不再是共享的准确检测器字典。我正在寻找其他方法。例如,我在 3.11 中注意到的一件事是,“Foo”实例“__dict__”对象是空的,但比普通的“{}”或“dict(vars(instance))”对象*大*。这几乎肯定是添加的新位向量,以允许按任意顺序设置属性。 (2认同)
  • @SB:对。“sys.getsizeof()”方法从来不可靠。我已将其替换为“ctypes”,以便于访问 C 实现细节。新版本的 is_shared() 现在可以正确报告在 3.11 及之前的 Python 版本之间共享的字典(此时我不会对未来的 3.12 兼容性做出任何声明)。 (2认同)