原标题:在Rust代码中编写Python是种怎样的体验?

作者 | Mara Bos,Rust资深工程师

译者 | Arvin,编辑 | 屠敏

来源 | CSDN(ID:CSDNnews)

大约一年前,我发布了一个名为inline-python(https://crates.io/crates/inline-python)的Rust类库,它允许大家使用python!{ .. }宏轻松地将一些Python混合到Rust代码中。在本系列中,我将从头展示开发此类库的过程。

预览

如果不熟悉inline-python类库,你可以执行以下操作:

fn main{

letwho = "world";

letn = 5;

python! {

fori inrange( 'n):

print(i, "Hello", ' who)

print( "Goodbye")

}

}

它允许你将Python代码直接嵌入Rust代码行之间,甚至直接在Python代码中使用Rust变量。

我们将从一个比这个简单得多的案例开始,然后逐步努力以达到这个结果(甚至更多!)。

运行Python代码

首先,让我们看一下如何在Rust中运行Python代码。让我们尝试使第一个简单的示例生效:

fn main{

println!( "Hello ...");

run_python( "print("... World!")");

}

我们可以使用std::process::命令来运行python可执行文件并传递python代码,从而实现run_python,但如果我们希望能够定义和读回Python变量,那么最好从使用PyO3库开始。

PyO3为我们提供了Python的Rust绑定。它很好地包装了Python C API,使我们可以直接在Rust中与各种Python对象交互。(甚至在Rust中编写Python库,但这是另一个主题。)

它的Python::run 功能完全符合我们的需求。它将Python代码作为&str,并允许我们使用两个可选的PyDicts 来定义范围内的任何变量。让我们试一试吧:

fn run_python(code: &str) {

let py = pyo3::Python::acquire_gil; // Acquire the 'global interpreter lock', asPython isnotthread-safe.

py.python.run(code, None, None).unwrap; // No locals, no globals.

}

$ cargo run

Compiling scratchpad v 0. 1.0

Finished dev [unoptimized + debuginfo] target(s) in0. 29s

Running `target/debug/scratchpad`

Hello ...

... World!

看,这就成功了!

基于规则的宏

在字符串中编写Python不是最便捷的方法,所以我们尝试改进它。宏允许我们在Rust中自定义语法,所以让我们尝试一下:

fn main {

println!( "Hello ...");

python! {

print( "... World!")

}

}

宏通常是使用macro_rules!进行定义,您可以基于标记和表达式之类的内容使用高级“查找和替换”规则来定义宏。(有关macro_rules!的介绍请参见Rust Book中有关宏的章节,有关Rust宏所有的细节都可以在《Rust宏的小书》中找到。)

由macro_rules!定义的宏在编译时无法执行任何代码,这些宏仅是应用了基于模式的替换规则。它非常适合vec![],甚至是lazy_static!{ .. },但对于解析和编译正则表达式(例如regex!("a.*b"))之类的功能而言,还不够强大。

在宏的匹配规则中,我们可以匹配表达式,标识符,类型和许多其他内容。由于“有效的Python代码”不是一个选项,所以我们只能让宏接受所有内容:大量的原始的符号:

macro_rules! python {

($($code :tt)*) => {

...

}

}

(有关macro_rules!工作原理的详细信息,请参见上面链接的资源。)

对宏的调用应该产生run_python(".."),这是一个包裹了所有Python代码的字符串文本。幸运的是:有一个内建宏为我们把内容放到一个字符串里,叫做stringify!,因此我不必从头开始。

macro_rules! python {

($($code :tt)*) => {

run_python(stringify!($($code)*));

}

}

结果如下:

$ cargo r

Compiling scratchpad v 0. 1.0

Finished dev [unoptimized + debuginfo] target(s) in0. 32s

Running `target/debug/scratchpad`

Hello ...

... World!

如愿以偿得到了期望结果!

但是,如果我们有不止一行的Python代码会怎样?

fn main {

println!( "Hello ...");

python! {

print( "... World!")

print( "Bye.")

}

}

$ cargo r

Compiling scratchpad v0 .1.0

Finished dev [unoptimized + debuginfo] target(s) in0.31s

Running `target/debug/scratchpad`

Hello ...

thread 'main'panicked at 'called `Result::unwrap` on an `Err` value: PyErr { type: Py(0x7f1c0a5649a0, PhantomData) }', src/main.rs: 9: 5

note: run with`RUST_BACKTRACE=1`environment variable to display a backtrace

很不幸,我们失败了。

为了进行调试,我们需要正确输出PyErr,并显示我们传递给Python::run的确切Python代码:

fn run_python(code: &str) {

println!( "-----");

println!( "{}", code);

println!( "-----");

let py = pyo3::Python::acquire_gil;

iflet Err(e) = py.python.run(code, None, None) {

e. print(py.python);

}

}

$ cargo r

Compiling scratchpad v0 .1.0

Finished dev [unoptimized + debuginfo] target(s) in0.27s

Running `target/ debug/scratchpad`

Hello ...

-----

print( "... World!") print( "Bye.")

-----

File "", line 1

print( "... World!") print( "Bye.")

^

SyntaxError: invalid syntax

很显然,两行Python代码落在同一行,在Python中这是无效的语法。

现在我们遇到了必须克服的最大问题:stringify!把空白符搞乱了.

空白符和符号

让我们仔细研究一下stringify!:

fn main{

println!( "{}", stringify!(

a 123b c

x( y + z)

// comment

...

)) ;

}

$ cargo r

Compiling scratchpad v 0. 1.0

Finished dev [unoptimized + debuginfo] target(s) in0. 21s

Running `target/debug/scratchpad`

a 123b c x(y + z) ...

它不仅删除了所有不必要的空格,还删除了注释。因为它的工作原理是处理单词(token),不再是源代码里面的:a,123,b等。

Rustc编译器做的第一件事就是将源代码分为单词,这使得解析后的工作更容易进行,不必处理诸如1,2,3,这样的个别字符,只需处理诸如“integer literal 123”这样的单词。另外,空白和注释在分词之后就消失了,因为它们对编译器来说没有意义。

stringify!是一种将一串单词转换回字符串的方法,但它是基于“最佳效果”的:它将单词转换回文本,并且仅在需要时才在单词周围插入空格(以避免将b、c转换为bc)。

所以这是一个死胡同。Rustc不小心把宝贵的空白符丢掉了,但这在Python中非常重要。

我们可以尝试猜测一下哪些代码的空格必须用换行符代替,但是缩进肯定会成为一个问题:

fn main {

leta = stringify!(

ifFalse:

x

y

);

letb = stringify!(

ifFalse:

x

y

);

dbg!(a);

dbg!(b);

dbg!(a == b);

}

$ cargo r

Compiling scratchpad v0 .1.0

Finished dev [unoptimized + debuginfo] target(s) in0.20s

Running `target/debug/scratchpad`

[ src/main.rs:12] a = "if False : x y"

[ src/main.rs:13] b = "if False : x y"

[ src/main.rs:14] a == b = true

这两个Python代码片段有不同的含义,但是stringify!给了我们相同的结果。

在放弃之前,让我们尝试一下其他类型的宏。

过程宏

Rust的过程宏是定义宏的另一种方法。尽管macro_rules!只能定义“函数样式的宏”(带有!标记的宏),过程宏也可以定义自定义派生宏(例如#[derive(Stuff)])和属性宏(例如#[stuff])。

过程宏是作为编译器插件实现的。您需要编写一个函数,该函数可以访问编译器看到的单词流,然后就可以执行所需的任何操作,最后需要返回一个新的单词流供编译器使用(或者用于自定义的用途):

#[proc_macro]

pub fn python( input: TokenStream) -> TokenStream{

todo!

}

上述单词流不够好。因为我们需要源代码,而不仅仅是单词。虽然目前还没有成功,但是让我们继续吧,也许过程宏更大的灵活性能够解决问题。

由于过程宏在编译过程中运行Rust代码,因此它们需要使用单独的proc-macro类库中,这个类库在您编译其他内容之前已经被编译好。

$ cargo new--lib python-macro

Created library `python-macro`package

查看python-macro/Cargo.toml:

[lib]

proc-macro= true

查看Cargo.toml:

[dependencies]

python-macro= { path = "./python-macro"}

让我们从一个只有panics (todo!)的实现开始,在输出TokenStream之后:

// python-macro/src/lib.rs

extern crate proc_macro;

useproc_macro:: TokenStream;

#[proc_macro]

pub fn python(input: TokenStream) -> TokenStream {

dbg!(input.to_string);

todo!

}

// src/main.rs

usepython_macro:: python;

fn main {

println!( "Hello ...");

python! {

print( "... World!")

print( "Bye.")

}

}

$ cargo r

Compiling python-macro v0 .1.0

Compiling scratchpad v0 .1.0

error[E0658]: procedural macros cannot be expanded to statements

--> src/main.rs: 5: 5

|

5| / python! {

6| | print( "... World!")

7| | print( "Bye.")

8| | }

| |_____^

|

= note: see issue #54727 for more information

= help: add ` #![feature(proc_macro_hygiene)]` to the crate attributes to enable

天啊,这里发生了什么?

Rust错误为“ 过程宏不能扩展为语句 ”,以及有关启用“hygienic macros”的内容。Macro hygiene是Rust宏的出色功能,不会意外地将任何名称“泄漏”给外界(反之亦然)。如果宏扩展使用了名为的x的临时变量,则它将与宏外部的任何代码中出现的变量x分开。

但是,此功能对于过程宏还不稳定。因此,过程宏除了作为一个单独的项(例如在文件范围内,但不在函数内)之外,不允许出现在任何地方。

接下来,我们会发现存在一个非常可怕但令人着迷的解决方法—让我们启用实验功能#![feature(proc_macro_hygiene)]并继续我们的冒险。

(如果你将来读到这篇文章时,proc_macro_hygiene已经稳定下来了:你可以跳过最后几段。^ ^)

$ sed -i '1i#![feature(proc_macro_hygiene)]'src/main.rs

$ cargo r

Compiling scratchpad v0 .1.0

[python-macro/src/lib.rs: 6] input.to_string = "print("... World!") print("Bye.")"

error: proc macro panicked

--> src/main.rs:6:5

|

6| / python! {

7| | print( "... World!")

8| | print( "Bye.")

9| | }

| |_____^

|

= help: message: notyet implemented

error: aborting due to previous error

error: could notcompile `scratchpad`.

在向我们展示了它的字符串输入参数之后,我们的过程宏即如预期般地崩溃了:

print( "... World!") print( "Bye.")

正如预期的那样,空白符再次被丢弃了。:(

是时候选择放弃了。

不过或者..也许有一种方法可以解决这个问题。

重建空白符

尽管rustc编译器只在解析和编译时使用单词,但是在某种程度上它仍然可以准确地知道何时报告错误。单词中没有换行符,但是它仍然知道我们的错误发生在第6到第9行。那它如何做到的?

事实证明,单词中包含很多信息。它们包含一个Span,是单词在源文件中的开始和结束的位置。Span可以告诉单词在哪个文件、行和列编号处开始和结束。

如果我们能够得到这些信息,我们就可以通过在单词之间放置空格和换行符来重新构造空白符,以匹配它们的行和列信息。

提供这些信息的函数还不稳定,而且还没有#![feature(proc_macro_span)]。让我们启用它,看看我们得到了什么:

#![feature(proc_macro_span)]

extern crate proc_macro;

useproc_macro:: TokenStream;

#[proc_macro]

pub fn python(input: TokenStream) -> TokenStream {

fort in input {

dbg!(t.span.start);

}

todo!

}

$ cargo r

Compiling python-macro v0 .1.0

Compiling scratchpad v0 .1.0

[ python-macro/src/lib.rs:9] t.span.start = LineColumn {

line: 7,

column: 8,

}

[ python-macro/src/lib.rs:9] t.span.start = LineColumn {

line: 7,

column: 13,

}

[ python-macro/src/lib.rs:9] t.span.start = LineColumn {

line: 8,

column: 8,

}

[ python-macro/src/lib.rs:9] t.span.start = LineColumn {

line: 8,

column: 13,

}

真棒!我们得到了一些数据。

但是只有四个单词了。原来("... World!") 这里只出现一个单词,而不是三个((,"... World!",和))。如果看一下TokenStream的文档,我们会发现它并没有提供单词流,而是单词树。显然,Rust的词法分析器已经匹配了括号(以及大括号和方括号),并且它不仅给出了线性的单词列表,而且还给出了单词树。括号内的单词可以看成是某个单词组的后代。

让我们修改过程宏以递归地遍历组内的所有单词(并改进一下输出):

#[proc_macro]

pub fn python( input: TokenStream) -> TokenStream {

print( input);

todo!

}

fn print( input: TokenStream) {

fort ininput{

iflet TokenTree::Group(g) = t {

println!( "{:?}: open {:?}", g.span_open.start, g.delimiter);

print(g.stream);

println!( "{:?}: close {:?}", g.span_close.start, g.delimiter);

} else{

println!( "{:?}: {}", t.span.start, t.to_string);

}

}

}

$ cargo r

Compiling python-macro v 0. 1.0

Compiling scratchpad v 0. 1.0

LineColumn { line: 7, column: 8}: print

LineColumn { line: 7, column: 13}: openParenthesis

LineColumn { line: 7, column: 14}: "... World!"

LineColumn { line: 7, column: 26}: closeParenthesis

LineColumn { line: 8, column: 8}: print

LineColumn { line: 8, column: 13}: openParenthesis

LineColumn { line: 8, column: 14}: "Bye."

LineColumn { line: 8, column: 20}: closeParenthesis

符合预期,太棒了!

现在要重建空白符,如果我们不在正确的行中,我们需要插入换行符,如果我们不在正确的列中,则需要插入空格。让我们来看看效果:

#![feature(proc_macro_span)]

extern crate proc_macro;

useproc_macro::{ TokenTree, TokenStream, LineColumn};

#[proc_macro]

pub fn python(input: TokenStream) -> TokenStream {

let mut s = Source {

source: String::new,

line: 1,

col: 0,

};

s.reconstruct_from(input);

println!( "{}", s.source);

todo!

}

struct Source {

source: String,

line: usize,

col: usize,

}

impl Source {

fn reconstruct_from(&mut self, input: TokenStream) {

fort in input {

iflet TokenTree::Group(g) = t {

let s = g.to_string;

self.add_whitespace(g.span_open.start);

self.add_str(&s[. .1]); // the '[', '{' or '('.

self.reconstruct_from(g.stream);

self.add_whitespace(g.span_close.start);

self.add_str(&s[s.len - 1..]); // the ']', '}' or ')'.

} else{

self.add_whitespace(t.span.start);

self.add_str(&t.to_string);

}

}

}

fn add_str(&mut self, s: &str) {

// Let's assume for now s contains no newlines.

self.source += s;

self.col += s.len;

}

fn add_whitespace(&mut self, loc: LineColumn) {

whileself.line < loc.line {

self.source.push( 'n');

self.line += 1;

self.col = 0;

}

whileself.col < loc.column {

self.source.push( ' ');

self.col += 1;

}

}

}

$ cargo r

Compiling python-macro v0 .1.0

Compiling scratchpad v0 .1.0

print( "... World!")

print( "Bye.")

error: proc macro panicked

看来这是行得通的,但是这些额外的换行符和空格又是怎么回事?对比下源文件,这是对的,第一个标记从第7行第8列开始,因此它正确地将print放在第8列的第7行。我们要查找的位置正是.rs文件中的确切位置。

开始时多余的换行符不是问题(空行在Python中无效)。它甚至具有很好的副作用:当Python报告错误时,它报告的行号将与.rs文件中的行号匹配。

但是,这8个空格是个问题。尽管我们内部的Python代码python!{..}相对于Rust代码是适当缩进的,但我们提取的Python代码应以“零”缩进级别开始。否则,Python将发生无效缩进的错误。

让我们从所有列号中减去第一个标记的列号:

start_col: None,

//

start_col: Option,

//

let start_col = * self.start_col.get_or_insert(loc.column);

let col = loc.column.checked_sub(start_col).expect( "Invalid indentation.");

whileself.col < col {

self.source.push( ' ');

self.col += 1;

}

//

$ cargo r

Compiling python-macro v0 .1.0

Compiling scratchpad v0 .1.0

print( "... World!")

print( "Bye.")

error: proc macro panicked

结果太棒了!

现在,我们只需要把这个字符串转换为字符串文字标记 并将其放在run_python;周围即可:

TokenStream::from_iter(vec![

TokenTree::from(Ident::new( "run_python", Span::call_site)),

TokenTree::Group(Group::new(

Delimiter::Parenthesis,

TokenStream::from(TokenTree::from(Literal::string(&s.source))),

)),

TokenTree::from(Punct::new( ';', Spacing::Alone)),

])

太糟糕了,直接使用TokenTree太困难了,尤其是从头开始制作trees和streams。

如果只有一种方法可以编写我们要生成的Rust代码,那就只能是quote类库的quote!宏:

letsource= s.source;

quote!( run_python( #source); ).into

现在使用我们的原始run_python函数对其进行测试:

#![feature(proc_macro_hygiene)]

usepython_macro:: python;

fn run_python(code: &str) {

let py = pyo3::Python::acquire_gil;

iflet Err(e) = py.python.run(code, None, None) {

e. print(py.python);

}

}

fn main {

println!( "Hello ...");

python! {

print( "... World!")

print( "Bye.")

}

}

$ cargo r

Compiling scratchpad v 0. 1.0

Finished dev [unoptimized + debuginfo] target(s) in0. 31s

Running `target/debug/scratchpad`

Hello ...

... World!

Bye.

终于成功了!

封装成类库

现在我们把它变成一个可重用的库:

删除fn main,

重命名main.rs为lib.rs,

给类库起个好名字,例如inline-python,

公开run_python,

更改quote!中的run_python调用改为::inline_python::run_python,同时添加pub python_macro::python;从python!这个类库中重新导出宏。

下一步计划

可能还有很多内容需要改进,还有很多错误需要发现,但是至少我们现在可以在Rust代码行之间运行Python片段了。

目前最大的问题是,这还不是很有用,因为没有数据可以(轻松)越过Rust-Python的边界。

在第2部分中,我们将研究如何使Rust变量用于Python代码。

更新:在等待第2部分的同时,还有第1A部分,只是它没有改进我们的python!{}宏,但涉及了人们向我询问的一些细节。具体来说,它涉及:

为什么要像这样在Rust内部使用Python,

语法问题,例如使用Python的单引号字符串

使用Span::source_text的选项,当我第一次编写这段代码时,它其实还不存在。

原文链接:

https://blog.m-ou.se/writing-python-inside-rust-1/返回搜狐,查看更多

责任编辑:

我们编写的python代码在运行过程中_在Rust代码中编写Python是种怎样的体验?相关推荐

  1. python运行过程中会被编译成二进制_Python代码在运行过程中,会被编译成二进制代码。_学小易找答案...

    [单选题]1. ( )是违反设备安全操作规程的错误做法. [单选题]Thank you for your letter ___________ 24th March. (1.0分) [单选题]超外差接 ...

  2. Python逐块执行另一个Python程序中的代码观察运行过程

    今天是10月24日,也是一年一度的程序员节,祝所有热爱代码的朋友们节日快乐! 祝所有程序员1024节日快乐 全国高校教师Python课程高级研修班(线上,11月20-21日) 中国大学MOOC&quo ...

  3. c++hello world代码_在 Rust 代码中编写 Python 是种怎样的体验?

    作者 | Mara Bos,Rust资深工程师译者 | Arvin 责编 | 屠敏头图 | CSDN 下载自东方 IC出品 | CSDN(ID:CSDNnews) 以下为译文: 大约一年前,我发布了一 ...

  4. c++hello world代码_在Rust代码中编写Python是种怎样的体验?

    作者 | Mara Bos,Rust资深工程师译者 | Arvin,编辑 | 屠敏来源 | CSDN(ID:CSDNnews)大约一年前,我发布了一个名为inline-python(https://c ...

  5. Python Pycharm在运行过程中,查看每个变量(show variables)

    MATLAB里面有一个很贴心的功能就是你可以随时查看变量的值,以及变量的类型是什么: 在进行代码调试的时候,可以清楚的看到是哪些值出现了问题,但是由于MATLAB的深度学习生态环境还是没有Python ...

  6. python exe文件运行依赖环境_python将py代码文件转换为EXE脱离环境运行

    如何将python将py代码文件编译成为EXE文件,让其能够脱离python环境独立运行? 方法是有的,下面大家请看如何来操作. 我的环境是VS2017,同时安装了ironpython 一.安装pyi ...

  7. python做的游戏放到微信_【1、 创建一个python的文件,运行后,文字版方式模拟微信游戏“跳一跳?】...

    python脚本学习过程推荐 学习流程: 一:打础 1.找到合适的书籍(推荐Python核心编程2Dive into Python),大致读一次,循环啊判,常用类啊,搞懂(太难的跳过) 2.勤练习py ...

  8. python 运行r语言_如何在R中运行Python

    python 运行r语言 尽管我很喜欢R,但很显然Python还是一种很棒的语言-既适用于数据科学又适用于通用计算. R用户想要在Python中做一些事情可能有充分的理由. 也许这是一个很棒的库,还没 ...

  9. 小程序执行运行过程原理_活性污泥法基本原理、净化反应过程、工艺类型和运行过程中存在的问题...

    ↑ 点击上方"表面活性剂平台"关注我们 活性污泥法实质上是天然水体自净作用的人工强化,能从污水中去除溶解态和胶体态的可生物降解有机物以及能被活性污泥吸附的悬浮固体和其他物质,具有对 ...

最新文章

  1. 为什么说混合云是新基建的流行架构?文末彩蛋!
  2. SAP IQ02 将A序列号改成B序列号后,无修改记录?
  3. Python常见问题(7):Python图形用户接口 Graphic User Interface FAQ
  4. 智能合约重构社会契约(7)以太坊总结
  5. 部分视图传viewbag_无法在ASP.NET MVC3的部分视图中访问ViewBag
  6. 遍历XML引擎版本以适应代码
  7. fail-safe fail-fast知多少
  8. (十五)深入浅出TCPIP之Hello CDN
  9. 嵌入式OS入门笔记-以RTX为案例:二.快速移植到RTX
  10. 海南小地图(echart)
  11. java 泛型研究 初探索(一)
  12. 下行期的资本,正在追逐「猫品牌」
  13. 网站流量模型(1)介绍
  14. 第十六天(配置BPDU,TCN BPDU)
  15. 百度地图level对应距离(比例尺级别对应的多少米)
  16. 云麦体脂秤华为体脂秤_华为、小米、联想的智能体脂秤三国杀
  17. 前端生成二维码 微信小程序
  18. vmware 桥接模式设置桥接到无线网卡
  19. PHPstudy设计简单登录界面
  20. TwinCAT与Step 7编程的区别

热门文章

  1. NRF51822蓝牙服务(1)——LED读写
  2. php把buffer转化为图片_php base64转换成图片的方法
  3. 十六进制字符串与字节数组转换工具
  4. 【论文】铁路综合视频监控系统图像质量诊断技术研究
  5. Java笔试面试-克隆和序列化
  6. excel筛选出自己想要的数据并标记
  7. 菜鸟LeetCode-动态规划
  8. c语言链表中何时用点何时用箭头,链表基本操作及其过程详细叙述
  9. Maven仓库配置中央仓库
  10. CocoaChina12月源码精选