PyPy 与 CPython的异同

pypy支持的扩展模块(对应Python/Modules/中的模块)

  • pypy支持的内建模块:

    • __builtin__:内建模块,包含一些常用的函数,如abs()等;
    • __pypy__:提供一个由pypy解析器提供的特殊功能模块;
    • _ast:抽象句法树模块的内建模块,一般直接使用ast模块;
    • _codecs:注册表与基类的编解码器的内建模块,一般直接使用codecs模块;
    • _collections:容器数据类型的内建模块,一般直接使用collections模块;
    • _continuation
    • _ffi
    • _hashlib:安全散列与消息摘要的内建模块,一般直接使用hashlib模块;
    • _io:io内建模块,一般直接使用io模块;
    • _locale:国际化的内建模块,一般直接使用locale模块;
    • _lsprof:Python分析器的内建模块,一般直接使用lsprof模块;
    • _md5:md5的内建模块,一般直接使用md5模块;
    • _minimal_curses:字符显示的终端处理curses的内建模块,一般直接使用curses模块,_minimal_curses仅是一个残留,只实现了部分功能,一般使用_curses
    • _multiprocessing:基于进程的并行的multiprocessing的内建模块,一般直接使用multiprocessing模块;
    • _random:生成伪随机数的内建模块,一般直接使用random模块;
    • _rawffi_rawffi是一个通过libffi来调用C的动态库方法和创建C对象(数组或结构体) 的内建模块;
    • _sha:SHA-1的内建模块,在pypy中还有sha模块,但是一般调用hashlib,在Ptyhon2.7中sha仍存在,但已经是Deprecated,Python3.X中sha已不再使用,一般直接使用hashlib模块;
    • _socket:Socket的内建模块,一般直接使用socket模块;
    • _sre:是实现正则表达式re大部分功能的内建模块,一般直接使用re模块;
    • _ssl:TLS/SSL的内建模块,一般直接使用ssl模块;
    • _warnings:警告控制的内建模块,一般直接使用warnings模块;
    • _weakref:弱引用的内建模块,一般直接使用weakref模块;
    • _winreg:Windows注册表访问的内建模块,一般直接使用winreg模块;
    • array:高效数值数组的内建模块;
    • binascii:二进制码与ASCII码间转化的内建模块;
    • bz2:对bzip2压缩支持的内建模块;
    • cStringIO:对内存进行文件操作(Read and write strings as files)的C实现的内建模块;
    • cmath:提供数学运算的C实现的内建模块;
    • cpyextcpyext内建模块提供让pypy使用CPython的扩展模块;
    • crypt:Unix密码验证的内建模块;
    • errno:标准错误记号的内建模块;
    • exceptions:异常处理的内建模块;
    • fcntl:系统调用文件锁fcntl()和ioctl()的内建模块;
    • gc:垃圾回收的内建模块;
    • imp:访问import模块接口的内建模块;
    • itertools:高效循环的迭代函数集合的内建模块;
    • marshal:序列化与反序列化的内建模块之一;
    • math:提供数学运算的内建模块;
    • mmap:内存映射文件支持的内建模块;
    • operator:针对函数标准操作的内建模块;
    • parser:访问Python解析树的内建模块;
    • posix:POSIX调用的内建模块;
    • pyexpat:解析xml的内建模块,一般直接使用xml模块;;
    • select:提供系统内I/O多路复用的内建模块;
    • struct:将字节解析为打包的二进制数据的内建模块;
    • symbol:Python解析树中的常量的内建模块;
    • sys:系统相关的参数与函数的内建模块;
    • termios:POSIX风格的tty控制的内建模块;
    • thread:线程的内建模块;
    • time:时间日期的内建模块;
    • token:Python解析树中的常量的内建模块;
    • unicodedata:Unicode字符数据库的内建模块;
    • zipimport:从ZIP归档中导入的内建模块;
    • zlib:兼容gzip压缩的内建模块;
  • 通过纯Python重写的模块在 lib_pypy/ (部分使用了cffi),比如:ctypescPicklecmathdbmdatetime

垃圾回收机制(gc)的不同

pypy的垃圾回收机制实现并不是使用引用计数,所以Object并不会在不被引用的情况下立即释放掉。最明显的影响是,文件(socket等)将不会在离开作用范围后立马被关闭掉。对于写打开的文件,这可能会导致写入的数据在缓冲区中一段时间,是的磁盘上文本被截断或还未写入。还可能导致文件打开数量超过系统的限制。

如果需要调试程序中哪里没有正确关闭文件,可以使用-X track-resources来运行程序。这样,每当GC关闭一个文件(或者socket)将会产生一个ResourceWarning。这个警告会包含文件(或者socket)打开创建的位置,方便定位问题。

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
import time
import gc
print("start")
i = 1
while True:
print(i)
i += 1
open("/data/test.log", "rw")
if (i % 10) == 0:
gc.collect()

print("finished")

__del__和弱引用的影响

这个问题,会影响到__del__方法调用的准确时间,因为pypy的回收是不确定的。这同样影响到弱引用(weak references),使得弱引用会比预期的存活时间长。这导致弱引用代理(由weakref.proxy(object[, callback])返回)的实用性降低:这使得弱引用代理在目标对象的引用失效后仍然能够被访问,且会在某个时刻突然失效并在下次访问时引起ReferenceError错误。所有使用到弱引用代理的必须小心处理ReferenceError(或者,更好的方法是使用weakref.ref()而不是weakref.proxy())。

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
>>>> import weakref
>>>> import gc
>>>> class A(object):
.... def __init__(self):
.... self.test_attr = 100
....
>>>> def test_func(refrence):
.... print("callback function!")
....
>>>> a = A()
>>>>
>>>> x = weakref.proxy(a, test_func)
>>>> x.test_attr
100
>>>> a.test_attr
100
>>>> del a
>>>>
>>>> x.test_attr
100
>>>> gc.collect()
callback function!
0
>>>> x
<weakproxy at 0x00007f6f19011dc0; dead>
>>>> x.test_attr
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ReferenceError: weakly referenced object no longer exists
>>>>
>>>> a = A()
>>>> x = weakref.ref(a, test_func)
>>>> print x
<weakref at 0x00007f6f190631a0; to 'A'>
>>>> b = x()
>>>> b.test_attr
100
>>>> del a
>>>> gc.collect()
0
>>>> x
<weakref at 0x00007f6f190631a0; to 'A'>
>>>> b.test_attr
100

某些情况下,由于CPython的引用计数,弱引用会在它指向的对象之前或之后立即释放掉,如果在之后释放掉,那么回调函数将会被调用。但在pypy类似的情况下,对象和弱引用会被认为同时释放,回调将不会被调用。

GC的其他影响

如果一个对象有__del__方法,在pypy中,__del__被调用的次数不会多于1次。但在CPython中,__del__在对象被“复活”然后“死亡”,__del__可能会被调用多次。(此处有一段不是很懂,详见Blog[1] [2].)

GC的差异也会间接的影响到其他方面。比如,pypy代码中的生成器,将会比CPython中更迟的被垃圾回收。这会影响到yield关键字,如果yieldtry:或者with:中,这会有一个issue 736

1
2
3
4
5
6
7
8
9
# import gc
def g():
try:
yield 1
finally:
print "finally"

g().next()
# gc.collect()

这段代码,在pypy中是没有打印出finally,在CPython中是有打印的。原因是,在pypy中,finally只有在对象被垃圾回收机制回收时才会打印。如果在最后调用gc.collect(),将能够打印出来。

使用默认的GC(minimark),内建函数id()将会像CPython中那样工作。但使用其他GC,id()返回的数字,并不是内存地址(因为一个对象的地址可能会改变),而且太频繁调用将会引起性能问题。

如果程序中有很长的链表对象,每一个中都有指向下一个的引用,并且都有__del__,这会导致pypy的GC的性能下降。但在其他情况下,pypy的GC性能普遍比CPython好。

__del__还有另外一个不同的地方,如果往一个已经存在的类中动态加入__del__,它将不会调用:

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

在pypy中,如果你将__del__动态绑定给Python的旧式类的对象(对于CPython的新式类也是不工作的),将会得到一个RuntimeWarning。修改这个issuse的方法是,在类中定义__del__,仅包含pass(或者其他实现),再在运行时动态修改;

CPython会在程序结束的时候去自动执行gc.collect(),但pypy并不会。

内建类型(types)的子类

官方实现上,CPython对于内建类型的子类的重载方法是否会被隐式调用没有确定任何规则。类似的,这些被重载的方法不会被同一个对象的其他内置方法调用。例如:一个在dict子类中被重载的__getitem__()将不会被其内置函数get()调用。

上述的情况在CPython中和PyPy中都是正确的。不同的是,一个除self以外的另一对象的内置方法是否会调用一个被重载的方法。这通常在CPython中是不会被调用的,而在PyPy中则会被调用。例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

在字典(dictionary)中作为key的自定义类的对象

1
2
3
4
5
6
7
8
9
10
11
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 xhash(y) == hash(x),同时,hash(x)是在x插入在一个字典中的时候计算的。如果条件满足,这个__evil_eq__方法将会被调用。

PyPy使用一个特殊的策略来优化,一个用户自定义类的实例作为Keys在字典当中,且这个自定义类是没有重载__hash____eq____cmp__:当使用这个策略的时候,__eq____cmp__将不会被调用,而是通过查找id(identity)。所以在上述代码的情况下,PyPy将能保证__eq__不会被调用。

在其他情况下(比如,有一个自定义的__hash____eq__y中),PyPy将会和CPython一样。

原始数据类型的对象标识,isid

原始数据类型的对象标识是根据值来判断想等,而不是包装器的标识。这意味着对于一个任意值的整数(interger)x来说x + 1 is x + 1总是返回True。这个规则适合如下的数据类型:

  • int
  • float
  • long
  • complex
  • str(只对空字符串或单个字符的字符串有效)
  • unicode(只对空字符串或单个字符的字符串有效)
  • tuple(只对空元组有效)
  • frozenset(只对空的frozenset有效)

这个改变需要id()也要做出相应的改变。id()需要满足如下情况:x is y <=> id(x) <=> id(y)。因此上述类型的id()将返回一个从参数计算而得到的值,因此可以大于sys.maxint(所以可以是任意长)。

记住,一个长度大于等于2的字符串,可以相等==,equal)却不一定相同is,identical)。类似的,尽管x包含一个元组且x == (2,),但x is (2,)不一定返回True。这个规则只适应于上述的类型。strunicodetuplefrozenset是在PyPy5.4版本中才适应这个规则。在5.4之前,尽管x等于?(),但是if x is "?" 或者 if x is ()将会返回False。这个5.4中的新行为是更为接近CPython,Cpython可以精确的缓存空tuplefirzenset和长度小于等于1的字符串或者unicodes(对于字符串和unicodes并不总是会缓存,当大部分情况是)。

对于float,“is”是针对一个对象的float“位模式”。所以float('nan') is float('nan')在PyPy上返回True,在CPython上返回False,因为这是两个对象。但是0.0 is -0.0都返回False,因为“位模式”不同。一般的,float('nan') == float('nan')总是返回False。当在容器中使用时(在列表项或者集合中),判断相等采用if x is y or x == y(不论在Cpython中或PyPy中)。因此,因为所有的nans在PyPy中是相同的,所以不能在一个集合中使用多次,不像CPython(Issuse #1974)

其他

  • hash()方法的随机bug在pypy中是忽略的,所以会有以下不同:
1
2
3
4
5
6
7
Python 3.4.0 (default, Apr 11 2014, 13:05:11) 
[GCC 4.8.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> a = "1"
>>>hash(a)
-7159617557763069555
>>>exit()

再次运行:

1
2
3
4
5
6
7
Python 3.4.0 (default, Apr 11 2014, 13:05:11) 
[GCC 4.8.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> a = "1"
>>> hash(a)
461546025534110251
>>> exit()

两次的hash值是不同的,而在pypy中,两次的hash值是一样的:

1
2
3
4
5
6
7
8
Python 2.7.3 (2.2.1+dfsg-1ubuntu0.3, Sep 30 2015, 15:18:40)
[PyPy 2.2.1 with GCC 4.8.4] on linux2
Type "help", "copyright", "credits" or "license" for more information.
And now for something completely different: ``pypy is more stable than debian''
>>>> a = "1"
>>>> hash(a)
6272018864
>>>> exit()
1
2
3
4
5
6
7
8
9
Python 2.7.3 (2.2.1+dfsg-1ubuntu0.3, Sep 30 2015, 15:18:40)
[PyPy 2.2.1 with GCC 4.8.4] on linux2
Type "help", "copyright", "credits" or "license" for more information.
And now for something completely different: ``we still have to write software
with a metaspace bubble in it''
>>>> a = "1"
>>>> hash(a)
6272018864
>>>> exit()
  • 不能在一个类型对象中存储一个非字符串的key。例子:
1
2
3
4
class A(object):
locals()[42] = 3

# 在PyPy中是不行的
  • sys.setrecursionlimit(n)仅仅是设置一个大概的值。这个通过设置一个n * 768bytes的可用栈空间来实现的。在Linux上,这个依赖于编译器的设置,默认值768KB大约能满足1400次调用。

  • 因为对于字典的实现是不同的,每次调用__hash____eq__所获的准确数字并不相同。由于CPython也没有一个准确的保证,所以不要依赖它。

  • 对于__class__的赋值使用,是基于CPython2.5上的。对于CPython2.6和CPython2.7多个特性,PyPy仍未支持(如果需要的话,是可以支持的,但在PyPy上需要比CPython2.6/2.7注意更多情况?)。

  • __builtins__的不同:
    在python中有一个内建模块,中有一些常用函数。而该模块在Python启动后、且没有执行程序员所写的任何代码前,Python会首先加载该内建函数到内存。

    在pypy和python2.X中,该内建模块是__builtin__,在Python3.X中,该内建模块是builtins

    __builtins__则是对于这个内建模块的引用。

    无论任何地方要想使用内建模块,都必须在该位置所处的作用域中导入__builtin__(builtins)内建模块;而对于__builtins__却不用导入,它在任何模块都直接可见。

    在Python2/3中的主模块__mian__中,__builtins__是对内建模块__builtin__(builtins)本身的引用,即__builtins__完全等价于__builtin__(builtins)。

    在Python2/3中的主模块__mian__中,__builtins__是对内建模块__builtin__.__dict__(builtins.__dict__)的引用,此时__builtins__的类型是字典。

  • 对于使用无效参数直接调用内建类型的内部“魔法”函数,结果会有略微的不同。比如:[].__add__(None)(2).__add__(None)在PyPy上都将返回NotImplemented;但在CPython 中,只有(2).__add__(None)会返回NotImplemented[].__add__(None)将引起TypeError。(当然,[] + None2 + None无论在PyPy和CPython中都会引起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,内置类型具有各种方式实现的属性。根据方式,如果试图写入(或删除)一个只读(或不可删除)属性时,将会引起一个TypeError或者AttributeError。PyPy试图在一致性和兼容性之间取舍。这意味着一些小地方将不会和CPython引起相同的异常,比如:del (lambda:None).__closure__

  • 在纯Python中,如果写一个类class A(object): def f(self): pass,且这个类有一个没有重载f()的子类B,这样B.f(x)仍然会检查xB的一个实例。在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(...)

  • 在PyPy中,新式类中的__dict__属性将会返回一个普通的dict,而不是像CPython中那样返回一个dict的代理。改变dict将会改变类型,反之亦然。对于内置对象,PyPy将会返回一个不可修改的dict(但行为和普通的dict类似)。

  • gc模块的一些方法和属性会和CPython中有一些略微的不同,比如:gc.enablegc.disable有被支持,但并不是启用或禁用GC,而是启用或禁用终结器(finalizers)的执行。

  • 过去的版本中,PyPy在交互模式中启动时打印一段来之#pypy IRC的随机一行。但在release版本中,这个行为已经被抑制了。但设置一个PYPY_IRC_TOPIC环境变量将会启动这个行为。要注意使用到的软件包完全禁用这个功能。

  • PyPy的readline模块是完全重写的,它并不是GUN的readline。它添加了多行的支持(详见multiline_input())。但另一方面parse_and_bind()的调用会被忽略(issue #2072)。

  • sys.getsizeof()总会引起TypeError。这是因为此函数的内存分析器很有可能给出与PyPy实际上不同的结果。让sys.getsizeof()返回一个数字(需要足够的工作)是可行的,但是这个数字有可能不能准确代表着这个对象使用的内存大小。在于系统其余部分(?)隔绝的情况下,这个数字甚至反应不了一个对象使用的内存大小。比如一个实例有maps,这个maps会经常在许多实例之间共享。在这种情况下,这个maps会被sys.getsizeof()的实现忽略,但在某些情况下,如果这个maps有着很多唯一的实例,那么它的内存开销是很重要的。相反的,即使是不同的对象,相等的字符串可以共享它们的内部数据,或者一些空的container会一直共享它们部分的内存,直到它们不再为空。更奇怪的是,一些list会在你对它进行读操作时创建一些对象;如果尝试估计range(10**6)的内存大小综合,这个操作自身会创建一百万个整型对象,但这些对象在开始时不存在的。在CPython中也会有类似的问题,只是少一些。以上就是不实现sys.getsizeof()的原因。

  • timeit模块在PyPy中的表现会有所不同:它但打印的时平均时间和标准差,而不是CPython中的最小值,因为最小值通常是有误差的。

  • sysconfig模块中的get_config_vars方法和distutil.sysconfig这两个是未完成的。在POSIX平台,CPython在Makefile中获取配置变量来编译生成解析器。PyPy也需要在编译的期间生成这些值,但还未完成。

  • "%d" % d"%x" % x等类似的结构中,当x是一个long的子类的实例时,并且这个子类重载了__str____hex____oct__这几个特殊方法时:PyPy将不会调用这些特殊方法;CPython是会的,但仅限于long的子类,不包括int。CPython的这些表现是很混乱的:比如,对于%x应该调用__hex__()__hex__()的功能是支持返回一个类似-0x123L的字符串了;然后%x会去掉0x和最后的L,保留其余部分。如果重载的__hex__()返回一个格式不正确的字符串,那么将会获得一个异常(在CPython2.7.13之前会之前crash)。

  • 在CPython2.7.13中,sqlite模块有一个更新,当有一个提交时它将不再重置所有的游标(cursors)。这会使得PyPy中有一些麻烦(CPython也可能会有),所以PyPy并没有移植这个更改:http://bugs.python.org/issue29006

-