使用 Rust 后,我​​使用 Python 的方式发生了变化

使用 Rust 后,我​​使用 Python 的方式发生了变化

Using type hints where possible, and sticking to the classic “make illegal state unrepresentable” principle.
尽可能使用类型提示,并坚持经典的“使非法状态不可表示”原则。


近年来,Rust 因其安全性而闻名,并逐渐被各大科技公司所拥抱——那么,其他主流语言是否可以参考 Rust 的编程思想呢?我以Python为例,做了一些尝试。


几年前我开始使用 Rust 进行编程,它逐渐改变了我用其他编程语言(尤其是 Python)设计程序的方式。在开始 Rust 之前,我通常以一种非常动态、不太严格的方式编写 Python 代码,没有类型提示,到处传递和返回字典,偶尔会回到“字符串类型”接口。然而,在体验了 Rust 类型系统的严格性,并注意到它“通过构造”防止的所有问题之后,每当我返回 Python 时,我都会突然变得相当焦虑,因为我没有得到同样的保证。


需要明确的是,我在这里所说的“保证”不是内存安全(Python 本身就已经相对安全),而是“理智”——设计难以或不可能被滥用的 API,以便防止未定义行为的概念以及各种错误。


在 Rust 中,错误使用接口通常会导致编译错误。在Python中,这样的错误程序仍然可以执行,但是如果您使用类型检查器(例如pyright)或带有类型分析器的IDE(例如PyCharm),您可以获得类似级别的快速反馈以了解潜在问题。


最终,我开始在我的 Python 程序中采用 Rust 的一些概念,这些概念基本上可以归结为两件事:尽可能使用类型提示,并坚持经典的“使非法状态不可表示”原则。我尝试对将要维护一段时间的程序以及一次性实用程序脚本执行此操作 - 因为根据我的经验,后者往往会成为前者,并且这种方法使程序更易于理解和修改。


在本文展示l了一些将此方法应用于 Python 程序的示例。虽然这不完全是先进的科学,但记录它们可能会有用。

Type Hint 类型提示

First and foremost, use type hints wherever possible, especially in function signatures and class attributes. When I see a function signature like this:
首先也是最重要的,尽可能使用类型提示,尤其是在函数签名和类属性中。当我看到这样的函数签名时:

def find_item(records, check):

Looking at the function signature itself, I have absolutely no idea what’s going on in it: is it a list, dictionary or database connection? Is it a boolean or a function? What is the return value of this function? What happens if it fails? Will it throw an exception or return some value? To find answers to these questions, I either have to read the function’s body (and usually recursively read the bodies of other functions it calls, which is very annoying), or I can only read its documentation (if there is one). While the documentation may contain useful information about the function, it should not be necessary to use the documentation to answer the preceding question. Many questions can be answered by a built-in mechanism, namely type hints.
看看函数签名本身,我完全不知道其中发生了什么:它是列表、字典还是数据库连接?它是布尔值还是函数?这个函数的返回值是多少?如果失败会怎样?它会抛出异常或返回一些值吗?为了找到这些问题的答案,我要么必须阅读函数的主体(并且通常递归地阅读它调用的其他函数的主体,这非常烦人),要么我只能阅读它的文档(如果有的话)。虽然文档可能包含有关该函数的有用信息,但不必使用文档来回答前面的问题。许多问题可以通过内置机制(即类型提示)来回答。

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

Does writing the function signature take more time? Yes.
编写函数签名是否需要更多时间?是的。

But is this a problem? No, unless my encoding speed is limited by the number of characters written per minute, which is not common. Writing out the type explicitly forces me to think about what interface the function actually provides, and how to make it as strict as possible so that it’s hard for callers to use it incorrectly. With the function signature above, I can get a good idea of how to use the function, what parameters to pass, and what can be expected to return from the function. Also, unlike doc comments, which are easily outdated when the code changes, the type checker alerts me when I change types but don’t update the function’s callers. If I’m interested in something, I can also just use it and immediately see what that type looks like.
但这有问题吗?不,除非我的编码速度受到每分钟写入的字符数的限制,这并不常见。显式写出类型迫使我思考该函数实际提供的接口是什么,以及如何使其尽可能严格,以便调用者很难错误地使用它。通过上面的函数签名,我可以很好地了解如何使用该函数、要传递哪些参数以及函数预计会返回什么。此外,与代码更改时很容易过时的文档注释不同,类型检查器会在我更改类型但不更新函数的调用者时提醒我。如果我对某些东西感兴趣,我也可以直接使用它并立即看到该类型是什么样子。

Of course, I’m not an absolutist, and if describing a single parameter requires nesting five levels of type hints, I’ll usually give up and use a simpler but less precise type. In my experience, this doesn’t happen very often, and if it does, it might actually signal a problem with your code — if your function arguments can be both numbers and tuples of strings or characters A dictionary that maps strings to integers, which probably means you need to refactor and simplify it.
当然,我不是绝对主义者,如果描述单个参数需要嵌套五层类型提示,我通常会放弃并使用更简单但不太精确的类型。根据我的经验,这种情况并不经常发生,如果发生的话,它实际上可能表明你的代码有问题 - 如果你的函数参数可以是数字和字符串或字符的元组将字符串映射到整数的字典,它可能意味着您需要重构和简化它。

使用Dataclasses 数据类而不是Tuples 元组或Dictionaries字典


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

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: strzip_code: int@dataclasses.dataclass
class Person:name: strcity: Cityage: int
def find_person(...) -> Person:


您仍然需要为创建的类考虑一个名称,但除此之外它尽可能干净,并且您可以获得所有属性的类型注释。


通过这个数据类,我明确了函数返回的是什么。当我调用此函数并处理返回值时,IDE 的自动完成功能会显示属性的名称和类型。这听起来可能微不足道,但对我来说,这是一个巨大的生产力优势。此外,当代码重构和属性更改时,我的 IDE 和类型检查器会提醒我,并向我显示需要在何处进行所有更改,而无需我执行程序。对于一些简单的重构(例如属性重命名),IDE 甚至可以为我进行这些更改,此外,通过显式命名的类型,我可以构建一个词汇表(例如 Person、City),然后与其他函数和类共享。

Algebraic Data Types 代数数据类型


对我来说,Rust 有一个大多数主流语言最缺乏的功能:代数数据类型(ADT)。它是一个非常强大的工具,可以显式描述代码处理的数据的形状。例如,当我在 Rust 中处理数据包时,我可以显式枚举收到的所有可能类型的数据包,并为每个数据包分配不同的数据(字段):

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: Protocolsize: int@dataclass
class Payload:data: str@dataclass
class Trailer:data: strchecksum: int
Packet = typing.Union[Header, Payload, Trailer]
# or `Packet = Header | Payload | Trailer` since Python 3.10

这里,Packet 定义了一个新类型,可以表示报头、有效负载或尾部数据包。但是,这些类别之间没有明确的标识符来区分它们,因此当您想在程序中区分它们时,可以使用一些方法,例如使用“instanceof”运算符或模式匹配。

def handle_is_instance(packet: Packet):if isinstance(packet, Header):print("header {packet.protocol} {packet.size}")elif isinstance(packet, Payload):print("payload {packet.data}")elif isinstance(packet, Trailer):print("trailer {packet.checksum} {packet.data}")else:assert Falsedef handle_pattern_matching(packet: Packet):match packet:case Header(protocol, size): print(f"header {protocol} {size}")case Payload(data): print("payload {data}")case Trailer(data, checksum): print(f"trailer {checksum} {data}")case _: assert False

这里,我们必须在代码中包含一些分支逻辑,以便函数在收到意外数据时崩溃。在 Rust 中,这将是一个编译时错误,而不是 .assert False 。

联合类型的好处之一是它是在联合类之外定义的。因此,该类不知道它包含在联合中,这减少了代码耦合。此外,您甚至可以使用同一类创建多个不同的联合类型:

Packet = Header | Payload | Trailer
PacketWithData = Payload | Trailer

联合类型对于自动(反)序列化也非常有用。最近我发现了一个很棒的序列化库,名为 pyserde,它基于备受推崇的 Rust serde 序列化框架。在许多其他不错的功能中,它利用类型注释来序列化和反序列化联合类型,而无需编写额外的代码:

import serde...
Packet = Header | Payload | Trailer
@dataclass
class Data:packet: Packet
serialized = serde.to_dict(Data(packet=Trailer(data="foo", checksum=42)))
# {'packet': {'Trailer': {'data': 'foo', 'checksum': 42}}}
deserialized = serde.from_dict(Data, serialized)
# Data(packet=Trailer(data='foo', checksum=42))

您甚至可以选择如何序列化联合标签,就像使用 serde 一样。我长期以来一直在寻找类似的功能,因为它对于序列化和反序列化联合类型非常有用。然而,在我尝试过的大多数其他序列化库中,实现这一点相当乏味。

例如,在使用机器学习模型时,我可以使用联合类型将各种类型的神经网络(例如分类或分割 CNN 模型)存储在单个配置文件中。我还发现对不同版本的数据进行版本控制很有用,如下所示:

Config = ConfigV1 | ConfigV2 | ConfigV3


通过反序列化,我可以读取所有以前版本的配置格式,从而保持向后兼容性。

Use NewType 使用新类型


在 Rust 中,定义不添加任何新行为的数据类型是很常见的,但用于指定某些其他常见数据类型(例如整数)的域和预期用途。这种模式称为“NewType”,在 Python 中也可用,例如:

class Database:def get_car_id(self, brand: str) -> int:def get_driver_id(self, name: str) -> int:def get_ride_info(self, car_id: int, driver_id: int) -> RideInfo:db = Database()car_id = db.get_car_id("Mazda")
driver_id = db.get_driver_id("Stig")
info = db.get_ride_info(driver_id, car_id)

发现错误?
get_ride_info 函数的参数位置颠倒了。由于汽车 ID 和驾驶员 ID 是简单整数,因此类型是正确的,尽管函数调用在语义上是错误的。
我们可以通过使用“NewType”为不同类型的 ID 定义单独的类型来解决这个问题:

from typing import NewTypefrom typing import NewType# Define a new type called "CarId", which is internally an `int`
CarId = NewType("CarId", int)
# Ditto for "DriverId"
DriverId = NewType("DriverId", int)
class Database:def get_car_id(self, brand: str) -> CarId:def get_driver_id(self, name: str) -> DriverId:def get_ride_info(self, car_id: CarId, driver_id: DriverId) -> RideInfo:
db = Database()
car_id = db.get_car_id("Mazda")
driver_id = db.get_driver_id("Stig")
# Type error here -> DriverId used instead of CarId and vice-versa
info = db.get_ride_info(<error>driver_id</error>, <error>car_id</error>)


这是一个非常简单的模式,可以帮助捕获那些难以发现的错误,特别是在处理许多不同类型的 ID 和混合在一起的某些指标时。

Use Constructor 使用构造函数


我真正喜欢 Rust 的原因之一是它实际上没有构造函数。相反,人们倾向于使用普通函数来创建(最好是正确初始化的)结构体实例。在Python中,没有构造函数重载的概念,因此如果需要以多种方式构造一个对象,通常会导致一个方法有很多参数,这些参数以不同的方式用于初始化,并且不能真正一起使用。

Instead, I like to create “constructor” functions with an explicit name so that it’s clear how the object is constructed and from what data:
相反,我喜欢创建具有显式名称的“构造函数”函数,以便清楚地了解对象是如何构造的以及由哪些数据构造:

class Rectangle: @staticmethoddef from_x1x2y1y2(x1: float, ...) -> "Rectangle":@staticmethoddef from_tl_and_size(top: float, left: float, width: float, height: float) -> "Rectangle":


这样做使得对象的构造更加清晰,不允许用户传递无效数据,并且更清楚地表达构造对象的意图。

Conclusion 结论


无论如何,我确信Python 代码中还有更多“完整模式”,但目前我能想到的就是以上这些。如果您也有一些类似想法的例子或意见,请留下回复并告诉我。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/2980.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Pytorch】(十三)PyTorch模型部署: TorchScript

文章目录 &#xff08;十三&#xff09;PyTorch模型部署Pytorch动态图的优缺点TorchScriptPytorch模型转换为TorchScripttorch.jit.tracetorch.jit.scripttrace和script的区别总结script 和 trace 混合使用保存和加载模型 &#xff08;十三&#xff09;PyTorch模型部署 Pytorc…

科学高效备考AMC8和AMC10竞赛,吃透2000-2024年1850道真题和解析

如何科学、有效地备考AMC8、AMC10美国数学竞赛&#xff1f;多做真题&#xff0c;吃透真题是科学有效的方法之一&#xff0c;通过做真题&#xff0c;可以帮助孩子找到真实竞赛的感觉&#xff0c;而且更加贴近比赛的内容&#xff0c;可以通过真题查漏补缺&#xff0c;更有针对性的…

成功解决ImportError: cannot import name ‘builder‘ from ‘google.protobuf.internal

成功解决ImportError: cannot import name builder from google.protobuf.internal 目录 解决问题 解决思路 解决方法 解决问题 ImportError: cannot import name builder from google.protobuf.internal 解决思路 导入错误:无法从“google.protobuf.internal”导入名称“…

在React函数组件中使用错误边界和errorElement进行错误处理

在React 18中,函数组件可以使用两种方式来处理错误: 使用 ErrorBoundary ErrorBoundary 是一种基于类的组件,可以捕获其子组件树中的任何 JavaScript 错误,并记录这些错误、渲染备用 UI 而不是冻结的组件树。 在函数组件中使用 ErrorBoundary,需要先创建一个基于类的 ErrorB…

网络通信安全

一、网络通信安全基础 TCP/IP协议简介 TCP/IP体系结构、以太网、Internet地址、端口 TCP/IP协议简介如下&#xff1a;&#xff08;from文心一言&#xff09; TCP/IP&#xff08;Transmission Control Protocol/Internet Protocol&#xff0c;传输控制协议/网际协议&#xff0…

用友NC Cloud importhttpscer接口任意文件上传漏洞

声明 本文仅用于技术交流&#xff0c;请勿用于非法用途 由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;文章作者不为此承担任何责任。 一、漏洞描述 用友NC Cloud的importhttpscer接口如果存在任意文件上传…

开源文本嵌入模型M3E

进入正文前&#xff0c;先扯点题外话 这两天遇到一个棘手的问题&#xff0c;在用 docker pull 拉取镜像时&#xff0c;会报错&#xff1a; x509: certificate has expired or is not yet valid 具体是下面&#x1f447;这样的 rootDS918:/volume2/docker/xiaoya# docker pul…

恒峰智慧科技—森林守护者:森林消防泵如何助力灭火?

在茂密的森林中&#xff0c;一场突如其来的火灾可能带来无法估量的破坏。幸运的是&#xff0c;森林消防泵的出现&#xff0c;帮助我们对抗这些威胁。本文将深入探讨森林消防泵如何在灭火工作中发挥重要作用。 一、森林消防泵的功能和重要性&#xff1a; 首先&#xff0c;我们需…

探索人工智能的边界:GPT 4.0与文心一言 4.0免费使用体验全揭秘!

探索人工智能的边界&#xff1a;GPT与文心一言免费试用体验全揭秘&#xff01; 前言免费使用文心一言4.0的方法官方入口进入存在的问题免费使用文心一言4.0的方法 免费使用GPT4.0的方法官方入口进入存在的问题免费使用GPT4.0的方法 前言 未来已来&#xff0c;人工智能已经可以…

Matlab|基于元模型优化算法的主从博弈多虚拟电厂动态定价和能量管理

1 主要内容 该程序复现《基于元模型优化算法的主从博弈多虚拟电厂动态定价和能量管理》模型&#xff0c;建立运营商和多虚拟电厂的一主多从博弈模型&#xff0c;研究运营商动态定价行为和虚拟电厂能量管理模型&#xff0c;模型为双层&#xff0c;首先下层模型中&#xff0c;构建…

【Android】android 10 jar_sdk_library添加

前言 当前项目遇到客户&#xff0c;Android 10 平台&#xff0c;需要封装jar_sdk_library给第三方应用使用。其中jar_sdk_library中存在aidl文件。遇到无法编译通过问题。 解决 system/tools/aidl修改 Android.bp修改

frp改造Windows笔记本实现家庭版免费内网穿透

文章目录 前言frp原理Windows服务端IP检验IP固定软件下载端口放行端口映射开机启动 NAS客户端端口查询软件下载端口检验穿透测试自启设置 Ubuntu客户端软件下载后台启动 后记 前言 之前一直用花生壳远程控制一个服务器&#xff0c;但最近内网的网络策略似乎发生了变化&#xf…

信息系统项目管理师0068:数据标准化(5信息系统工程—5.2数据工程—5.2.2数据标准化)

点击查看专栏目录 文章目录 5.2.2数据标准化1.元数据标准化2.数据元标准化3.数据模式标准化4.数据分类与编码标准化5.数据标准化管理记忆要点总结5.2.2数据标准化 数据标准化是实现数据共享的基础。数据标准化主要为复杂的信息表达、分类和定位建立相应的原则和规范,使其简单化…

谷歌发布基于声学建模的无限虚拟房间增强现实鲁棒语音识别技术

声学室模拟允许在AR眼镜上以最少的真实数据进行训练&#xff0c;用于开发鲁棒的语音识别声音分离模型。 随着增强现实&#xff08;AR&#xff09;技术的强大和广泛应用&#xff0c;它能应用到各种日常情境中。我们对AR技术的潜能感到兴奋&#xff0c;并持续不断地开发和测试新…

Adobe Illustrator 2024 v28.4.1 (macOS, Windows) - 矢量绘图

Adobe Illustrator 2024 v28.4.1 (macOS, Windows) - 矢量绘图 Acrobat、After Effects、Animate、Audition、Bridge、Character Animator、Dimension、Dreamweaver、Illustrator、InCopy、InDesign、Lightroom Classic、Media Encoder、Photoshop、Premiere Pro、Adobe XD 请…

ChatGPT实战100例 - (18) 用事件风暴玩转DDD

文章目录 ChatGPT实战100例 - (18) 用事件风暴玩转DDD一、标准流程二、定义目标和范围三、准备工具和环境四、列举业务事件五、 组织和排序事件六、确定聚合并引入命令七、明确界限上下文八、识别领域事件和领域服务九、验证和修正模型十、生成并验证软件设计十一、总结 ChatGP…

解线性方程组——(Gauss-Seidel)高斯-赛德尔迭代法 | 北太天元

一、Gauss-Seidel迭代法 n 3 n3 n3时 A ( a 11 a 12 a 13 a 21 a 22 a 23 a 31 a 32 a 33 ) , b ( b 1 b 2 b 3 ) , A\begin{pmatrix} a_{11} & a_{12} &a_{13}\\ a_{21} & a_{22} &a_{23}\\ a_{31} & a_{32} &a_{33}\\ \end{pmatrix} ,\quad b\be…

缓存神器-JetCache

序言 今天和大家聊聊阿里的一款缓存神器 JetCache。 一、缓存在开发实践中的问题 1.1 缓存方案的可扩展性问题 谈及缓存&#xff0c;其实有许多方案可供选择。例如&#xff1a;Guava Cache、Caffine、Encache、Redis 等。 这些缓存技术都能满足我们的需求&#xff0c;但现…

《从零开始的Java世界》10File类与IO流

《从零开始的Java世界》系列主要讲解Javase部分&#xff0c;从最简单的程序设计到面向对象编程&#xff0c;再到异常处理、常用API的使用&#xff0c;最后到注解、反射&#xff0c;涵盖Java基础所需的所有知识点。学习者应该从学会如何使用&#xff0c;到知道其实现原理全方位式…

LAMP(Linux+Apache+MySQL+PHP)环境介绍、配置、搭建

LAMP(LinuxApacheMySQLPHP)环境介绍、配置、搭建 LAMP介绍 LAMP是由Linux&#xff0c; Apache&#xff0c; MySQL&#xff0c; PHP组成的&#xff0c;即把Apache、MySQL以及PHP安装在Linux系统上&#xff0c;组成一个环境来运行PHP的脚本语言。Apache是最常用的Web服务软件&a…