创建一个类实例需要调用多少个函数?

5
了解到在Python中调用函数是很耗费时间的,因此这个问题的答案与优化决策有关,例如将一个直接使用函数的数字方法与面向对象方法进行比较。因此,我想知道:
  • 通常需要多少个函数调用?
  • 最少需要多少个函数调用?
  • 什么会增加函数调用次数?
  • 用户创建的类与内置类相比如何?
  • 对象删除(包括垃圾回收)呢?
我的谷歌技能无法找到答案。 编辑:为了概括评论并防止更多的关闭票,以下是一些澄清:
  • 我对Python实例创建的时间复杂度与调用普通Python函数进行比较感兴趣
  • 出于本问题的目的,让我们限制在最新的CPython版本。

1
你的意思是一旦调用,一个Python类需要多少条指令才能设置好? - octopusgrabbus
3
您想要统计哪些函数调用?是Python函数调用还是Python解释器中的C函数调用? - Mark Byers
2
@关闭投票者:我无法理解为什么这个问题不是事实相关的或者不需要专业知识。它涉及到确切可衡量的事情,并且回答这个问题需要深入了解Python内部的知识。 - Lauritz V. Thaulow
1
@lazyr -- 这可能不是你想要的答案,但为什么不设置一些简单的测试,并通过 timeit 计时呢? - mgilson
2
如果您指定特定的实现可能会有所帮助 - 问题在于Python是一种语言规范 - 而不是一种实现。 CPython,Cython,Jython,IronPython,PyPy(或带有Psyco等的CPython),将根据底层平台(CPython的,.NET,JVM,混合,JIT等)和垃圾收集,线程,IO等具有不同的优化...即使是相同实现的不同版本也会有所不同 - 例如,算法已更改 - 更不用说新/旧样式类,__slots__,元类等等。 - Jon Clements
显示剩余8条评论
2个回答

7

请查看Eli Bendersky的Python对象创建

引用结论:

为了不把森林看成树木,让我们回到这篇文章的开头提出的问题。当CPython执行j = Joe()时会发生什么?
  • 由于Joe没有显式元类,因此type是它的类型。因此调用了typetp_call插槽,即type_call
  • type_call首先调用Joe的tp_new插槽:
    • 由于Joe没有显式基类,所以它的基类是object。因此调用了object_new
    • 由于Joe是一个Python定义的类,它没有自定义的tp_alloc插槽。因此,object_new调用了PyType_GenericAlloc
    • PyType_GenericAlloc分配并初始化了一个足够大的内存块来容纳Joe。
  • type_call然后继续在新创建的对象上调用Joe.__init__
    • 由于Joe没有定义__init__,因此调用了它的基类的__init__,即object_init
    • object_init什么也不做。
  • 新对象从type_call返回,并绑定到名称j

谢谢!那是一篇非常有趣的文章。 - Lauritz V. Thaulow
@lazyr:在这个简短的引语中,可以推断出你五个问题中的四个答案。 - Steven Rumbalski
1
请注意,这些调用的大部分是C调用,因此OP对Python中函数调用的开销的担忧并不一定适用。 - Daniel Roseman
@DanielRoseman:非常好的观点。但请注意,在Python中,我们至少有三个地方可以自定义此过程——在__init____new__和定义__call__的元类中。 - Steven Rumbalski

3

我按照评论中的建议使用timeit测试了这些案例:

def a():
    pass

class A(object):
    pass

class B(object):
    def __init__(self):
        pass

class NOPType(type):
    pass

class C(object):
    __metaclass__ = NOPType
    def __init__(self):
        pass

class D(object):
    def __new__(cls, *args, **kwargs):
        return super(D, cls).__new__(cls)

    def __init__(self):
        pass

class E(A):
    def __init__(self):
        super(E, self).__init__()

测试结果:

$ python -m timeit -s "import tst" "tst.a()"
10000000 次循环中,最佳时间为 0.149 微秒每次循环
$ python -m timeit -s "import tst" "tst.A()"
10000000 次循环中,最佳时间为 0.169 微秒每次循环
$ python -m timeit -s "import tst" "tst.B()"
1000000 次循环中,最佳时间为 0.384 微秒每次循环
$ python -m timeit -s "import tst" "tst.C()"
1000000 次循环中,最佳时间为 0.397 微秒每次循环
$ python -m timeit -s "import tst" "tst.D()"
1000000 次循环中,最佳时间为 1.09 微秒每次循环
$ python -m timeit -s "import tst" "tst.E()"
1000000 次循环中,最佳时间为 0.827 微秒每次循环

使用函数调用作为基准,以下是结果:

  • 基本实例化需要的时间增加了1.1倍。
  • 添加一个__init__方法将因子增加到2.6
  • 添加一个无操作元类只会略微增加,为2.7
  • 相反,添加一个基本的__new__,与7.3个函数调用等效
  • 具有单个子类的类相当于5.6个函数调用

对于最后两个结果,如果将对super的调用替换为其返回值,则可以减去约2。

这应该给出了在CPython 2.7中比较Python类和Python函数的时间开销大致的估计。


+1. 假设你的对象将拥有数据。如果你要创建大量的对象,你可能希望通过使用__slots__来优化内存使用。我建议不要使用__slots__,但如果你需要优化,可以考虑它。 - Steven Rumbalski
这实际上并没有直接回答原始问题,但这是一件好事! - user3850
@hop 我在回答评论时发现我的问题表述与我实际想要知道的不符。然而,我决定不更新问题,因为这样一来评论就会失去意义。 - Lauritz V. Thaulow

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接