Brython 工作原理 - 中文

Brython 目的是可以用 Python 代替 Javascript 同样支持前端应用开发。

典型的在 HTML 中使用 Brython 的例子

  1. <html>
  2. <head>
  3. <script src="/path/to/brython.js"></script>
  4. </head>
  5. <body onload="brython()">
  6. <script type="text/python">
  7. ...
  8. </script>
  9. </body>
  10. </html>
  11. 复制代码

brython.js 是处理特定任务的各个脚本最小组合,这些任务可以是在编译时(将 Python 代码转换成 JS 代码)或运行时(Python 内置对象的实现,如:py_list.js 中的 listtuplepy_string.js 中的 str 等),开发时都是独立的脚本,然后使用 /scripts/make_dist.py 脚本打包成 brython.js

brython.js 暴露两个全局的参数: 一个是用来在页面加载时的回调方法 brython 和一个包含了运行 Python 脚本所需全部内置对象的 __BRYTHON__

brython() 会检查页面中所有的 scripts,如果 script 标签设置了 type="text/python",那么它将被当做 Python 代码读取然后解析成 Javascript,并且最后通过 eval() 或者 new Function(func_name, source)(module) 的方式执行,第二种形式用来避免在某些浏览器上产生内存泄漏。

如果 script 标签有 src 属性,就会通过 Ajax 请求的方式获取文件的内容,然后像上边那样解析和执行。

Brython 将 Python 转换成 Javascript 的流程如下:

tokenizer 读取源码中的 tokens,并主动将他们解析成抽象代码树或者提出语法错误 SyntaxError 或者缩进错误 IndentationError

  • 将该语法书进行转换(添加或修改节点),将 Python 语句转换成 Javascript 语句。
  • 如果设置了调试等级,就会添加一些而外的节点到内置对象上用来设置脚本的名称和行号。
  • 被转换后的语法书支持一个 to_js() 的方法,会返回转换后的 Javascript 代码。

所有这些操作在 py2js.js 中完成:

  • brython() 是脚本中最后一个方法。
  • py2js() 是负责完成代码转换的方法。
  • py2js() 方法调用了 tokenize() 生成的 tokenizer。
  • tokenizer 构建的语法树是 $Node 类的实例。
  • 每个新语句都会穿检出一个 $Node 实例,并会为这个实例生成一个上下文 context。
  • 新的 tokens 通常可以使用下边的形式来改变 context 的状态
  1. context = transition(context, token_type, token_value)
  2. 复制代码
  • context 是脚本中一些类的实例,这些类都是以 $ 开头并以 Ctx 结尾,举个例子:当 tokenizer 遇到关键词 try 时,transition() 方法会返回一个 $TryCtx 的实例。

Brython 内置对象

__BRYTHON__ 有一个 builtins 的属性,该属性存储了所有的 Python 内置对象的名称(classes, functions, execptions, objects),与 Python 语法相对应,如 Python 中的 int 对应了 __BRYTHON_.builtins.int。只有与 Javascripts 命名规则冲突了的名称需要被更改,如 super() 被更改为 __BRYTHON__.builtins.?super

Python 对象的实现

Python 对象都会用原生的 Javascript 对象实现。

  • Python 字符串 string 用 Javascript 字符串 string 实现。

  • Python 列表 list 和元组 tuple 用 Javascript 数组实现 array 实现。

  • Python 整数 integer 如果在 Javascript 数字安全范围内 [-2^53-1, 2^53-1]会用 number 实现,超出范围会用一个 internal 类实现。

  • Python 浮点数 float 会用 javascript 数字类 Number 实现。

  • 其他的 Python 类(包括内置和用户自定义的),都会作为 Javascript 对象实现,并保留该类的属性和方法。

最小的类的实现代码如下:

  1. $B.make_class = function(name, factory) {
  2. // Builds a basic class object
  3. var A = {
  4. __class__: _b_.type,
  5. __mro__: [object],
  6. __name__: name,
  7. $is_class: true
  8. }
  9. A.$factory = factory
  10. return A
  11. }
  12. 复制代码

factory 是创建类实例的方法,实例的 __class__ 属性设置为 class 对象,这个类的 __mro__ 属性用于在类实例上进行属性解析。

Python 函数 Functions 的实现

Python functions 同样作为 Javascript functions 实现,但是他们在函数的定义和调用上有很多的不同。

定义一个 Python 函数,可以通过多种方式指定他的参数,如:

  1. # 命名参数
  2. def f(x):
  3. # 带默认值的参数
  4. def f(x=1):
  5. # 可变参数
  6. def f(*x):
  7. # 关键字参数
  8. def f(**x)
  9. 复制代码

Python 的函数调用也可以通过以上的几种方式调用:固定位置的参数 f(2),带关键字的参数 f(x=2),包装后的迭代参数 f(*args),以及包装后字典参数 f(**kw)

Javascript 同样有多种方式处理参数:命名参数 function f(x),以及在函数内部使用 arguments 对象处理参数,如 function f() {var x = arguments[0]}。函数的调用可以直接用函数名加参数 f(x),也可以用 callapply 方法调用。

函数调用时,被传到 Python 函数中的参数时通过以下方式转换的:

  • 固定位置的参数保持不变
  • 元组参数会被展开成固定位置参数
  • 关键字参数包括字典参数,会被冯成一个单独的参数放在参数列表的最后一位,这个参数是一个 Javascript - 对象,该对象有两个 keys$nat 值为 "kw"kw 值是根据顺序排列的关键字参数的 key-value 对象。

示例如下:

  1. f(1, *t, x=2, **d)
  2. # t=['a', 'b'], d={'z': 99} 会被转换成
  3. f(1, 'a', 'b', {$nat: 'kw', kw: {x: 2, z: 99}}})
  4. 复制代码

Python 函数的定义会被转换成 Javascript 函数定义,参数值使用 argument 对象设置在函数体的开始位置,argument 是通过 py_utils.js 中的 $B.args 函数设置。这个函数采用从 Python 函数初始化得来的参数:

  1. $B.args = function($fname, argcount, slots, var_names, $args, $dobj, extra_pos_args, extra_kw_args)
  2. 复制代码
  • $fname 是 Python 的函数名。
  • argcount 是函数所需命名参数的数量,不包含带默认值的参数和关键字参数。
  • slots 是一个 Javascript 对象,key 值依次是函数所需的命名参数,值都是 null
  • var_names 是函数所需命名采纳数的列表,与 Object.keys(slots) 等效,出于性能原因,列表在函数主题中显示创建,而不是每次函数调用时创建。
  • $args 保存了传递给函数的可被遍历的参数,通常设置为 Javascript 内置的 arguments 对象。
  • $dobj 是一个保存了命名参数默认值得 Javascript 字面量,如果没有默认值就设为 {}
  • extra_pos_args 是额外位置参数的名称,没有就是 null
  • extra_kw_args 是二外关键词参数的名称,没有就是 null

示例如下:

  1. # python 函数定义
  2. def f(x):
  3. // 转成 Javascript 函数
  4. var $ns = $B.args('f', 1, {x: null}, ['x'], arguments, {}, null, null)
  5. 复制代码
  1. # python 函数定义
  2. def f(x, y=1)
  3. // 转成 Javascript 函数
  4. var $ns = $B.args('f', 2, {x: null, y:null}, ['x', 'y'], arguments, {y: 1}, null, null)
  5. 复制代码
  1. # python 函数定义
  2. def f(x, *t)
  3. // 转成 Javascript 函数
  4. var $ns = $B.args('f', 1, {x: null}, ['x'], arguments, {}, 't', null)
  5. 复制代码
  1. # python 函数定义
  2. def f(x, y=1, *t, **d)
  3. // 转成 Javascript 函数
  4. var $ns = $B.args('f', 2, {x: null, y: null}, ['x', 'y'], arguments, {y: 1}, 't', 'd')
  5. 复制代码

$B.args 检查传递给函数的参数,并在缺少参数或意外参数时抛出异常。否则返回的对象将按传递的参数名称以及附加参数的持有者名称索引。如:在最后一个示例中,$ns 将包含 xytd

命名解析

一个 Python 程序会被分成几个块:模块 modules、函数 functions、类 classes、解析式 comprehensions,Brython 为每个块定义了一个 Javascript 变量,该变量将保留该块中绑定的所有者名称(成为块名称对象)。

基于词法分析,包括 globalnonlocal 关键词,通常可以知道名称绑定在哪个块中,它被翻译为与块名称对象相同名称的属性。 当名称被引用(如:print(x))且未绑定(如:x=1)时,转换实际上是对 check_def('a', X['a']) 函数的调用,其中 X 是块名称对象,check_def(name, obj) 是一个检查 obj 是否定义的函数,如果未定义就会为这个名称抛出 NameErrorUnboundLocalError 的错误。这样做是因为如果名称绑定在块中的某个位置,则在引用该名称时可能尚未绑定,如:

  1. # example 1 : raises NameError
  2. def f():
  3. a
  4. a = f()
  5. # example 2 : raises NameError
  6. class A:
  7. def __init__(self):
  8. a
  9. a = A()
  10. # example 3 : raises NameError
  11. if False:
  12. a = 0
  13. a
  14. # example 4 : raises UnboundLocalError
  15. def f():
  16. if False:
  17. a = 9
  18. a
  19. f()
  20. 复制代码

如果语法解析表示明确定义了引用名称,它将被简单的转换为 X['a'],这种情况是名称已在之前行的块级别中被绑定,而不是在缩进级别中,如:

  1. x = 0
  2. print(x)
  3. 复制代码

无法确定块的唯一情况是该程序是通过 from some_module import * 导入的,在这种情况下:

  • 无法知道脚本中引用的名称如 range 是内置的 range 类,还是 some_modules 导入的某个名称。
  • 如果引用了未在脚本中明确绑定的名称,那么愈发分享就无法确定是否应该引发 NameError。 这种情况下,名称将转换为对函数的调用,改函数将在运行时根据模块实际导入的名称选择值,或引发 NameError。

执行帧

Brython 通过 stack 来处理执行帧。每次程序进入一个新的模块或函数时(包括匿名行数和解析式),有关全局和局部环境的信息将置于栈顶,当函数或模块退出时,栈顶的元素会被移除。

这是通过调用插入到生成的 Javascript 代码中的 enter_frame() 和 leave_frame() 函数来完成的。

内置函数 globals() 和 locals() 使用该栈,并在出现异常时构建回溯信息。

indexedDB 缓存标准库模块

这个功能需要两个条件:

浏览器必须支持 indexedDB 数据库 页面必须引用 brython_stdlib.js,或者 CPython 生成的简化版的 brython_modules.js

该功能主要是将 stdlib 模块的 Javascript 版本代码存储到 indexedDB 数据库中:对于每个新版本的 Brython 转换仅进行一次,生成的 Javascript 存储在客户端而不是通过网络发送,并且 indexedDB 可以轻松处理几兆的数据。 不行的是,indexedDB 操作是异步的,而 import 是阻塞的。如:

  1. import datetime
  2. print(datetime.datetime.now())
  3. 复制代码

在运行时无法使用 indexedDB 获取 datetime 模块,因为 import 语句后的代码不在 indexedDB 异步请求完成时的回调函数中。

解决方法是在转码时扫描脚本,对于源代码中的每个 import 语句,要导入的模块名都存储在列表中。当转码结束时,Brython 会进入一个使用任务栈的执行循环(在 py2js.js 中定义的函数 loop())。可能的任务如下:

  • 调用函数 inImported() 检查模块是否已经在已导入的模块中,如果在则返回到 loop()
  • 如果没有,添加一个任务到栈中:调用函数 idb_get(),该函数向 indexedDB 数据库发送请求,以查看 Python 模块的 Javascript 版本是否已存储,添加任务后,返回 loop()
  • 在请求的回调函数中(function idb_load()):
    • 如果数据库存在 Javascript 版本代码,则将其存储在 Brython 变量(__BRYTHON__.precompiled)中,然后返回 loop()
    • 否则,该模块的 Python 源码(在 brython_stdlib.js 中)将被转换,并添加另一个任务到堆栈中:一个存储 Javascript 代码到 indexDB 数据库的请求。该请求的回调函数会添加另外一个任务:调用 idb_get() ,这次肯定会成功。
  • 该堆栈最后一个任务是执行最初的脚本。

运行时,当导入标准库中的模块时,将执行存储在 __BRYTHON__.precompiled 中的 Javascript 以前已经进行了 Python 到 Javascript 的转换。

缓存更新 indexDB 数据库与浏览器关联并可以在浏览器请求、关闭浏览器、重启电脑等时保持不变。当 stdlib 中的 Python 源码更改或转换引擎更改时,上述过程必须定义一种更新数据库中存储 Javascript 版本的方法。 为此,缓存更新依赖时间戳,每个 Brython 版本都带有时间戳,由脚本 make_dist.py 更新。当 stdlib 中的脚本被预编译并存储在 indexedDB 数据库中时,数据库中的记录将时间戳字段设置为此 Brython 时间戳。如果 HTML 使用了新的 Brython 版本,它会有不同的时间戳,并在 idb_load() 的结果中执行新的转换。 如果使用 brython_modules.js 而不是 brython_stdlib.js,则会定义一个补充时间戳。

局限性 依赖对 import moduleXfrom moduleY import foo 的静态语法分析来检测需要导入的模块。它不适用于使用内置函数 __import__() 执行导入,也不适用于传递给 exec() 的代码。在这些情况下,将使用先前在每次页面加载时进行即时编译的解决方案。

该机制仅针对标准库中的模块或 brython_modules.js 中的模块实现。目前,尚未实现将其用于站点包或应用程序目录中的模块。


原文地址:github.com/brython-dev…

卡内基梅隆大学在线 Python ide:academy.cs.cmu.edu/ide