在从 __init__ 调用的函数中声明变量是否仍然使用密钥共享字典?
Does declaring variables in a function called from __init__ still use a key-sharing dictionary?
出于清楚和组织的原因,我一直试图在 __init__
中声明 class 属性。最近,由于为 Python 3.3 添加了 PEP 412,我了解到严格遵循这种做法也有额外的非美学好处。具体来说,如果所有属性都在 __init__
中定义,那么对象可以通过共享它们的键和哈希来减少 space。
我的问题是,在 __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
在这种情况下,Dog
的所有实例将共享密钥 height
和 weight
。 Cat
的实例是否也共享密钥 height
和 weight
(彼此之间,当然不是 Dog
s)。
顺便说一句,你会如何测试这个?
请注意,Brandon Rhodes 在他的 Dictionary Even Mightier talk:
中谈到了密钥共享
If a single key is added that is not in the prototypical set of keys, you loose the key sharing
does object key-sharing happen when attributes are declared in a function that is called by __init__
?
是的,无论您从何处设置属性,假设在初始化后两者都具有相同的键集,实例字典使用共享键字典实现。两种情况都减少了内存占用。
您可以通过使用 sys.getsizeof
获取实例字典的大小然后将其与从中创建的类似字典进行比较来对此进行测试。 dict.__sizeof__
的实现基于此区分 return 不同的尺寸:
# on 64bit version of Python 3.6.1
print(sys.getsizeof(vars(c)))
112
print(getsizeof(dict(vars(c))))
240
所以,要找出答案,您需要做的就是比较这些。
关于您的编辑:
"If a single key is added that is not in the prototypical set of keys, you loose the key sharing"
正确,这是我(目前)发现 破坏 共享密钥用法的两件事之一:
- 在实例字典中使用非字符串键。这只能以愚蠢的方式完成。 (你可以使用
vars(inst).update
)
相同class的两个实例的字典内容有偏差,可以通过修改实例字典来解决。 (添加到其中的单个键不在原型键集中)
我不确定添加 单个 键时是否会发生这种情况,这是一个可能会更改的实现细节。 (附录:参见 Martijn 的评论)
有关此问题的相关讨论,请参阅我在此处进行的问答:
这两件事都会导致 CPython 使用 'normal' 字典。当然,这是不应依赖的实现细节。您可能会或可能不会在 Python 的其他实现和/或 CPython.
的未来版本中找到它
我认为您指的是 PEP 的以下段落(在 Split-Table dictionaries section 中):
When resizing a split dictionary it is converted to a combined table. If resizing is as a result of storing an instance attribute, and there is only instance of a class, then the dictionary will be re-split immediately. Since most OO code will set attributes in the __init__
method, all attributes will be set before a second instance is created and no more resizing will be necessary as all further instance dictionaries will have the correct size.
因此,无论添加什么内容,字典键都将保持共享状态,在创建第二个实例之前。在 __init__
中这样做是实现这一目标的最合乎逻辑的方法。
这不是意味着稍后设置的属性不共享;它们 仍然可以在实例之间共享 ;只要您不导致任何词典合并。因此,在您创建第二个实例后,仅当发生以下任一情况时,密钥才会停止共享:
- 新属性导致字典调整大小
- 新属性不是字符串属性(字典针对常见的所有键都是字符串的情况进行了高度优化)。
- 属性以不同的顺序插入;例如
a.foo = None
首先设置,然后第二个实例 b
首先设置 b.bar = None
,这里 b
有一个不兼容的插入顺序,因为共享字典有 foo
首先.
- 一个属性被删除。这会扼杀共享 即使是一个实例 。如果您关心共享词典,请不要删除属性。
所以当你有两个个实例(和两个字典共享密钥)时,只要你不触发任何以上情况,您的实例将继续共享密钥。
这也意味着将设置属性委托给一个名为 from __init__
的辅助方法不会影响上述场景,这些属性仍然在一秒钟之前设置实例被创建。毕竟 __init__
还不能 return 在第二种方法 returned 之前。
换句话说,您不必太担心在哪里设置属性。在 __init__
方法中设置它们可以让您更轻松地避免组合场景,但是 在创建第二个实例之前设置的任何属性 保证是共享密钥的一部分。
至于如何测试这个:用sys.getsizeof()
function看内存大小;如果创建 __dict__
映射的 copy 会导致更大的对象,则共享 __dict__
table:
import sys
def shared(instance):
return sys.getsizeof(vars(instance)) < sys.getsizeof(dict(vars(instance)))
快速演示:
>>> 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)
仅当达到阈值时(对于具有 8 个备用槽的空字典,在添加第 6 个键时调整大小),字典是否松开共享 属性。
字典在大约 2/3 满时调整大小,调整大小通常会使 table 大小翻倍。因此下一次调整大小将在添加第 11 个键时发生,然后是 22,然后是 43,依此类推。因此对于 large 实例字典,您有更多的喘息空间。
出于清楚和组织的原因,我一直试图在 __init__
中声明 class 属性。最近,由于为 Python 3.3 添加了 PEP 412,我了解到严格遵循这种做法也有额外的非美学好处。具体来说,如果所有属性都在 __init__
中定义,那么对象可以通过共享它们的键和哈希来减少 space。
我的问题是,在 __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
在这种情况下,Dog
的所有实例将共享密钥 height
和 weight
。 Cat
的实例是否也共享密钥 height
和 weight
(彼此之间,当然不是 Dog
s)。
顺便说一句,你会如何测试这个?
请注意,Brandon Rhodes 在他的 Dictionary Even Mightier talk:
中谈到了密钥共享If a single key is added that is not in the prototypical set of keys, you loose the key sharing
does object key-sharing happen when attributes are declared in a function that is called by
__init__
?
是的,无论您从何处设置属性,假设在初始化后两者都具有相同的键集,实例字典使用共享键字典实现。两种情况都减少了内存占用。
您可以通过使用 sys.getsizeof
获取实例字典的大小然后将其与从中创建的类似字典进行比较来对此进行测试。 dict.__sizeof__
的实现基于此区分 return 不同的尺寸:
# on 64bit version of Python 3.6.1
print(sys.getsizeof(vars(c)))
112
print(getsizeof(dict(vars(c))))
240
所以,要找出答案,您需要做的就是比较这些。
关于您的编辑:
"If a single key is added that is not in the prototypical set of keys, you loose the key sharing"
正确,这是我(目前)发现 破坏 共享密钥用法的两件事之一:
- 在实例字典中使用非字符串键。这只能以愚蠢的方式完成。 (你可以使用
vars(inst).update
) 相同class的两个实例的字典内容有偏差,可以通过修改实例字典来解决。 (添加到其中的单个键不在原型键集中)
我不确定添加 单个 键时是否会发生这种情况,这是一个可能会更改的实现细节。 (附录:参见 Martijn 的评论)
有关此问题的相关讨论,请参阅我在此处进行的问答:
这两件事都会导致 CPython 使用 'normal' 字典。当然,这是不应依赖的实现细节。您可能会或可能不会在 Python 的其他实现和/或 CPython.
的未来版本中找到它我认为您指的是 PEP 的以下段落(在 Split-Table dictionaries section 中):
When resizing a split dictionary it is converted to a combined table. If resizing is as a result of storing an instance attribute, and there is only instance of a class, then the dictionary will be re-split immediately. Since most OO code will set attributes in the
__init__
method, all attributes will be set before a second instance is created and no more resizing will be necessary as all further instance dictionaries will have the correct size.
因此,无论添加什么内容,字典键都将保持共享状态,在创建第二个实例之前。在 __init__
中这样做是实现这一目标的最合乎逻辑的方法。
这不是意味着稍后设置的属性不共享;它们 仍然可以在实例之间共享 ;只要您不导致任何词典合并。因此,在您创建第二个实例后,仅当发生以下任一情况时,密钥才会停止共享:
- 新属性导致字典调整大小
- 新属性不是字符串属性(字典针对常见的所有键都是字符串的情况进行了高度优化)。
- 属性以不同的顺序插入;例如
a.foo = None
首先设置,然后第二个实例b
首先设置b.bar = None
,这里b
有一个不兼容的插入顺序,因为共享字典有foo
首先. - 一个属性被删除。这会扼杀共享 即使是一个实例 。如果您关心共享词典,请不要删除属性。
所以当你有两个个实例(和两个字典共享密钥)时,只要你不触发任何以上情况,您的实例将继续共享密钥。
这也意味着将设置属性委托给一个名为 from __init__
的辅助方法不会影响上述场景,这些属性仍然在一秒钟之前设置实例被创建。毕竟 __init__
还不能 return 在第二种方法 returned 之前。
换句话说,您不必太担心在哪里设置属性。在 __init__
方法中设置它们可以让您更轻松地避免组合场景,但是 在创建第二个实例之前设置的任何属性 保证是共享密钥的一部分。
至于如何测试这个:用sys.getsizeof()
function看内存大小;如果创建 __dict__
映射的 copy 会导致更大的对象,则共享 __dict__
table:
import sys
def shared(instance):
return sys.getsizeof(vars(instance)) < sys.getsizeof(dict(vars(instance)))
快速演示:
>>> 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)
仅当达到阈值时(对于具有 8 个备用槽的空字典,在添加第 6 个键时调整大小),字典是否松开共享 属性。
字典在大约 2/3 满时调整大小,调整大小通常会使 table 大小翻倍。因此下一次调整大小将在添加第 11 个键时发生,然后是 22,然后是 43,依此类推。因此对于 large 实例字典,您有更多的喘息空间。