PSJay Blog

#FIXME, seriously

用 `accio` 代替 `import`:深入理解自举和 Python 语法

| Comments

本文翻译自 Replacing import with accio: A Dive into Bootstrapping and Python’s Grammar


Hacker School,我用哈利波特里的咒语覆写内置函数和语句创造了另一个次元的 Python,在 Hacker School 你就能这么干!

尽管这个项目一开始是一个玩笑,但我在著名的 Hacker School 推进者 Allison Kaptur 的指导下,还是快速地深入了解了 Python 的内部实现并且编译了一个 Python 来编译这个 Python。最终都是为了把 import 语句用 accio 代替。

但在我们正式编译哈利波特版的 Python(我爱称它为 Nagini)之前,我们现在说说 Python 内部的一些基础知识,当然,我们以拼写为例。

覆写内置函数(Builtin Functions)

Python 的内置函数存在于 __builtins__ 模块,它在启动的时候会被自动导入。

1
2
>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BufferError', 'BytesWarning', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'None', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'ReferenceError', 'RuntimeError', 'RuntimeWarning', 'StandardError', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '_', '__debug__', '__doc__', '__import__', '__name__', '__package__', 'abs', 'all', 'any', 'apply', 'basestring', 'bin', 'bool', 'buffer', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'cmp', 'coerce', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'execfile', 'exit', 'file', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'intern', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'long', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'raw_input', 'reduce', 'reload', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'unichr', 'unicode', 'vars', 'xrange', 'zip']

覆写 Python 内置函数也是想当地简单:

1
2
3
4
5
6
7
8
9
10
11
>>> wingardium_leviosa = __builtins__.float

>>> del __builtins__.float

>>> float(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'float' is not defined

>>> wingardium_leviosa(3)
3.0

然而,覆写 import 没有那么容易。让我们试试:

1
2
3
4
5
>>> accio = import
  File "<stdin>", line 1
    accio = import
                 ^
SyntaxError: invalid syntax

Python 期待 import 之后会跟上一个模块名,因此它抛出了一个 SyntaxError。这是由于 import x 是一条语句,而不是一个函数调用。

嗯。我想起了我们刚刚在执行 dir(__builtins__) 时被列出的 __import__ 函数。也许我们可以覆写它:

1
2
3
4
5
6
7
8
>>> accio = __builtins__.__import__
>>> accio sys
  File "<stdin>", line 1
    accio sys
            ^
SyntaxError: invalid syntax

# :(

我们把 accio 当作一个函数来调用会发生什么呢?

1
2
3
4
>>> accio(sys)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'sys' is not defined

也许我们应该把 ‘sys’ 当作一个字符串传进去?

1
2
3
4
5
6
7
8
>>> accio('sys')
<module 'sys' (built-in)>

# Ooh!

>>> sys = accio('sys')
>>> sys
<module 'sys' (built-in)>

啊哈。所以 import x 语句也许做了这样的事情:

  1. x 调用了 __import__ 函数: __builtins__.__import__('x')
  2. __import__ 的返回值赋值给模块中的 x

import sys 就像是这个命令的缩写:

1
>>> sys = __builtins__.__import__('sys')

(这里我只描述了简单的 import 语句,但是像 from x import y.w, y.z 的更复杂一些的语句的工作方式也是类似的。)

所以我们有一种方法可以把 accio 作为函数添加,而不是语句。我觉得不快乐。

就找点儿乐子,我们可以把 import 删了么?

1
2
3
4
5
6
7
8
9
10
11
>>> del import
  File "<stdin>", line 1
    del import
             ^
SyntaxError: invalid syntax

>>> del __builtins__.__import__
>>> import os
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: __import__ not found

差不多像那么回事儿!虽然我想要 import os 抛出 SyntaxError 而不是 ImportError,因为很显然 import 是一种输入错误,用户需要知道应该输入 accio

因此,为了完整地把 import 使用 accio 代替,我们将要学习 Python 是怎样定义语句的。

语法

Eli Bendersky 写了篇很棒的关于给 Python 加上 until 语句的 博文 。但我们需要的是替换一个语句,而不是增加一个,因此我们的方法会有一点不太一样。

不管怎样,我们应该可以从 Python 源代码中的 Grammar 文件开始改变 Python 的语句。Python 源代码!这还不算好玩儿嘛?!

Python 源代码存储在了一个 Mercurial 仓库里,所以我们首先应该安装 Mercurial。

1
$ brew install mercurial

然后我们就可以克隆 CPython(就像 git clone):

1
$ hg clone http://hg.python.org/cpython

这大概需要整整一分钟,去弄杯咖啡吧。

在 Python 的 Mercurial 仓库里,不同版本的 Python 拥有不同的分支。默认情况下我们在 Python3 分支。我仍然在我的机器上使用 Python2,所以 checkout 到 2.7 版本:

1
2
$ cd cpython
$ hg checkout 2.7

现在开始 编译 CPython 看看成不成功!

1
2
$ ./configure --with-pydebug
$ make -s -j2

出现了一个警告信息,告诉我有一些模块不能被构建,但是人类已经阻止不了我了。人类是无法阻止我们的!继续吧。

似乎我们可以从 Grammar/Grammar 文件着手,那我们就在这儿试试水吧。它是这样的。搜索 ‘import’ 会把我们引到 52-60 行:

1
2
3
4
5
6
7
8
9
import_stmt: import_name | import_from
import_name: 'import' dotted_as_names
import_from: ('from' ('.'* dotted_name | '.'+)
              'import' ('*' | '(' import_as_names ')' | import_as_names))
import_as_name: NAME ['as' NAME]
dotted_as_name: dotted_name ['as' NAME]
import_as_names: import_as_name (',' import_as_name)* [',']
dotted_as_names: dotted_as_name (',' dotted_as_name)*
dotted_name: NAME ('.' NAME)*

牛!我们仅仅读读这儿就可以大概弄明白这意味着什么。好像 import_stmtimport_name 或者 import_from,分别代表 import xfrom x import y 两种格式。我们在 53-55 行仅把 import 替换成 accio 会发生什么呢?我们试试。在修改并且保存 Grammar 文件之后,输入一下命令进行编译:

1
$ make -s -j2

奥。就这么简单该多好。有错误发生了:

1
2
3
4
5
6
7
8
9
10
11
Traceback (most recent call last):
  File "/Users/amyhanlon/projects/nagini/cpython/Lib/runpy.py", line 151, in _run_module_as_main
    mod_name, loader, code, fname = _get_module_details(mod_name)
  File "/Users/amyhanlon/projects/nagini/cpython/Lib/runpy.py", line 113, in _get_module_details
    code = loader.get_code(mod_name)
  File "/Users/amyhanlon/projects/nagini/cpython/Lib/pkgutil.py", line 283, in get_code
    self.code = compile(source, self.filename, 'exec')
  File "/Users/amyhanlon/projects/nagini/cpython/Lib/sysconfig.py", line 4
    import sys
             ^
SyntaxError: invalid syntax

尝试执行一个 Python 脚本的时候发生了这个错误!编译 CPython 需要运行 Python 脚本!有意思。这时我们想起了 Python 是自举的(bootstrapped)。我们回过头看看 Python 开发者文档 然后我们会找到:「大量的 CPython 源码都是用 Python 写的: 在编写这份文档的时候,CPython 源代码中的 Python 已经稍多于 C 了。」

然后我们会想——在 CPython 编译时,难道它用我们正在编译的 Python 来执行 Python 脚本?或者是使用了一个已经编译好的常规的 Python,比如我们系统环境中的 Python?如果它使用的是我们正在编译的这个,我们需要修改这些 .py 文件,把 import 替换成 accio。不然我们要怎样?常规的 Python 只认识 import 而不是 accio

我们来看一眼 Lib 目录下的一个 .py 文件。这是 Lib/keyword.py 的第一行:

1
#! /usr/bin/env python

啊哈!这个脚本使用我们系统的 Python 执行的!我们系统的 Python 只认识 import。所以 keyword.py 需要的是 import 而不是 accio。但是,我们既然在一条 import 语句上遇到了 SyntaxError,这至少意味着在编译过程中,至少有时候我们是需要使用 accio 而不是 import 的。嗯…有什么好办法么?

Yo Dawg, 我听说你喜欢蟒蛇

如果我们做一些疯狂的事情,比如编译一个中介 Python 对 importaccio 支持,然后用这个 Python 来编译只支持 accio 的 Python 呢?(这个点子是 Allison Kaptur的。)

所以,为了中介 Python,我们需要这样来修改 Grammar 文件:

1
2
3
4
5
import_name: 'import' dotted_as_names | 'accio' dotted_as_names
import_from: (('from' ('.'* dotted_name | '.'+)
              'import' ('*' | '(' import_as_names ')' | import_as_names)) |
              ('from' ('.'* dotted_name | '.'+)
              'accio' ('*' | '(' import_as_names ')' | import_as_names)))

importaccio 都应该被这样的 Python 支持 。来编译吧。

1
$ make -s -j2

耶!没有错误!只有我们没有做任何修改之前的那个警告信息!现在我们要修改 $PATH 来把这个 Python 当作我们的系统环境 Python(但仅针对当前的终端会话)。这样,这个中介 Python 将会被用于编译我们那个最终的 Python。让我们给刚刚编译出来的 python.exe 创建一个符号连接,然后将它加入到 $PATH 中:

1
2
3
4
$ mkdir bin
$ cd bin
$ ln -s ../python.exe python
$ export PATH=`pwd`:$PATH

现在,我们需要拷贝整个 cpython 目录来编译我们最后的 Python:

1
2
3
$ cd ../
$ cp -r cpython nagini-python
$ cd nagini-python

我们要修改 Grammar 文件让它只支持 accio

1
2
3
import_name: 'accio' dotted_as_names
import_from: ('from' ('.'* dotted_name | '.'+)
              'accio' ('*' | '(' import_as_names ')' | import_as_names))

然后我们要把每一个 .py 文件中的所有 import 替换成 accio。我们将用一条 bash 命令来完成:

1
$ for i in `find . -name '*.py'`; do sed -i '' 's/[[:<:]]import[[:>:]]/accio/g' $i; done

现在我们仅需编译新的 Python 了!

1
2
3
4
5
$ mkdir bin
$ cd bin
$ ln -s ../python.exe python
$ export PATH=`pwd`:$PATH
$ python

然后我们来试试…

1
2
3
4
5
6
7
8
>>> import sys
  File "<stdin>", line 1
    import sys
             ^
SyntaxError: invalid syntax
>>> accio sys
>>> sys.modules.keys()
['copy_reg', 'sre_compile', '_sre', 'encodings', 'site', '__builtin__', 'sysconfig', '__main__', 'encodings.encodings', 'abc', 'posixpath', '_weakrefset', 'errno', 'encodings.codecs', 'sre_constants', 're', '_abcoll', 'types', '_codecs', 'encodings.__builtin__', '_warnings', 'genericpath', 'stat', 'zipimport', '_sysconfigdata', 'warnings', 'UserDict', 'encodings.ascii', 'sys', '_osx_support', 'codecs', 'os.path', 'sitecustomize', 'signal', 'traceback', 'linecache', 'posix', 'encodings.aliases', 'exceptions', 'sre_parse', 'os', '_weakref']

我了个去成功了!!!!!!

就这么回事儿。就为了个玩笑,我们刚刚编译了两个 Python 糊弄住了源代码。盆友,为你自己干一杯啤酒吧。胜利。

我超级乱的并且还没真打算公开的 GitHub 仓库 包含了这两个版本的 Python,仅供查阅。

Comments