社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

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

CSDN • 3 年前 • 428 次点击  

作者 | Mara Bos,Rust资深工程师
译者 | Arvin 责编 | 屠敏
图 | CSDN 下载自东方 IC
出品 | CSDN(ID:CSDNnews)


以下为译文:

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


预览


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

fn main() {
    let who = "world";
    let n = 5;
    python! {
        for i in range('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'as Python is not thread-safe.
    py.python().run(code, NoneNone).unwrap(); // No locals, no globals.
}


$ cargo run
   Compiling scratchpad v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.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 v0 .1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.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) in 0.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();
    if let 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) in 0.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 123    b   c
        x ( y + z )
        // comment
        ...
    ))
;
}


$ cargo r
   Compiling scratchpad v0 .1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.21s
     Running `target/debug/scratchpad`
123 b c x(y + z) ...

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

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

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

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

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

fn main() {
    let a = stringify!(
        if False:
            x()
        y()
    );
    let b = stringify!(
        if False:
            x()
            y()
    );
    dbg!(a);
    dbg!(b);
    dbg!(a == b);
}


$ cargo r
   Compiling scratchpad v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.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;
use proc_macro::TokenStream;

#[proc_macro]
pub fn python(input: TokenStream) -> TokenStream {
    dbg!(input.to_string());
    todo!()
}

// src/main.rs
use python_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:6input.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: not yet implemented

error: aborting due to previous error

error: could not compile `scratchpad`.

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

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

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

是时候选择放弃了。

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


重建空白符


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

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

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

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

#![feature(proc_macro_span)]

extern crate proc_macro;
use proc_macro::TokenStream;

#[proc_macro]
pub fn python(input: TokenStream) -> TokenStream {
    for t 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) {
    for t in input {
        if let 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 v0.1.0
   Compiling scratchpad v0.1.0
LineColumn { line: 7, column: 8 }: print
LineColumn { line: 7, column: 13 }: open Parenthesis
LineColumn { line: 7, column: 14 }: "... World!"
LineColumn { line: 7, column: 26 }: close Parenthesis
LineColumn { line: 8, column: 8 }: print
LineColumn { line: 8, column: 13 }: open Parenthesis
LineColumn { line: 8, column: 14 }: "Bye."
LineColumn { line: 8, column: 20 }: close Parenthesis

符合预期,太棒了!

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

#![feature(proc_macro_span)]

extern crate proc_macro;
use proc_macro::{TokenTreeTokenStreamLineColumn};

#[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) {
        for t in input {
            if let 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) {
        while self.line             self.source.push('\n');
            self.line += 1;
            self.col = 0;
        }
        while self.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

看来这是行得通的,但是这些额外的换行符和空格又是怎么回事?对比下源文件,这是对的,第一个标记从第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.");
    while self.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!宏:




    
    let source = s.source;
    quote!( run_python(#source); ).into()

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

#![feature(proc_macro_hygiene)]
use python_macro::python;

fn run_python(code: &str) {
    let py = pyo3::Python::acquire_gil();
    if let 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 v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.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/

本文为 CSDN 翻译,转载请注明来源出处。

更多精彩推荐

☞AI 世界的硬核之战,Tengine 凭什么成为最受开发者欢迎的主流框架?

☞说了这么多 5G,最关键的技术在这里

360金融新任首席科学家:别指望AI Lab做成中台

AI图像智能修复老照片,效果惊艳到我了

程序员内功修炼系列:10 张图解谈 Linux 物理内存和虚拟内存

当 DeFi 遇上 Rollup,将擦出怎样的火花?

你点的每个“在看”,我都认真当成了喜欢
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/62497
 
428 次点击