15

我最近遇到了Pyodide 项目

我已经使用 Pyodide 构建了一个小演示,但是虽然我花了很多时间查看源代码,但对我来说如何重定向print来自 python 的输出(除了修改 CPython 源代码)并不明显(还),而且,如何将输出从 matplotlib.pyplot 重定向到浏览器。

从源代码来看,FigureCanvasWasm确实有一个show()带有适当后端的方法,用于绘制到浏览器画布 - 但是,我不清楚如何实例化这个类并调用它的show()方法,或者实际上是否有另一种更明显的重定向方式绘制到画布上。

因此,我的问题是:

  1. 如何重定向print()消息
  2. 如何强制 pyodide 在浏览器中绘制 matplotlib 图形?

这是我的测试页面:

<!doctype html>
<meta charset="utf-8">
<html lang="en">
<html>
<head>
    <title>Demo</title>
    <script src="../../pyodide/build/pyodide.js"></script>
</head>
<body>
</body>
    <script type="text/javascript">
      languagePluginLoader.then(() => {
      pyodide.loadPackage(['matplotlib']).then(() => {
          pyodide.runPython(`
                  import matplotlib.pyplot as plt
                  plt.plot([1, 2, 3, 4])
                  plt.ylabel('some numbers')
                  #fig = plt.gcf()
                  #fig.savefig(imgdata, format='png')                  
                  print('Done from python!')`
          );
          //var image = pyodide.pyimport('imgdata');
          //console.log(image);
      });});

    </script>
<html>
4

4 回答 4

12

首先让我们看看我们是否可以在浏览器中显示任何内容;例如一个普通的字符串。Python 变量存储在pyodide.globals属性中。因此,我们可以从那里获取 python 对象并将其放入<div>页面上的元素中。

<!doctype html>
<meta charset="utf-8">
<html>
<head>
    <title>Demo</title>
    <script src="../pyodide/pyodide.js"></script>
</head>
<body>
</body>
    <script type="text/javascript">
      languagePluginLoader.then(() => {
          pyodide.runPython(`my_string = "This is a python string." `);

          document.getElementById("textfield").innerText = pyodide.globals.my_string;
      });

    </script>

    <div id="textfield"></div>
<html>

现在我想我们可以对 matplotlib 图做同样的事情。以下将显示文档中保存的 png 图像。

<!doctype html>
<meta charset="utf-8">
<html lang="en">
<html>
<head>
    <title>Demo</title>
    <script src="../pyodide/pyodide.js"></script>
</head>
<body>
</body>
    <script type="text/javascript">
      languagePluginLoader.then(() => {
      pyodide.loadPackage(['matplotlib']).then(() => {
          pyodide.runPython(`
                import matplotlib.pyplot as plt
                import io, base64

                fig, ax = plt.subplots()
                ax.plot([1,3,2])

                buf = io.BytesIO()
                fig.savefig(buf, format='png')
                buf.seek(0)
                img_str = 'data:image/png;base64,' + base64.b64encode(buf.read()).decode('UTF-8')`
          );

          document.getElementById("pyplotfigure").src=pyodide.globals.img_str

      });});

    </script>

    <div id="textfield">A matplotlib figure:</div>
    <div id="pyplotdiv"><img id="pyplotfigure"/></div>
<html>

我还没有研究过backends.wasm_backend,所以这可能允许上述更自动化的方式。

于 2019-06-19T14:48:32.123 回答
7

使用 wasm 后端时,图形的 canvas 属性是FigureCanvasWasm的一个实例。调用show()画布的方法应该足以在浏览器中显示图形。不幸的是,画布方法中的一个小错误create_root_element()阻止了图形的显示。此方法创建一个div将包含图形的元素。它首先尝试创建一个碘化物输出 div 元素。如果失败,则会创建一个纯 HTML div 元素。然而,这个元素永远不会附加到文档中,因此仍然是不可见的。

以下是来自FigureCanvasWasm的代码行

def create_root_element(self):
    # Designed to be overridden by subclasses for use in contexts other
    # than iodide.
    try:
        from js import iodide
        return iodide.output.element('div')
    except ImportError:
        return document.createElement('div')

该评论表明非碘化物代码是一个需要通过覆盖该方法来扩展的存根。这需要继承 FigureCanvasWasm,将其安装为 pyodide 模块并配置 matplotlib 以使用该后端。

然而,有一个捷径,因为 python 允许覆盖实例的方法,而不修改类,如问题394770 所示。将以下代码放入您的 HTML 文档中会在浏览器中显示一个图形

import numpy as np
from matplotlib import pyplot as plt
from js import document

x = np.linspace(0, 2*np.pi, 100)
y = np.sin(x)

f = plt.figure()
plt.plot(x,y)

# ordinary function to create a div
def create_root_element1(self):
    div = document.createElement('div')
    document.body.appendChild(div)
    return div

#ordinary function to find an existing div
#you'll need to put a div with appropriate id somewhere in the document
def create_root_element2(self):
    return document.getElementById('figure1')

#override create_root_element method of canvas by one of the functions above
f.canvas.create_root_element = create_root_element1.__get__(
    create_root_element1, f.canvas.__class__)

f.canvas.show()

最初,工具栏没有显示图标。我必须在 pyodide 旁边下载、解压缩和安装fontawesome,并在标题中包含以下行来获取这些

<link rel="stylesheet" href="font-awesome-4.7.0/css/font-awesome.min.css">

编辑:关于你问题的第一部分,将输出流重定向到浏览器,你可以看看它是如何在 pyodide 的console.html中完成的。

它用 StringIO 对象替换 sys.stdout

pyodide.runPython(`
    import sys
    import io
    sys.stdout = io.StringIO()
`);

然后运行 ​​python 代码(可以完全忽略它在 wasm 上下文中运行的事实)

pyodide.runPython(`
    print("Hello, world!")
`);

最后,将标准输出缓冲区的内容发送到输出元素

var stdout = pyodide.runPython("sys.stdout.getvalue()")
var div = document.createElement('div');
div.innerText = stdout;
document.body.appendChild(div);
于 2020-01-02T22:48:00.260 回答
1

我为 Python 创建了一个简单的交互式 shell。如果您需要更多详细信息,请阅读我的教程。

const output = document.getElementById("output")
const code = document.getElementById("code")

code.addEventListener("keydown", function (event) {
    if (event.ctrlKey && event.key === "Enter") {
        evaluatePython()
    }
})

function addToOutput(s) {
    output.value += `>>>${code.value}\n${s}\n`
    output.scrollTop = output.scrollHeight
    code.value=''
}

output.value = 'Initializing...\n'
// init pyodide
languagePluginLoader.then(() => { output.value += 'Ready!\n' })

function evaluatePython() {
    pyodide.runPythonAsync(code.value)
        .then(output => addToOutput(output))
        .catch((err) => { addToOutput(err) })
}
<!DOCTYPE html>

<head>
    <script type="text/javascript">
        // this variable should be changed if you load pyodide from different source
        window.languagePluginUrl = 'https://pyodide-cdn2.iodide.io/v0.15.0/full/';
    </script>

    <script src="https://pyodide-cdn2.iodide.io/v0.15.0/full/pyodide.js"></script>
</head>

<body>
    Output:
    </div>
    <textarea id='output' style='width: 100%;' rows='10' disabled></textarea>
    <textarea id='code' value=''  rows='2'></textarea>
    <button id='run' onclick='evaluatePython()'>Run</button>
    <p>You can execute any Python code. Just enter something in the box above and click the button (or Ctrl+Enter).</p>
    <div><a href='https://github.com/karray/truepyxel/demo.html'>Source code</a></div>
</body>

</html>

这是matplotlib. 请注意,这将加载一堆依赖项,最多需要几分钟。

let python_code = `
from js import document
import numpy as np
import scipy.stats as stats
import matplotlib.pyplot as plt
import io, base64

def generate_plot_img():
  # get values from inputs
  mu = int(document.getElementById('mu').value)
  sigma = int(document.getElementById('sigma').value)
  # generate an interval
  x = np.linspace(mu - 3*sigma, mu + 3*sigma, 100)
  # calculate PDF for each value in the x given mu and sigma and plot a line 
  plt.plot(x, stats.norm.pdf(x, mu, sigma))
  # create buffer for an image
  buf = io.BytesIO()
  # copy the plot into the buffer
  plt.savefig(buf, format='png')
  buf.seek(0)
  # encode the image as Base64 string
  img_str = 'data:image/png;base64,' + base64.b64encode(buf.read()).decode('UTF-8')
  # show the image
  img_tag = document.getElementById('fig')
  img_tag.src = img_str
  buf.close()
`

languagePluginLoader.then(()=>pyodide.runPythonAsync(python_code).then(()=>document.getElementById('status').innerHTML='Done!'))
<!DOCTYPE html>

<head>
    <script type="text/javascript">
        // this variable should be changed if you load pyodide from different source
        window.languagePluginUrl = 'https://pyodide-cdn2.iodide.io/v0.15.0/full/';
    </script>

    <script src="https://pyodide-cdn2.iodide.io/v0.15.0/full/pyodide.js"></script>
</head>

<body>
Status: <strong id='status'>Initializing...</strong>
<br><br>
mu:
<input id='mu' value='1' type="number">
<br><br>
sigma:
<input id='sigma' value='1' type="number">
<br><br>
<button onclick='pyodide.globals.generate_plot_img()'>Plot</button>
<br>
<img id="fig" />
</body>

</html>

于 2020-10-18T23:36:39.183 回答
1

要从 pyodide 显示 print() 调用,您可以使用 loadPyodide 上的参数来重定向标准输出:

var paragraph = document.getElementById("p");

pyodide = await loadPyodide({
    indexURL : "https://cdn.jsdelivr.net/pyodide/v0.18.1/full/",
    stdin: window.prompt,
    stdout: (text) => {paragraph.textContent += text;},
    stderr: (text) => {paragraph.textContent += text;}
  });

https://github.com/pyodide/pyodide/blob/main/src/js/pyodide.js

于 2021-10-08T22:09:49.373 回答