为什么对魔术方法的显式调用比"含糖"语法慢?

Hen*_*ter 11 python performance syntactic-sugar magic-methods

当我遇到一组奇怪的时序结果时,我正在搞乱一个小的自定义数据对象需要可以清洗,可比较和快速.这个对象的一些比较(和散列方法)只是委托给一个属性,所以我使用的是:

def __hash__(self):
    return self.foo.__hash__()
Run Code Online (Sandbox Code Playgroud)

但经过测试,我发现它hash(self.foo)明显更快.出于好奇,我测试__eq__,__ne__和其他神奇的比较,才发现所有的人跑得更快,如果我用含糖的形式(==,!=,<等).为什么是这样?我认为加糖形式必须在引擎盖下进行相同的函数调用,但也许情况并非如此?

Timeit结果

设置:围绕控制所有比较的实例属性的薄包装器.

Python 3.3.4 (v3.3.4:7ff62415e426, Feb 10 2014, 18:13:51) [MSC v.1600 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import timeit
>>> 
>>> sugar_setup = '''\
... import datetime
... class Thin(object):
...     def __init__(self, f):
...             self._foo = f
...     def __hash__(self):
...             return hash(self._foo)
...     def __eq__(self, other):
...             return self._foo == other._foo
...     def __ne__(self, other):
...             return self._foo != other._foo
...     def __lt__(self, other):
...             return self._foo < other._foo
...     def __gt__(self, other):
...             return self._foo > other._foo
... '''
>>> explicit_setup = '''\
... import datetime
... class Thin(object):
...     def __init__(self, f):
...             self._foo = f
...     def __hash__(self):
...             return self._foo.__hash__()
...     def __eq__(self, other):
...             return self._foo.__eq__(other._foo)
...     def __ne__(self, other):
...             return self._foo.__ne__(other._foo)
...     def __lt__(self, other):
...             return self._foo.__lt__(other._foo)
...     def __gt__(self, other):
...             return self._foo.__gt__(other._foo)
... '''
Run Code Online (Sandbox Code Playgroud)

测试

我的自定义对象是包装a datetime,所以这是我使用的,但它应该没有任何区别.是的,我在测试中创建了日期时间,所以显然有一些相关的开销,但是从一个测试到另一个测试的开销是不变的,所以它不应该有所作为.为简洁起见,我省略了__ne____gt__测试,但这些结果基本上与这里显示的相同.

>>> test_hash = '''\
... for i in range(1, 1000):
...     hash(Thin(datetime.datetime.fromordinal(i)))
... '''
>>> test_eq = '''\
... for i in range(1, 1000):
...     a = Thin(datetime.datetime.fromordinal(i))
...     b = Thin(datetime.datetime.fromordinal(i+1))
...     a == a # True
...     a == b # False
... '''
>>> test_lt = '''\
... for i in range(1, 1000):
...     a = Thin(datetime.datetime.fromordinal(i))
...     b = Thin(datetime.datetime.fromordinal(i+1))
...     a < b # True
...     b < a # False
... '''
Run Code Online (Sandbox Code Playgroud)

结果

>>> min(timeit.repeat(test_hash, explicit_setup, number=1000, repeat=20))
1.0805227295846862
>>> min(timeit.repeat(test_hash, sugar_setup, number=1000, repeat=20))
1.0135617737162192
>>> min(timeit.repeat(test_eq, explicit_setup, number=1000, repeat=20))
2.349765956168767
>>> min(timeit.repeat(test_eq, sugar_setup, number=1000, repeat=20))
2.1486044757355103
>>> min(timeit.repeat(test_lt, explicit_setup, number=500, repeat=20))
1.156479287717275
>>> min(timeit.repeat(test_lt, sugar_setup, number=500, repeat=20))
1.0673696685109917
Run Code Online (Sandbox Code Playgroud)
  • 哈希:
    • 明确: 1.0805227295846862
    • 加糖: 1.0135617737162192
  • 等于:
    • 明确: 2.349765956168767
    • 加糖: 2.1486044757355103
  • 少于:
    • 明确: 1.156479287717275
    • 加糖: 1.0673696685109917

Mar*_*ers 13

两个原因:

  • API查找仅查看类型.他们不看self.foo.__hash__,他们寻找type(self.foo).__hash__.这是一个较少的字典.

  • C槽查找比纯Python属性查找(将使用__getattribute__)更快; 相反,查找方法对象(包括描述符绑定)完全在C中完成,绕过__getattribute__.

因此,您必须在type(self._foo).__hash__本地缓存查找,即使这样,调用也不会像C代码那样快.如果速度非常快,请坚持使用标准库函数.

另一个原因,以避免直接调用魔术方法是比较运营商做更多的不仅仅是一个调用魔术方法; 方法也反映了版本; 因为x < y,如果x.__lt__没有定义或x.__lt__(y)返回NotImplemented单身,y.__gt__(x)也咨询.