技术文档

pypy 与 python的异同

2016-12-21  本文已影响1071人  JianMing

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

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

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

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

测试代码:

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())。

测试代码:

>>>> 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

# 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__,它将不会调用:

>>>> 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中则会被调用。例子:

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的自定义类的对象

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。这个规则适合如下的数据类型:

这个改变需要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)

其他

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()

再次运行:

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值是一样的:

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()
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()
class A(object):
    locals()[42] = 3

# 在PyPy中是不行的
上一篇 下一篇

猜你喜欢

热点阅读