两大热门解释器比较——PyPy和CPython的区别(官方文档翻译)

PyPy和CPython的区别

本页记录了一些PyPy和CPython的区别和不兼容的地方,有一些不同是有意为之,因为我们认为有一些CPython的行为在有些情况下是错误的,而我们并不想复制这些错误。

在这里没有被列出来的差别应当被认为是PyPy的bug。

与垃圾回收策略有关的区别

PyPy使用或实现的垃圾回收器是不基于引用计数的,所以当对象不再可达时,它们并不会被立刻释放。这带来的最明显的影响是当文件(或套接字等)不再被使用时,不会被立刻关闭。对于那些为写入目的而打开的文件而言,数据可以暂时留在它们的输出缓冲区中,使磁盘上的文件显示为空或是被删除,并且可能会让你达到操作系统并发打开文件的数量上限。

如果你正在调试一个文件在你的程序中没有正确关闭的情况,你可以使用-X track-resources

命令行选项,如果它是给定的,对于每个垃圾回收器关闭的文件或是套接字都会生成一个ResourceWarning警告,该警告将会包含创建文件或套接字的位置堆栈追踪,以便让你更容易发现是哪部分程序没有显式的关闭文件。

如果在CPython中不强制的使用垃圾回收的引用计数方法,那么修复这种差异本质上来说是不可能的。在CPython中的影响无疑被描述为实现的副作用,并不作为一个语言设计决策:依赖于此的程序基本上是虚假的。如果试图在语言规范中强制执行CPython的行为,这将是一个过于严格的限制,因为它没有机会被Jython或IronPython(或任何其他Python到Java或 .NET的移植)所采用。

在某些情况下,当我们接近操作系统的极限时,强制使用完整的垃圾回收这种幼稚的想法会非常糟糕,如果你的程序制造了大量打开的文件,那么它就会起作用,但仅仅只是对每n个文件强制进行一次完整的垃圾回收。n的值是一个常数,但是程序可以占用任意数量的内容,这会导致一个完整的垃圾回收周期拥有任意的长度。最终结果是PyPy将在垃圾回收中花费任意大的一部分运行时间—并降低实际的运行速度,不是10%,100%或是1000%,而是任何因素。

在我们所知的所有解决方案中,没有比修复程序更好的解决方案了,如果这个问题出现在第三方的代码中,那你需要去找到作者,并向他解释这个问题:并且他们需要关闭打开的文件,一边实现在任何非基于CPython的Python运行。


这里有一些技术细节,这个问题会影响调用__del__的精确时间,在PyPy中这是不可靠也是不及时的(也不是在Jython和IronPython)。这也意味着弱引用可能会保持比预期更长的时间。使得弱代理(即weakref.proxy()返回的结果)不那么有用:在PyPy中,他们看起来会获得更长一些,然后突然间他们就会死去,并在下一次访问时引发ReferenceError错误。任何使用弱代理的代码都必须小心地在任何使用它们的地方捕获这种ReferenceError(或者更好的选择,完全不使用weakref.proxy(),而是使用weakref.ref())。

要注意一个weakref回调文档中的细节:

如果提供的不是None而是回调,并且返回的weakref仍然时活动的,当对象将要完成时,回调函数将被调用。

有些情况下,由于CPython的refcount语义,weakref在它指向的对象之前或之后立即终止(通常带有一些循环引用)。如果它恰好在之后死亡, 那么回调函数将会被唤醒。在PyPy的相同情况中,对象和weakref将同时被认为是死的,并且回调函数将不会被唤醒(Issue #2030)。


GC中的差异还有一些额外的含义,最突出的是,如果一个对象有一个__del__,那么这个__del__在PyPy中将从来不会被调用超过一次;但是如果对象死亡并且又再次复活,CPython将会调用相同的__del__数次(至少在老的CPython中它是可靠的,新的CPython尝试调用析构函数的次数不超过一次,但也有反例);剩下的__del__方法在相互指向的对象上按“正确的”顺序调用,就像在CPython中一样,但与CPython不同的是,如果对象之间相互引用存在死循环,那么剩下的__del__方法无论如何都会被调用;CPython会把它们放到gc模块的garbage列表中。更多的信息可以在博客上找到[1] [2]。

请注意,这种差异在某些情况下可能会间接地出现,比如,在PyPy中比在CPython中更晚地对中间挂起的生成器进行垃圾收集。如果yield关键字被挂起时本身包含在try:或一个with:块中,您可以在 issue 736看到区别。

使用默认的GC(调用minimark),内置函数id()的工作方式和在CPython中一样。对于其他GCs,它返回的数字不是真正的地址(因为对象可以移动几次),多次调用会导致性能问题。

注意如果你又一个很长的对象链,每一个带有对下一个的引用,并且每一个都有__del__,PyPy的GC将表现得很差。好的一方面是,在其它大多数情况下,基准测试已经表明PyPy的GC表现比CPython的好很多。

另一个不同之处在于,如果你在一个现有的类中添加了一个del__,它将不会被调用:

>>>> class A(object):
....     pass
....
>>>> A.__del__ = lambda self: None
__main__:1: RuntimeWarning: a __del__ method added to an existing type will not be called

更令人费解的是:事实也是如此,对于老年代的类,如果您将__del__附加到一个实例上(即使在CPython中,这也不适用于新样式的类)。那你会在PyPy中获得一个RuntimeWarning警告。要修复这些情况,只需确保类中有一个始于类的__del__方法即可(即使只包含pass;以后替换或覆盖它都可以)。

最后注意:CPython试图在程序结束时自动执行gc.collect();而PyPy不是(在CPython和PyPy中都可以设计这样一种情况,即在所有对象死亡之前需要几个gc.collect()。这使得CPython的方法只在“大多数时候”有效)。

内置类型的子类

在官方的说法中,对于何时隐式调用或不隐式调用内建类型的子类的重写方法,CPython没有任何规则。作为一个近似,这些方法永远不会被同一对象的其他内置方法调用,例如,在dict的子类中被重写的__getitem__()不会被例如内置的get()方法调用。

以上在CPython和PyPy中都是正确的。对于内置函数或方法是否会调用self之外的另一个对象的重写方法,可能会出现差异,在PyPy中,它们通常在CPython不需要的情况下被调用。两个例子:

class D(dict):
    def __getitem__(self, key):
        return "%r from D" % (key,)

class A(object):
    pass

a = A()
a.__dict__ = D()
a.foo = "a's own foo"
print a.foo


# CPython => a's own foo
# PyPy => 'foo' from D

glob = D(foo="base item")
loc = {}
exec "print foo" in glob, loc
# CPython => base item
# PyPy => 'foo' from D

改变已经用作字典键的对象的类

考虑如下代码片段:

class X(object):
    pass

def __evil_eq__(self, other):
    print 'hello world'
    return False

def evil(y):
    d = {X(): 1}
    X.__eq__ = __evil_eq__
    d[y] # might trigger a call to __eq__?

[点击并拖拽以移动]

在CPython中,__evil_eq__可以被调用,尽管尽管没有办法编写可靠地调用它的测试。当y is not x时,hash(y) ==hash(x),在这里,当x被插入到字典中时,hash(x)被计算。如果条件偶然得到满足,则调用__evil_eq__

PyPy使用一种特殊的策略来优化字典,字典的键是用户定义的类的实例,这些类不覆盖默认的__hash____eq____cmp__:当使用这个策略时,__eq____cmp__从来不会被调用,相反,查找是通过identity完成的,所以在上述情况下,可以保证剩余的__eq__不会被调用。

注意在其它情况下(例如,你在y中有定制的__hash____eq__)其行为于CPython相同。

忽略异常

在很多情况下,CPython能悄悄的吞下异常,发送这种情况的确切时间很长,尽管大多数的情况都很罕见,最著名的地方是定制丰富的比较方法(如__eq__);字典查询;调用一些内置函数,如isinstance()

除非这种行为在设计和文档中清楚地表现出来(例如hasattr()),否则在大多数情况下,PyPy会让异常传播。

对象标识的基本值,is和id

原始值的对象标识通过值相等起作用,不是通过包装器的身份。这意味着x + 1是x + 1总是正确的,对于任意整数x,该规则适用于下列类型:

  • int
  • float
  • long
  • complex
  • str(仅为空字符串或单字符字符串时)
  • unicode(仅为空字符串或单字符字符串时)
  • tuple(仅为空元组时)
  • frozenset(仅为空frozenset时)
  • 未绑定的方法对象(仅适用于Python 2)

这个更改还需要对id进行一些更改,id满足下面的条件:x is y <=> id(x) == id(y)。因此,上述类型的id将返回从参数计算得到的值,并且可以比sys.maxint更大(即它可以是任意长的)。

注意,长度为2或2以上的字符串可以相等但不完全相同。同样的,x is (2,)不一定是正确的,即使x包含元组和x ==(2,)。唯一性规则只适用于上面描述的特定情况。在PyPy 5.4中添加了strunicodetuplefrozenset规则;在此之前,即使x等于"?"(),像if x is "?"if x is ()这样的测试也可能失败。pypy5.4中添加的新行为更接近CPython,它精确地缓存empty tuple/frozenset以及(通常但不总是)长度<= 1的字符串和单线字符。

注意,对于float,每个float的“位模式”只有一个对象。所以float(‘nan’) is float(‘nan’)在PyPy中是True,但在CPython上不是,因为它们是两个对象。但是0.0 is -0.0总会是False。当在容器中使用时(例如列表项或集合),使用的确切相等规则是“if x is y or x == y”(在CPython和PyPy上);结果,因为所有nan在PyPy中都是相同的,所以不像CPython,你不能在一个集合中有几个,另一个结果是cmp(float(‘nan’), float(‘nan’)) == 0,因为cmp首先检查参数是否相同(这个cmp调用没有好的值可以返回,因为cmp假装在浮点数上有一个总的顺序,但这对于nan是错误的)

C-API的区别

外部的C-API在PyPy中被重新实现为内部的cpyext模块。我们支持大多数文档化的C-API,但有时内部的C-abstractions会在CPython上泄漏并被滥用,甚至在不知情的情况下。例如,在内部使用元组后,不支持对PyTupleObject的赋值,甚至通过另一个C-API函数调用。在CPython上,只要refcount为1,这个操作就会成功。在PyPy上,它总是会引发一个SystemError('PyTuple_SetItem called on tuple after use of tuple")异常(这里明确列出了搜索引擎)。

另一个类似的问题是,在调用PyType_Ready后,新函数指针被赋值到任何tp_as_*结构。例如,在CPython上调用PyType_Ready后使用不同的函数重写tp_as_number.nb_int将导致x.__int__()调用旧函数(通过类__dicrt__查找)和新函数被调用为int(x)(通过__slot__查找)。在PyPy上,我们总是调用剩余的__new__(),而不是旧的,不幸的是,这种古怪的行为是完全支持NumPy所必需的。

表现差异

CPython有一个优化,可以使重复的字符串不会二次连接的。例如,这类代码在O(n)时间内运行:

s = ''
for string in mylist:
    s += string

在PyPy中,这段代码总是具有二次复杂度。需要注意的还有,CPython的优化是脆弱的,无论如何只要在代码中有一点点变化就会崩溃。

parts = []
for string in mylist:
    parts.append(string)
s = "".join(parts)

其它方面

  • 随机哈希化(-R)在PyPy中式被忽略的。在CPython3.4以前,它没有什么意义。而CPython >= 3.4和PyPy3都实现了随机化的SipHash算法,忽略了-R
  • 你不能在类型对象中存储非字符串键。例如:
 class A(object):
    locals()[42] = 3

它将不会起作用。

  • sys.setrecursionlimit(n)仅通过将可用堆栈空间设置为n * 768字节设置近似的限制,在Linux中,根据编译器的设置,默认的768KB对于大约1400次调用已经足够了。

  • 由于dictionary的实现是不同的,所以__hash____eq__被调用的确切次数是不同的。因为CPython也不提供任何特定的保证,所以不要依赖它。

  • 对剩余__class__的分配仅限于在CPython 2.5上工作的情况。在CPython 2.6和2.7中,它在更多的情况下工作,到目前为止PyPy还不支持这些(如果需要,可以支持它,而且它在PyPy上比在CPython 2.6/2.7上更适用)。

  • __builtins__名称总是引用其余的__builtins__模块,而不是像在CPython中那样是一个字典。分配给__builtins__是没有效果的。(对于像RestrictedPython这样的工具的使用,看issue #2653)。

  • 直接调用带有无效参数的一些内置类型的内部魔术方法可能会有略微不同的结果。比如,在PyPy
    [].__add__(None)(2).__add__(None)都返回NotImplemented ;在CPython中,只有后者这样做,而前者引发TypeError。(当然,[]+None2+None会在所有地方引发TypeError)由于PyPy没有内部c级slots,这一差异是实现细节。

  • 在CPython中,[].__add__是一个method-wrapper,而list.__add__是一个slot wrapper。在PyPy上,这些是常规绑定或未绑定的方法对象。这有时会使检查内置类型的一些工具感到困惑。比如,标准库的inspect模块有一个ismethod()函数,它在未绑定的方法对象上返回True,但在method-wrapperslot wrapper上返回False。在PyPy上我们看不出两者的区别,所以ismethod([].__add__) == ismethod(list.__add__) == True

  • 在CPython中,内置类型具有可以通过各种方式实现的属性。如果您试图写入(或删除)一个只读(或不可删除)属性(这取决于方法),你会得到一个TypeErrorAttributeError。PyPy试图在完全一致和完全兼容之间找到一些中间地带。这意味着一些极端情况不会产生相同的例外,像是del (lambda:None).__closure__

  • 在纯Python中,如果你写了class A(object): def f(self): pass并且有一个不覆盖f()的子类B,那么B.f(x)仍然检查x是否是B的一个实例。在CPython中,用C编写的类型使用不同的规则。如果A用C实现,任何A的实例都将被B.f(x)接受(实际上,在这个情况下B.f is A.f)。一些代码,可以工作在CPython,但不PyPy包括:
    datetime.datetime.strftime(datetime.date.today(), ...)(在这里,datetime.datedatetime.datetime的超类),无论如何,正确的解决方法是首先使用常规方法调用:datetime.date.today().strftime(...)

  • gc模块的一些函数和属性的行为略有不同:比如,gc.enablegc.disable是被支持的,但是“启用和禁用GC”在PyPy中与在CPython中有不同的含义。这些函数实际上启用和禁用了主要集合和执行器的终结。

  • 在交互模式下,PyPy在启动时打印来自过去# PyPy IRC主题的随机行。在发布版本中,这种行为被禁止,但是设置环境变量PYPY_IRC_TOPIC将使其恢复。

  • PyPy的readline模块是从头重写的:它不是GNU的readline。它应该是基本兼容的,还增加了多行支持(参见multiline_input())。另一方面,parse_and_bind()调用被忽略(issue #2072)。

  • getsizeof()总是引发TypeError。这是因为使用这个函数的内存分析器很可能给出与PyPy上实际不一致的结果。可以让sys.getsizeof()返回一个数字(完成足够的工作),但是这可能表示也可能不表示对象使用了多少内存。孤立地询问一个对象使用了多少,甚至没有真正意义。比如,实例具有映射,通常在多个实例之间共享;在这种情况下,映射可能会被sys.getsizeof()的实现忽略,但是在某些情况下,如果有许多实例具有唯一的映射,那么它们的开销就很重要。相反地,相等字符串可以共享它们的内部字符串数据,即使它们是不同的对象——或者空容器可以共享它们的内部部分,只要它们是空的。更奇怪的是,有些列表在你阅读时创建对象;如果您试图估计内存中的大小range(10**6)是所有项大小的总和,那么该操作本身将创建一百万个从未存在过的整数对象。请注意,CPython上也存在这些问题,只是没有那么多。由于这个原因,我们没有显式地实现sys.getsizeof()

  • PyPy下的timeit模块表现不同:它输出的是平均时间和标准偏差,而不是最小值,因为最小值常常会引起误解。

  • sysconfigdistutilsget_config_vars方法还没有完成。在POSIX平台上,CPython捕获用于构建解释器的Makefile中的配置变量。PyPy应该在编译期间将值编译进去,但现在还没有这样做。

  • “%d“ % x“%x” % x和类似的构造,其中xlong的子类的实例,它覆盖了特殊方法__str____hex____oct__:PyPy不调用特殊方法;CPython可以做到——但仅当它是long的子类,而不是int的子类。CPython的行为非常混乱:例如,对于%x,它调用余下的__hex__(),它应该返回一个字符串,如-0x123L;然后去掉0x和最后的L,剩下的保留下来。如果你从剩余的__hex__()中返回一个意外的字符串,你会得到一个异常(或者CPython 2.7.13之前的崩溃)。

  • 在PyPy中,以**kwarg形式传递的字典只能包含字符串键,即使对于dict()dict.update()也是如此。CPython 2.7允许在这两种情况下使用非字符串键(就我们所知,只有在那里)。例如,在CPython3上,这个代码会产生一个TypeErrorx以及任何PyPy: dict(**{1: 2})。(注意dict(**d1)等于dict(d1))。

  • PyPy3:__class__堆类型和非堆类型之间的属性分配。CPython允许模块子类型,但是对于例如intfloat子类型就不是这样了。目前PyPy不支持剩余的__class__属性分配给任何非heaptype子类型。

  • 在PyPy中,优化模块和类字典时假设很少从它们中删除属性。因此,例如del foo.bar中foo是一个包含函数bar的模块(或类),它的速度明显比CPython慢。

  • CPython中的各种内置函数只接受位置参数,而不接受关键字参数。可以认为这是一个长时间运行的历史细节:较新的函数倾向于接受关键字参数,较旧的函数偶尔也会这样做。在PyPy中,大多数内置函数接受关键字参数(help()显示参数名称)。但是不要过分依赖它,因为如果CPython也开始接受参数,以后版本的PyPy可能不得不重命名它们。

  • PyPy3: distutils已得到增强,允许在VS%0.f0COMNTOOLS所指向的目录中查找VsDevCmd.bat。f0COMNTOOLS(通常是VS140COMNTOOLS)环境变量。CPython会在这个值之上搜索vcvarsall.bat

  • SyntaxError努力给出失败原因的细节,所以错误消息与CPython中的不一样。

  • 在PyPy上对字典和集合进行排序。在CPython < 3.6时,它们不是;在CPython上>= 3.6字典(而不是集合)是有序的。

  • PyPy2拒绝加载单独的.pyc文件,即删除.py文件后仍然存在的.pyc文件。PyPy3的行为与CPython类似。我们可以在PyPy2中解决这个差异:当前版本反映了我们对CPython的这个细节的烦恼,在开发PyPy时,它经常让我们感到困扰。(如果您确实需要,在翻译PyPy时传递--lonepycfile标志就可以了。)

动态扩展库

我们支持的扩展模块列表:
作为内置模块支持(在pypy/module/):

__builtin__ __pypy__ _ast _codecs _collections _continuation _ffi _hashlib _io _locale _lsprof _md5 _minimal_curses _multiprocessing _random _rawffi _sha _socket _sre _ssl _warnings _weakref _winreg array binascii bz2 cStringIO cmath cpyext crypt errno exceptions fcntl
gc imp itertools marshal math mmap operator parser posix pyexpat
select signal struct symbol sys termios thread time token unicodedata
zipimport zlib

在Windows上转换时,会跳过一些只使用unix的模块,而构建以下模块:

_winreg

用纯Python(可能使用cffi)重写支持:查看lib_pypy/目录。我们这样支持的模块的例子:ctypes, cPickle, cmath, dbm, datetime…注意,有些模块在这里和上面的列表中都有;默认情况下,使用内置模块(但可以在翻译时禁用)。

上面和lib_pypy/中都没有提到的扩展模块(即用C编写的标准CPython模块)在PyPy中是不可用的。(您可能有机会在cpyext中使用它们。)

你可能感兴趣的:(后端通用知识)