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

像寫 Rust 一樣寫 Python!

譯文 精選
開發(fā) 前端
在本文中,我將展示幾個(gè)應(yīng)用于Python程序的此類模式示例。這不是火箭科學(xué),但我仍然覺得記錄它們可能會(huì)有用。

作者丨kobzol

策劃丨千山

審校丨云昭

幾年前,我開始使用Rust編程,它逐漸改變了我使用其他編程語(yǔ)言(尤其是Python)設(shè)計(jì)程序的方式。在我開始使用Rust之前,我通常以一種非常動(dòng)態(tài)和類型松散的方式編寫Python代碼,沒有類型提示,到處傳遞和返回字典,偶爾回退到“字符串類型”接口。然而,在經(jīng)歷了Rust類型系統(tǒng)的嚴(yán)格性,并注意到它“通過構(gòu)造”防止的所有問題之后,每當(dāng)我回到Python并且沒有得到相同的保證時(shí),我突然變得非常焦慮。

需要明確的是,這里的“保證”并不是指內(nèi)存安全(Python本身是合理的內(nèi)存安全),而是“穩(wěn)健性”——設(shè)計(jì)很難或完全不可能被濫用的API的概念,從而防止未定義的行為和各種錯(cuò)誤。在Rust中,錯(cuò)誤使用的接口通常會(huì)導(dǎo)致編譯錯(cuò)誤。在Python中,您仍然可以執(zhí)行此類不正確的程序,但如果您使用類型檢查器(如pyright)或帶有類型分析器的IDE(如PyCharm),您仍然可以獲得類似級(jí)別的有關(guān)可能問題的快速反饋。

最終,我開始在我的Python程序中采用Rust的一些概念。它基本上可以歸結(jié)為兩件事——盡可能多地使用類型提示,并堅(jiān)持讓非法狀態(tài)無法表示的原則。我嘗試對(duì)將維護(hù)一段時(shí)間的程序和 oneshot實(shí)用程序腳本都這樣做。主要是因?yàn)楦鶕?jù)我的經(jīng)驗(yàn),后者經(jīng)常變成前者:)根據(jù)我的經(jīng)驗(yàn),這種方法導(dǎo)致程序更容易理解和更改。

在本文中,我將展示幾個(gè)應(yīng)用于Python程序的此類模式示例。這不是火箭科學(xué),但我仍然覺得記錄它們可能會(huì)有用。

注意:這篇文章包含了很多關(guān)于編寫Python代碼的觀點(diǎn)。我不想在每句話中都加上“恕我直言”,所以將這篇文章中的所有內(nèi)容僅作為我對(duì)此事的看法,而不是試圖宣傳一些普遍的真理:)另外,我并不是說所提出的想法是所有這些都是在Rust中發(fā)明的,當(dāng)然,它們也被用于其他語(yǔ)言。

一、ype hint

首要的是盡可能使用類型提示,特別是在函數(shù)簽名和類屬性中。當(dāng)我讀到一個(gè)像這樣的函數(shù)簽名時(shí):

def find_item(records, check):

我不知道簽名本身發(fā)生了什么。是records列表、字典還是數(shù)據(jù)庫(kù)連接?是check布爾值還是函數(shù)?這個(gè)函數(shù)返回什么?如果失敗會(huì)發(fā)生什么,它會(huì)引發(fā)異常還是返回None?為了找到這些問題的答案,我要么必須去閱讀函數(shù)體(并且經(jīng)常遞歸地閱讀它調(diào)用的其他函數(shù)的函數(shù)體——這很煩人),要么閱讀它的文檔(如果有的話)。雖然文檔可能包含有關(guān)函數(shù)功能的有用信息,但沒有必要將它也用于記錄前面問題的答案。很多問題都可以通過內(nèi)置機(jī)制——類型提示——來回答。

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

我寫簽名花了更多時(shí)間嗎?是的。那是問題嗎?不,除非我的編碼受到每分鐘寫入的字符數(shù)的瓶頸,而這并沒有真正發(fā)生。明確地寫出類型迫使我思考函數(shù)提供的實(shí)際接口是什么,以及如何使其盡可能嚴(yán)格,以使其調(diào)用者難以以錯(cuò)誤的方式使用它。通過上面的簽名,我可以很好地了解如何使用該函數(shù)、將什么作為參數(shù)傳遞給它以及我期望從中返回什么。此外,與代碼更改時(shí)很容易過時(shí)的文檔注釋不同,當(dāng)我更改類型并且不更新函數(shù)的調(diào)用者時(shí),類型檢查器會(huì)對(duì)我大喊大叫。如果我對(duì)什么是Item感興趣,我可以直接使用Go to definition并立即查看該類型的外觀。

在這方面,我不是一個(gè)絕對(duì)主義者,如果需要五個(gè)嵌套類型提示來描述單個(gè)參數(shù),我通常會(huì)放棄并給它一個(gè)更簡(jiǎn)單但不精確的類型。根據(jù)我的經(jīng)驗(yàn),這種情況不會(huì)經(jīng)常發(fā)生。如果它確實(shí)發(fā)生了,它實(shí)際上可能表明代碼有問題——如果你的函數(shù)參數(shù)可以是一個(gè)數(shù)字、一個(gè)字符串元組或一個(gè)將字符串映射到整數(shù)的字典,這可能表明你可能想要重構(gòu)和簡(jiǎn)化它。

二、數(shù)據(jù)類(dataclass)而不是元組(tuple)或字典(dictionary)

使用類型提示是一回事,但這僅僅描述了函數(shù)的接口是什么。第二步實(shí)際上是使這些接口盡可能精確和“鎖定”。一個(gè)典型的例子是從一個(gè)函數(shù)返回多個(gè)值(或一個(gè)復(fù)雜的值)。懶惰而快速的方法是返回一個(gè)元組:

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

太好了,我們知道我們要返回三個(gè)值。這些是什么?第一個(gè)字符串是人的名字嗎?第二串姓氏?電話號(hào)碼是多少?是年齡嗎?在某些列表中的位置?社會(huì)安全號(hào)碼?這種輸入是不透明的,除非你查看函數(shù)體,否則你不知道這里發(fā)生了什么。

下一步“改進(jìn)”這可能是返回一個(gè)字典:

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

現(xiàn)在我們實(shí)際上知道各個(gè)返回的屬性是什么,但我們必須再次檢查函數(shù)體才能找出答案。從某種意義上說,類型變得更糟,因?yàn)楝F(xiàn)在我們甚至不知道各個(gè)屬性的數(shù)量和類型。此外,當(dāng)這個(gè)函數(shù)發(fā)生變化并且返回的字典中的鍵被重命名或刪除時(shí),沒有簡(jiǎn)單的方法可以用類型檢查器找出來,因此它的調(diào)用者通常必須用非常手動(dòng)和煩人的運(yùn)行-崩潰-修改代碼來改變循環(huán)。

正確的解決方案是返回一個(gè)強(qiáng)類型對(duì)象,其命名參數(shù)具有附加類型。在Python中,這意味著我們必須創(chuàng)建一個(gè)類。我懷疑在這些情況下經(jīng)常使用元組和字典,因?yàn)樗榷x類(并為其命名)、創(chuàng)建帶參數(shù)的構(gòu)造函數(shù)、將參數(shù)存儲(chǔ)到字段等容易得多。自Python 3.7 (并且更快地使用package polyfill),有一個(gè)更快的解決方案-dataclasses.

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


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


def find_person(...) -> Person:

你仍然需要為創(chuàng)建的類考慮一個(gè)名稱,但除此之外,它已經(jīng)盡可能簡(jiǎn)潔了,并且你可以獲得所有屬性的類型注釋。

有了這個(gè)數(shù)據(jù)類,我就有了函數(shù)返回內(nèi)容的明確描述。當(dāng)我調(diào)用此函數(shù)并處理返回值時(shí),IDE自動(dòng)完成功能將向我顯示其屬性的名稱和類型。這聽起來可能微不足道,但對(duì)我來說這是一個(gè)巨大的生產(chǎn)力優(yōu)勢(shì)。此外,當(dāng)代碼被重構(gòu)并且屬性發(fā)生變化時(shí),我的IDE和類型檢查器將對(duì)我大喊大叫并向我顯示所有必須更改的位置,而我根本不必執(zhí)行程序。對(duì)于一些簡(jiǎn)單的重構(gòu)(例如屬性重命名),IDE甚至可以為我進(jìn)行這些更改。此外,通過明確命名的類型,我可以構(gòu)建術(shù)語(yǔ)詞匯表( Person,City),然后可以與其他函數(shù)和類共享。

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

在大多數(shù)主流語(yǔ)言中,我可能最缺乏的Rust是代數(shù)數(shù)據(jù)類型(ADT)2。它是一個(gè)非常強(qiáng)大的工具,可以明確描述我的代碼正在處理的數(shù)據(jù)的形狀。例如,當(dāng)我在Rust中處理數(shù)據(jù)包時(shí),我可以顯式枚舉所有可以接收的各種數(shù)據(jù)包,并為它們中的每一個(gè)分配不同的數(shù)據(jù)(字段):

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

通過模式匹配,我可以對(duì)各個(gè)變體做出反應(yīng),編譯器會(huì)檢查我沒有遺漏任何情況:

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

這對(duì)于確保無效狀態(tài)不可表示并因此避免許多運(yùn)行時(shí)錯(cuò)誤是非常寶貴的。ADT在靜態(tài)類型語(yǔ)言中特別有用,如果你想以統(tǒng)一的方式使用一組類型,你需要一個(gè)共享的“名稱”來引用它們。如果沒有ADT,這通常是使用OOP接口和/或繼承來完成的。當(dāng)使用的類型集是開放式的時(shí),接口和虛方法有它們的位置,但是當(dāng)類型集是封閉的,并且你想確保你處理所有可能的變體時(shí),ADT和模式匹配更合適。

在動(dòng)態(tài)類型語(yǔ)言(如Python)中,實(shí)際上不需要為一組類型共享名稱,主要是因?yàn)槟踔敛槐匾婚_始就為程序中使用的類型命名。但是,通過創(chuàng)建聯(lián)合類型,使用類似于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這里定義了一個(gè)新類型,它可以是報(bào)頭、有效載荷或尾部數(shù)據(jù)包。當(dāng)我想確保只有這三個(gè)類有效時(shí),我現(xiàn)在可以在程序的其余部分中使用此類型(名稱)。請(qǐng)注意,類沒有附加明確的“標(biāo)簽”,因此當(dāng)我們要區(qū)分它們時(shí),我們必須使用eginstanceof或模式匹配:

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

可悲的是,在這里我們必須(或者更確切地說,應(yīng)該)包括煩人的assert False分支,以便函數(shù)在接收到意外數(shù)據(jù)時(shí)崩潰。在Rust中,這將是一個(gè)編譯時(shí)錯(cuò)誤。

注意:Reddit上的幾個(gè)人已經(jīng)提醒我,assert False實(shí)際上在優(yōu)化構(gòu)建( ) 中完全優(yōu)化掉了python -O ...。因此,直接引發(fā)異常會(huì)更安全。還有typing.assert_never來自Python 3.11 的,它明確地告訴類型檢查器落到這個(gè)分支應(yīng)該是一個(gè)“編譯時(shí)”錯(cuò)誤。

聯(lián)合類型的一個(gè)很好的屬性是它是在作為聯(lián)合一部分的類之外定義的。因此該類不知道它被包含在聯(lián)合中,這減少了代碼中的耦合。您甚至可以使用相同的類型創(chuàng)建多個(gè)不同的聯(lián)合:

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

聯(lián)合類型對(duì)于自動(dòng)(反)序列化也非常有用。最近我發(fā)現(xiàn)了一個(gè)很棒的序列化庫(kù),叫做pyserde,它基于古老的Rustserde序列化框架。在許多其他很酷的功能中,它能夠利用類型注釋來序列化和反序列化聯(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)合標(biāo)簽的序列化方式,與serde.我一直在尋找類似的功能,因?yàn)樗鼘?duì)(反)序列化聯(lián)合類型非常有用。dataclasses_json但是,在我嘗試過的大多數(shù)其他序列化庫(kù)(例如或)中實(shí)現(xiàn)它非常煩人dacite。

例如,在使用機(jī)器學(xué)習(xí)模型時(shí),我使用聯(lián)合將各種類型的神經(jīng)網(wǎng)絡(luò)(例如分類或分段CNN模型)存儲(chǔ)在單個(gè)配置文件格式中。我還發(fā)現(xiàn)對(duì)不同格式的數(shù)據(jù)(在我的例子中是配置文件)進(jìn)行版本化很有用,如下所示:

Config = ConfigV1 | ConfigV2 | ConfigV3

通過反序列化Config,我能夠讀取所有以前版本的配置格式,從而保持向后兼容性。

四、使用newtype

在Rust中,定義不添加任何新行為的數(shù)據(jù)類型是很常見的,但只是用于指定其他一些非常通用的數(shù)據(jù)類型(例如整數(shù))的域和預(yù)期用途。這種模式被稱為“newtype”3,它也可以用在Python中。這是一個(gè)激勵(lì)人心的例子:

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被交換。沒有類型錯(cuò)誤,因?yàn)槠嘔D 和司機(jī)ID都是簡(jiǎn)單的整數(shù),因此類型是正確的,即使在語(yǔ)義上函數(shù)調(diào)用是錯(cuò)誤的。

我們可以通過使用“NewType”為不同類型的ID定義單獨(dú)的類型來解決這個(gè)問題:

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 (CarId vs DriverId)或某些不應(yīng)混合在一起的指標(biāo)(Speed vs Lengthvs等)。Temperature

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

我非常喜歡Rust的一件事是它本身沒有構(gòu)造函數(shù)。相反,人們傾向于使用普通函數(shù)來創(chuàng)建(理想情況下正確初始化)結(jié)構(gòu)實(shí)例。在Python中,沒有構(gòu)造函數(shù)重載,因此如果您需要以多種方式構(gòu)造一個(gè)對(duì)象,有人會(huì)導(dǎo)致一個(gè)__init__方法有很多參數(shù),這些參數(shù)以不同的方式用于初始化,并且不能真正一起使用。

相反,我喜歡創(chuàng)建具有明確名稱的“構(gòu)造”函數(shù),這使得如何構(gòu)造對(duì)象以及從哪些數(shù)據(jù)構(gòu)造對(duì)象變得顯而易見:

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)造對(duì)象變得更加清晰,并且不允許類的用戶在構(gòu)造對(duì)象時(shí)傳遞無效數(shù)據(jù)(例如通過組合y1和width)。

六、使用類型編碼不變量

使用類型系統(tǒng)本身來編碼只能在運(yùn)行時(shí)跟蹤的不變量是一個(gè)非常通用和強(qiáng)大的概念。在Python(以及其他主流語(yǔ)言)中,我經(jīng)??吹筋愂强勺儬顟B(tài)的毛茸茸的大球。這種混亂的根源之一是試圖在運(yùn)行時(shí)跟蹤對(duì)象不變量的代碼。它必須考慮理論上可能發(fā)生的許多情況,因?yàn)轭愋拖到y(tǒng)并沒有使它們成為不可能(“如果客戶端已被要求斷開連接,現(xiàn)在有人試圖向它發(fā)送消息,但套接字仍然是連接”等)。

1.Client

這是一個(gè)典型的例子:

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

……容易吧?你只需要仔細(xì)閱讀文檔,并確保你永遠(yuǎn)不會(huì)違反上述規(guī)則(以免調(diào)用未定義的行為或崩潰)。另一種方法是用各種斷言填充類,這些斷言會(huì)在運(yùn)行時(shí)檢查所有提到的規(guī)則,這會(huì)導(dǎo)致代碼混亂、遺漏邊緣情況以及出現(xiàn)錯(cuò)誤時(shí)反饋速度較慢(編譯時(shí)與運(yùn)行時(shí))。問題的核心是客戶端可以存在于各種(互斥的)狀態(tài)中,但不是單獨(dú)對(duì)這些狀態(tài)進(jìn)行建模,而是將它們?nèi)亢喜橐粋€(gè)類型。

讓我們看看是否可以通過將各種狀態(tài)拆分為單獨(dú)的類型4來改進(jìn)這一點(diǎn)。

首先,擁有一個(gè)Client不與任何東西相連的東西是否有意義?好像不是這樣。這樣一個(gè)未連接的客戶端在您無論如何調(diào)用之前無法執(zhí)行任何操作connect 。那么為什么要允許這種狀態(tài)存在呢?我們可以創(chuàng)建一個(gè)調(diào)用的構(gòu)造函數(shù) connect,它將返回一個(gè)連接的客戶端:

def connect(address: str) -> Optional[ConnectedClient]:
  pass

class ConnectedClient:
  def authenticate(...):
  def send_message(...):
  def close(...):

如果該函數(shù)成功,它將返回一個(gè)支持“已連接”不變量的客戶端,并且你不能connect再次調(diào)用它來搞砸事情。如果連接失敗,該函數(shù)可以引發(fā)異?;蚍祷豊one或一些顯式錯(cuò)誤。

類似的方法可以用于狀態(tài)authenticated。我們可以引入另一種類型,它保持客戶端已連接并已通過身份驗(yàn)證的不變性:

class ConnectedClient:
  def authenticate(...) -> Optional["AuthenticatedClient"]:

class AuthenticatedClient:
  def send_message(...):
  def close(...):

只有當(dāng)我們真正擁有an的實(shí)例后AuthenticatedClient,我們才能真正開始發(fā)送消息。

最后一個(gè)問題是方法close。在 Rust 中(由于 破壞性移動(dòng)語(yǔ)義),我們能夠表達(dá)這樣一個(gè)事實(shí),即當(dāng)close調(diào)用方法時(shí),您不能再使用客戶端。這在 Python 中是不可能的,所以我們必須使用一些變通方法。一種解決方案可能是回退到運(yùn)行時(shí)跟蹤,在客戶端中引入布爾屬性,并斷言close它send_message尚未關(guān)閉。另一種方法可能是close完全刪除該方法并僅將客戶端用作上下文管理器:

with connect(...) as client:
    client.send_message("foo")
# Here the client is closed

沒有close可用的方法,你不能意外關(guān)閉客戶端兩次。

2.強(qiáng)類型邊界框

對(duì)象檢測(cè)是我有時(shí)從事的一項(xiàng)計(jì)算機(jī)視覺任務(wù),其中程序必須檢測(cè)圖像中的一組邊界框。邊界框基本上是帶有一些附加數(shù)據(jù)的美化矩形,當(dāng)你實(shí)現(xiàn)對(duì)象檢測(cè)時(shí),它們無處不在。關(guān)于它們的一個(gè)惱人的事情是有時(shí)它們被規(guī)范化(矩形的坐標(biāo)和大小在interval中[0.0, 1.0]),但有時(shí)它們被非規(guī)范化(坐標(biāo)和大小受它們所附圖像的尺寸限制)。當(dāng)你通過許多處理數(shù)據(jù)預(yù)處理或后處理的函數(shù)發(fā)送邊界框時(shí),很容易把它搞砸,例如兩次規(guī)范化邊界框,這會(huì)導(dǎo)致調(diào)試起來非常煩人的錯(cuò)誤。

這在我身上發(fā)生過幾次,所以有一次我決定通過將這兩種類型的bbox分成兩種不同的類型來徹底解決這個(gè)問題:

@dataclass
class NormalizedBBox:
  left: float
  top: float
  width: float
  height: float


@dataclass
class DenormalizedBBox:
  left: float
  top: float
  width: float
  height: float

通過這種分離,規(guī)范化和非規(guī)范化的邊界框不再容易混合在一起,這主要解決了問題。但是,我們可以進(jìn)行一些改進(jìn)以使代碼更符合人體工程學(xué):

通過組合或繼承減少重復(fù):

@dataclass
class BBoxBase:
  left: float
  top: float
  width: float
  height: float

# Composition
class NormalizedBBox:
  bbox: BBoxBase

class DenormalizedBBox:
  bbox: BBoxBase

Bbox = Union[NormalizedBBox, DenormalizedBBox]

# Inheritance
class NormalizedBBox(BBoxBase):
class DenormalizedBBox(BBoxBase):

添加運(yùn)行時(shí)檢查以確保規(guī)范化的邊界框?qū)嶋H上是規(guī)范化的:

class NormalizedBBox(BboxBase):
  def __post_init__(self):
    assert 0.0 <= self.left <= 1.0
    ...
  • 添加一種在兩種表示之間進(jìn)行轉(zhuǎn)換的方法。在某些地方,我們可能想知道顯式表示,但在其他地方,我們想使用通用接口(“任何類型的 BBox”)。在那種情況下,我們應(yīng)該能夠?qū)ⅰ叭魏?BBox”轉(zhuǎn)換為以下兩種表示之一:
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

有了這個(gè)界面,我可以兩全其美——為了正確性而分開的類型,以及為了人體工程學(xué)而使用統(tǒng)一的界面。

注意:如果你想向返回相應(yīng)類實(shí)例的父類/基類添加一些共享方法,你可以typing.Self從Python 3.11 開始使用:

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)

3.更安全的互斥鎖

Rust中的互斥鎖和鎖通常在一個(gè)非常漂亮的接口后面提供,有兩個(gè)好處:

當(dāng)你鎖定互斥量時(shí),你會(huì)得到一個(gè)保護(hù)對(duì)象,它會(huì)在互斥量被銷毀時(shí)自動(dòng)解鎖,利用古老的RAII機(jī)制:

{
  let guard = mutex.lock(); // locked here
  ...
} // automatically unlocked here

這意味著你不會(huì)意外地忘記解鎖互斥體。C++ 中也常用非常相似的機(jī)制,盡管不帶保護(hù)對(duì)象的顯式lock/unlock接口也可用于std::mutex,這意味著它們?nèi)匀豢梢员诲e(cuò)誤使用。

受互斥量保護(hù)的數(shù)據(jù)直接存儲(chǔ)在互斥量(結(jié)構(gòu))中。使用這種設(shè)計(jì),如果不實(shí)際鎖定互斥體就不可能訪問受保護(hù)的數(shù)據(jù)。您必須先鎖定互斥量才能獲得守衛(wèi),然后使用守衛(wèi)本身訪問數(shù)據(jù):

let lock = Mutex::new(41); // Create a mutex that stores the data inside
let guard = lock.lock().unwrap(); // Acquire guard
*guard += 1; // Modify the data using the guard

這與主流語(yǔ)言(包括Python)中常見的互斥鎖API形成鮮明對(duì)比,其中互斥鎖和它保護(hù)的數(shù)據(jù)是分開的,因此你很容易忘記在訪問數(shù)據(jù)之前實(shí)際鎖定互斥鎖:

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鎖實(shí)現(xiàn)了上下文管理器接口,這意味著你可以在塊中使用它們with以確保它們?cè)谧饔糜蚪Y(jié)束時(shí)自動(dòng)解鎖。通過一點(diǎn)努力,我們可以走得更遠(yuǎn):

import contextlib
from threading import Lock
from typing import ContextManager, Generic, TypeVar

T = 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 data
mutex = Mutex([])

# Lock the mutex for the scope of the `with` block
with mutex.lock() as value:
  # value is typed as `list` here
  value.append(1)

使用這種設(shè)計(jì),你只能在實(shí)際鎖定互斥鎖后才能訪問受保護(hù)的數(shù)據(jù)。顯然,這仍然是Python,因此你仍然可以打破不變量——例如,通過在互斥量之外存儲(chǔ)另一個(gè)指向受保護(hù)數(shù)據(jù)的指針。但是除非你的行為是敵對(duì)的,否則這會(huì)使Python中的互斥接口使用起來更安全。

不管怎樣,我確信我在我的Python代碼中使用了更多的“穩(wěn)健模式”,但目前我能想到的就是這些。如果你有類似想法的一些示例或任何其他評(píng)論,請(qǐng)告訴我。

  1. 公平地說,如果你使用某種結(jié)構(gòu)化格式(如 reStructuredText),文檔注釋中的參數(shù)類型描述可能也是如此。在那種情況下,類型檢查器可能會(huì)使用它并在類型不匹配時(shí)警告你。但是,如果你無論如何都使用類型檢查器,我認(rèn)為最好利用“本機(jī)”機(jī)制來指定類型——類型提示。
  2. aka discriminated/tagged unions, sum types, sealed classes, etc.  
  3. 是的,除了這里描述的,新類型還有其他用例,別再對(duì)我大喊大叫了。
  4. 這被稱為typestate 模式。
  5. 除非你努力嘗試,例如手動(dòng)調(diào)用魔術(shù)__exit__方法。

原文鏈接:https://kobzol.github.io/rust/python/2023/05/20/writing-python-like-its-rust.html

責(zé)任編輯:武曉燕 來源: 51CTO技術(shù)棧
相關(guān)推薦

2023-02-15 08:17:20

VSCodeTypeScrip

2023-02-03 16:03:17

TypescriptJavaScript

2022-10-12 08:05:04

PlantUML代碼運(yùn)行環(huán)境

2013-01-29 10:07:13

建筑設(shè)計(jì)師寫程序程序員

2023-03-06 09:20:53

扁平化管理代碼

2013-12-31 09:19:23

Python調(diào)試

2013-12-17 09:02:03

Python調(diào)試

2021-05-20 08:37:32

multiprocesPython線程

2023-04-05 14:19:07

FlinkRedisNoSQL

2017-05-22 10:33:14

PythonJuliaCython

2017-03-15 16:17:20

學(xué)習(xí)命令計(jì)算機(jī)

2022-12-21 15:56:23

代碼文檔工具

2014-09-22 09:27:57

Python

2020-08-25 08:56:55

Pythonawk字符串

2015-03-16 12:50:44

2013-08-22 10:17:51

Google大數(shù)據(jù)業(yè)務(wù)價(jià)值

2011-01-18 10:45:16

喬布斯

2012-06-08 13:47:32

Wndows 8Vista

2015-02-05 13:27:02

移動(dòng)開發(fā)模塊SDK

2012-03-21 10:15:48

RIM越獄
點(diǎn)贊
收藏

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