PyPy 和 CPython 的区别¶
本页记录了 PyPy Python 解释器和 CPython 之间的少数差异和不兼容性。其中一些差异是“设计使然”,因为我们认为 CPython 的行为在某些情况下存在错误,我们不想复制这些错误。
未在此处列出的差异应被视为 PyPy 的错误。
缺少 sys.getrefcount
¶
由于上述不同的策略,sys.getrefcount()
会返回一个不可靠的数字。因此 PyPy 没有实现它,尝试使用它会引发 AttributeError: module 'sys' has no attribute 'getrefcount'
。请注意,更新版本的 CPython 也改变了 sys.getrefcount()
的含义。
内置类型的子类¶
正式来说,CPython 对于何时精确地调用内置类型的子类的重写方法没有规则。作为近似,这些方法永远不会被同一个对象的其他内置方法调用。例如,在 dict
的子类中重写的 __getitem__()
不会被例如内置的 get()
方法调用。
以上内容在 CPython 和 PyPy 中都是正确的。关于内置函数或方法是否会调用与 self
不同的另一个对象的重写方法,可能会出现差异。在 PyPy 中,它们经常在 CPython 不会调用的情况下被调用。两个例子
class D(dict):
def __getitem__(self, key):
if key == 'print':
return print
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
print('==========')
glob = D(foo="base item")
loc = {}
exec("print(foo)", glob, loc)
# CPython => base item, and never looks up "print" in D
# PyPy => 'foo' from D, and looks up "print" in 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)
,其中 hash(x)
是在 x
被插入到字典中时计算的。如果碰巧满足条件,那么就会调用 __evil_eq__
。
PyPy 使用一种特殊的策略来优化字典,这些字典的键是用户定义类的实例,这些类没有覆盖默认的 __hash__
、__eq__
和 __cmp__
:当使用这种策略时,__eq__
和 __cmp__
永远不会被调用,而是通过身份进行查找,因此在上面的情况下,保证不会调用 __eq__
。
请注意,在所有其他情况下(例如,如果你在 y
中有自定义的 __hash__
和 __eq__
),行为与 CPython 完全相同。
忽略异常¶
在许多极端情况下,CPython 可以静默地吞掉异常。尽管大多数情况非常罕见,但发生这种情况的确切列表相当长。最著名的例子是自定义的丰富比较方法(如 __eq__);字典查找;对某些内置函数(如 isinstance())的调用。
除非这种行为在设计上明确存在并有文档记录(例如,对于 hasattr()),否则在大多数情况下,PyPy 会让异常传播。
基本值的物件标识,is
和 id
¶
基本值的物件标识通过值相等来工作,而不是通过包装器的标识。这意味着 x + 1 is x + 1
始终为真,对于任意整数 x
。该规则适用于以下类型
int
float
long
complex
str
(仅限空字符串或单字符字符串)unicode
(仅限空字符串或单字符字符串)tuple
(仅限空元组)frozenset
(仅限空冻结集)- 未绑定方法对象(仅限 Python 2)
此更改需要对 id
进行一些更改。 id
满足以下条件:x is y <=> id(x) == id(y)
。因此,上述类型的 id
将返回一个从参数计算出的值,因此可以大于 sys.maxint
(即它可以是任意长的)。
请注意,长度大于或等于 2 的字符串可以相等,但并不相同。类似地,x is (2,)
不一定为真,即使 x
包含一个元组并且 x == (2,)
。唯一性规则仅适用于上述特定情况。 str
、unicode
、tuple
和 frozenset
规则是在 PyPy 5.4 中添加的;在此之前,像 if x is "?"
或 if x is ()
这样的测试可能会失败,即使 x
等于 "?"
或 ()
。在 PyPy 5.4 中添加的新行为更接近 CPython 的行为,CPython 会精确地缓存空元组/冻结集,以及(通常但不总是)长度 <= 1 的字符串和 Unicode 字符串。
请注意,对于浮点数,只有每个浮点数的“位模式”有一个“is
”对象。所以 float('nan') is float('nan')
在 PyPy 上为真,但在 CPython 上不为真,因为它们是两个对象;但 0.0 is -0.0
始终为假,因为位模式不同。像往常一样,float('nan') == float('nan')
始终为假。当在容器中使用时(例如作为列表项或集合中的项),使用的精确相等规则是“if x is y or x == y
”(在 CPython 和 PyPy 上都是);因此,由于所有 nans
在 PyPy 中都是相同的,因此您不能在集合中包含多个 nans
,这与 CPython 不同。(问题 #1974)。另一个结果是 cmp(float('nan'), float('nan')) == 0
,因为 cmp
首先使用 is
检查参数是否相同(从对 cmp
的此调用中没有好的值可以返回,因为 cmp
假设浮点数上存在一个全序,但这对于 NaN 是错误的)。
扩展中允许的 ABI 标签¶
CPython 支持使用具有 abi3
ABI 标签的模块的有限 C-API,并且还将导入没有 ABI 或平台标签的扩展模块。这可以通过比较 _imp.extension_suffix()
调用来观察(在 Python3.9、x86_64、linux 上)
python | _imp.extension_suffixes() | 备注 |
---|---|---|
cpython3.9 | [“.cpython-39-x86_64-linux-gnu.so”, | 普通 [1] |
“.abi3.so”, | 有限的 C-API | |
“.so”] | 裸扩展 | |
pypy3.9 | [‘.pypy39-pp73-x86_64-linux-gnu.so’] | 普通 [1] |
脚注
[1] | (1, 2) 普通扩展使用 <python-tag>-<abi-tag>-<platform-tag> |
CMake 将在 支持 PyPy3.9 的正确后缀,计划在 2023 年初发布 3.26 版
C-API 差异¶
外部 C-API 已在 PyPy 中重新实现为内部 cpyext 模块。我们支持大多数已记录的 C-API,但有时内部 C 抽象会泄漏到 CPython 并被滥用,甚至可能在不知情的情况下。例如,在内部使用元组后,即使是另一个 C-API 函数调用,也不支持对 PyTupleObject
进行赋值。在 CPython 中,只要引用计数为 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__()
(通过类 __dict__
查找),而新函数被调用以获取 int(x)
(通过插槽查找)。在 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 中被忽略。在 3.4 之前的 CPython 中,它 意义不大。CPython >= 3.4 和 PyPy3 都实现了随机化的 SipHash 算法,并忽略了-R
。您不能在类型对象中存储非字符串键。例如
class A(object): locals()[42] = 3
将不起作用。
sys.setrecursionlimit(n)
仅近似地设置限制,方法是将可用堆栈空间设置为n * 768
字节。在 Linux 上,根据编译器设置,默认的 768KB 足以进行大约 1400 次调用。由于字典的实现不同,
__hash__
和__eq__
被调用的确切次数也不同。由于 CPython 也没有给出任何具体保证,因此不要依赖它。__builtins__
名称始终引用__builtin__
模块,而不是像在 CPython 中有时那样引用字典。对__builtins__
的赋值没有效果。(对于 RestrictedPython 等工具的使用,请参阅 问题 #2653。)直接调用一些内置类型的内部魔术方法,并传入无效参数,可能会得到略微不同的结果。例如,
[].__add__(None)
和(2).__add__(None)
在 PyPy 上都返回NotImplemented
;在 CPython 上,只有后者返回NotImplemented
,而前者会抛出TypeError
。(当然,[]+None
和2+None
在任何地方都会抛出TypeError
。)这种差异是实现细节,由于 PyPy 没有内部的 C 级插槽而出现。在 CPython 上,
[].__add__
是一个method-wrapper
,list.__add__
是一个slot wrapper
,而list.extend
是一个(内置的)method
对象。在 PyPy 上,它们都是普通的 method 或 function 对象(或者在 PyPy2 上是 unbound method 对象)。这偶尔会让一些检查内置类型的工具感到困惑。例如,标准库inspect
模块有一个函数ismethod()
,它在 unbound method 对象上返回 True,但在 method-wrapper 或 slot wrapper 上返回 False。在 PyPy 上,我们无法区分它们。因此,在 PyPy2 上,我们有ismethod([].__add__) == ismethod(list.extend) == True
;在 PyPy3 上,我们有isfunction(list.extend) == True
。在 CPython 上,所有这些都是 False。在 CPython 中,内置类型具有可以以多种方式实现的属性。根据实现方式,如果您尝试写入(或删除)只读(或不可删除)属性,您将得到
TypeError
或AttributeError
。PyPy 试图在这两者之间找到一个平衡点,既要保持一致性,又要保持兼容性。这意味着一些极端情况不会抛出相同的异常,例如del (lambda:None).__closure__
。在纯 Python 中,如果您编写
class A(object): def f(self): pass
并有一个子类B
没有重写f()
,那么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.date
是datetime.datetime
的超类)。无论如何,正确的解决方法是使用常规的 method 调用:datetime.date.today().strftime(...)
gc
模块中的一些函数和属性的行为略有不同:例如,gc.enable
和gc.disable
是支持的,但“启用和禁用 GC”在 PyPy 中的含义与在 CPython 中不同。这些函数实际上启用和禁用了主要收集和终结器的执行。在交互模式下启动时,PyPy 会从过去的 #pypy IRC 主题中随机打印一行。在发布版本中,此行为被抑制,但设置环境变量 PYPY_IRC_TOPIC 会将其恢复。请注意,下游软件包提供者已知会完全禁用此功能。
PyPy 的 readline 模块从头开始重写:它不是 GNU 的 readline。它应该基本兼容,并且它添加了多行支持(参见
multiline_input()
)。另一方面,parse_and_bind()
调用被忽略(问题 #2072)。sys.getsizeof()
始终引发TypeError
(并且对象没有__sizeof__
方法)。这是因为使用此函数的内存分析器很可能在 PyPy 上给出与实际情况不符的结果。让sys.getsizeof()
返回一个数字(经过足够的工作)是可能的,但这可能代表或可能不代表对象使用的内存量。从与系统其余部分隔离的角度来看,询问一个对象使用多少内存实际上没有意义。例如,实例具有映射,这些映射通常在许多实例之间共享;在这种情况下,映射可能会被sys.getsizeof()
的实现忽略,但在某些情况下,如果存在许多具有唯一映射的实例,它们的开销很重要。相反,相等的字符串即使是不同的对象,也可能共享其内部字符串数据——即使是 unicode 字符串及其 utf8 编码的bytes
版本也是共享的——或者空容器只要为空,就可以共享其内部部分。更奇怪的是,某些列表在您读取它们时会创建对象;如果您尝试估计range(10**6)
的内存大小,作为所有项目大小的总和,该操作本身将创建一个一百万个之前不存在的整数对象。请注意,这些问题中的一些也存在于 CPython 中,只是程度较轻。出于这个原因,我们明确地不实现sys.getsizeof()
(也不实现__sizeof__
)。timeit
模块在 PyPy 下的行为有所不同:它打印平均时间和标准差,而不是最小值,因为最小值通常具有误导性。sysconfig
和distutils.sysconfig
的get_config_vars
方法不完整。在 POSIX 平台上,CPython 从用于构建解释器的 Makefile 中获取配置变量。PyPy 应该在编译期间将这些值烘焙进去,但目前还没有这样做。CPython 的
sys.settrace()
有时会在for
或yield from
行的末尾报告exception
,用于StopIteration
,有时则不会。问题在于它发生在定义不明确的案例子集中。PyPy 试图模拟这种情况,但精确的案例集并不完全相同。"%d" % x
和"%x" % x
以及类似的构造,其中x
是long
子类的实例,它覆盖了特殊方法__str__
或__hex__
或__oct__
:PyPy 不会调用特殊方法;CPython 会调用——但前提是它是long
的子类,而不是int
。CPython 的行为非常混乱:例如,对于%x
,它调用__hex__()
,该方法应该返回类似于-0x123L
的字符串;然后0x
和最后的L
被删除,其余部分被保留。如果您从__hex__()
返回一个意外的字符串,您将得到一个异常(或在 CPython 2.7.13 之前崩溃)。在 PyPy 中,作为
**kwargs
传递的字典只能包含字符串键,即使对于dict()
和dict.update()
也是如此。CPython 2.7 允许在这两种情况下(并且据我们所知,只有在这两种情况下)使用非字符串键。例如,这段代码在 CPython 3.x 以及任何 PyPy 上都会产生TypeError
:dict(**{1: 2})
。(请注意,dict(**d1)
等效于dict(d1)
。)将值赋给
__class__
的操作仅在 CPython 2.5 中有效。在 CPython 2.6 和 2.7 中,它在更多情况下有效,但 PyPy 目前不支持这些情况。(如果需要,可以支持,但它在 PyPy 上的有效情况可能比在 CPython 2.6/2.7 上更多。)在 PyPy 3 中,__class__
属性赋值在堆类型和非堆类型之间是不允许的。CPython 允许对模块子类型进行此操作,但不允许对例如int
或float
子类型进行此操作。目前,PyPy 不支持对任何非堆类型子类型进行__class__
属性赋值。在 PyPy 中,模块和类字典在删除属性很少发生的假设下进行了优化。因此,例如
del foo.bar
(其中foo
是包含函数bar
的模块(或类))比 CPython 慢得多。CPython 中的各种内置函数只接受位置参数,不接受关键字参数。这可以被认为是一个历史遗留问题:较新的函数倾向于接受关键字参数,而较旧的函数偶尔也会被修复以接受关键字参数。在 PyPy 中,大多数内置函数都接受关键字参数(
help()
显示了参数名称)。但不要过度依赖它,因为如果 CPython 开始接受关键字参数,PyPy 的未来版本可能需要重命名参数。PyPy3:
distutils
已增强,允许在VS%0.f0COMNTOOLS
(通常为VS140COMNTOOLS
)环境变量指向的目录中找到VsDevCmd.bat
。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 中不可用。