没有延迟,没有宕机,平均每分钟核酸采样7.6万,这是一个奇迹,一个由中国电信上海公司、天翼云科技上海分公司、华为共同创造的奇迹。

频年来,Rust 以其安全性出名,渐渐被各大科技巨头所拥抱——那么,其他主滥调语是否可以参考 Rust 的编程念念想呢?本文作家以 Python 为例,作念了一番尝试。
原文汇注:https://kobzol.github.io/rust/python/2023/05/20/writing-python-like-its-rust.html
未经允许,阻滞转载!
作家 | Jakub Beránek译者 | ChatGPT 责编 | 郑丽媛出品 | CSDN(ID:CSDNnews)从几年前脱手,我尝试用 Rust 进行编程,它渐渐改变了我在其他编程言语中瞎想姿色的景观,尤其是 Python。
在脱手用 Rust 之前,我平庸是以一种相等动态、不太严谨的景观来编写 Python 代码,莫得类型请示,到处传递和复返字典,偶尔还回退到“字符串类型”接口。联系词,在体验了 Rust 类型系统的严格性,并庄重到它“通过 construction”防御的总计问题后,每当我回到 Python 时,就会片刻变得颠倒急躁,因为我莫得得到相同的保证。
明确少许,我在这里所说的“保证”并不是指内存安全(Python 在原多情况下已相对安全),而是指“健全性”——瞎想很难或根蒂不可能被虚耗的 API,从而防御未界说活动和各式空虚的主意。
在 Rust 中,空虚使用的接口平庸会导致编译空虚。而在 Python 中,这么的空虚姿色照旧可以实施的,但如果你使用类型查验器(如 pyright)或带有类型分析器的 IDE(如 PyCharm),你就可以得到类似水平的快速反馈,以了解可能存在的问题。
最终,我脱手在我的 Python 姿色中经受一些来自 Rust 的主意,基本上可以归结为两点:尽可能使用类型请示,以及坚合手经典的“使违纪景色不可示意”原则。我试着对那些将被激情一段时候的姿色以及一次性实用剧本皆这么作念——因为字据我的造就,后者时时会形成前者,而这种步履会让姿色更易于瓦解和修改。
在本文中,我将展示几个将该步履应用于 Python 姿色的示例。天然这并不是什么郁勃的科学,但我认为把它们记载下来可能会有用。
庄重:本文中包含了好多对于编写 Python 代码的不雅点,我不想在每句话中皆加上“在我看来”,是以请把本文中的一切皆只是看作是我对此问题的不雅点,而不是试图宣传某些普遍真义。相同,我也不主张本文所建议的想法皆是在 Rust 中发明的,它们在其他言语中也有使用。
类型请示
起首,最伏击的是要尽可能地使用类型请示,超越是在函数签名和类属性中。当我看到一个像这么的函数签名时:
def find_item(records, check):从函数签名自身来看,我完全无法瓦解其中发生了什么:它是一个列表,字典照旧数据库联结?是一个布尔值照旧函数?这个函数的复返值是什么?如果它失败了会发生什么?会激发颠倒照旧复返某个值?要找到这些问题的谜底,我要么去阅读函数的主体(平庸还要递归地阅读它调用的其他函数的主体,这相等烦东谈主),要么只可阅读它的文档(如果有的话)。天然文档中可能包含了对于该函数的有用信息,但不应该必须使用文档往复复前边的问题。好多问题可以通过内置机制,即类型请示往复复。
def find_item( records: List[Item], check: Callable[[Item], bool]) -> Optional[Item]:写函数签名是否破耗更多时候?是的。但这是个问题吗?不是,除非我的编码速率受到每分钟写入字符数目的截至,而这并不常见。明确地写出类型,迫使我念念考函数骨子提供的接口是什么,以及如何使其尽可能严格,让调用者难以空虚地使用它。通过上头的函数签名,我可以很好地了解如何使用函数,传递什么参数,以及可以祈望从函数中复返什么。此外,与文档审视不同的是,现代码发生变化时,文档审视很容易过期,而当我蜕变类型但未更新函数的调用者时,类型查验器会提醒我。如果我对什么感趣味趣味,我也可以径直使用,独立即看到该类型看起来是如何的。
天然,我并不是全皆主义者,如果形色单个参数需要嵌套五层类型请示,我平庸会烧毁,并使用一个更浅显但不太精准的类型。字据我的造就,这种情况不常发生,如果它确凿发生了,它骨子上可能预示了代码的问题——如果你的函数参数既可以是数字,又可以是字符串元组或将字符串映射为整数的字典,这可能意味着你需要重构和简化它。
使用数据类(Dataclasses)代替元组或字典
使用类型请示只是一方面,它仅形色了函数的接口是什么,第二步是尽可能准确地"锁定"这些接口。一个典型的例子是,从函数复返多个值(或单个复杂值),有一种懒惰且快速的步履是复返一个元组:
def find_person(...) -> Tuple[str, str, int]:很好,咱们知谈咱们要复返三个值,它们是什么?第一个字符串是这个东谈主的名字吗?第二个字符串是姓氏吗?数字是什么?是年岁吗?照旧某个列表中的位置?亦或是社会保险号码?这种类型的编码并不透明,除非你稽查函数体,不然你根蒂不知谈这代表着什么。
接下来如果要 "蜕变 "这少许,可以复返一个字典:
def find_person(...) -> Dict[str, Any]: ... return { "name": ..., "city": ..., "age": ... }当今,咱们骨子上可以知谈各个复返属性是什么了,但咱们又必须查验函数体本事发现。从某种风趣上说,这个类型变得更糟了,因为当今咱们甚而不知谈各个属性的数目和类型。此外,当这个函数发生变化,复返的字典中的键被重定名或删除时,用类型查验器是退却易发现的,因此调用者平庸必须资格相等繁琐的手动运行-崩溃-修改代码轮回来进行蜕变。
正确的治理决议是,复返一个具有附加类型的定名参数的强类型对象。在 Python 中,这意味着咱们需要创建一个类。我怀疑在这些情况下时时使用元组和字典,是因为相较于界说一个类(并为其定名),创建带参数的构造函数、将参数存储到字段中等要浅显得多。自从 Python 3.7(以及使用 polyfill 包的更早版块)版块之后,有了一个更快捷的治理决议:.dataclasses。
@dataclasses.dataclassclass City: name: str zip_code: int@dataclasses.dataclassclass Person: name: str city: City age: intdef find_person(...) -> Person:你仍然需要为创建的类想一个名字,但除此以外,它已尽可能精真金不怕火,而且你可以得到总计属性的类型审视。
通过这个数据类,我明确了函数复返的内容。当我调用这个函数并处理复返值时,IDE 的自动完到手能会露出属性的称号和类型。听起来这可能很微不及谈,但对我来说,这是一个很大的坐褥力上风。此外,现代码被重构、属性发生变化时,我的 IDE 和类型查验器会提醒我,并露出总计需要蜕变的位置,无需我实施姿色。对于一些浅显的重构(如属性重定名),IDE 甚而可以为我进行这些蜕变,此外,通过明细目名的类型,我可以确立一个词汇表(举例 Person、City),然后与其他函数和类分享。
代数数据类型
对我而言,在使用大普遍主滥调语时,最枯竭一项 Rust 的特质:代数数据类型(ADT)。它是一种相等辽远的器具,可以明确形色代码处理的数据体式。举例,当我在 Rust 中处理数据包时,我可以明确列举总计可能经受到的数据包种类,并为每个数据包分派不同的数据(字段):
enum Packet { Header { protocol: Protocol, size: usize }, Payload { data: Vec }, Trailer { data: Vec, 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 的器具仍然很有风趣,举例可以创建一个合股类型:
@dataclassclass Header: protocol: Protocol size: int@dataclassclass Payload: data: str@dataclassclass Trailer: data: str checksum: intPacket = 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 | TrailerPacketWithData = Payload | Trailer合股类型对于自动(反)序列化也相等有用。最近我发现了一个很棒的序列化库叫作念 pyserde,它是基于备受崇尚的 Rust serde 序列化框架诱骗的。除了许多其他可以的功能以外,它能专揽类型审视来序列化和反序列化合股类型,而无需编写独特的代码:
import serde...Packet = Header | Payload | Trailer@dataclassclass Data: packet: Packetserialized = 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通过反序列化,我能读取总计当年版块的竖立姿色,从而保合手向后兼容。
使用 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# 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-versainfo = db.get_ride_info(driver_id/error>, car_id>)这是一个相等浅显的模式,可以匡助捕捉那些难以发现的空虚,尤其安妥处理许多不同类型的 ID 和某些混在通盘的度量办法。
使用构造函数
我很可爱 Rust 的少许是,它莫得着实风趣上的构造函数。相背,东谈主们倾向于使用平常函数来创建(最佳是正确驱动化的)结构体实例。在 Python 中,莫得构造函数重载的主意,因此如果你需要以多种景观构造一个对象,平庸会导致一个步履有好多参数,这些参数以不同的景观用于驱动化,而且不成着实地通盘使用。
相背,我可爱用一个明确的名字来创建 "构造 "函数,以便表露地了解如何构造对象以及从哪些数据中构造:
class Rectangle: @staticmethod def from_x1x2y1y2(x1: float, ...) -> "Rectangle": @staticmethod def from_tl_and_size(top: float, left: float, width: float, height: float) -> "Rectangle":这么作念可以使对象的构造更表露,而且不允许用户传递无效数据,也愈加表露地抒发了构造对象的意图。
用类型对不变量进行编码
用类型系统自身来编码在运行时只可追踪的不变量,是一个相等通用且辽远的主意。在 Python(以偏激他主滥调语)中,我时时看到由一大堆可变景色构成的复杂类,导致这种浩大的原因之一是:代码试图在运行时追踪对象的不变量。它必须磋议许多在表面上可能发生的情况,因为这些情况并莫得被类型系统摒除(举例“如果客户端被条目断开联结,但有东谈主尝试向其发送音讯,而 Socket 仍处于联结景色”等)
客户端
底下是一个典型的例子:
class Client: """ Rules: - Do not call `send_message` before calling `connect` and then `authenticate`. - Do not call `connect` or `authenticate` multiple times. - Do not call `close` without calling `connect`. - Do not call any method after calling `close`. """ def __init__(self, address: str): def connect(self): def authenticate(self, password: str): def send_message(self, msg: str): def close(self):……很浅显,对吧?你只需要仔细阅读文档,并确保耐久不会违背提到的规章(以免激发未界说活动或崩溃)。另一种步履是在类中填入各式断言,在运行时查验总计提到的规章,这将导致浩大的代码、遗漏的边际情况和出错时较慢的反馈(编译时与运行时之间的区别)。问题的中枢在于客户端可以存在于各式(互斥的)景色中,但它们并莫得被分别建模成单独的类型,而是全部同一到一个类型中。
让咱们望望,是否可以通过将不同景色拆分为单独的类型来蜕变这少许。
(1)起首,一个莫得联结到任何东西的客户端是否有风趣?似乎莫得。在调用之前,这么一个莫得联结的客户端无法实施任何操作。那为什么要允许这种景色存在呢?咱们可以创建一个构造函数,它将复返一个联结的客户端:Clientconnectconnect。
def connect(address: str) -> Optional[ConnectedClient]: passclass ConnectedClient: def authenticate(...): def send_message(...): def close(...):如果函数到手,它将复返一个战胜“已联结”不变式的客户端,你也不成再调用它来搅散事情。如果联结失败,该函数可激发颠倒或复返一些显式空虚。
(2)类似的步履也可用于景色。咱们可以引入另一个类型,它领有客户端既联结又认证的不变性:authenticated。
class ConnectedClient: def authenticate(...) -> Optional["AuthenticatedClient"]:class AuthenticatedClient: def send_message(...): def close(...):只须当咱们着实有了一个实例,咱们本事脱手发送音讯。
(3)终末一个问题是步履。在 Rust 中(收成于骚扰性迁移语义),咱们大概抒发这么一个事实:当步履被调用时,你不成再使用客户端。但这在 Python 中是不可能的,是以咱们必须使用一些变通办法。有一个治理决议是回退到运行时追踪,在客户端引入一个布尔属性,并断言它还莫得被关闭。另一种步履是完全删除该步履,只将客户端当作一个高下文料理器:
with connect(...) as client: client.send_message("foo")# Here the client is closed由于莫得可用的步履,你无法不测地关闭客户端两次。
强类型的范围框
标的检测是一项我无意会参与的策画机视觉任务,其中姿色必须在图像中检测一组范围框。范围框基本上是带有一些附加数据的矩形,在达成标的检测时,它们随地可见。不外范围框有一个令东谈主歧视的问题是:无意它们是表率化的(矩形的坐标和大小在区间内),但无意它们长短表率化的(坐标和大小受其所附图像的尺寸截至)。当你通过许普遍据预处理或后处理的函数发送范围框时,很容易阻挡这少许,举例屡次表率化范围框,这就会导致相等吃力的调试空虚。
这种情况发生过好几次,是以我决定:将这两种类型的范围框拆分为两个单独的类型,以此来灵验治理问题:NormalizedBoundingBox 和 DenormalizedBoundingBox。
@dataclassclass NormalizedBBox: left: float top: float width: float height: float@dataclassclass DenormalizedBBox: left: float top: float width: float height: float这么分离之后,表率化和非表率化的范围框就退却易阻挡了。不外咱们还可以再作念一些蜕变,把代码变得更相宜“东谈主体工学”。(1)通过组合或秉承来减少类似:
@dataclassclass BBoxBase: left: float top: float width: float height: float# Compositionclass NormalizedBBox: bbox: BBoxBaseclass DenormalizedBBox: bbox: BBoxBaseBbox = Union[NormalizedBBox, DenormalizedBBox]# Inheritanceclass NormalizedBBox(BBoxBase):class DenormalizedBBox(BBoxBase):(2)添加一个运行时查验,以确保范围框照实是表率化的:
class NormalizedBBox(BboxBase): def __post_init__(self): assert 0.0 1.0 ...(3)添加一个在两种示意之间进行调度的步履。在某些情况下,咱们可能想要知谈明确的示意景观,但无意候咱们也但愿能使用通用接口(“任何类型的范围框”)进行操作。在这种情况下,咱们应该大概将“任何范围框”调度为其中一种示意景观:
class BBoxBase: def as_normalized(self, size: Size) -> "NormalizeBBox": def as_denormalized(self, size: Size) -> "DenormalizedBBox":class NormalizedBBox(BBoxBase): def as_normalized(self, size: Size) -> "NormalizedBBox": return self def as_denormalized(self, size: Size) -> "DenormalizedBBox": return self.denormalize(size)class DenormalizedBBox(BBoxBase): def as_normalized(self, size: Size) -> "NormalizedBBox": return self.normalize(size) def as_denormalized(self, size: Size) -> "DenormalizedBBox": return self通过这个接口,我可以兼顾正确性和东谈主性化的和解界面。
庄重:如果你想给父类/基类添加一些分享步履,复返对应类的实例,你可以在 Python 3.11 中使用 typing.Self。
class BBoxBase: def move(self, x: float, y: float) -> typing.Self: ...class NormalizedBBox(BBoxBase): ...bbox = NormalizedBBox(...)# The type of `bbox2` is `NormalizedBBox`, not just `BBoxBase`bbox2 = bbox.move(1, 2)更安全的互斥锁
在 Rust 中,互斥锁平庸通过一个相等好的接口提供,这有两个平正:
(1)当你锁定互斥锁时,会得到一个“守卫”对象,该对象在捐躯时可自动解锁互斥锁,主要专揽了可靠的 RAII 机制:
{ let guard = mutex.lock(); // locked here ...} // automatically unlocked here这意味着,不会出现健忘解锁互斥锁的情况。在 C++ 中也有类似机制如 std::mutex,但它提供了一种莫得“守卫”对象的显式/接口,这意味着其仍可能被空虚使用。
(2)受互斥锁保护的数据径直存储在互斥锁(结构体)中,通过这种瞎想,无法在莫得锁定互斥锁的情况下窥伺受保护的数据。你必须先锁定互斥锁以获取“守卫”对象,然后再窥伺数据:
let lock = Mutex::new(41); // Create a mutex that stores the data insidelet guard = lock.lock().unwrap(); // Acquire guard*guard += 1; // Modify the data using the guard这与主滥调语(包括 Python)中常见的互斥锁 API 完全不同——在主滥调语中,互斥锁和受其保护的数据是分开的,因此在窥伺数据之前很容易健忘锁定互斥锁:
mutex = Lock()def thread_fn(data): # Acquire mutex. There is no link to the protected variable. mutex.acquire() data.append(1) mutex.release()data = []t = Thread(target=thread_fn, args=(data,))t.start()# Here we can access the data without locking the mutex.data.append(2) # Oops天然在 Python 中,咱们无法得到与 Rust 完全相易的功能,但它也不是一无是处。Python 锁达成了高下文料理器接口,这意味着你可以在代码块中使用它们,确保它们在作用域收尾时自动解锁,甚而咱们还可以更进一步:使用 with 语句。
import contextlibfrom threading import Lockfrom typing import ContextManager, Generic, TypeVarT = TypeVar("T")# Make the Mutex generic over the value it stores.# In this way we can get proper typing from the `lock` method.class Mutex(Generic[T]): # Store the protected value inside the mutex def __init__(self, value: T): # Name it with two underscores to make it a bit harder to accidentally # access the value from the outside. self.__value = value self.__lock = Lock() # Provide a context manager `lock` method, which locks the mutex, # provides the protected value, and then unlocks the mutex when the # context manager ends. @contextlib.contextmanager def lock(self) -> ContextManager[T]: self.__lock.acquire() try: yield self.__value finally: self.__lock.release()# Create a mutex wrapping the datamutex = Mutex([])# Lock the mutex for the scope of the `with` blockwith mutex.lock() as value: # value is typed as `list` here value.append(1)使用这种瞎想,只须在锁定互斥锁之后,你本事窥伺受保护的数据。显明,这仍是 Python,如果你是特意的,不变量仍可以被骚扰——但这个步履已使得在 Python 中使用互斥锁接口愈加安全。
总之,我服气在我的 Python 代码中还有更多的 "健全性模式",但以上是我目下能猜测的全部。如果你也有一些类似想法的例子或意见,接待留言告诉我。
","gnid":"9b7dfe809732f7ab0","img_data":[{"flag":2,"img":[{"desc":"","height":"80","s_url":"http://p0.img.360kuai.com/t013d73ffee4a20366b_1.gif","title":"","url":"http://p0.img.360kuai.com/t013d73ffee4a20366b.gif","width":"640"},{"desc":"","height":"720","title":"","url":"http://p1.img.360kuai.com/t0122c035d14e447c5a.jpg","width":"1080"}]}],"original":0,"pat":"art_src_1,fts0,sts0","powerby":"pika","pub_time":1685496789000,"pure":"","rawurl":"http://zm.news.so.com/ddded3e1cc698c67c62f8f595bbe32f7","redirect":0,"rptid":"1d49756adf6900b3","rss_ext":[],"s":"t","src":"CSDN","tag":[],"title":"“用过 Rust 后,我写 Python 的步履皆变了!”","type":"zmt","wapurl":"http://zm.news.so.com/ddded3e1cc698c67c62f8f595bbe32f7","ytag":"科技:策画机技能","zmt":{"brand":{},"cert":"优质科技范围创作家","desc":"专科的中语IT技能社区剑侠情缘手游答题,与千万技能东谈主共成长。","fans_num":447,"id":"3084819448","is_brand":"0","name":"CSDN","new_verify":"5","pic":"http://p6.img.360kuai.com/t01a56772a1a256f5c1.png","real":1,"textimg":"http://p9.img.360kuai.com/bl/0_3/t017c4d51e87f46986f.png","verify":"0"},"zmt_status":0}","errmsg":"","errno":0}