Python:面向对象的开销?

4 投票
1 回答
692 浏览
提问于 2025-04-18 17:28

我一直在做一个实时应用,发现一些面向对象的设计模式在Python中引入了很大的开销(我测试的是2.7.5版本)。

简单来说,为什么当字典被另一个对象封装时,简单的访问字典值的方法会花费几乎5倍的时间呢?

比如,运行下面的代码时,我得到了:

Dict Access: 0.167706012726
Attribute Access: 0.191128969193
Method Wrapper Access: 0.711422920227
Property Wrapper Access: 0.932291030884

可执行代码:

class Wrapper(object):
    def __init__(self, data):
        self._data = data

    @property
    def id(self):
        return self._data['id']

    @property
    def name(self):
        return self._data['name']

    @property
    def score(self):
        return self._data['score']


class MethodWrapper(object):
    def __init__(self, data):
        self._data = data

    def id(self):
        return self._data['id']

    def name(self):
        return self._data['name']

    def score(self):
        return self._data['score']


class Raw(object):
    def __init__(self, id, name, score):
        self.id = id
        self.name = name
        self.score = score


data = {'id': 1234, 'name': 'john', 'score': 90}
wp = Wrapper(data)
mwp = MethodWrapper(data)
obj = Raw(data['id'], data['name'], data['score'])


def dict_access():
    for _ in xrange(100):
        uid = data['id']
        name = data['name']
        score = data['score']


def method_wrapper_access():
    for _ in xrange(100):
        uid = mwp.id()
        name = mwp.name()
        score = mwp.score()


def property_wrapper_access():
    for _ in xrange(100):
        uid = wp.id
        name = wp.name
        score = wp.score


def object_access():
    for _ in xrange(100):
        uid = obj.id
        name = obj.name
        score = obj.score


import timeit
print 'Dict Access:', timeit.timeit("dict_access()", setup="from __main__ import dict_access", number=10000)
print 'Attribute Access:', timeit.timeit("object_access()", setup="from __main__ import object_access", number=10000)
print 'Method Wrapper Access:', timeit.timeit("method_wrapper_access()", setup="from __main__ import method_wrapper_access", number=10000)
print 'Property Wrapper Access:', timeit.timeit("property_wrapper_access()", setup="from __main__ import property_wrapper_access", number=10000)

1 个回答

5

这是因为Python解释器(CPython)在处理你的调用、索引等操作时,会进行动态查找。动态查找让语言使用起来非常灵活,但也会影响性能。当你使用“方法包装器”时,至少会发生以下几步:

  • 查找 mwp.id - 它实际上是一个方法,但它也是一个分配给属性的对象,得像其他对象一样查找
  • 调用 mwp.id()
  • 在方法内部,查找 self._data
  • 查找 self._data__getitem__
  • 调用 __getitem__(这至少是一个C语言的函数,但你还是得经过这些动态查找才能到这里)

相比之下,你的“字典访问”测试案例只需要查找 __getitem__ 然后直接调用它。

正如Matteo Italia在评论中提到的,这与具体的实现有关。在Python生态系统中,现在还有PyPy(使用即时编译和运行时优化)、Cython(编译成C,支持可选的静态类型注解等)、Nuitka(编译成C++,可以直接使用代码)以及其他多种实现。

在CPython中优化这些查找的一种方法是直接获取对象的引用,并将它们分配给循环外的局部变量,然后在循环内部使用这些局部变量。这种优化可能会导致代码变得杂乱,或者破坏封装性。

撰写回答