阅读 59

skulpt开发指南(二)模块开发

skulpt模块认知

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

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

import mod
mod.add(1,2)
复制代码

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

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的运算

mod.add = new Sk.builtin.func(function(a, b) {
   return Sk.ffi.remapToJs(a) + Sk.ffi.remapToJs(b);
});
复制代码

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

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代码中加载模块的具体实现:

Sk.configure({
   output: outf,
   read: builtinRead,
   __future__: Sk.python3,
});
复制代码

再来看看builtinRead函数:

// 配置好外部模块匹配路径及模块文件路径,需要加载的外部模块都在这里配置
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:
mod.add(1,2)
复制代码
  • Javascript:
mod.add = new Sk.builtin.func(function(a, b) {
   return Sk.ffi.remapToJs(a) + Sk.ffi.remapToJs(b);
});
复制代码

类声明

  • python:
stack = mod.Stack()
stack.push(1)
stack.pop()
复制代码
  • Javascript:
// 使用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:
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:
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:
mod.sleep(1) // 等待一秒
复制代码
  • javascript:
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:
star=Actor('star')

def update():
  star.x+=1
  star.y+=1
复制代码
  • Javascript:

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

// 每秒调用60次,实现update回调
Sk.globals.update && Sk.misceval.callsimAsync(null, Sk.globals.update);
复制代码

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

调用python回调函数

  • Python:
def callback(result):
	print(result) // 3

mod.add(1,2,callback);
复制代码
  • javascript
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
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:
import mod
stack = mod.Stack()

mod.typeName(stack) // 'Stack'
复制代码
  • javascript:
mod.typeName = new Sk.builtin.func(function(arg) {
    return Sk.abstr.typeName(arg)
})
复制代码

检查传参数量

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

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

TypeError: __init__() takes at least 3 arguments (1 given) on line 4
复制代码

错误处理

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

throw new Sk.builtin.NotImplementedError("Not yet implemented");
复制代码

正常的抛出错误

throw new Sk.builtin.TypeError("Something went wrong");
复制代码

小结

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

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

文章分类
前端
文章标签