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

在Rust代码中编写Python是种怎样的体验?
文章图片
在Rust代码中编写Python是种怎样的体验?
文章图片

作者|MaraBos , Rust资深工程师
译者|Arvin , 编辑|屠敏
来源|CSDN(ID:CSDNnews)
大约一年前 , 我发布了一个名为inline-python(https://crates.io/crates/inline-python)的Rust类库 , 它允许大家使用python!{..}宏轻松地将一些Python混合到Rust代码中 。 在本系列中 , 我将从头展示开发此类库的过程 。
在Rust代码中编写Python是种怎样的体验?
文章图片
预览
如果不熟悉inline-python类库 , 你可以执行以下操作:
fnmain{letwho=''world'';letn=5;python!{foriinrange('n):print(i,''Hello'','who)print(''Goodbye'')}}它允许你将Python代码直接嵌入Rust代码行之间 , 甚至直接在Python代码中使用Rust变量 。
我们将从一个比这个简单得多的案例开始 , 然后逐步努力以达到这个结果(甚至更多!) 。
在Rust代码中编写Python是种怎样的体验?
文章图片
运行Python代码
首先 , 让我们看一下如何在Rust中运行Python代码 。 让我们尝试使第一个简单的示例生效:
fnmain{println!(''Hello...'');run_python(''print(''...World!'')'');}我们可以使用std::process::命令来运行python可执行文件并传递python代码 , 从而实现run_python , 但如果我们希望能够定义和读回Python变量 , 那么最好从使用PyO3库开始 。
PyO3为我们提供了Python的Rust绑定 。 它很好地包装了PythonCAPI , 使我们可以直接在Rust中与各种Python对象交互 。 (甚至在Rust中编写Python库 , 但这是另一个主题 。 )
它的Python::run功能完全符合我们的需求 。 它将Python代码作为&str , 并允许我们使用两个可选的PyDicts来定义范围内的任何变量 。 让我们试一试吧:
fnrun_python(code:&str){letpy=pyo3::Python::acquire_gil;//Acquirethe'globalinterpreterlock',asPythonisnotthread-safe.py.python.run(code,None,None).unwrap;//Nolocals,noglobals.}$cargorunCompilingscratchpadv0.1.0Finisheddev[unoptimized+debuginfo]target(s)in0.29sRunning`target/debug/scratchpad`Hello......World!看 , 这就成功了!
在Rust代码中编写Python是种怎样的体验?
文章图片
基于规则的宏
在字符串中编写Python不是最便捷的方法 , 所以我们尝试改进它 。 宏允许我们在Rust中自定义语法 , 所以让我们尝试一下:
fnmain{println!(''Hello...'');python!{print(''...World!'')}}宏通常是使用macro_rules!进行定义 , 您可以基于标记和表达式之类的内容使用高级“查找和替换”规则来定义宏 。 (有关macro_rules!的介绍请参见RustBook中有关宏的章节 , 有关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)*));}}结果如下:
$cargorCompilingscratchpadv0.1.0Finisheddev[unoptimized+debuginfo]target(s)in0.32sRunning`target/debug/scratchpad`Hello......World!如愿以偿得到了期望结果!
但是 , 如果我们有不止一行的Python代码会怎样?
fnmain{println!(''Hello...'');python!{print(''...World!'')print(''Bye.'')}}$cargorCompilingscratchpadv0.1.0Finisheddev[unoptimized+debuginfo]target(s)in0.31sRunning`target/debug/scratchpad`Hello...thread'main'panickedat'called`Result::unwrap`onan`Err`value:PyErr{type:Py(0x7f1c0a5649a0,PhantomData)}', src/main.rs:9:5note:runwith`RUST_BACKTRACE=1`environmentvariabletodisplayabacktrace很不幸 , 我们失败了 。
【在Rust代码中编写Python是种怎样的体验?】为了进行调试 , 我们需要正确输出PyErr , 并显示我们传递给Python::run的确切Python代码:
fnrun_python(code:&str){println!(''-----'');println!(''{}'',code);println!(''-----'');letpy=pyo3::Python::acquire_gil;ifletErr(e)=py.python.run(code,None,None){e.print(py.python);}}$cargorCompilingscratchpadv0.1.0Finisheddev[unoptimized+debuginfo]target(s)in0.27sRunning`target/debug/scratchpad`Hello...-----print(''...World!'')print(''Bye.'')-----File''<string>'',line1print(''...World!'')print(''Bye.'')^SyntaxError:invalidsyntax很显然 , 两行Python代码落在同一行 , 在Python中这是无效的语法 。
现在我们遇到了必须克服的最大问题:stringify!把空白符搞乱了.
在Rust代码中编写Python是种怎样的体验?
文章图片
空白符和符号
让我们仔细研究一下stringify!:
fnmain{println!(''{}'',stringify!(a123bcx(y+z)//comment...));}$cargorCompilingscratchpadv0.1.0Finisheddev[unoptimized+debuginfo]target(s)in0.21sRunning`target/debug/scratchpad`a123bcx(y+z)...它不仅删除了所有不必要的空格 , 还删除了注释 。 因为它的工作原理是处理单词(token) , 不再是源代码里面的:a , 123 , b等 。
Rustc编译器做的第一件事就是将源代码分为单词 , 这使得解析后的工作更容易进行 , 不必处理诸如1 , 2 , 3 , 这样的个别字符 , 只需处理诸如“integerliteral123”这样的单词 。 另外 , 空白和注释在分词之后就消失了 , 因为它们对编译器来说没有意义 。
stringify!是一种将一串单词转换回字符串的方法 , 但它是基于“最佳效果”的:它将单词转换回文本 , 并且仅在需要时才在单词周围插入空格(以避免将b、c转换为bc) 。
所以这是一个死胡同 。 Rustc不小心把宝贵的空白符丢掉了 , 但这在Python中非常重要 。
我们可以尝试猜测一下哪些代码的空格必须用换行符代替 , 但是缩进肯定会成为一个问题:
fnmain{leta=stringify!(ifFalse:xy);letb=stringify!(ifFalse:xy);dbg!(a);dbg!(b);dbg!(a==b);}$cargorCompilingscratchpadv0.1.0Finisheddev[unoptimized+debuginfo]target(s)in0.20sRunning`target/debug/scratchpad`[ src/main.rs:12]a=''ifFalse:xy''[ src/main.rs:13]b=''ifFalse:xy''[ src/main.rs:14]a==b=true这两个Python代码片段有不同的含义 , 但是stringify!给了我们相同的结果 。
在放弃之前 , 让我们尝试一下其他类型的宏 。
在Rust代码中编写Python是种怎样的体验?
文章图片
过程宏
Rust的过程宏是定义宏的另一种方法 。 尽管macro_rules!只能定义“函数样式的宏”(带有!标记的宏) , 过程宏也可以定义自定义派生宏(例如#[derive(Stuff)])和属性宏(例如#[stuff]) 。
过程宏是作为编译器插件实现的 。 您需要编写一个函数 , 该函数可以访问编译器看到的单词流 , 然后就可以执行所需的任何操作 , 最后需要返回一个新的单词流供编译器使用(或者用于自定义的用途):
#[proc_macro]pubfnpython(input:TokenStream)->TokenStream{todo!}上述单词流不够好 。 因为我们需要源代码 , 而不仅仅是单词 。 虽然目前还没有成功 , 但是让我们继续吧 , 也许过程宏更大的灵活性能够解决问题 。
由于过程宏在编译过程中运行Rust代码 , 因此它们需要使用单独的proc-macro类库中 , 这个类库在您编译其他内容之前已经被编译好 。
$cargonew--libpython-macroCreatedlibrary`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.rsexterncrateproc_macro;useproc_macro::TokenStream;#[proc_macro]pubfnpython(input:TokenStream)->TokenStream{dbg!(input.to_string);todo!}// src/main.rsusepython_macro::python;fnmain{println!(''Hello...'');python!{print(''...World!'')print(''Bye.'')}}$cargorCompilingpython-macrov0.1.0Compilingscratchpadv0.1.0error[E0658]:proceduralmacroscannotbeexpandedtostatements--> src/main.rs:5:5|5|/python!{6||print(''...World!'')7||print(''Bye.'')8||}||_____^|=note:seeissue#54727<https://github.com/rust-lang/rust/issues/54727>formoreinformation=help:add`#![feature(proc_macro_hygiene)]`tothecrateattributestoenable天啊 , 这里发生了什么?
Rust错误为“过程宏不能扩展为语句” , 以及有关启用“hygienicmacros”的内容 。 Macrohygiene是Rust宏的出色功能 , 不会意外地将任何名称“泄漏”给外界(反之亦然) 。 如果宏扩展使用了名为的x的临时变量 , 则它将与宏外部的任何代码中出现的变量x分开 。
但是 , 此功能对于过程宏还不稳定 。 因此 , 过程宏除了作为一个单独的项(例如在文件范围内 , 但不在函数内)之外 , 不允许出现在任何地方 。
接下来 , 我们会发现存在一个非常可怕但令人着迷的解决方法—让我们启用实验功能#![feature(proc_macro_hygiene)]并继续我们的冒险 。
(如果你将来读到这篇文章时 , proc_macro_hygiene已经稳定下来了:你可以跳过最后几段 。 ^^)
$sed-i'1i#![feature(proc_macro_hygiene)]' src/main.rs$cargorCompilingscratchpadv0.1.0[python-macro/ src/lib.rs:6]input.to_string=''print(''...World!'')print(''Bye.'')''error:procmacropanicked--> src/main.rs:6:5|6|/python!{7||print(''...World!'')8||print(''Bye.'')9||}||_____^|=help:message:notyetimplementederror:abortingduetopreviouserrorerror:couldnotcompile`scratchpad`.在向我们展示了它的字符串输入参数之后 , 我们的过程宏即如预期般地崩溃了:
print(''...World!'')print(''Bye.'')
正如预期的那样 , 空白符再次被丢弃了 。 :(
是时候选择放弃了 。
不过或者..也许有一种方法可以解决这个问题 。
在Rust代码中编写Python是种怎样的体验?
文章图片
重建空白符
尽管rustc编译器只在解析和编译时使用单词 , 但是在某种程度上它仍然可以准确地知道何时报告错误 。 单词中没有换行符 , 但是它仍然知道我们的错误发生在第6到第9行 。 那它如何做到的?
事实证明 , 单词中包含很多信息 。 它们包含一个Span , 是单词在源文件中的开始和结束的位置 。 Span可以告诉单词在哪个文件、行和列编号处开始和结束 。
如果我们能够得到这些信息 , 我们就可以通过在单词之间放置空格和换行符来重新构造空白符 , 以匹配它们的行和列信息 。
提供这些信息的函数还不稳定 , 而且还没有#![feature(proc_macro_span)] 。 让我们启用它 , 看看我们得到了什么:
#![feature(proc_macro_span)]externcrateproc_macro;useproc_macro::TokenStream;#[proc_macro]pubfnpython(input:TokenStream)->TokenStream{fortininput{dbg!(t.span.start);}todo!}$cargorCompilingpython-macrov0.1.0Compilingscratchpadv0.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]pubfnpython(input:TokenStream)->TokenStream{print(input);todo!}fnprint(input:TokenStream){fortininput{ifletTokenTree::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);}}}$cargorCompilingpython-macrov0.1.0Compilingscratchpadv0.1.0LineColumn{line:7,column:8}:printLineColumn{line:7,column:13}:openParenthesisLineColumn{line:7,column:14}:''...World!''LineColumn{line:7,column:26}:closeParenthesisLineColumn{line:8,column:8}:printLineColumn{line:8,column:13}:openParenthesisLineColumn{line:8,column:14}:''Bye.''LineColumn{line:8,column:20}:closeParenthesis符合预期 , 太棒了!
现在要重建空白符 , 如果我们不在正确的行中 , 我们需要插入换行符 , 如果我们不在正确的列中 , 则需要插入空格 。 让我们来看看效果:
#![feature(proc_macro_span)]externcrateproc_macro;useproc_macro::{TokenTree,TokenStream,LineColumn};#[proc_macro]pubfnpython(input:TokenStream)->TokenStream{letmuts=Source{source:String::new,line:1,col:0,};s.reconstruct_from(input);println!(''{}'',s.source);todo!}structSource{source:String,line:usize,col:usize,}implSource{fnreconstruct_from(&mutself,input:TokenStream){fortininput{ifletTokenTree::Group(g)=t{lets=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);}}}fnadd_str(&mutself,s:&str){//Let'sassumefornowscontainsnonewlines.self.source+=s;self.col+=s.len;}fnadd_whitespace(&mutself,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;}}}$cargorCompilingpython-macrov0.1.0Compilingscratchpadv0.1.0print(''...World!'')print(''Bye.'')error:procmacropanicked看来这是行得通的 , 但是这些额外的换行符和空格又是怎么回事?对比下源文件 , 这是对的 , 第一个标记从第7行第8列开始 , 因此它正确地将print放在第8列的第7行 。 我们要查找的位置正是.rs文件中的确切位置 。
开始时多余的换行符不是问题(空行在Python中无效) 。 它甚至具有很好的副作用:当Python报告错误时 , 它报告的行号将与.rs文件中的行号匹配 。
但是 , 这8个空格是个问题 。 尽管我们内部的Python代码python!{..}相对于Rust代码是适当缩进的 , 但我们提取的Python代码应以“零”缩进级别开始 。 否则 , Python将发生无效缩进的错误 。
让我们从所有列号中减去第一个标记的列号:
start_col:None,//<snip>start_col:Option<usize>,//<snip>letstart_col=*self.start_col.get_or_insert(loc.column);letcol=loc.column.checked_sub(start_col).expect(''Invalidindentation.'');whileself.col<col{self.source.push('');self.col+=1;}//<snip>$cargorCompilingpython-macrov0.1.0Compilingscratchpadv0.1.0print(''...World!'')print(''Bye.'')error:procmacropanicked结果太棒了!
现在 , 我们只需要把这个字符串转换为字符串文字标记并将其放在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;fnrun_python(code:&str){letpy=pyo3::Python::acquire_gil;ifletErr(e)=py.python.run(code,None,None){e.print(py.python);}}fnmain{println!(''Hello...'');python!{print(''...World!'')print(''Bye.'')}}$cargorCompilingscratchpadv0.1.0Finisheddev[unoptimized+debuginfo]target(s)in0.31sRunning`target/debug/scratchpad`Hello......World!Bye.终于成功了!
在Rust代码中编写Python是种怎样的体验?
文章图片
封装成类库
现在我们把它变成一个可重用的库:
删除fnmain ,
重命名main.rs为lib.rs ,
给类库起个好名字 , 例如inline-python ,
公开run_python ,
更改quote!中的run_python调用改为::inline_python::run_python , 同时添加pubpython_macro::python;从python!这个类库中重新导出宏 。
在Rust代码中编写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/
在Rust代码中编写Python是种怎样的体验?
文章图片
360金融首席科学家张家兴:别指望AILab做成中台我们想研发一个机器学习框架 , 6个月后失败了
八年 , 腾讯优图攒了多厚的技术“家底”?
无需训练RNN或生成模型 , 如何编写一个快速且通用的AI“讲故事”项目?
区块链重大技术分析:IBM、微软、苹果、Google都做了什么?


    推荐阅读