专栏名称: 数据分析与开发
伯乐在线旗下账号,分享数据库相关技术文章、教程和工具,另外还包括数据库相关的工作。偶尔也谈谈程序员人生 :)
目录
相关文章推荐
51好读  ›  专栏  ›  数据分析与开发

受 Rust 启发,是时候改变 Python 编程方式了

数据分析与开发  · 公众号  · 数据库  · 2025-03-24 08:30

正文

近年来,Rust因安全性受到科技公司青睐。其他主流语言能否借鉴Rust的思想?

在Rust中,错误使用接口会导致编译错误。在Python中,虽然错误代码仍能运行,但使用类型检查器(如pyright)或带类型分析的IDE(如PyCharm)可以获得快速反馈,发现潜在问题。

本文中,Python 中引入了 Rust 的一些理念:尽量使用类型提示,遵循“非法状态不可表示”原则。无论是长期维护的程序还是一次性脚本,我都这样做,因为后者往往会变成前者,而这种方法让程序更易理解和修改。

本文将展示一些应用此方法的Python示例,虽然不算高深,但记录下来或许有用。

类型提示

首先要尽可能使用类型提示,尤其是在函数说明和类属性中。当我看到这样的函数说明。

def find_item(records, check):

从函数说明本身来看,我完全不知道其中发生了什么:是列表、字典还是数据库连接?是布尔值还是函数?函数的返回值是什么?如果失败会发生什么?是抛出异常还是返回某个值?要找到这些问题的答案,我要么必须读取函数的主体(通常还要递归读取它调用的其他函数的主体,这非常烦人),要么只能读取它的文档(如果有的话)。虽然文档中可能包含有关函数的有用信息,但不一定要使用文档来回答前面的问题。许多问题都可以通过内置机制(即类型提示)来回答。

def find_item(
    records: List[Item],
    check: Callable[[Item], bool]
) -> Optional[Item]:

写函数说明要花更多时间吗?是的。

但这有问题吗?没有,除非我打字速度慢到每分钟只能敲几个字,但这很少见。明确写出类型能让我更清楚地思考函数到底提供了什么接口,以及如何让接口更严格,避免调用者用错。有了清晰的函数说明,我一眼就能知道怎么用这个函数、需要传什么参数、返回值是什么。而且,和文档注释不同,文档注释容易过时,但类型检查器会在类型变化时提醒我更新调用代码。如果我想了解某个东西的类型,直接看就行,非常直观。

当然,我也不是死板的人。如果一个参数的类型提示要嵌套五层,我通常会放弃,改用简单但不那么精确的类型。根据我的经验,这种情况很少见。如果真的遇到,那可能是代码设计有问题——如果一个参数既可以是数字、字符串、字符元组,又可以是字典映射字符串到整数,那可能意味着你需要重构和简化代码了。

使用数据类而非元组或字典

使用类型提示只是一方面,它只是描述了函数的接口,第二步是尽可能准确地 “锁定 ”这些接口。一个典型的例子是从函数返回多个值(或单个复杂值),懒惰而快速的方法是返回一个元组:

def find_person(…) -> Tuple[str, str, int]:

我们知道要返回三个值,但它们是什么?第一个字符串是人名吗?第二个是姓氏吗?数字是年龄、位置还是社保号?这种编码方式很不透明,除非看函数内部,否则根本不知道它代表什么。

如果想 改进 ,可以返回一个字典:

def find_person(...) -> Dict[str, Any]:
    ...
    return {
        "name": ...,
        "city": ...,
        "age": ...
    }

现在,我们至少能知道返回的属性是什么,但还是得看函数内部才能确定。某种程度上,类型变得更糟了,因为我们甚至不知道属性的数量和类型。而且,当函数变化时,比如字典的键被重命名或删除,类型检查器很难发现,调用者只能通过运行-崩溃-修改的繁琐循环来调整代码。

正确的解决方案是返回一个强类型的对象,并带有命名的参数。在Python中,这意味着要创建一个类。我猜很多人用元组或字典是因为定义一个类(还得给它起名字)比直接返回数据麻烦得多。但从Python 3.7开始(或者用polyfill包支持更早的版本),有了更简单的解决方案: dataclasses

@dataclasses.dataclass
class City:
    name: str
    zip_code: int

@dataclasses.dataclass
class Person:
    name: str
    city: City
    age: int

def find_person(...) -> Person:

虽然还是得给类起名字,但除此之外,这种方式非常简洁,而且所有属性都有类型注解。

通过这个数据类,函数的返回值变得非常明确。当我调用这个函数并处理返回值时,IDE的自动补全功能会显示属性的名称和类型。这听起来可能很小,但对我来说,这是提高效率的一大优势。此外,当代码重构或属性变化时,IDE和类型检查器会提醒我,并显示需要修改的地方,而不需要运行程序。对于一些简单的重构(比如属性重命名),IDE甚至可以自动完成这些更改。更重要的是,通过明确命名的类型,我可以建立一个共享的词汇表(比如 Person City ),并与其他函数和类共用。

代数数据类型

Rust 有一个大多数主流语言缺乏的强大功能:代数数据类型(ADT)。它能明确描述数据的形状。比如处理数据包时,可以枚举所有可能的类型并为每种类型分配不同字段:

enum Packet {
    Header { protocol: Protocol, size: usize },
    Payload { data: Vec<u8> },
    Trailer { data: Vec<u8>, checksum: usize }
}

通过模式匹配,可以处理每种情况,编译器会检查是否遗漏了任何可能:

fn handle_packet(packet: Packet) {
    match packet {
        Packet::Header { protocol, size } => ...,
        Packet::Payload { data } | Packet::Trailer { data, ... } => println!("{data:?}")
    }
}

ADT 能确保无效状态不可表示,避免运行时错误。它在静态类型语言中特别有用,尤其是当需要统一处理一组类型时。如果没有 ADT,通常需要用接口或继承来实现。如果类型集是封闭的,ADT 和模式匹配是更好的选择。

在 Python 这样的动态类型语言中,虽然不需要为类型集设置共享名称,但类似 ADT 的结构仍然有用。比如可以用联合类型:

@dataclass
class Header:
    protocol: Protocol
    size: int

@dataclass
class Payload:
    data: str

@dataclass
class Trailer:
    data: str
    checksum: int

Packet = Header | Payload | Trailer  # Python 3.10+

Packet 类型可以表示 Header Payload Trailer







请到「今天看啥」查看全文


推荐文章
少女兔  ·  嘿,小心肝!
8 年前
吃喝玩乐新分类  ·  别睡!你可能已经被你的好友给卖了!
7 年前
中国经济网  ·  280年后,人类竟是这般模样?!丨推广
7 年前