使用 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: str
    zip_code: int

@dataclasses.dataclass
class Person:
    name: str
    city: City
    age: 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: Protocol
    size: int

@dataclass
class Payload:
    data: str

@dataclass
class Trailer:
    data: str
    checksum: 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 False

def 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 NewType

from 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: 
    @staticmethod
    def from_x1x2y1y2(x1: float, ...) -> "Rectangle":
    
    @staticmethod
    def from_tl_and_size(top: float, left: float, width: float, height: float) -> "Rectangle":


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

Conclusion 结论


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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

PyTorch Conv2d 前向传递中发生了什么?

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

决策大模型专题(一)

决策智能不应该停留在以前的思维中了&#xff0c;现在开一个专题来学习一下决策论坛的老师们的精彩的内容。本内容来自决策大模型论坛&#xff0c;张伟楠老师的内容整理。 决策大模型 是新一代人工智能的底层技术&#xff0c;它可以去赋能&#xff0c;智能体也就是AI agent&a…

C++进阶:map与set容器的使用

目录 1. 关联式容器map与set2. set与multiset的接口与使用2.1 set的接口与使用2.1.1 成员函数2.1.2 迭代器2.1.3 容量相关2.1.4 修改相关 2.1.5 查找&#xff0c;计数与补充2.2 multiset的接口与使用 3. map与multimap的接口与使用3.1 map的接口与使用3.1.1 map的使用补充3.1.2…

小孩子不懂事,写着玩的

目录 Web攻防 特有漏洞 ASP安全 ASPX&#xff08;.NET&#xff09;安全 PHP安全 JavaWeb安全 JS&#xff0c;Node.js安全 Java安全 Python安全 通用漏洞 SQL注入 MySQL-root高权限读写注入 PostgreSQL-高权限读写注入 MSSQL-sa高权限读写执行注入 SQL注入体系 o…

QWidget 类

QWidget 类中包括框架的属性 QWidget 类中不包括框架的属性 总结:可使用以下两种方法设置部件的位置和大小 ①、通常使用 move()设置部件的位置,使用 resize()设置部件的大小。 ②、使用 setGeometry()函数同时设置部件的位置和大小。 ③、无法为部件指定包含边框在内的大…

C语言操作符和关键字

文章目录 操作符单目操作符sizeof&#xff08;类型&#xff09;强制类型转换 关系操作符、逻辑操作符、条件操作符逗号表达式 常见关键字typedefstaticstatic修饰局部变量static修饰全局变量static修饰函数 register寄存器关键词define定义常量和宏 操作符 单目操作符 C语言中…

echarts bar图表实现多个label显示

2024.0.23今天我学习了使用bar组件&#xff0c;可以渲染多个label显示的效果&#xff0c;如&#xff1a; 当我们有一个这样的图表时&#xff0c;根据需求需要在 这上面的顶部再显示一个空置床位数占用床位数的合计总值&#xff0c;如果直接添加一个label肯定是不行&#xff0c;…

深度学习-线性代数

目录 标量向量矩阵特殊矩阵特征向量和特征值 标量由只有一个元素的张量表示将向量视为标量值组成的列表通过张量的索引来访问任一元素访问张量的长度只有一个轴的张量&#xff0c;形状只有一个元素通过指定两个分量m和n来创建一个形状为mn的矩阵矩阵的转置对称矩阵的转置逻辑运…

[MYSQL索引优化] 分页查询优化

这里一共介绍两种常见的分页索引优化技巧,let go! 示例表: CREATE TABLE t_product (id int(0) NOT NULL,pname varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,price double(7, 2) NULL DEFAULT 0.00,promoteSales varchar(200) CHARA…

Linux进程详解三:进程状态

文章目录 进程状态Linux下的进程状态运行态-R阻塞态浅度休眠-S深度睡眠-D暂停状态-T暂停状态-t 终止态僵尸-Z死亡-X 孤儿进程 进程状态 进程的状态&#xff0c;本质上就是一个整型变量&#xff0c;在task_struct中的一个整型变量。 状态的存在决定了你的后续行为动作。 Linu…

DRF: 序列化器、View、APIView、GenericAPIView、Mixin、ViewSet、ModelViewSet的源码解析

前言&#xff1a;还没有整理&#xff0c;后续有时间再整理&#xff0c;目前只是个人思路&#xff0c;文章较乱。 注意路径匹配的“/” 我们的url里面加了“/”&#xff0c;但是用apifox等非浏览器的工具发起请求时没有加“/”&#xff0c;而且还不是get请求&#xff0c;那么这…

C++字符串中单词的提取以及按符号分隔

句子中的单词提取利用stringstream即可 如果要分割需配合getline使用 两者前提都是要先转化为字符串流。

Linux套接字编程详解

Linux套接字编程 预备知识IP地址和MAC地址套接字结构网络字节序 UDP套接字编程服务端代码客服端代码 TCP 套接字守护进程 计算器模块1 日志头文件序列化和反序列化 预备知识 IP地址和MAC地址 MAC地址用来在局域网中标识唯一主机 Ip地址用于在广域网中标识唯一主机 &#xff0…

李廉洋:4.24-4.25现货黄金,WTI原油区间震荡,走势分析。

黄金消息面分析&#xff1a;金银近日回调。随着伊朗方面淡化以色列最新反击&#xff0c;中东地区局势没有进一步发酵下&#xff0c;风险溢价下降金银出现较大幅度调整。由于近期高于预期的通胀数据&#xff0c;降息预期持续降温。昨日疲软的美国PMI以及以色列在加沙攻击的加剧支…

【Unity】AssetBundle加载与卸载

unity官方apiAssetBundle-LoadFromFileAsync - Unity 脚本 API 异步加载AB包 using UnityEngine; using System.Collections; using System.IO;public class LoadFromFileAsyncExample : MonoBehaviour {IEnumerator Start(){var bundleLoadRequest AssetBundle.LoadFromFil…

消息服务应用1——java项目使用websocket

在当前微服务项目中&#xff0c;由于业务模块众多&#xff0c;消息服务的使用场景变得异常活跃。而WebSocket由于其自身的可靠性强&#xff0c;实时性好&#xff0c;带宽占用更小的优势&#xff0c;在实时通讯应用场景中独占鳌头&#xff0c;加上HTML5标准的普及流行&#xff0…

OpenCompass 大模型评测实战——笔记

OpenCompass 大模型评测实战——笔记 一、评测1.1、为什么要做评测1.2、如何通过能力评测促进模型发展1.2.1、面向未来拓展能力维度1.2.2、扎根通用能力1.2.3、高质量1.2.4、性能评测 1.3、评测的挑战1.3.1、全面性1.3.2、评测成本1.3.3、数据污染1.3.4、鲁棒性 二、OpenCompas…

java-junit单元测试

问题 Junit框架 代码 工具类 // 工具类 public class StringUtils {// 获取字符串的最大下标public static int getMaxIndex(String str){// 这个地方是有问题的&#xff0c;应该是str.length() - 1 也没有进行str是否为空的判断return str.length() ;} }测试类 测试类类名&…

vcontact2:病毒聚类(失败)

Bitbucket 安装 mamba create --name vContact2 biopython1.78 mamba install -c bioconda vcontact20.11.3vim ~/envs/vContact2/lib/python3.9/site-packages/vcontact2/exports/summaries.py 把 np.warnings.filterwarnings(ignore) 改成 import warnings warnings.filte…

递归、搜索与回溯算法:FloodFill 算法

例题一 算法思路&#xff1a; 可以利⽤「深搜」或者「宽搜」&#xff0c;遍历到与该点相连的所有「像素相同的点」&#xff0c;然后将其修改成指定的像素即可。 全局变量&#xff1a; int dx[4] { 0,0,1,-1 }, dy[4] { 1,-1,0,0 }; int m, n; int precolor;//记录原先的颜色…
最新文章