写给 Python 程序员看的 Rust 介绍

本文和原文都基于 CC-NC-SA 协议分享。

Rust 和 Python 截然不同,这不仅体现在 Rust 是编译型语言,而另一者是解释型,两者在设计理念上就已经完全不同。
但即使出发点不一样,并不妨碍他们在 API 设计上的存在相似性,Python 程序员可能会因此对 Rust 抱有好感。

语法

作为一个程序员,你首先注意到的不同点肯定在于语法。和 Python 不一样,Rust 中存在着大量的花括号和分号,不过这些使得 Rust 在 Python 实现得不好的地方,比如匿名函数、闭包等等,使用起来简单、清晰。这些特性使得编写非缩进式的代码更加方便。

举个例子,在 Python 中打印“Hello world”三次可以这样做:

Rust版本:

除了关键字、函数名和花括号外并没有明显的不同。
语法上另一大不同之处在于 Rust 需要指定函数的参数类型,而 Python 不需要。
当然, Python 3 中新增的类型注释(type annotations)使用起来和 Rust 的语法很像。

Rust 函数调用的结尾会有一个惊叹符,它其实是宏。在编译期间,宏会展开成对应的东西。
具体看情况,究竟是用于字符串格式化还是打印,这样,编译器就能在编译阶段强制字符串类型了。
也就不会在打印的时候出现函数参数、类型不匹配的情况。

Traits vs Protocols(特性 vs 协议)

最常见的不同之处在于对象的行为。
在 Python 中,类可以通过实现特定的魔术方法来支持某行为,通常称之为遵从 x 协议——比如 __iter__ 方法用于返回一个迭代器对象。
这些方法应该在类中实现,之后(实例化之后)就不能在修改了。(请无视 Monkey Patch)

Rust 的理念和 Python 类似,不过它选“特性”而非魔术方法。特性的不同之处在于它仅作用于本地,你可以在别的模块中实现更多特性。
如果你想要给整数特殊的功能,并不会影响到(全局的)整数类型。

以一个自调用的类型为例。Python:

Rust:

Rust 的实现代码稍微长一些,但是它实现了 Python 代码中未处理的自动类型。
你可能还会注意到,Python 中的方法与类型声明在一起,而 Rust 中两者是分开声明的:struct 定义数据,impl MyType 定义类型所具备的方法,impl Add for MyType 是 Add 特性的实现。
在 Add 方法的实现中,我们还定义了方法返回结果的类型,但是避免了像 Python 中那样需要在运行时检查类型的复杂性。

另一点区别在于,Rust 构造器是明确的,而 Python 的更具迷惑性。
Python 初始化对象时会调用 __init__ 来初始化对象,在 Rust 中则需要手动定义一个功能类似的静态方法,通常是 new() 方法,来分配对象空间并构造。

错误处理/异常处理

Python 和 Rust 中的错误处理理念完全不同!
Python 中错误会以异常的形式抛出,而 Rust 中则是返回值,这听起来很奇怪,但却是是一个不错的设计。

从函数的返回值定义中就可以确定它可能“抛出”的异常,是一种非常清晰的声明方式,
和 Python “显胜于隐”的设计哲学不谋而合。(Python 中也鼓励手动抛出异常而非过多的条件判断。)

Rust 中的函数可以返回 Result,Result 是一种规范化的类型,分为成功和失败两种。
Result 表示这个方法成功时会返回 32位的整数类型,失败时返回 MyError。
如果你需要返回多种错误怎么办?

Python 函数可能会抛出任何错误,但并不会做出任何处理。比如,你在使用 requests 库时可能会遇到 SSL error 或者其它错误,并且只有当它发生时你才知道出错了,但如果文档中没有明确的说明,你永远都不知道它会返回什么样的错误。

Rust 不一样,函数的声明中就包含了会遇到什么样的错误。
如果需要返回两种以上的错误,通常是建议定义一个内部错误来整合。
以一个 HTTP 库为例,它可能会抛出 Unicode error、IO error、SSL error 等等。
你可以把它们都定义为一个只在你的库中使用的错误类型,用户也只需要知道它即可。

Rust 的错误链机制能够在你需要的时候回溯到产生错误的地方。
你可以在任意时候是使用 Box 这个所有错误的根来代替自定义错误。
相比之下,Rust 的错误更透明,而 Python 中会比较绕。

Rust 的错误处理机制是由 try! 宏提供的,下面是一个例子:

上面代码中的 File::openread_to_string 都会失败并返回 IO error,
try! 宏会将错误向上传递,并且会立即返回。返回信息是 success 还是 failure
由包裹函数是 Ok 还是 Err 确定。

try! 宏引用了 From 特性以运行错误转换。例如你可以通过修改返回值 io::ErrorMyError,并且通过实现 From 特性来实现 io::ErrorMyError 的转换,它也会被自动引用。

或者,你也可以使用 Box 作为返回值类型代替 io::Error,这样就可以传递任何类型的错误了。
这样做的坏处是,原本编译期就能确定错误的程序必须正式运行的时候才能确定。

如果你不打算处理异常而直接退出执行,可以 unwrap() 结果,这样成果返回的结果会是一个错误,程序会因此退出。

可变性和所有权

可变性(mutability)和所有权(ownership)是 Python 和 Rust 最本质区别的地方!
Python 会自动 GC ,因此运行的时候会有很多意想不到的事情发生,你可以随意地传递对象,虽然可能产生一些内存泄漏问题,不过大部分问题都会它都能在运行时自动解决。

Rust 不支持 GC,但仍然能够自动管理内存,这得益于其所有权跟踪机制,你创建的所有对象都被另一个对象所拥有。

对比 Python,你可以认为 Python 中的所有对象的所有权都是 Python 解释器。

Rust 中的所有权存在范围设定,一个调用对象列表的函数拥有这个列表的所有权,而列表拥有其中所有对象的所有权,而这一切都发生在函数的作用域中。

下面通过一个关于生命周期注释(lifetime annotation)和函数签名的更复杂的例子来帮助你理解什么是“所有权”。以实现上面的“加法”为例,接收者(receiver)和 Python 中一样命名为 self,不同的是值会被“移动到”函数中,而不像 Python 那样是通过可变引用来实现。也就是说,你可能是用 Python 这样实现:

当你将 MyType 的实例加入另一个对象时,就会将 self 泄漏到 global 列表中,
也就是说,运行上面的代码将获得

当你将一个 MyType 类型的实例和另一个对象相加时都将会使得自身泄漏到全局列表中,
也就是说,当你运行上面的代码时,第一个 MyType 将会被引用两次:被第二个实例引用,
以及被 global 所引用。

而 Rust 中不存在这样的问题,每个对象都只能有一个所有者。你在引用 self 时,
编译器将会将值“移动”过去,这时函数将无法找到原来的 self 也就无法将它 return 回去。
想要 return self 则必须向将它移动回去(将它从引用中删除)。
如果你想要把 self 泄露出去,编译器会负责“移动”值,但是这样函数就无法返回 self,因为它已经被移动了。想要返回它,你首先还是需要把它再移动回去(比如从列表中将它删除)。

如果你需要多次引用同一个对象该怎么办呢?Rust 提供的解决方案是“借用(borrow)”,“借用”这个变量的值。
借用的数量可以没有限制,但是不允许对借用的对象进行修改,或者可以修改但只允许存在一个借用。

操作不可变“借用”的函数被标记为 &self,使用可变借用的函数被标记为 &mut self。作为拥有者你只能“借出”引用。如果想要将值移出函数(比如返回),则不能有额外的借出,并且将所有权移动后不能再借出。

这可能会颠覆你思考程序的方式,但习惯它能帮你更好地理解程序。

运行时的“借用”和可变的所有者

目前为止,都能在运行时验证所有的所有权并没有问题,但编译时无法验证所有权怎么办?

你有以下可选方案:第一种方案你可以使用 mutex,mutex 保证只有一个用户可以可变地借用对象,但对象的所有者是 mutex。这样你就可以在视线获取对象时,永远只有一个线程能够取到它。

这也意味着,你不得不使用 mutex 锁,否则会导致数据竞争,无法通过编译。

如果你想像 Python 一样编程,不用找出内存的所有者?

在那种情况下你可以将一个对象封装在引用计数器中,并在运行时将其借出。

这种方式非常类似 Python,同时也可能导致循环。Python 会在 GC 的时候解除循环,但 Rust 不会。

不妨用个比较复杂的例子来比较一下:

上面的代码会生成 35 个线程,都用于计算斐波那契数,最后将所有结果聚合并排序。
你可能会注意到 mutex(锁)和结果数组直接并无关系。

Rust 版本和 Python 版本最大的不同之处在于使用了 B 树 map 而不是 hash 表,结果存放于 Arc mutex 中。

这是为什么?这里使用 B 树是因为它能够自动排序,而这正是我们所需要的。

将值存放在 mutex 中是因为这样可以在运行时将其锁住。

关系创建后,我们将其置入 Arc,因为 Arc 会管理它所封装的事物的引用计数,本例中即 mutex。也就是说,直到最后一个线程运行结束时 mutex 才会被删除。非常简洁!

下面解释下这段代码是如何工作的:首先数 20 次,和在 Python 中一样,每次都运行一个本地函数。和 Python 中不同的是,我们在这里可以使用闭包。之后将 Arc 复制到本地线程中,也就是说每个线程都会有自己的 Arc(这会 Arc 的增加引用计数,但会在线程结束的时候释放)。之后我们使用本地函数 spawn 一个新线程,这会将闭包移动到线程中。

之后每个线程都会执行 Fibonacci 函数,When we lock our Arc we get back a result we can unwrap and the insert into.
先不要管解包过程,这只是将明确的结果转换为 panic。重点在于,你只有在解除 mutex 锁之后才能获得结果映射,所以千万不能忘记解锁。

之后我们将所有线程集中到 vector 中,最后迭代所有线程,合并并打印结果。

这里有两点需要注意的地方:可见类型非常少。当然,Arc 和 Fibonacci 函数接受 64 位 unsigned 整数,除此之外,没有任何类型是可见的。在我们也可以使用 B-tree 映射来代替哈希表,因为 Rust 内置了这个类型。

迭代的运行方式和 Python 几乎完全一致,不同之处在于,这上面这个 Rust 例子中我们需要引入 mutex,因为编译器不知道线程会怎样结束,而 mutex 是不必要的。当然 Rust 也有不需要引入 mutex 的 API,只不过在 Rust 1.0 中还不是稳定版本。

性能优化会如你所期望地那样进行。(这里的优化情况很糟糕,因为只是未来提供一个线程执行的例子。)

Unicode

我最喜欢的话题是 Unicode :) ,这也是 Python 和 Rust 相差最大的地方。

Python (2 和 3)都使用着相似的 Unicode 模型,将 Unicode 数据映射至字符数组。

在 Rust 中,Unicode 都是 UTF-8 格式存储,我之前也提到过为什么这比 Python 或者 C# 的解决方案要好得多(参见 UCS vs UTF-8 as Internal String Encoding)。
非常有趣的是 Rust 是如何处理丑陋的编码问题的。

首先 Rust 一开始就意识到操作系统(不论是 Windows Unicode 还是 Linux 非 Unicode)的 API 都非常糟糕,它没有像 Python 一样强制使用 Unicode,但是实现了一套低廉的字符转化系统,这在现实使用中非常出色,也使得 Rust 拥有高效的字符处理能力。

对于大多数支持 UTF-8 的程序来说,编码、解码并不需要,只需要简单地验证编码的正确性,并不需要的对 UTF-8 字符编码后再输出。如果需要集成 Windows Unicode API,只需要在内部使用 WTF-8 进行编码,可以很高效地和 UTF-16 这样的 UCS2 编码之间进行转换。

无论何时,你都可以将 Unicode 和字节码互相转换,之后检验以确保所有操作都按预期进行了。这使得编写协议既快速又高效。和 Python 那种不断编码、解码的方式相比,只需要支持 O(1) 的字符索引。

得益于 Unicode 优秀的存储模型,Rust 还自带或者太 crates.io 上提供了很多 Unicode 处理的 API,包括 case folding、分类、Unicode 正则表达式、Unicode 正常化、标准的 URI/IRI/URL API、分割操作以及命名映射等等。

缺点呢?"föo"[1] 的结果不是 'ö',但你本来就不应该这样做。

下面有一个用于示范如何和 OS 集成的例子,执行它会打印出当前目录的信息和文件名:

所有 IO 操作都使用了上面用过的 Path 对象,包含了 OS 内部的路径属性。根据系统使用的编码,它可能是字节、Unicode 或者 OS .display() 格式化(这会返回一个能将自身格式化为字符串的对象)前的调用格式。这很方便,因为你不会像在 Python 3 中那样无意中漏掉错误编码的字符串,它提供了清晰的区分。

库以及应用发布

Rust 提供了一个称为“cargo”的工具,作用基本相当于 Python 中的 virtualenv+pip+setuptools ,
不过默认情况下它尽能保证一个版本的 Rust 正常工作。
“cargo” 能够同时支持库的不同版本,并且可以直接从 git 仓库或者 crates.io 源中安装程序,通常安装 Rust 的时候就已经自带了 cargo。

Rust 会代替 Python 吗?

Python 和 Rust 之间并没有直接关系,

  • 对于科学计算等领域,丰富的库和文档的作用非常之大,Rust 不可能在短期内影响到 Python 在其中的地位。
  • 对于脚本这样的领域,只要 Python 能解决,我不认为你会考虑选择 Rust。
  • 虽然肯定会有很多 Python 程序员去学习 Rust,但就和很多 Python 学习 Go 一样,并不是以替代 Python 为目的。

Rust 是一门非常强大的语言,拥有稳定的基金会、友好的社区、人性化的许可证,可能会在编程语言的民主制度掀起一场革命。

Rust 几乎不需要运行时支持,因此通过 ctypes 或 CFFI 来和 Python 交互并不难,直接使用 Python 封装的 Rust 二进制库并非不可能。

1 收藏 2 评论

可能感兴趣的话题



直接登录
最新评论
  • Destro 学生 2016/05/01

    很出色,但是我想这句“Rust 函数调用的结尾会有一个惊叹符,它其实是宏”意译是不是好一点呢——毕竟Rust的函数调用后面是没有惊叹号的。

  • Destro 学生 2016/05/01

    Unicode部分的例子很实用,可惜通不过编译,把ResultBox>改成Result<()>就可以了。

跳到底部
返回顶部