Python社区  »  Python

Boa 如何使用 ES Module 加载 Python 包

设计稿智能生成代码 • 6 天前 • 15 次点击  
阅读 199

Boa 如何使用 ES Module 加载 Python 包

作者:泽阳 
Pipcook contributor

Pipcook 是淘系技术部 D2C 团队研发的一款面向前端开发者的机器学习应用框架,我们希望 Pipcook 能成为前端人员学习和实践机器学习的一个平台,从而推进前端智能化的进程。

Boa 是 Pipcook 背后的技术,通过它,开发者可以调用到任何 Python 的函数,当然就包括:numpy、scikit-learn、jieba 等大家熟知的 Python 库,如果想直接使用 tensorflow、pytorch、tvm 等也不在话下。


在未实现此功能之前,Boa 加载 Python 库是下面这样子的:

const boa = require('@pipcook/boa');
const { range, len } = boa.builtins();
const { getpid } = boa.import('os');
const numpy = boa.import('numpy');
复制代码


通过 boa.import 函数传入 Python 库的名称来进行导入,如果需要加载的包很多的话可能 import() 会显得冗余。


为此我们开发了自定义的导入语义声明,使用 ES Module 实现更简洁的导入语句:

import { getpid } from 'py:os';
import { range, len } from 'py:builtins';
import {
  array as NumpyArray,
  int32 as NumpyInt32,
} from 'py:numpy';
复制代码


上面的功能实现依赖于 Node 的实验性功能 --experimental-loader, 它还有个未公开的别名 --loader, 一开始这个功能刚推出来的时候其实是 --loader , 因为是考虑到是实验性的所以后面的版本加上了 experimental.

--experimental-loader


启动程序的时候通过指定此 flag , 接受指定后缀为 mjs 的文件来实现自定义加载器,mjs 文件提供几种钩子来拦截默认的加载器:

  • resolve
  • getFormat
  • getSource
  • transformSource
  • getGlobalPreloadCode
  • dynamicInstantiate


执行的顺序为:

                                      ( Each `import` runs the process once )                                  
                          |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -|
  ( run once )            |                         ---> dynamicInstantiate             |
getGlobalPreloadCode  ->  | resolve -> getFormat -> |                                   |
                          |                         ---> getSource -> transformSource   |
                          |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|
复制代码

getGlobalPreloadCode Hook


只运行一次,用于在应用程序启动时在全局范围内运行一些代码,可以通过 globalThis 对象将变量挂在到全局范围内,目前只提供一个 getBuiltin(类似 require) 函数加载内置模块:

/**
 * @returns {string} Code to run before application startup
 */
export function getGlobalPreloadCode() {
  return `\
globalThis.someInjectedProperty = 42;
console.log('I just set some globals!');

const { createRequire } = getBuiltin('module');

const require = createRequire(process.cwd() + '/<preload>');
`;
}
复制代码


那么在所有的模块中都可以使用 someInjectedProperty 这个变量了。

resolve Hook


是加载器的入口,拦截 import 语句和 import() 函数,可以对导入的字符串进行判断返回特定逻辑的 url

const protocol = 'py:';

/**
 * @param {string} specifier
 * @param {object} context
 * @param {string} context.parentURL
 * @param {function} defaultResolve
 * @returns {object} response
 * @returns {string} response.url
 */
export function resolve(specifier, context, defaultResolve) {
  if (specifier.startsWith(protocol)) {
    return {
      url: specifier
    };
  }
  return defaultResolve(specifier, context, defaultResolve);
}
复制代码


参数定义:

  • specifier - import 语句 或 import() 表达式中的字符串
// eg:

import os from 'py:os'; // 'py:os'
async function load() {
  const sys = await import('py:sys'); // 'py:sys'
}
复制代码
  • parentURL - 导入该模块的父模块 url
// eg: 
// app.mjs

import os from 'py:os'; // file:///.../app.mjs
复制代码


这里有个细节点,defaultResolve 函数的第一个参数只接受三种协议的字符串:

  • data: - javascript wasm 字符串
  • nodejs: - built-in 模块
  • file: - 第三方或用户自定义模块


Boa 自定义的 py: 协议并不能通过默认 resolve Hook 的参数检查,所以上面做了判断匹配后直接返回 url 传递给 getFormat Hook.

getFormat Hook


提供多种方式定义如何解析 resolve Hook 传递下来的 url:

  • builtin - Node.js 内置模块
  • commonjs - CommonJS 模块
  • dynamic - 动态实例化, 触发 dynamicInstantiate Hook
  • json - JSON 文件
  • module - ECMAScript 模块
  • wasm - WebAssembly 模块


从功能描述来看,只有 dynamic 符合我们的需求:




    
export function getFormat(url, context, defaultGetFormat) {
  // DynamicInstantiate hook triggered if boa protocol is matched
  if (url.startsWith(protocol)) {
    return {
      format: 'dynamic'
    }
  }

  // Other protocol are assigned to nodejs for internal judgment loading
  return defaultGetFormat(url, context, defaultGetFormat);
}
复制代码


url 匹配上 boa 协议则触发 dynamicInstantiate Hook, 其他情况下都交给默认的解析器去判断加载。

dynamicInstantiate Hook


提供不同于 getFormat 几种解析格式的动态加载模块方式:

/**
 * @param {string} url
 * @returns {object} response
 * @returns {array} response.exports
 * @returns {function} response.execute
 */
export function dynamicInstantiate(url) {
  const moduleInstance = boa.import(url.replace(protocol, ''));
  // Get all the properties of the Python Object to construct named export
  // const { dir } = boa.builtins();
  const moduleExports = dir(moduleInstance);
  return {
    exports: ['default', ...moduleExports],
    execute: exports => {
      for (let name of moduleExports) {
        exports[name].set(moduleInstance[name]);
      }
      exports.default.set(moduleInstance);
    }
  };
}
复制代码


使用 boa.import() 加载 Python 模块,并使用 Python builtins  内置的 dir 函数获取模块的全部属性。
钩子需要预先提供导出列表传递给 exports 参数,用于支持 Named exports,加上 default 是为了支持 Default exports
execute 函数在初始化动态钩子时设置指定命名对应的属性。

getSource Hook


用于传递源码字符串,提供不同于默认加载器从磁盘读取文件的方式获取源码,比如网络、内存和硬编码等:

export async function getSource(url, context, defaultGetSource) {
  const { format } = context;
  if (someCondition) {
    // For some or all URLs, do some custom logic for retrieving the source.
    // Always return an object of the form {source: <string|buffer>}.
    return {
      source: `export const message = 'Woohoo!'.toUpperCase();`
    };
  }
  // Defer to Node.js for all other URLs.
  return defaultGetSource(url, context, defaultGetSource);
}
复制代码


这里比较有意思的是可以从不同渠道获取源码,比如实现和 Deno 一样的方式从网络获取源码。

transformSource Hook


等待 getSource Hook 执行完毕加载完源码后,该钩子可以对已加载的源码进行修改操作:

export async function transformSource(source,
                                      context,
                                      defaultTransformSource) {
  const { url, format } = context;
  if (source && source.replace) {
    // For some or all URLs, do some custom logic for modifying the source.
    // Always return an object of the form {source: <string|buffer>}.
    return {
      source: source.replace(`'A message';`, `'A message'.toUpperCase();`)
    };
  }
  // Defer to Node.js for all other sources.
  return defaultTransformSource(
    source, context, defaultTransformSource);
}
复制代码


上面提供的例子将源码中的特定字符串替换成新字符串, 还可以实现即时编译:

export function transformSource(source, context, defaultTransformSource) {
  const { url, format } = context;

  if (extensionsRegex.test(url)) {
    return {
      source: CoffeeScript.compile(source, { bare: true })
    };
  }

  // Let Node.js handle all other sources.
  return defaultTransformSource(source, context, defaultTransformSource);
}
复制代码

最后


相比之前 Node.js 只能用内置的几种方式加载模块,如今开放出来的几种钩子可以组合出很多有趣的功能,大家可以去挖掘更多有意思的场景。

参考链接

相关文章

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/63070
 
15 次点击  
分享到微博