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

news2024/11/20 4:29:02

使用 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.coloradmin.cn/o/1624323.html

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

相关文章

LeetCode78:子集

题目描述 给你一个整数数组 nums &#xff0c;数组中的元素 互不相同 。返回该数组所有可能的 子集 &#xff08;幂集&#xff09;。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。 代码 class Solution { public:vector<vector<int>> res;vector<…

xgp怎么取消续费 手把手介绍微软商店xgp关闭自动续费教程

Xbox是美国微软公司创建的电子游戏品牌&#xff0c;随2001年11月第一代Xbox游戏机发布而首次推出。其产品和服务包括家用游戏机、游戏制作与发行、线上游戏服务和内容订阅服务。Xbox已发售的家用游戏机有Xbox、Xbox360、Xbox One、Xbox One S、Xbox One X、Xbox Series X、Xbox…

亚服战网安装卡45%的解决方案

解决方案 先ping cn.patch.battlenet.com.cn这个域名&#xff0c;正常来说是一定会失败的&#xff0c;如果成功就不卡45%了然后ping tw.patch.battle.net 这个域名&#xff0c;这个域名应该是能成功的。 去该网址解析一下这个域名的ip&#xff0c;https://site.ip138.com/tw.p…

漏洞修复优先级考虑-不错的思路

权威说法&#xff1a; 漏洞利用预测评分系统 &#xff08;EPSS&#xff09; 是一项数据驱动的工作&#xff0c;用于估计软件漏洞在野外被利用的可能性&#xff08;概率&#xff09; https://www.first.org/epss/ GitHub - TURROKS/CVE_Prioritizer: Streamline vulnerability…

Kubernetes TDengine 系列|安装 TDengine 的 Grafana 插件|Grafana监控TDengine数据

为了让Grafana 能够监控到TDengine 数据&#xff0c;快速集成搭建数据监测报警系统&#xff0c;所以直接安装TDengine 插件。 目录 一、安装 TDengine 的 Grafana 插件1、下载TDengine grafana插件2、解压到指定目录3、配置未签名插件 二、配置数据源&#xff0c;简单查询TDen…

JDK的安装和配置

目录 1.Java 开发工具包在上方已关联资源下载使用2.JAVA_HOME3.CLASSPATH4.PATH5.安装jdk17情况需要将path变量中删除6.包内含有visualvm7.注意&#xff1a;如果安装JDK17在我们安装的时候可能会自动进行环境变量配置&#xff0c;我们需要在环境变量配置PATH中删除如下信息8.验…

汽车信息安全--如何理解TrustZone(1)

目录 1.车规MCU少见TrustZone 2. 什么是TrustZone 2.1 TrustZone隔离了什么&#xff1f; 2.2 处理器寄存器和异常处理 3.小结 1.车规MCU少见TrustZone 在车规MCU里&#xff0c;谈到信息安全大家想到的大多可能都是御三家的HSM方案&#xff1a;英飞凌的HSM\SHE、瑞萨的ICU…

循环神经网络介绍(RNN)

序列模型 定义&#xff1a;自然语言处理、音频、视频以及其他序列数据的模型 类型&#xff1a; 语音识别 情感分析 机器翻译 特点&#xff1a; 序列数据前后之间有很强的关联性 序列数据的输入输出长度不固定 循环神经网络 定义&#xff1a;循环&#xff08;递归&#xff…

revit\navisworks各种安装问题

You have entered a nonvalid serial number &#xff0c;怎么都不给你一个机会输出序列号&#xff0c;怎么办&#xff1f; step1: C:\Program Files (x86)\Common Files\Autodesk Shared\AdskLicensing目录下找到uninstall.exe&#xff0c;右键管理员模式运行&#xff0c;会…

7.MyBatis 操作数据库(初阶)

文章目录 1.什么是MyBatis2.为什么要学习 MyBatis&#xff1f;3.通过spring框架创建MyBatis项目3.1使用MyBatis查询数据库3.2 mysql连接不上报错解决方法 4.MyBatis的基础操作4.1企业建表规范&#xff1a;4.2MyBatis基本实现4.3单元测试4.4使用MyBatis可能遇到的问题4.5配置MyB…

初学python记录:力扣2385. 感染二叉树需要的总时间

题目&#xff1a; 给你一棵二叉树的根节点 root &#xff0c;二叉树中节点的值 互不相同 。另给你一个整数 start 。在第 0 分钟&#xff0c;感染 将会从值为 start 的节点开始爆发。 每分钟&#xff0c;如果节点满足以下全部条件&#xff0c;就会被感染&#xff1a; 节点此…

一款面向个人和企业的本地云存储解决方案——派盘

派盘是一款什么软件? 派盘是一款面向个人和企业的本地云存储解决方案,它利用了本地硬盘的存储容量,通过“云化”的方式,可以将本地硬盘变成云存储空间,拥有强大的数据保护功能,保证了数据的私密性和安全性。 派盘不仅可以帮助个人用户打造“数字第二大脑”,还可以帮助团…

03 - 伪目标

---- 整理自狄泰软件唐佐林老师课程 文章目录 1. 思考2. 伪目标的引入2.1 伪目标的语法&#xff1a;先声明&#xff0c;后使用2.2 伪目标的妙用&#xff1a;规则调用&#xff08;函数调用&#xff09;2.3 绕开 .PHONY 关键字定义伪目标 1. 思考 Makefile 中的 目标 究竟是什么&…

1.3K Star我上位机项目中用了这个开源项目

软件介绍 ClientServerProject的软件是一款基于C-S&#xff08;客户端-服务器&#xff09;架构的通用开发框架&#xff0c;为中小型系统的快速开发提供强大的支持。该框架由服务端、客户端以及公共组件三部分组成&#xff0c;不仅提供了基础的账户管理、版本控制、软件升级、公…

车企如何利用数据技术,指导汽车全生命周期的业务运营?

引言&#xff1a;数据正作为重点&#xff0c;为行业提供不可或缺的指导 《汽车数据发展研究报告&#xff08;2023&#xff09;》指出&#xff0c;汽车行业正由传统硬件制造向“电动化、智能化、网联化”方向转变。德勤预测&#xff0c;到 2025 年&#xff0c;汽车行业 20%的利…

vue集成百度地图vue-baidu-map

文章目录 vue集成百度地图vue-baidu-map1. Vue Baidu Map文档地址2. 设置npm数据源3. 安装vue-baidu-map4. 配置vue-baidu-map4.1 main.js全局注册4.2 vue页面设置4.3 效果 vue集成百度地图vue-baidu-map 1. Vue Baidu Map文档地址 https://dafrok.github.io/vue-baidu-map/#…

WCH RISC CH32V303RCT6 单片机的SDI Printf 虚拟串口功能 类似RTT打印功能 简单分析

参考&#xff1a; 有关于 SDI printf 更多的信息和资料吗&#xff1f; 关于 CH32 系列 MCU SDI 虚拟串口功能的使用 【CH32X035 评估板测评】 教你使用 SDI 接口重定向 printf 0.前言 有段时间没有看CH32V单片机的开发了&#xff0c;今天帮新来的同事调试时候看到debug.c里面有…

Confluence 快捷键大揭秘:提高效率的小窍门

使用 Confluence 快捷键的好处有&#xff1a; 1.提高工作效率&#xff1b; 2.更流畅地进行编辑、导航和管理操作&#xff1b; 3.减少误操作&#xff1b; 4.展现专业水平。 更多精彩内容&#xff1a; 成为 Jira 大师&#xff1a;效率达人的必备秘诀 Jira Cloud 项目管理专栏 PMO…

WPF —— lCommand命令实例

首先在标签页面设置一个Button按钮 <Button Width"100" Height"40" Content"测试" ></Button> 1 创建一个类 继承于ICommand这个接口&#xff0c; 这个接口一般包含三部分&#xff1a; 俩个方法&#xff1a;一个判断指令是不是…

神经网络算法

神经网络基础&#xff08;用来做特征提取的&#xff09; 一、前向传播二、反向传播三、整体架构四、激活函数五、数据预处理六、参数初始化七、DROP-OUT&#xff08;在测试过程中因模型太过复杂而采取的一种随机杀死的方法&#xff09;八、总结 一、前向传播 1、线性函数 1&am…