自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

受 Rust 啟發(fā),是時(shí)候改變 Python 編程方式了

開發(fā) 前端
Rust 并沒(méi)有構(gòu)造函數(shù)。相反,人們傾向于使用普通函數(shù)來(lái)創(chuàng)建(最好是正確初始化的)結(jié)構(gòu)體實(shí)例。在 Python 中,沒(méi)有構(gòu)造函數(shù)重載的概念,所以如果你需要以多種方式構(gòu)造一個(gè)對(duì)象,通常會(huì)產(chǎn)生一個(gè)帶有許多參數(shù)的方法,這些參數(shù)以不同的方式用于初始化,并不能一起使用。

近年來(lái),Rust因安全性受到科技公司青睞。其他主流語(yǔ)言能否借鑒Rust的思想?

在Rust中,錯(cuò)誤使用接口會(huì)導(dǎo)致編譯錯(cuò)誤。在Python中,雖然錯(cuò)誤代碼仍能運(yùn)行,但使用類型檢查器(如pyright)或帶類型分析的IDE(如PyCharm)可以獲得快速反饋,發(fā)現(xiàn)潛在問(wèn)題。

本文中,Python 中引入了 Rust 的一些理念:盡量使用類型提示,遵循“非法狀態(tài)不可表示”原則。無(wú)論是長(zhǎng)期維護(hù)的程序還是一次性腳本,我都這樣做,因?yàn)楹笳咄鶗?huì)變成前者,而這種方法讓程序更易理解和修改。

本文將展示一些應(yīng)用此方法的Python示例,雖然不算高深,但記錄下來(lái)或許有用。

類型提示

首先要盡可能使用類型提示,尤其是在函數(shù)說(shuō)明和類屬性中。當(dāng)我看到這樣的函數(shù)說(shuō)明。

def find_item(records, check):

從函數(shù)說(shuō)明本身來(lái)看,我完全不知道其中發(fā)生了什么:是列表、字典還是數(shù)據(jù)庫(kù)連接?是布爾值還是函數(shù)?函數(shù)的返回值是什么?如果失敗會(huì)發(fā)生什么?是拋出異常還是返回某個(gè)值?要找到這些問(wèn)題的答案,我要么必須讀取函數(shù)的主體(通常還要遞歸讀取它調(diào)用的其他函數(shù)的主體,這非常煩人),要么只能讀取它的文檔(如果有的話)。雖然文檔中可能包含有關(guān)函數(shù)的有用信息,但不一定要使用文檔來(lái)回答前面的問(wèn)題。許多問(wèn)題都可以通過(guò)內(nèi)置機(jī)制(即類型提示)來(lái)回答。

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

寫函數(shù)說(shuō)明要花更多時(shí)間嗎?是的。

但這有問(wèn)題嗎?沒(méi)有,除非我打字速度慢到每分鐘只能敲幾個(gè)字,但這很少見(jiàn)。明確寫出類型能讓我更清楚地思考函數(shù)到底提供了什么接口,以及如何讓接口更嚴(yán)格,避免調(diào)用者用錯(cuò)。有了清晰的函數(shù)說(shuō)明,我一眼就能知道怎么用這個(gè)函數(shù)、需要傳什么參數(shù)、返回值是什么。而且,和文檔注釋不同,文檔注釋容易過(guò)時(shí),但類型檢查器會(huì)在類型變化時(shí)提醒我更新調(diào)用代碼。如果我想了解某個(gè)東西的類型,直接看就行,非常直觀。

當(dāng)然,我也不是死板的人。如果一個(gè)參數(shù)的類型提示要嵌套五層,我通常會(huì)放棄,改用簡(jiǎn)單但不那么精確的類型。根據(jù)我的經(jīng)驗(yàn),這種情況很少見(jiàn)。如果真的遇到,那可能是代碼設(shè)計(jì)有問(wèn)題——如果一個(gè)參數(shù)既可以是數(shù)字、字符串、字符元組,又可以是字典映射字符串到整數(shù),那可能意味著你需要重構(gòu)和簡(jiǎn)化代碼了。

使用數(shù)據(jù)類而非元組或字典

使用類型提示只是一方面,它只是描述了函數(shù)的接口,第二步是盡可能準(zhǔn)確地 “鎖定 ”這些接口。一個(gè)典型的例子是從函數(shù)返回多個(gè)值(或單個(gè)復(fù)雜值),懶惰而快速的方法是返回一個(gè)元組:

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

我們知道要返回三個(gè)值,但它們是什么?第一個(gè)字符串是人名嗎?第二個(gè)是姓氏嗎?數(shù)字是年齡、位置還是社保號(hào)?這種編碼方式很不透明,除非看函數(shù)內(nèi)部,否則根本不知道它代表什么。

如果想改進(jìn),可以返回一個(gè)字典:

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

現(xiàn)在,我們至少能知道返回的屬性是什么,但還是得看函數(shù)內(nèi)部才能確定。某種程度上,類型變得更糟了,因?yàn)槲覀兩踔敛恢缹傩缘臄?shù)量和類型。而且,當(dāng)函數(shù)變化時(shí),比如字典的鍵被重命名或刪除,類型檢查器很難發(fā)現(xiàn),調(diào)用者只能通過(guò)運(yùn)行-崩潰-修改的繁瑣循環(huán)來(lái)調(diào)整代碼。

正確的解決方案是返回一個(gè)強(qiáng)類型的對(duì)象,并帶有命名的參數(shù)。在Python中,這意味著要?jiǎng)?chuàng)建一個(gè)類。我猜很多人用元組或字典是因?yàn)槎x一個(gè)類(還得給它起名字)比直接返回?cái)?shù)據(jù)麻煩得多。但從Python 3.7開始(或者用polyfill包支持更早的版本),有了更簡(jiǎn)單的解決方案:dataclasses。

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

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

def find_person(...) -> Person:

雖然還是得給類起名字,但除此之外,這種方式非常簡(jiǎn)潔,而且所有屬性都有類型注解。

通過(guò)這個(gè)數(shù)據(jù)類,函數(shù)的返回值變得非常明確。當(dāng)我調(diào)用這個(gè)函數(shù)并處理返回值時(shí),IDE的自動(dòng)補(bǔ)全功能會(huì)顯示屬性的名稱和類型。這聽(tīng)起來(lái)可能很小,但對(duì)我來(lái)說(shuō),這是提高效率的一大優(yōu)勢(shì)。此外,當(dāng)代碼重構(gòu)或?qū)傩宰兓瘯r(shí),IDE和類型檢查器會(huì)提醒我,并顯示需要修改的地方,而不需要運(yùn)行程序。對(duì)于一些簡(jiǎn)單的重構(gòu)(比如屬性重命名),IDE甚至可以自動(dòng)完成這些更改。更重要的是,通過(guò)明確命名的類型,我可以建立一個(gè)共享的詞匯表(比如Person、City),并與其他函數(shù)和類共用。

代數(shù)數(shù)據(jù)類型

Rust 有一個(gè)大多數(shù)主流語(yǔ)言缺乏的強(qiáng)大功能:代數(shù)數(shù)據(jù)類型(ADT)。它能明確描述數(shù)據(jù)的形狀。比如處理數(shù)據(jù)包時(shí),可以枚舉所有可能的類型并為每種類型分配不同字段:

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

通過(guò)模式匹配,可以處理每種情況,編譯器會(huì)檢查是否遺漏了任何可能:

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

ADT 能確保無(wú)效狀態(tài)不可表示,避免運(yùn)行時(shí)錯(cuò)誤。它在靜態(tài)類型語(yǔ)言中特別有用,尤其是當(dāng)需要統(tǒng)一處理一組類型時(shí)。如果沒(méi)有 ADT,通常需要用接口或繼承來(lái)實(shí)現(xiàn)。如果類型集是封閉的,ADT 和模式匹配是更好的選擇。

在 Python 這樣的動(dòng)態(tài)類型語(yǔ)言中,雖然不需要為類型集設(shè)置共享名稱,但類似 ADT 的結(jié)構(gòu)仍然有用。比如可以用聯(lián)合類型:

@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、PayloadTrailer。雖然這些類沒(méi)有明確的標(biāo)識(shí)符來(lái)區(qū)分,但可以通過(guò) isinstance 或模式匹配來(lái)處理:

def handle_packet(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 中,遺漏情況會(huì)導(dǎo)致編譯錯(cuò)誤,而在 Python 中需要用 assert False 來(lái)處理意外數(shù)據(jù)。

聯(lián)合類型的好處是它在類之外定義,減少了代碼耦合。同一個(gè)類可以用于多個(gè)聯(lián)合類型:

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

聯(lián)合類型對(duì)自動(dòng)序列化也非常有用。比如使用 pyserde 庫(kù),可以輕松序列化和反序列化聯(lián)合類型:

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))

聯(lián)合類型還可以用于版本化配置,保持向后兼容性:

Config = ConfigV1 | ConfigV2 | ConfigV3

通過(guò)反序列化,可以讀取所有舊版本的配置格式。

使用 NewType

在 Rust 中,定義不添加任何新行為的數(shù)據(jù)類型很常見(jiàn),但這些數(shù)據(jù)類型用于指定其他常見(jiàn)數(shù)據(jù)類型(如整數(shù))的域和預(yù)期用途。這種模式被稱為 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)

發(fā)現(xiàn)錯(cuò)誤?

函數(shù) get_ride_info 的參數(shù)位置顛倒了。由于汽車 ID 和駕駛員 ID 都是簡(jiǎn)單整數(shù),因此類型是正確的,盡管函數(shù)調(diào)用在語(yǔ)義上是錯(cuò)誤的。

我們可以通過(guò)使用 NewType 為不同類型的 ID 定義不同的類型來(lái)解決這個(gè)問(wèn)題:

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>)

這是一個(gè)非常簡(jiǎn)單的模式,可以幫助捕捉那些難以發(fā)現(xiàn)的錯(cuò)誤,尤其是在處理許多不同類型的 ID 和某些指標(biāo)混合在一起時(shí)。

使用構(gòu)造函數(shù)

Rust 并沒(méi)有構(gòu)造函數(shù)。相反,人們傾向于使用普通函數(shù)來(lái)創(chuàng)建(最好是正確初始化的)結(jié)構(gòu)體實(shí)例。在 Python 中,沒(méi)有構(gòu)造函數(shù)重載的概念,所以如果你需要以多種方式構(gòu)造一個(gè)對(duì)象,通常會(huì)產(chǎn)生一個(gè)帶有許多參數(shù)的方法,這些參數(shù)以不同的方式用于初始化,并不能一起使用。

相反,我喜歡創(chuàng)建具有明確名稱的 “構(gòu)造函數(shù)”,這樣就可以清楚地知道對(duì)象是如何構(gòu)造的,以及是通過(guò)哪些數(shù)據(jù)構(gòu)造的:

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

這樣做可以使對(duì)象的構(gòu)造更加清晰,不允許用戶傳遞無(wú)效數(shù)據(jù),并能更清楚地表達(dá)構(gòu)造對(duì)象的意圖。

寫在最后

總之,我確信我的 Python 代碼中還有更多的 “完整模式”,但以上是我目前能想到的全部。歡迎討論!

責(zé)任編輯:武曉燕 來(lái)源: 數(shù)據(jù)STUDIO
相關(guān)推薦

2018-10-18 09:58:41

物聯(lián)網(wǎng)IOT數(shù)字化

2020-08-11 08:55:42

VSCode開發(fā)代碼

2016-12-29 11:18:05

2017-04-18 18:59:04

2022-03-02 09:49:14

Rust編程語(yǔ)言

2019-08-27 08:45:10

Python編程語(yǔ)言代碼

2023-10-19 15:25:40

2021-10-28 19:10:51

RustPythonjs

2017-09-15 18:16:56

人工智能Python

2024-04-07 00:00:01

TypeScript語(yǔ)言REST

2017-02-17 07:46:29

2018-08-21 05:12:10

2024-01-02 07:34:38

CentOSLinuxRedhat

2021-09-24 09:15:19

Windowsfx 1LinuxWindows 11

2015-06-15 11:05:13

DCIM數(shù)據(jù)中心

2013-06-05 13:49:41

EclipseIntelliJ

2019-11-27 14:27:33

編程語(yǔ)言PythonJava

2021-10-09 14:35:20

物聯(lián)網(wǎng)IOT人工智能

2022-07-06 23:28:53

元宇宙Web3.0

2016-06-05 17:13:36

博科/網(wǎng)絡(luò)自動(dòng)化
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)