Rust 和 Python 有些相通的地方


事先说明,所谓相通,就是便于方便理解它,但是它们本质是不一样的。

或者也能说思想是类似的,但是实现是完全不一样的。

目前只能挑想到的点浅浅地说一下,不保证完整覆盖和足够深入,如有错误还请谅解并指出。

打印显示,这应该是最常用到,但是又很容易忽视其基本原理的一个地方。

例如我有很多书,然后我就声明了这样一个对象叫 Book(这个代码是网上已有的 https://realpython.com/python-repr-vs-str/

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def __repr__(self):
        class_name = type(self).__name__
        return f"{class_name}(title={self.title!r}, author={self.author!r})"

    def __str__(self):
        return f'"{self.title}" by {self.author}'


odyssey = Book("The Odyssey", "Homer")

print(repr(odyssey))  # Book(title='The Odyssey', author='Homer')
print(str(odyssey))  # "The Odyssey" by Homer

如果你用 python str repr 作为关键词去 Google 搜,你会看到有我参考到的这一篇,以及还有很多关于 python 中 str 和 repr 有何区别和如何使用的详细讲解。

如果简单说,__str__ 就是面向人,用于友好显示,__repr__ 就是面向程序,用于代码调试和代码生成等场合。大多数情况下,你可能关注 __str__ 更多一些。

在 python 里面,有一个全局可使用的 str 函数(其实它是 class),通过调用对象上的 __str__ 来把一切东西都转为字符串,然后跟字符串相关的一切函数例如 print 也是同理。当然,还有一些降级动作,例如如果找不到 __str__ 就用 __repr__ 什么的,这里就不深究了。

而同样,repr 则调用的是 __repr__

然后,我们把这块代码用 rust 实现一下。

use std::fmt::{Display, Formatter, Write};

#[derive(Debug)]
struct Book {
    title: String,
    author: String,
}

impl Book {
    fn new(title: String, author: String) -> Self {
        Self {
            title,
            author,
        }
    }
}

impl Display for Book {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.write_str(format!("\"{}\" by {}", self.title, self.author).as_str())
    }
}


fn main() {
    let odyssey = Book::new("The Odyssey".to_owned(), "Homer".to_owned());

    println!("{:?}", odyssey);  // Book { title: "The Odyssey", author: "Homer" }
    println!("{}", odyssey);  // "The Odyssey" by Homer
}

这两份代码,可以对比参照着仔细看看。

impl Display for Book 里面的 fn fmt 方法基本就是对应着 def __str__ ,然后结构体上面的 #[derive(Debug)] 就对应着 def __repr__ ,你看是不是这个关系。

然后你看 rust 这边后面都是用 println! 来打印,里面用了不同的占位符来让程序分别去调用不同的实现来打印,其实 python 也是有类似的:

print(f"{odyssey!r}")
print(f"{odyssey!s}")

以及如果不是使用 f string 而是使用 “{}".format 方法也是同样有用的。

好,总结一下。

场景 Python Rust
面向代码 实现 __repr__ 然后通过 repr 或 !r 调用 实现 Debug 然后通过 {:?} 调用
面向人类 实现 __str__ 然后通过 str 或 !s 调用 实现 Display 然后通过 {} 调用

Python 参考 https://docs.python.org/3/library/string.html

Rust 参考 https://doc.rust-lang.org/std/fmt/

所以到这里,你会发现 Python 里面经常被人整得神神秘秘玄乎玄乎的 Magic Method 或者又叫 Dunder Method,你可以理解为大家在设计数据结构或者对象的时候都需要去遵循的一组“接口协议”即可,你实现了它们,那么就可用通过内置或者规范的调用方法进行使用,实现你想要的效果。

而在 Rust 这边,这个对我们还没学过几门语言的人来说几乎全新的 Trait 这个词,它更接近“接口协议”这种理解。你实现了它们,就会按照设计好的规范去调用它,得到你想要的结果。

在 Python 这边,由于经常使用内置数据结构,可能还不怎么经常需要写 __str__ ,但是在 Rust 这边,实现 Display 几乎是很常用的需求,因为很多调用最终都会最终调到它身上来。

由于 Python 遵循了 C 系语言的设计规范,而 Rust 在很多地方也借鉴了 C 系语言的设计规范,所以你能看到 Rust 在很多地方有着 和 Python 类似的设计规范,例如字符格式化的一些格式占位符、日期格式化的格式占位符等等,其实这些也大大降低了理解和切换语言思维的难度,是个好事情。

由于 Python 的极度灵活性,和它所常用的场景,导致要么经常不设计自己合理的数据类,要么被继承击昏了头脑绕来绕去,最后使得代码难以维护。

代码被创造出来其实是容易的,真的很容易,哪怕是从网络上直接 CV 下来的很多时候也能跑得很好。

然而真正难的是维护,现实世界里面的商业项目,是不断添加新功能、特性,需求的不断变化,不断去满足现实世界里面一些看起来奇奇怪怪的使用场景,场景和条件组合的持续不断扩展,外界条件的持续变化,你没覆盖到的各种边界意外事件,各种特殊情况的特别处理等等,还有可能已经过时的一些历史包袱。

可能这也是大多数都总是更偏向于写一个新的,而抛弃那个旧的的原因之一。

偏向只是偏向,但很多时候,你并没有这样的机会。

一旦选定,就只能是原地打转而已。