编码指南

本文档描述了使用 PyPy 代码库时的编码要求和约定。请仔细阅读并提出您可能遇到的任何问题。本文档没有过多地讨论编码风格问题。我们主要遵循 PEP 8,但如有疑问,请遵循代码库中已有的风格。

概述和动机

我们正在用 Python 编写 Python 解释器,利用 Python 众所周知的作为语言来解决算法问题的能力。乍一看,人们可能会认为这除了更好地理解解释器的工作原理之外,什么也做不到。这本身就值得去做,但我们有更大的目标。

CPython 与 PyPy

与 CPython 实现相比,Python 扮演了 C 代码的角色。我们用 Python 本身重写了 CPython 解释器。我们也可以在 C 级编写更灵活的解释器,但我们想用 Python 来提供解释器的另一种描述。

明显的优势是,这种描述更短、更易于阅读,许多实现细节消失了。这种方法的缺点是,只要在 CPython 之上运行,这个解释器就会慢得令人难以忍受。

为了再次获得一个有用的解释器,我们需要将我们对 Python 的高级描述翻译成低级描述。一种相当直接的方法是对 PyPy 解释器进行全程序分析,并再次创建一个 C 源代码。还有很多其他方法,但让我们坚持这种比较规范的方法。

应用程序级和解释器级执行和对象

由于 Python 用于实现我们所有的代码库,因此需要注意一个至关重要的区别:解释器级对象和应用程序级对象之间的区别。后者是你在编写普通 Python 程序时所处理的对象。然而,解释器级代码不能调用操作或访问应用程序级对象的属性。你可以在 PyPy 中立即识别出任何解释器级代码,因为一半的变量和对象名称以 w_ 开头,这表明它们是 封装 的应用程序级值。

让我们用一个简单的例子来说明区别。要对两个变量 ab 的内容求和,可以编写简单的应用程序级 a+b - 相反,等效的解释器级代码是 space.add(w_a, w_b),其中 space 是一个对象空间的实例,而 w_aw_b 是两个变量的封装版本的典型名称。

记住 CPython 如何处理相同的问题会有所帮助:CPython 中的解释器级代码是用 C 编写的,因此加法的典型代码是 PyNumber_Add(p_a, p_b),其中 p_ap_b 是类型为 PyObject* 的 C 变量。这在概念上类似于我们在 Python 中编写解释器级代码的方式。

此外,在 PyPy 中,我们必须明确区分解释器级和应用程序级异常:应用程序异常始终包含在 OperationError 的实例中。这使得我们能够轻松地区分解释器级代码中的错误(或 bug)与我们在解释的 Python 应用程序级程序中出现的错误。

应用程序级通常更可取

应用程序级代码的级别要高得多,因此编写和调试起来也相应地更容易。例如,假设我们要实现 dict 对象的 update 方法。在应用程序级进行编程,我们可以编写一个明显的、简单的实现,它看起来像 update可执行定义,例如

def update(self, other):
    for k in other.keys():
        self[k] = other[k]

如果我们只能在解释器级进行编码,我们将不得不编写更低级和更复杂的代码,例如

def update(space, w_self, w_other):
    w_keys = space.call_method(w_other, 'keys')
    w_iter = space.iter(w_keys)
    while True:
        try:
            w_key = space.next(w_iter)
        except OperationError as e:
            if not e.match(space, space.w_StopIteration):
                raise       # re-raise other app-level exceptions
            break
        w_value = space.getitem(w_other, w_key)
        space.setitem(w_self, w_key, w_value)

这个解释器级实现看起来更类似于 C 源代码。它仍然比其 C 对应物更具可读性,因为它不包含内存管理细节,并且可以使用 Python 的原生异常机制。

无论如何,应用程序级实现比解释器级实现更具可读性、更优雅、更易于维护,这一点应该很明显(事实上,dict.update 在 PyPy 中确实是使用应用程序级实现的)。

事实上,在 PyPy 的几乎所有部分,你都会在解释器级代码中间找到应用程序级代码。除了某些引导问题(应用程序级函数需要在执行之前对对象空间进行一定程度的初始化),应用程序级代码通常更可取。我们有一个抽象(称为“网关”),它允许函数的调用者不知道某个特定函数是在应用程序级还是解释器级实现的。

我们的运行时解释器是“RPython”

为了使 C 代码生成器可行,解释器级的所有代码都必须限制在 Python 语言的一个子集内,并且我们遵守一些规则,这些规则使代码转换为更低级语言成为可能。应用程序级代码仍然可以使用 Python 的全部表达能力。

与源到源的翻译(例如 Starkiller 或最近的 ShedSkin)不同,我们从构成 Python 解释器的实时 Python 代码对象开始翻译。在执行解释字节码的工作时,我们的 Python 实现必须以静态方式运行,通常被称为“RPythonic”。

但是,当 PyPy 解释器作为 Python 程序启动时,它可以使用 Python 语言的所有功能,直到达到某个时间点,从该时间点开始,所有正在执行的内容都必须是静态的。也就是说,在初始化期间,我们的程序可以自由使用 Python 的全部动态性,包括动态代码生成。

当前实现中可以找到一个非常优雅的例子:为了定义 Python 解释器的所有操作码,模块 dis 被导入并用于初始化我们的字节码解释器。(参见 pypy/interpreter/pyopcode.py 中的 __initclass__)。这使我们免于向 PyPy 添加额外的模块。导入代码在启动时运行,我们被允许使用 CPython 内置的导入函数。

启动代码完成后,所有生成的物体、函数、代码块等都必须遵守某些运行时限制,我们将在下面进一步描述。以下是这样做的原因:在翻译过程中,会执行整个程序分析(“类型推断”),它利用 RPython 中定义的限制。这使代码生成器能够为纯整数对象发出有效的机器级替换,例如。

包装规则

包装

PyPy 由两个级别的 Python 源代码组成:一方面是看起来像普通 Python 代码的应用程序级代码,它实现了一些功能,就像人们期望从 Python 代码中获得的那样(例如,可以给出一些内置函数的纯 Python 实现,例如 zip())。还存在解释器级代码,用于必须更直接地操作解释器数据和对象的那些功能(例如,解释器的主循环以及各种对象空间)。

应用程序级代码不会显式地看到对象空间:它使用对象空间来支持它操作的对象运行,但这只是隐式的。应用程序级代码不需要特别的约定。以下内容仅关于解释器级代码。(理想情况下,没有应用程序级变量应该被命名为 spacew_xxx 以避免混淆。)

在上面的示例中大量使用的 w_ 前缀表示,根据 PyPy 的编码约定,我们正在处理包装(或装箱)对象,即对象空间构建以实现相应应用程序级对象的解释器级对象。每个对象空间都提供 wrapunwrapint_winterpclass_w 等操作,这些操作在简单内置类型的对象之间进行两个级别的移动;每个对象空间还使用适当的解释器级类实现其他 Python 类型,这些类具有一定程度的内部结构。

例如,应用程序级 Python list标准对象空间 实现为 W_ListObject 的实例,它具有实例属性 wrappeditems(一个解释器级列表,其中包含应用程序级列表的项作为包装对象)。

这些规则将在下面更详细地描述。

命名约定

  • space:对象空间仅在解释器级代码中可见,根据约定,它通过名称 space 传递。
  • w_xxx:应用程序级代码看到的任何对象都是由对象空间显式管理的对象。从解释器级角度来看,这被称为包装对象。w_ 前缀用于任何类型的应用程序级对象。
  • xxx_w:包装对象的解释器级容器,例如包含包装对象的列表或字典。不要与作为列表或字典的包装对象混淆:这些是普通的包装对象,因此它们使用 w_ 前缀。

w_xxx 的操作

核心字节码解释器将包装对象视为黑盒。不允许直接检查它们。允许的操作都在对象空间上实现:它们被称为 space.xxx(),其中 xxx 是一个标准操作名称(addgetattrcalleq…)。它们在 对象空间文档 中有说明。

一个小警告:不要 使用 w_x == w_yw_x is w_y!这条规则的理由是,即使两个包装器包含看起来在应用程序级别相同的对象,也没有理由认为它们以任何方式相关。要检查相等性,请使用 space.is_true(space.eq(w_x, w_y)),或者更好的方法是使用快捷方式 space.eq_w(w_x, w_y),它直接返回一个解释器级别的布尔值。要检查同一性,请使用 space.is_true(space.is_(w_x, w_y)),或者更好的方法是使用 space.is_w(w_x, w_y)

应用程序级异常

解释器级代码可以自由地使用异常。但是,所有应用程序级异常在解释器级别都表示为 OperationError。换句话说,所有在应用程序级别可能可见的异常在内部都是 OperationError。这是对象空间操作(space.add() 等)报告的所有错误的情况。

要引发应用程序级异常

from pypy.interpreter.error import oefmt

raise oefmt(space.w_XxxError, "message")

raise oefmt(space.w_XxxError, "file '%s' not found in '%s'", filename, dir)

raise oefmt(space.w_XxxError, "file descriptor '%d' not open", fd)

要捕获特定的应用程序级异常

try:
    ...
except OperationError as e:
    if not e.match(space, space.w_XxxError):
        raise
    ...

此结构捕获所有应用程序级异常,因此我们必须将其与我们感兴趣的特定 w_XxxError 进行匹配,并重新引发其他异常。异常实例 e 包含两个可以检查的属性:e.w_typee.w_value。不要使用 e.w_type 来匹配异常,因为这将错过子类的实例异常。

PyPy 中的模块

从应用程序程序可见的模块是从解释器或应用程序级文件导入的。PyPy 几乎重用了 CPython 标准库中的所有 python 模块,目前来自 2.7.8 版本。我们有时需要 修改模块,更常见的是回归测试,因为它们依赖于 CPython 的实现细节。

如果我们不只是修改原始的 CPython 模块,而是需要从头开始重写它,我们将把它放到 lib_pypy/ 中,作为一个纯应用程序级模块。

当我们需要访问解释器级对象时,我们将模块放到 pypy/module 中。此类模块使用 混合模块机制,这使得在实现中使用解释器级和应用程序级部分变得很方便。请注意,没有为纯解释器级模块提供额外的工具,你只需编写一个混合模块,并将应用程序级部分留空。

确定模块实现的位置

在运行 py.py 时,您可以交互式地找出模块的来源。以下是可能位置的示例

>>>> import sys
>>>> sys.__file__
'/home/hpk/pypy-dist/pypy/module/sys'

>>>> import cPickle
>>>> cPickle.__file__
'/home/hpk/pypy-dist/lib_pypy/cPickle..py'

>>>> import os
>>>> os.__file__
'/home/hpk/pypy-dist/lib-python/2.7/os.py'
>>>>

模块目录/导入顺序

以下是 PyPy 查找 Python 模块的顺序

pypy/module

混合解释器/应用程序级内置模块,例如 sys__builtin__ 模块。

PYTHONPATH 的内容

PYTHONPATH 环境变量中指定的 : 分隔的目录列表中查找应用程序级模块。

lib_pypy/

包含模块的纯 Python 重新实现。

lib-python/2.7/

修改后的 CPython 库。

修改 CPython 库模块或回归测试

尽管 PyPy 与 CPython 非常兼容,但我们有时需要更改我们复制的标准库中包含的模块,这通常是由于 PyPy 默认情况下使用所有新式类,而 CPython 在许多地方依赖于某些类是旧式类。

我们只是将这些更改保留在原位,为了查看发生了哪些更改,我们有一个名为 vendor/stdlib 的分支,其中包含未修改的 cpython stdlib

实现混合解释器/应用程序级模块

如果模块需要访问 PyPy 的解释器级别,则它将被实现为混合模块。

混合模块是 pypy/module 中的目录,其中包含一个 __init__.py 文件,其中包含每个模块中的名称来自何处的规范。只有指定的名称将导出到混合模块的应用程序级命名空间。

有时需要用 C(或任何目标语言)真正编写一些函数。请参阅 rffi 的详细信息。

应用程序级定义

应用程序级规范位于 pypy/module 目录中 __init__.py 文件中找到的 appleveldefs 字典中。例如,在 pypy/module/__builtin__/__init__.py 中,您会找到以下条目,指定 __builtin__.locals 来自何处

...
'locals'        : 'app_inspect.locals',
...

app_ 前缀表示子模块 app_inspect 在应用程序级别被解释,并且将相应地提取 locals 的包装函数值。

解释器级定义

解释器级规范位于 pypy/module 目录中 __init__.py 文件中找到的 interpleveldefs 字典中。例如,在 pypy/module/__builtin__/__init__.py 中,以下条目指定 __builtin__.len 来自何处

...
'len'       : 'operation.len',
...

operation 子模块位于解释器级别,并且预计 len 将公开到应用程序级别。以下是 operation.len() 的定义

def len(space, w_obj):
    "len(object) -> integer\n\nReturn the number of items of a sequence or mapping."
    return space.len(w_obj)

公开的解释器级函数通常接受一个 space 参数和一些包装的值(请参阅 包装规则)。

您还可以在 interpleveldefs 字典中使用一个方便的快捷方式:即括号中的表达式,以直接指定解释器级表达式(而不是从文件中间接提取它)

...
'None'          : '(space.w_None)',
'False'         : '(space.w_False)',
...

解释器级表达式在执行时具有 space 绑定。

在 pypy/module 下添加一个条目(例如 mymodule)将自动创建一个新的配置选项(例如 –withmod-mymodule 和 –withoutmod-mymodule(后者是默认值))用于 py.py 和 translate.py。

测试 lib_pypy/ 中的模块

您可以前往 pypy/module/test_lib_pypy/ 目录并调用测试工具(“py.test” 或 “python ../../pypy/test_all.py”)来针对 lib_pypy 层次结构运行测试。这使我们能够快速测试我们用 Python 编写的重新实现与 CPython 的兼容性。

测试 pypy/module 中的模块

只需切换到 pypy/module 或其子目录,然后 按照通常方式运行测试

测试 lib-python 中的模块

为了让 CPython 的回归测试在 PyPy 上运行,您可以切换到 lib-python/ 目录并运行测试工具以开始兼容性测试。(XXX 检查 Windows 兼容性以生成测试报告)。

命名约定和目录布局

目录和文件命名

  • 目录/模块/命名空间始终为 **小写**
  • 在目录和文件名中不要使用复数名称
  • __init__.py 通常为空,除了 pypy/objspace/*pypy/module/*/__init__.py
  • 不要使用超过 4 层目录嵌套
  • 保持文件名简洁且易于补全。

Python 对象的命名

  • 类名使用 **驼峰式命名法**
  • 函数/方法使用小写字母和 _ 分隔
  • 对象空间类使用 XyzObjSpace 命名。例如:
    • StdObjSpace
    • FlowObjSpace
  • 在解释器级别和 ObjSpace 中,所有封装的值前面都有一个 w_,表示“封装的值”。这包括 w_self。在应用程序级别的 Python 代码中不要使用 w_

提交和分支到仓库

  • 编写良好的日志消息,因为许多人会阅读差异。

  • 以前称为 trunk 的分支在 Mercurial 中称为 default 分支。Mercurial 中的分支始终与仓库的其余部分一起推送。要创建一个名为 try1 的分支(假设仓库中不存在名为 try1 的分支),您应该执行以下操作

    hg branch try1
    

    只有在提交后,分支才会被记录到仓库中。要切换回默认分支

    hg update default
    

    有关更多详细信息,请使用帮助或参考 官方 Wiki

    hg help branch
    

在 PyPy 中进行测试

我们的测试基于 py.test 工具,该工具允许您编写无需样板代码的单元测试。目录中所有模块的测试通常都位于名为 **test** 的子目录中。基本上有两种类型的单元测试

  • **解释器级别测试**。它们在与 PyPy 解释器相同的级别运行。
  • **应用程序级别测试**。它们在应用程序级别运行,这意味着它们看起来像普通的 Python 代码,但它们是由 PyPy 解释的。

解释器级别测试

您可以像这样编写测试函数和方法

def test_something(space):
    # use space ...

class TestSomething(object):
    def test_some(self):
        # use 'self.space' here

请注意,测试函数的 test 前缀和测试类的 Test 前缀是必需的。在这两种情况下,您都可以使用 py.test 工具在模块全局级别导入 Python 模块并使用普通的 “assert” 语句。

应用程序级别测试

为了测试 PyPy 的一致性和良好行为,通常编写“正常”的应用程序级 Python 代码就足够了,这些代码不需要了解任何特定的编码风格或限制。如果我们有选择,我们通常使用应用程序级测试,这些测试位于以 apptest_ 前缀开头的文件,看起来像这样

# spaceconfig = {"usemodules":["array"]}
def test_this():
    # application level test code

这些应用程序级测试函数将在 PyPy 之上运行,即它们无法访问解释器细节。

默认情况下,它们在未翻译的 PyPy 之上运行,而未翻译的 PyPy 运行在主机解释器之上。当传递 -D 选项时,它们直接在主机解释器之上运行,在这种情况下,主机解释器通常是翻译后的 pypy 可执行文件。

pypy3 -m pytest -D pypy/

请注意,在解释模式下,pytest 的功能只有一小部分可用。为了配置对象空间,主机解释器将解析可选的 spaceconfig 声明。此声明必须采用有效的 json 字典形式。

混合级测试(已弃用)

混合级测试类似于应用程序级测试,区别在于它们只是嵌入在解释器级测试文件中的应用程序级代码片段,如下所示

class AppTestSomething(object):
    def test_this(self):
        # application level test code

您不能从全局级别使用导入的模块,因为它们是在解释器级别导入的,而您测试的代码是在应用程序级别运行的。如果您需要使用模块,则必须在测试函数中导入它们。

可以使用 AppTest 的 setup_class 方法将数据传递到 AppTest。所有附加到该类的包装对象,并且以 w_ 开头,都可以通过 self 访问(但没有 w_),在实际的测试方法中。一个例子

class AppTestErrno(object):
    def setup_class(cls):
        cls.w_d = cls.space.wrap({"a": 1, "b", 2})

    def test_dict(self):
        assert self.d["a"] == 1
        assert self.d["b"] == 2

另一种可能性是使用 cls.space.appexec,例如

class AppTestSomething(object):
    def setup_class(cls):
        arg = 2
        cls.w_result = cls.space.appexec([cls.space.wrap(arg)], """(arg):
            return arg ** 6
            """)

    def test_power(self):
        assert self.result == 2 ** 6

它在应用程序级别使用给定的参数执行代码字符串函数。请注意在 setup_class 中使用 w_result,但在测试中使用 self.result。以下是在 setup_class 中定义应用程序级类的方法,该类可以在后续测试中使用

class AppTestSet(object):
    def setup_class(cls):
        w_fakeint = cls.space.appexec([], """():
            class FakeInt(object):
                def __init__(self, value):
                    self.value = value
                def __hash__(self):
                    return hash(self.value)

                def __eq__(self, other):
                    if other == self.value:
                        return True
                    return False
            return FakeInt
            """)
        cls.w_FakeInt = w_fakeint

    def test_fakeint(self):
        f1 = self.FakeInt(4)
        assert f1 == 4
        assert hash(f1) == hash(4)

命令行工具 test_all

您可以通过调用以下命令运行几乎所有 PyPy 的测试

python test_all.py file_or_directory

它是位于 py/bin/ 目录中的通用 py.test 实用程序的同义词。要修改测试执行的开关,请传递 -h 选项。

覆盖率报告

为了获得覆盖率报告,包含了 pytest-cov 插件。它添加了一些额外的要求(coveragecov-core),并且一旦安装了它们,就可以通过以下方式调用覆盖率测试

python test_all.py --cov file_or_direcory_to_cover file_or_directory

测试约定

  • 添加功能需要添加相应的测试。(通常先编写测试更有意义,这样您就可以确定它们实际上会失败。)
  • 在整个 pypy 源代码中,都有 test/ 目录,其中包含单元测试。这些脚本通常可以直接执行,或者由 pypy/test_all.py 集体运行。

更改文档和网站

本地检出中的文档/网站文件

PyPy 的大部分文档都保存在 pypy/doc 中。您可以简单地编辑或添加包含 ReST 标记文件的 ‘.rst’ 文件。这里有一个 ReST 快速入门,但您也可以查看现有的文档,看看它们是如何工作的。

请注意,https://pypy.pythonlang.cn/ 的网站是单独维护的。它位于存储库 https://github.com/pypy/pypy.org

自动测试文档/网站更改

我们自动检查引用完整性和 ReST 规范。为了运行测试,您需要安装 sphinx。然后转到文档目录的本地检出并运行 Makefile

cd pypy/doc
make html

如果您没有看到任何错误,那么您的修改至少不会产生 ReST 错误或错误的本地引用。现在您将在文档目录中拥有 .html 文件,您可以将浏览器指向这些文件!

此外,如果您还想检查文档中的远程引用,请执行

make linkcheck

这将检查远程 URL 是否可访问。