【问题标题】:Why are explicit calls to magic methods slower than "sugared" syntax?为什么显式调用魔术方法比“加糖”语法慢?
【发布时间】:2014-05-21 08:52:10
【问题描述】:

当我遇到一组看起来很奇怪的计时结果时,我正在处理一个需要可散列、可比较和快速的小型自定义数据对象。这个对象的一些比较(和散列方法)只是委托给一个属性,所以我使用了类似的东西:

def __hash__(self):
    return self.foo.__hash__()

但是在测试中,我发现hash(self.foo) 明显更快。好奇,我测试了__eq____ne__ 和其他神奇的比较,结果发现所有如果我使用含糖形式(==!=、@ 987654327@等)。为什么是这样?我认为糖化形式必须在后台进行相同的函数调用,但也许不是这样?

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)
... '''

测试

我的自定义对象包装了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
... '''

结果

>>> 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
  • 哈希:
    • 显式: 1.0805227295846862
    • 加糖: 1.0135617737162192
  • 相等:
    • 显式: 2.349765956168767
    • 加糖: 2.1486044757355103
  • 小于:
    • 显式: 1.156479287717275
    • 加糖: 1.0673696685109917

【问题讨论】:

    标签: python performance magic-methods syntactic-sugar


    【解决方案1】:

    两个原因:

    • API 查找仅查看 类型。他们不看self.foo.__hash__,他们寻找type(self.foo).__hash__。这样就少了一本字典。

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

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

    避免直接调用魔术方法的另一个原因是比较运算符更多不仅仅是调用一个魔术方法;这些方法也反映了版本;对于x &lt; y,如果未定义x.__lt__x.__lt__(y) 返回NotImplemented 单例,则还咨询y.__gt__(x)

    【讨论】:

      猜你喜欢
      • 2015-08-08
      • 1970-01-01
      • 2011-10-13
      • 1970-01-01
      • 2011-09-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多