成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

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

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

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

本文中,Python 中引入了 Rust 的一些理念:盡量使用類型提示,遵循“非法狀態(tài)不可表示”原則。無論是長期維護的程序還是一次性腳本,我都這樣做,因為后者往往會變成前者,而這種方法讓程序更易理解和修改。

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

類型提示

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

def find_item(records, check):

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

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

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

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

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

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

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

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

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

如果想改進,可以返回一個字典:

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

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

正確的解決方案是返回一個強類型的對象,并帶有命名的參數(shù)。在Python中,這意味著要創(chuàng)建一個類。我猜很多人用元組或字典是因為定義一個類(還得給它起名字)比直接返回數(shù)據(jù)麻煩得多。但從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:

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

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

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

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

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 能確保無效狀態(tài)不可表示,避免運行時錯誤。它在靜態(tài)類型語言中特別有用,尤其是當需要統(tǒng)一處理一組類型時。如果沒有 ADT,通常需要用接口或繼承來實現(xiàn)。如果類型集是封閉的,ADT 和模式匹配是更好的選擇。

在 Python 這樣的動態(tài)類型語言中,雖然不需要為類型集設(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 類型可以表示 HeaderPayloadTrailer。雖然這些類沒有明確的標識符來區(qū)分,但可以通過 isinstance 或模式匹配來處理:

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

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

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

聯(lián)合類型對自動序列化也非常有用。比如使用 pyserde 庫,可以輕松序列化和反序列化聯(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

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

使用 NewType

在 Rust 中,定義不添加任何新行為的數(shù)據(jù)類型很常見,但這些數(shù)據(jù)類型用于指定其他常見數(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)錯誤?

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

我們可以通過使用 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>)

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

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

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

相反,我喜歡創(chuàng)建具有明確名稱的 “構(gòu)造函數(shù)”,這樣就可以清楚地知道對象是如何構(gòu)造的,以及是通過哪些數(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":

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

寫在最后

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

責(zé)任編輯:武曉燕 來源: 數(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編程語言

2024-04-07 00:00:01

TypeScript語言REST

2017-09-15 18:16:56

人工智能Python

2019-08-27 08:45:10

Python編程語言代碼

2018-08-21 05:12:10

2017-02-17 07:46:29

2024-01-02 07:34:38

CentOSLinuxRedhat

2021-10-28 19:10:51

RustPythonjs

2023-10-19 15:25:40

2021-09-24 09:15:19

Windowsfx 1LinuxWindows 11

2021-10-09 14:35:20

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

2022-07-06 23:28:53

元宇宙Web3.0

2019-11-27 14:27:33

編程語言PythonJava

2013-06-05 13:49:41

EclipseIntelliJ

2015-06-15 11:05:13

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

2016-06-05 17:13:36

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

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

主站蜘蛛池模板: 精品一区二区三区日本 | 国产精品不卡视频 | 男人的天堂在线视频 | 久久久久久久久久久成人 | 91网站在线观看视频 | 国产在线精品一区二区 | 久久久久国产精品午夜一区 | 求个av网址 | 男人天堂网址 | 久久丝袜视频 | 国产亚洲精品久久yy50 | 免费一级黄色 | 国产成人精品久久二区二区91 | 久久人人网 | 亚州激情| 日本电影韩国电影免费观看 | 韩日av片 | 日本淫视频 | 秋霞a级毛片在线看 | 青青久在线视频 | 欧美精品一区二区在线观看 | 国产日韩欧美 | 久久久久国产一级毛片高清网站 | 亚洲精品视频免费 | 色在线视频网站 | 亚洲欧美日韩一区二区 | 99中文字幕| 国产精品日韩欧美一区二区 | 成人综合一区二区 | 国产探花在线精品一区二区 | 久久人人爽人人爽人人片av免费 | 国产激情一区二区三区 | 日韩欧美国产一区二区三区 | 日韩一区二区在线视频 | 欧美性网站 | 亚洲丝袜天堂 | 麻豆久久久9性大片 | 国产精品一区二区无线 | 亚洲 欧美 综合 | 国产美女网站 | 久久亚洲91|