skulpt模块认知

通过上一篇的讲解我们应该知道skulpt实现python模块是用javascript写的并且由skulpt提供的一些jsAPI来使python能够以想要的形式来调用。

比如我想在python调用模块内的某个方法是这么写的:

1
2
import mod
mod.add(1,2)

那么js编写的模块就可以这么声明这个add函数:

1
2
3
4
5
6
7
8
var $builtinmodule = function (name) {
var mod = {__name__: new Sk.builtin.str("mod")}
// 使用Sk.builtin.func能让python理解这是个函数
mod.add = new Sk.builtin.func(function(a, b) {
return Sk.ffi.remapToJs(a) + Sk.ffi.remapToJs(b);
});
return mod;
}

这里要讲两个非常重要的api:Sk.ffi.remapToJsSk.ffi.remapToPy

在上面的这段函数声明里,参数a和b都是从python传进来的,可能会存在与js语言数据类型不一致导致的错乱,所以需要通过Sk.ffi.remapToJs将python的参数转成js再来进行js的运算

1
2
3
mod.add = new Sk.builtin.func(function(a, b) {
return Sk.ffi.remapToJs(a) + Sk.ffi.remapToJs(b);
});

而js的逻辑想要给python用的话最好也要用Sk.ffi.remapToPy转一下:

1
2
3
4
mod.add = new Sk.builtin.func(function(a, b) {
const result = Sk.ffi.remapToJs(a) + Sk.ffi.remapToJs(b)
return Sk.ffi.remapToPy(result);
});

加载模块

在上一篇中有讲到最简单的模块实现,其中html的部分就包含了加载外部模块的能力

在下面的这段skulpt初始化配置的代码里有一个read钩子函数builtinRead,这个钩子函数就是用来处理python代码中加载模块的具体实现:

1
2
3
4
5
Sk.configure({
output: outf,
read: builtinRead,
__future__: Sk.python3,
});

再来看看builtinRead函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 配置好外部模块匹配路径及模块文件路径,需要加载的外部模块都在这里配置
var externalLibs = {
"./mod/__init__.js": "./mod.js",
};
function builtinRead(file) {
/**
* file参数代表当前加载的模块路径,一个模块名会按照以下6种路径和优先级查找,假设模块名为mod:
* src/builtin/mod.js
* src/builtin/mod/__init__.js
* src/lib/mod.js
* src/lib/mod/__init__.js
* ./mod.js
* ./mod/__init__.js
* 前面四种路径一把是skulpt匹配自带模块用的。
* */
console.log("Attempting file: " + Sk.ffi.remapToJs(file));

// 匹配外部模块
if (externalLibs[file] !== undefined) {
// 使用skulpt提供的promiseToSuspension,等待异步任务执行完才能继续
return Sk.misceval.promiseToSuspension(
fetch(externalLibs[file]).then(
function (resp){ return resp.text(); }
));
}

if (Sk.builtinFiles === undefined || Sk.builtinFiles.files[file] === undefined) {
throw "File not found: '" + file + "'";
}

// 匹配不到外部模块再从内置模块找
return Sk.builtinFiles.files[file];
}

在上面代码中的Sk.misceval.promiseToSuspensionAPI会非常有用,在javascript中经常有依赖的异步任务(如加载外链的js、实现sleep函数等)需要等待回调完成才能继续后面的python逻辑,Sk.misceval.promiseToSuspension就是处理这种问题的API,它的传参是一个promise对象,只要promise内部在什么时候resolve就可以决定在什么时候交回主线程。

python调用js模块

python调用js模块功能的方式肯定不是只有单纯的函数调用一种方式,下面我会列出常见的python调用方式对应的js模块的写法:

函数声明

  • python:
1
mod.add(1,2)
  • Javascript:
1
2
3
mod.add = new Sk.builtin.func(function(a, b) {
return Sk.ffi.remapToJs(a) + Sk.ffi.remapToJs(b);
});

类声明

  • python:
1
2
3
stack = mod.Stack()
stack.push(1)
stack.pop()
  • Javascript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用Sk.misceval.buildClass声明class
mod.Stack = Sk.misceval.buildClass(mod, function($gbl, $loc) {
// $loc.__init__就是构造器函数
$loc.__init__ = new Sk.builtin.func(function(self) {
// self就是当前上下文
// self.xxx扩展私有成员变量
self.stack = [];
});
// $loc.xx 扩展外部成员变量,在类里的函数声明第一个参数被self保留,第二个参数开始才算参数
$loc.push = new Sk.builtin.func(function(self,x) {
self.stack.push(x);
});
$loc.pop = new Sk.builtin.func(function(self) {
return self.stack.pop();
});
}, "Stack", []);

PS: 类声明里的函数声明第一个参数被self保留,注意是第二个参数开始才是真的参数,与正常的函数声明有所不同

实例对象属性监听

当我的js模块想要监听在python实例化对象的某个属性变化时:

  • Python:
1
2
3
4
5
6
7
stack = mod.Stack()
stack.push(1)
stack.push(2)
print(stack.stack) // [1,2]

stack.stack = [2,3,4]
print(stack.stack) // [2,3,4]
  • Javascript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
mod.Stack = Sk.misceval.buildClass(mod, function($gbl, $loc) {
// $loc.__init__就是构造器函数
$loc.__init__ = new Sk.builtin.func(function(self) {
// self就是当前上下文
// self.xxx扩展私有成员变量
self.stack = [];
});
// $loc.xx 扩展外部成员变量
$loc.push = new Sk.builtin.func(function(self,x) {
self.stack.push(x);
});
$loc.pop = new Sk.builtin.func(function(self) {
return self.stack.pop();
});
// getter
const stackGetter = new Sk.builtin.func(function(self) {
return Sk.ffi.remapToPy(self.stack)
})
// setter
const stackSetter = new Sk.builtin.func(function (self, val) {
const newStack = Sk.ffi.remapToJs(val)
self.stack = newStack;
})
// 对属性stack进行监听,相当于js的defineProperty的作用
$loc.stack = Sk.misceval.callsim(Sk.builtins.property, stackGetter, stackSetter);

}, "Stack", []);

异步任务

当python需要等待js的异步任务完成才继续的时候可以用Sk.misceval.promiseToSuspension来实现,比如我想在python实现一个sleep函数:

  • Python:
1
mod.sleep(1) // 等待一秒
  • javascript:
1
2
3
4
5
6
7
mod.sleep = new Sk.builtin.func(function(delay) {
return new Sk.misceval.promiseToSuspension(new Promise(function(resolve) {
Sk.setTimeout(function() {
resolve(Sk.builtin.none.none$);
}, Sk.ffi.remapToJs(delay)*1000);
}));
});

Sk.misceval.promiseToSuspension接收一个promise,只要promise内部在什么时候resolve就可以决定在什么时候交回主线程。

js模块调用python

调用python全局变量

有些python库会需要主程序声明一些全局函数来让python库能够作为回调函数告知外部触发,比如pygame-zero的update事件:

  • Python:
1
2
3
4
5
star=Actor('star')

def update():
star.x+=1
star.y+=1
  • Javascript:

skulpt提供Sk.globals使js能调用python主程序的全局变量

1
2
// 每秒调用60次,实现update回调
Sk.globals.update && Sk.misceval.callsimAsync(null, Sk.globals.update);

Sk.misceval.callsimAsync可以作为异步任务触发python的函数,防止阻塞主线程

调用python回调函数

  • Python:
1
2
3
4
def callback(result):
print(result) // 3

mod.add(1,2,callback);
  • javascript
1
2
3
4
5
mod.add = new Sk.builtin.func(function(a, b, cb) {
const result = Sk.ffi.remapToJs(a) + Sk.ffi.remapToJs(b);
// Sk.misceval.callsim调用python传过来的函数
Sk.misceval.callsim(cb, result);
});

类型校验

类型判断

Sk.abstr.typeName用于判断变量类型,返回字符串格式的类型名,已知的类型名如下:

  • int
  • str
  • list
  • tuple
  • dict
  • float
  • lng
  • set
  • bool
1
2
3
4
5
6
new Sk.builtin.func(function (description) {
// Sk.abstr.typeName判断参数的数据类型
if (Sk.abstr.typeName(description) !== "str") {
throw new Sk.builtin.TypeError("Error description should be a string");
}
})

Sk.abstr.typeName还可以得到实例对象所属的类名,类似js的xxx.constructor.name

  • python:
1
2
3
4
import mod
stack = mod.Stack()

mod.typeName(stack) // 'Stack'
  • javascript:
1
2
3
mod.typeName = new Sk.builtin.func(function(arg) {
return Sk.abstr.typeName(arg)
})

检查传参数量

  • Javascript:
1
2
// 指定函数名为__init__的函数传参数量至少3个,最多5个
Sk.builtin.pyCheckArgs('__init__', arguments, 3, 5, false, false);

如果不符合规则会抛出一段内部错误:

1
TypeError: __init__() takes at least 3 arguments (1 given) on line 4

错误处理

对于未实现的功能抛出错误

1
throw new Sk.builtin.NotImplementedError("Not yet implemented");

正常的抛出错误

1
throw new Sk.builtin.TypeError("Something went wrong");

小结

这篇的内容全部都掌握了的话对于开发一个能适应各种python调用形式的skulpt模块基本是完全足够的,你甚至可以拿现有的js库封装成相对应同样功能的python库,比如用echarts封装成pyecharts的用法,这样你就可以很快的实现了一个能够在线运行的pyecharts了,还有各种游戏库、3D库都是可以实现的。

用javascript的生态开拓python在线运行的市场,也不失为一种优秀的策略。