__slots__ 是如何运作的?您如何实施自己的?
How do __slots__ really work and how can you implement your own?
我一直在闲逛 __slots__
并稍微搜索了一下,但我仍然对某些细节感到困惑:
我知道 __slots__
生成了某种描述符:
>>> class C:
... __slots__ = ('x',)
...
>>> C.x
<member 'x' of 'C' objects>
>>> C.x.__get__
<method-wrapper '__get__' of member_descriptor object at 0x7f001de183a8>
>>> C.x.__get__
<method-wrapper '__get__' of member_descriptor object at 0x7f001de183a8>
但我想知道:实际存储的值在哪里?
因为到目前为止我看到的描述符的常见 recipe/idiom 是:
>>> class Descr:
...
... def __init__(self, attrname):
... self.attrname = attrname
...
... def __get__(self, obj, cls):
... return obj.__dict__[self.attrname]
...
... def __set__(self, obj, val):
... obj.__dict__[self.attrname] = val
...
... class C:
...
... def __init__(self, x):
... self.x = x
与__slots__
一起使用时,有两个问题:
- 与名称冲突:
>>> class C:
...
... __slots__ = ('x',)
...
... def __init__(self, x):
... self.x = x
...
... x = Descr('x')
...
Traceback (most recent call last)
...
ValueError: 'x' in __slots__ conflicts with class variable
因此,解决方法是将实际属性命名为“_x”。
- 否
__dict__
(除非您明确将其添加到__slots__
):
>>> class C:
...
... __slots__ = ('_x',)
...
... def __init__(self, x):
... self._x = x
...
... x = Descr('_x')
...
>>> c = C(0)
>>> c.x
Traceback (most recent call last)
...
AttributeError: 'C' object has no attribute '__dict__'
所以你必须使用 getattr()
和 setattr()
来代替。
您可以使用通用描述符结束,该描述符可以与 __dict__
和 __slots__
:
一起使用
class WorksWithDictAndSlotsDescriptor:
def __init__(self, attr_name):
self.attr_name = attr_name
def __get__(self, instance, owner):
try:
return instance.__dict__[self.attr_name]
except AttributeError:
return getattr(instance, self.attr_name)
def __set__(self, instance, value):
try:
instance.__dict__[self.attr_name] = value
except AttributeError:
setattr(instance, self.attr_name, value)
(如果同时存在 __slots__
和 __dict__
,则不会正常工作。)
但最近我找到了一种使用包装器劫持 __get__
和 __set__
方法的方法:
def slot_wrapper(cls, slotname, slot, descriptor):
'''Wrapper replacing a slot descriptor with another one'''
class InnerDescr(descriptor):
def __get__(self, obj, cls):
print("Hijacking __get__ method of a member-descriptor")
return slot.__get__(obj, cls)
def __set__(self, obj, val):
print("Hijacking __set__ method of a member-descriptor")
slot.__set__(obj, val)
return InnerDescr(slotname, cls)
(用例是添加类型检查和数据验证,以及强制封装。)
因此,在创建 class 之后(或在使用元 class 之前),您可以为插槽和您的描述符保留相同的名称。
效果很好,但感觉有点脏...我认为实现我自己的槽以继续为描述符使用同一个名称可能会更好。但是我不知道怎么办。
所以这里有一些问题:
实际存储的值在哪里(因为没有字典)?我在想它是用 C 语言实现的,不能用 Python 代码直接访问。
如何在不损失性能优化的情况下实现纯 python 等效?
坚持使用我的包装纸更好吗?
Where are actually storerd the values (since there is no dict) ? I was thinking it's something implemented in C and not directly accessible with Python code.
直接在对象本身中为 PyObject *
指针分配内存。您可以在 Objects/typeobject.c
中看到处理。生成的描述符将访问为其在适当类型的对象中的插槽保留的内存。
How can I implement a pure python equivalent without losing performance optimization?
你不能。您可以获得的最接近的是扩展 tuple
.
It is prefarable to stick with my wrapper?
没有。不要将您的插槽命名为与您希望由其他描述符处理的属性相同的名称。这样做就像命名两个非插槽描述符一样;对于如何处理具有该名称的属性,您表达了两个相互矛盾的意图。
看看你的整体前景,我一直在为成员描述符上的自定义 setter 研究性能解决方案已有一段时间了,这是迄今为止我想出的最好的解决方案:
(在 wine 上使用 Anaconda 2.3.0 (Python 3.4.3) 测试,在 linux 上使用 Python 3.5.2 测试)
注意:此解决方案并不试图成为 pythonic,也不是作为问题的直接答案,而是对所需结果的替代实现。
class A(object):
__slots__ = ( 'attr', )
attrget = A['attr'].__get__
attrset = A['attr'].__set__
def setter( obj, val ):
if type( val ) is int:
attrset( obj, val )
else:
raise TypeError( 'int type expected, got %s'%type( val ) )
setattr( A, 'attr', property( attrget, setter ) )
# ^ this is safer than A.__dict__['attr'] as a mapping_proxy is read-only
有趣的事实: 对于 i = A()
,虽然 i.attr
效率较低(更多 CPU 尖峰),但实际上快了约 20ns (相对于我的机器)与基本 member_descriptor 平均相比。
这同样适用于没有自定义 setter.
的 i.attr = 0
(请自行测试,timeit 应该类似地工作(除了它包括 for 循环的时间)。(请注意,我的测试没有更改值),并确保 运行 多次测试)
这是 Python 3.5.2 在 linux 上的测试结果:
10000 iterations; threshold of min + 250ns:
________code___|_______min______|_______max______|_______avg______|_efficiency
⡇⢠⠀⠀⠀⠀⠀⠀⠀⠀⡄⠀⢰⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀
⣿⣾⣴⣄⣤⣄⣄⣰⣦⣤⣧⣰⣼⣤⣤⣆⡀⡀⢀⣄⣠⣀⣴⣶⣦⣤⣤⣦⣶⣄⣄⣠⣄⣴⣤⣶⣸⣦⣤⣤⣴⣴⣴⣷⣶⣴⣦⣤⣶⣆⣤⣤⣦⣶⣤⣴⣠⣷⣤⣶⣾⣷⣤⣆
i.mdsc = 1 | 564.964ns | 17341.983ns | 638.568ns | 88.473%
⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⢠⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⣷⣶⣶⣴⣤⣤⣦⣶⣶⣦⣧⣼⣼⣴⣄⣦⡄⣄⣀⣄⣴⡄⣼⣾⣶⣦⣴⣧⣶⣄⣄⣴⣦⣾⣴⣴⣤⣦⣆⣶⣴⣤⣴⣷⣿⣼⣾⣦⣷⣦⣧⣾⣦⣿⣤⣴⣤⣿⣤⣧⣾⣷⣶⣧
i.prop = 1 | 538.013ns | 8267.001ns | 624.045ns | 86.214%
10000 iterations; threshold of min + 175ns:
____code___|_______min______|_______max______|_______avg______|_efficiency
⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡆
⣇⣴⣠⣤⣠⣄⣤⣄⣀⣀⣀⡀⣀⣀⣀⣄⣠⣠⣄⣦⣤⣤⣄⣤⣤⣠⣤⣧⣤⣤⣠⣤⣤⣤⣤⣤⣤⣼⣤⣤⣤⣶⣤⣶⣦⣤⣀⣄⣤⣤⣤⣤⣤⣤⣤⣤⣤⣶⣦⣷⣤⣶⣄⣧
i.mdsc | 364.962ns | 27579.023ns | 411.621ns | 88.665%
⡇⢠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⠀
⣷⣾⣦⣦⣴⣠⣿⣦⣠⣠⣄⣀⣄⡄⣠⣴⣠⣤⣴⣴⣦⣼⣤⣦⣤⣤⣤⣧⣴⣶⣦⣶⣶⣶⣶⣶⣦⣶⣶⣶⣷⣿⣷⣿⣷⣾⣶⣶⣶⣾⣾⣾⣶⣶⣴⣶⣴⣾⣷⣿⣿⣷⣶⣶
i.prop | 341.039ns | 2000.015ns | 400.054ns | 85.248%
最后,如果您对这个答案投反对票,请解释原因。
(如果您的测试与我的测试不相符,请不要投票)
^ 我的结果只是一个例子,显示了性能的小幅提升,不应从表面上看。
我一直在闲逛 __slots__
并稍微搜索了一下,但我仍然对某些细节感到困惑:
我知道 __slots__
生成了某种描述符:
>>> class C:
... __slots__ = ('x',)
...
>>> C.x
<member 'x' of 'C' objects>
>>> C.x.__get__
<method-wrapper '__get__' of member_descriptor object at 0x7f001de183a8>
>>> C.x.__get__
<method-wrapper '__get__' of member_descriptor object at 0x7f001de183a8>
但我想知道:实际存储的值在哪里?
因为到目前为止我看到的描述符的常见 recipe/idiom 是:
>>> class Descr:
...
... def __init__(self, attrname):
... self.attrname = attrname
...
... def __get__(self, obj, cls):
... return obj.__dict__[self.attrname]
...
... def __set__(self, obj, val):
... obj.__dict__[self.attrname] = val
...
... class C:
...
... def __init__(self, x):
... self.x = x
与__slots__
一起使用时,有两个问题:
- 与名称冲突:
>>> class C:
...
... __slots__ = ('x',)
...
... def __init__(self, x):
... self.x = x
...
... x = Descr('x')
...
Traceback (most recent call last)
...
ValueError: 'x' in __slots__ conflicts with class variable
因此,解决方法是将实际属性命名为“_x”。
- 否
__dict__
(除非您明确将其添加到__slots__
):
>>> class C:
...
... __slots__ = ('_x',)
...
... def __init__(self, x):
... self._x = x
...
... x = Descr('_x')
...
>>> c = C(0)
>>> c.x
Traceback (most recent call last)
...
AttributeError: 'C' object has no attribute '__dict__'
所以你必须使用 getattr()
和 setattr()
来代替。
您可以使用通用描述符结束,该描述符可以与 __dict__
和 __slots__
:
class WorksWithDictAndSlotsDescriptor:
def __init__(self, attr_name):
self.attr_name = attr_name
def __get__(self, instance, owner):
try:
return instance.__dict__[self.attr_name]
except AttributeError:
return getattr(instance, self.attr_name)
def __set__(self, instance, value):
try:
instance.__dict__[self.attr_name] = value
except AttributeError:
setattr(instance, self.attr_name, value)
(如果同时存在 __slots__
和 __dict__
,则不会正常工作。)
但最近我找到了一种使用包装器劫持 __get__
和 __set__
方法的方法:
def slot_wrapper(cls, slotname, slot, descriptor):
'''Wrapper replacing a slot descriptor with another one'''
class InnerDescr(descriptor):
def __get__(self, obj, cls):
print("Hijacking __get__ method of a member-descriptor")
return slot.__get__(obj, cls)
def __set__(self, obj, val):
print("Hijacking __set__ method of a member-descriptor")
slot.__set__(obj, val)
return InnerDescr(slotname, cls)
(用例是添加类型检查和数据验证,以及强制封装。)
因此,在创建 class 之后(或在使用元 class 之前),您可以为插槽和您的描述符保留相同的名称。
效果很好,但感觉有点脏...我认为实现我自己的槽以继续为描述符使用同一个名称可能会更好。但是我不知道怎么办。
所以这里有一些问题:
实际存储的值在哪里(因为没有字典)?我在想它是用 C 语言实现的,不能用 Python 代码直接访问。
如何在不损失性能优化的情况下实现纯 python 等效?
坚持使用我的包装纸更好吗?
Where are actually storerd the values (since there is no dict) ? I was thinking it's something implemented in C and not directly accessible with Python code.
直接在对象本身中为 PyObject *
指针分配内存。您可以在 Objects/typeobject.c
中看到处理。生成的描述符将访问为其在适当类型的对象中的插槽保留的内存。
How can I implement a pure python equivalent without losing performance optimization?
你不能。您可以获得的最接近的是扩展 tuple
.
It is prefarable to stick with my wrapper?
没有。不要将您的插槽命名为与您希望由其他描述符处理的属性相同的名称。这样做就像命名两个非插槽描述符一样;对于如何处理具有该名称的属性,您表达了两个相互矛盾的意图。
看看你的整体前景,我一直在为成员描述符上的自定义 setter 研究性能解决方案已有一段时间了,这是迄今为止我想出的最好的解决方案:
(在 wine 上使用 Anaconda 2.3.0 (Python 3.4.3) 测试,在 linux 上使用 Python 3.5.2 测试)
注意:此解决方案并不试图成为 pythonic,也不是作为问题的直接答案,而是对所需结果的替代实现。
class A(object):
__slots__ = ( 'attr', )
attrget = A['attr'].__get__
attrset = A['attr'].__set__
def setter( obj, val ):
if type( val ) is int:
attrset( obj, val )
else:
raise TypeError( 'int type expected, got %s'%type( val ) )
setattr( A, 'attr', property( attrget, setter ) )
# ^ this is safer than A.__dict__['attr'] as a mapping_proxy is read-only
有趣的事实: 对于 i = A()
,虽然 i.attr
效率较低(更多 CPU 尖峰),但实际上快了约 20ns (相对于我的机器)与基本 member_descriptor 平均相比。
这同样适用于没有自定义 setter.
的 i.attr = 0
(请自行测试,timeit 应该类似地工作(除了它包括 for 循环的时间)。(请注意,我的测试没有更改值),并确保 运行 多次测试)
这是 Python 3.5.2 在 linux 上的测试结果:
10000 iterations; threshold of min + 250ns:
________code___|_______min______|_______max______|_______avg______|_efficiency
⡇⢠⠀⠀⠀⠀⠀⠀⠀⠀⡄⠀⢰⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀
⣿⣾⣴⣄⣤⣄⣄⣰⣦⣤⣧⣰⣼⣤⣤⣆⡀⡀⢀⣄⣠⣀⣴⣶⣦⣤⣤⣦⣶⣄⣄⣠⣄⣴⣤⣶⣸⣦⣤⣤⣴⣴⣴⣷⣶⣴⣦⣤⣶⣆⣤⣤⣦⣶⣤⣴⣠⣷⣤⣶⣾⣷⣤⣆
i.mdsc = 1 | 564.964ns | 17341.983ns | 638.568ns | 88.473%
⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⢠⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⣷⣶⣶⣴⣤⣤⣦⣶⣶⣦⣧⣼⣼⣴⣄⣦⡄⣄⣀⣄⣴⡄⣼⣾⣶⣦⣴⣧⣶⣄⣄⣴⣦⣾⣴⣴⣤⣦⣆⣶⣴⣤⣴⣷⣿⣼⣾⣦⣷⣦⣧⣾⣦⣿⣤⣴⣤⣿⣤⣧⣾⣷⣶⣧
i.prop = 1 | 538.013ns | 8267.001ns | 624.045ns | 86.214%
10000 iterations; threshold of min + 175ns:
____code___|_______min______|_______max______|_______avg______|_efficiency
⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡆
⣇⣴⣠⣤⣠⣄⣤⣄⣀⣀⣀⡀⣀⣀⣀⣄⣠⣠⣄⣦⣤⣤⣄⣤⣤⣠⣤⣧⣤⣤⣠⣤⣤⣤⣤⣤⣤⣼⣤⣤⣤⣶⣤⣶⣦⣤⣀⣄⣤⣤⣤⣤⣤⣤⣤⣤⣤⣶⣦⣷⣤⣶⣄⣧
i.mdsc | 364.962ns | 27579.023ns | 411.621ns | 88.665%
⡇⢠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⠀
⣷⣾⣦⣦⣴⣠⣿⣦⣠⣠⣄⣀⣄⡄⣠⣴⣠⣤⣴⣴⣦⣼⣤⣦⣤⣤⣤⣧⣴⣶⣦⣶⣶⣶⣶⣶⣦⣶⣶⣶⣷⣿⣷⣿⣷⣾⣶⣶⣶⣾⣾⣾⣶⣶⣴⣶⣴⣾⣷⣿⣿⣷⣶⣶
i.prop | 341.039ns | 2000.015ns | 400.054ns | 85.248%
最后,如果您对这个答案投反对票,请解释原因。
(如果您的测试与我的测试不相符,请不要投票)
^ 我的结果只是一个例子,显示了性能的小幅提升,不应从表面上看。