歡迎光臨
每天分享高質量文章

理解 Python 的 Dataclasses(一) | Linux 中國

如果你正在閱讀本文,那麼你已經意識到了 Python 3.7 以及它所包含的新特性。就我個人而言,我對 Dataclasses 感到非常興奮,因為我等了它一段時間了。
— Shikhar Chauhan


致謝
編譯自 | 
https://medium.com/mindorks/understanding-python-dataclasses-part-1-c3ccd4355c34
 
 作者 | Shikhar Chauhan
 譯者 | MjSeven ?????共計翻譯:70 篇 貢獻時間:214 天

如果你正在閱讀本文,那麼你已經意識到了 Python 3.7 以及它所包含的新特性。就我個人而言,我對 Dataclasses 感到非常興奮,因為我等了它一段時間了。

本系列包含兩部分:

1. Dataclass 特點概述
2. 在下一篇文章概述 Dataclass 的 fields

介紹

Dataclasses 是 Python 的類(LCTT 譯註:更準確的說,它是一個模組),適用於儲存資料物件。你可能會問什麼是資料物件?下麵是定義資料物件的一個不太詳細的特性串列:

◈ 它們儲存資料並代表某種資料型別。例如:一個數字。對於熟悉 ORM 的人來說,模型實體就是一個資料物件。它代表一種特定的物體。它包含那些定義或表示物體的屬性。
◈ 它們可以與同一型別的其他物件進行比較。例如:一個數字可以是 greater than(大於)、less than(小於) 或 equal(等於) 另一個數字。

當然還有更多的特性,但是這個串列足以幫助你理解問題的關鍵。

為了理解 Dataclasses,我們將實現一個包含數字的簡單類,並允許我們執行上面提到的操作。

首先,我們將使用普通類,然後我們再使用 Dataclasses 來實現相同的結果。

但在我們開始之前,先來談談 Dataclasses 的用法。

Python 3.7 提供了一個裝飾器 dataclass[1],用於將類轉換為 dataclass

你所要做的就是將類包在裝飾器中:

  1. from dataclasses import dataclass

  2. @dataclass

  3. class A:

  4. ...

現在,讓我們深入瞭解一下 dataclass 帶給我們的變化和用途。

初始化

通常是這樣:

  1. class Number:

  2.    def __init__(self, val):

  3.        self.val = val

  4. >>> one = Number(1)

  5. >>> one.val

  6. >>> 1

用 dataclass 是這樣:

  1. @dataclass

  2. class Number:

  3.    val:int

  4. >>> one = Number(1)

  5. >>> one.val

  6. >>> 1

以下是 dataclass 裝飾器帶來的變化:

1. 無需定義 __init__,然後將值賦給 selfdataclass 負責處理它(LCTT 譯註:此處原文可能有誤,提及一個不存在的 d
2. 我們以更加易讀的方式預先定義了成員屬性,以及型別提示[2]。我們現在立即能知道 val 是 int 型別。這無疑比一般定義類成員的方式更具可讀性。

Python 之禪: 可讀性很重要

它也可以定義預設值:

  1. @dataclass

  2. class Number:

  3.    val:int = 0

表示

物件表示指的是物件的一個有意義的字串表示,它在除錯時非常有用。

預設的 Python 物件表示不是很直觀:

  1. class Number:

  2.    def __init__(self, val = 0):

  3.    self.val = val

  4. >>> a = Number(1)

  5. >>> a

  6. >>> <__main__.Number object at 0x7ff395b2ccc0>

這讓我們無法知悉物件的作用,並且會導致糟糕的除錯體驗。

一個有意義的表示可以透過在類中定義一個 __repr__ 方法來實現。

  1. def __repr__(self):

  2.    return self.val

現在我們得到這個物件有意義的表示:

  1. >>> a = Number(1)

  2. >>> a

  3. >>> 1

dataclass 會自動新增一個 __repr__  函式,這樣我們就不必手動實現它了。

  1. @dataclass

  2. class Number:

  3.    val: int = 0

  1. >>> a = Number(1)

  2. >>> a

  3. >>> Number(val = 1)

資料比較

通常,資料物件之間需要相互比較。

兩個物件 a 和 b 之間的比較通常包括以下操作:

◈ a < b
◈ a > b
◈ a == b
◈ a >= b
◈ a <= b

在 Python 中,能夠在可以執行上述操作的類中定義方法[3]。為了簡單起見,不讓這篇文章過於冗長,我將只展示 == 和 < 的實現。

通常這樣寫:

  1. class Number:

  2.    def __init__( self, val = 0):

  3.       self.val = val

  4.    def __eq__(self, other):

  5.        return self.val == other.val

  6.    def __lt__(self, other):

  7.        return self.val < other.val

使用 dataclass

  1. @dataclass(order = True)

  2. class Number:

  3.    val: int = 0

是的,就是這樣簡單。

我們不需要定義 __eq__ 和 __lt__ 方法,因為當 order = True 被呼叫時,dataclass 裝飾器會自動將它們新增到我們的類定義中。

那麼,它是如何做到的呢?

當你使用 dataclass 時,它會在類定義中新增函式 __eq__ 和 __lt__ 。我們已經知道這點了。那麼,這些函式是怎樣知道如何檢查相等併進行比較呢?

生成 __eq__ 函式的 dataclass 類會比較兩個屬性構成的元組,一個由自己屬性構成的,另一個由同類的其他實體的屬性構成。在我們的例子中,自動生成的 __eq__ 函式相當於:

  1. def __eq__(self, other):

  2.    return (self.val,) == (other.val,)

讓我們來看一個更詳細的例子:

我們會編寫一個 dataclass 類 Person 來儲存 name 和 age

  1. @dataclass(order = True)

  2. class Person:

  3.    name: str

  4.    age:int = 0

自動生成的 __eq__ 方法等同於:

  1. def __eq__(self, other):

  2.    return (self.name, self.age) == ( other.name, other.age)

請註意屬性的順序。它們總是按照你在 dataclass 類中定義的順序生成。

同樣,等效的 __le__ 函式類似於:

  1. def __le__(self, other):

  2.    return (self.name, self.age) <= (other.name, other.age)

當你需要對資料物件串列進行排序時,通常會出現像 __le__ 這樣的函式的定義。Python 內建的 sorted[4] 函式依賴於比較兩個物件。

  1. >>> import random

  2. >>> a = [Number(random.randint(1,10)) for _ in range(10)] #generate list of random numbers

  3. >>> a

  4. >>> [Number(val=2), Number(val=7), Number(val=6), Number(val=5), Number(val=10), Number(val=9), Number(val=1), Number(val=10), Number(val=1), Number(val=7)]

  5. >>> sorted_a = sorted(a) #Sort Numbers in ascending order

  6. >>> [Number(val=1), Number(val=1), Number(val=2), Number(val=5), Number(val=6), Number(val=7), Number(val=7), Number(val=9), Number(val=10), Number(val=10)]

  7. >>> reverse_sorted_a = sorted(a, reverse = True) #Sort Numbers in descending order

  8. >>> reverse_sorted_a

  9. >>> [Number(val=10), Number(val=10), Number(val=9), Number(val=7), Number(val=7), Number(val=6), Number(val=5), Number(val=2), Number(val=1), Number(val=1)]

dataclass 作為一個可呼叫的裝飾器

定義所有的 dunder(LCTT 譯註:這是指雙下劃線方法,即魔法方法)方法並不總是值得的。你的用例可能只包括儲存值和檢查相等性。因此,你只需定義 __init__ 和 __eq__ 方法。如果我們可以告訴裝飾器不生成其他方法,那麼它會減少一些開銷,並且我們將在資料物件上有正確的操作。

幸運的是,這可以透過將 dataclass 裝飾器作為可呼叫物件來實現。

從官方檔案[5]來看,裝飾器可以用作具有如下引數的可呼叫物件:

  1. @dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)

  2. class C:

1. init:預設將生成 __init__ 方法。如果傳入 False,那麼該類將不會有 __init__ 方法。
2. repr__repr__ 方法預設生成。如果傳入 False,那麼該類將不會有 __repr__ 方法。
3. eq:預設將生成 __eq__ 方法。如果傳入 False,那麼 __eq__ 方法將不會被 dataclass 新增,但預設為 object.__eq__
4. order:預設將生成 __gt____ge____lt____le__ 方法。如果傳入 False,則省略它們。

我們在接下來會討論 frozen。由於 unsafe_hash 引數複雜的用例,它值得單獨釋出一篇文章。

現在回到我們的用例,以下是我們需要的:

1. __init__
2. __eq__

預設會生成這些函式,因此我們需要的是不生成其他函式。那麼我們該怎麼做呢?很簡單,只需將相關引數作為 false 傳入給生成器即可。

  1. @dataclass(repr = False) # order, unsafe_hash and frozen are False

  2. class Number:

  3.    val: int = 0

  4. >>> a = Number(1)

  5. >>> a

  6. >>> <__main__.Number object at 0x7ff395afe898>

  7. >>> b = Number(2)

  8. >>> c = Number(1)

  9. >>> a == b

  10. >>> False

  11. >>> a < b

  12. >>> Traceback (most recent call last):

  13. File “<stdin>”, line 1, in <module>

  14. TypeError: not supported between instances of Number and Number

Frozen(不可變) 實體

Frozen 實體是在初始化物件後無法修改其屬性的物件。

無法建立真正不可變的 Python 物件

在 Python 中建立物件的不可變屬性是一項艱巨的任務,我將不會在本篇文章中深入探討。

以下是我們期望不可變物件能夠做到的:

  1. >>> a = Number(10) #Assuming Number class is immutable

  2. >>> a.val = 10 # Raises Error

有了 dataclass,就可以透過使用 dataclass 裝飾器作為可呼叫物件配合引數 frozen=True 來定義一個 frozen 物件。

當實體化一個 frozen 物件時,任何企圖修改物件屬性的行為都會引發 FrozenInstanceError

  1. @dataclass(frozen = True)

  2. class Number:

  3.    val: int = 0

  4. >>> a = Number(1)

  5. >>> a.val

  6. >>> 1

  7. >>> a.val = 2

  8. >>> Traceback (most recent call last):

  9. File “<stdin>”, line 1, in <module>

  10. File “<string>”, line 3, in __setattr__

  11. dataclasses.FrozenInstanceError: cannot assign to field val

因此,一個 frozen 實體是一種很好方式來儲存:

◈ 常數
◈ 設定

這些通常不會在應用程式的生命週期內發生變化,任何企圖修改它們的行為都應該被禁止。

後期初始化處理

有了 dataclass,需要定義一個 __init__ 方法來將變數賦給 self 這種初始化操作已經得到了處理。但是我們失去了在變數被賦值之後立即需要的函式呼叫或處理的靈活性。

讓我們來討論一個用例,在這個用例中,我們定義一個 Float 類來包含浮點數,然後在初始化之後立即計算整數和小數部分。

通常是這樣:

  1. import math

  2. class Float:

  3.    def __init__(self, val = 0):

  4.        self.val = val

  5.        self.process()

  6.    def process(self):

  7.        self.decimal, self.integer = math.modf(self.val)

  8. >>> a = Float( 2.2)

  9. >>> a.decimal

  10. >>> 0.2000

  11. >>> a.integer

  12. >>> 2.0

幸運的是,使用 post_init[6]  方法已經能夠處理後期初始化操作。

生成的 __init__  方法在傳回之前呼叫 __post_init__ 傳回。因此,可以在函式中進行任何處理。

  1. import math

  2. @dataclass

  3. class FloatNumber:

  4.    val: float = 0.0

  5.    def __post_init__(self):

  6.        self.decimal, self.integer = math.modf(self.val)

  7. >>> a = Number(2.2)

  8. >>> a.val

  9. >>> 2.2

  10. >>> a.integer

  11. >>> 2.0

  12. >>> a.decimal

  13. >>> 0.2

多麼方便!

繼承

Dataclasses 支援繼承,就像普通的 Python 類一樣。

因此,父類中定義的屬性將在子類中可用。

  1. @dataclass

  2. class Person:

  3.    age: int = 0

  4.    name: str

  5. @dataclass

  6. class Student(Person):

  7.    grade: int

  8. >>> s = Student(20, "John Doe", 12)

  9. >>> s.age

  10. >>> 20

  11. >>> s.name

  12. >>> "John Doe"

  13. >>> s.grade

  14. >>> 12

請註意,Student 的引數是在類中定義的欄位的順序。

繼承過程中 __post_init__ 的行為是怎樣的?

由於 __post_init__ 只是另一個函式,因此必須以傳統方式呼叫它:

  1. @dataclass

  2. class A:

  3.    a: int

  4.    def __post_init__(self):

  5.        print("A")

  6. @dataclass

  7. class B(A):

  8.    b: int

  9.    def __post_init__(self):

  10.        print("B")

  11. >>> a = B(1,2)

  12. >>> B

在上面的例子中,只有 B 的 __post_init__ 被呼叫,那麼我們如何呼叫 A 的 __post_init__ 呢?

因為它是父類的函式,所以可以用 super 來呼叫它。

  1. @dataclass

  2. class B(A):

  3.    b: int

  4.    def __post_init__(self):

  5.        super().__post_init__() # 呼叫 A post init

  6.        print("B")

  7. >>> a = B(1,2)

  8. >>> A

  9.    B

結論

因此,以上是 dataclass 使 Python 開發人員變得更輕鬆的幾種方法。

我試著徹底改寫大部分的用例,但是,沒有人是完美的。如果你發現了錯誤,或者想讓我註意相關的用例,請聯絡我。

我將在另一篇文章中介紹 dataclasses.field[7] 和 unsafe_hash

在 Github[8] 和 Twitter[9] 關註我。

更新:dataclasses.field 的文章可以在這裡[10]找到。


via: https://medium.com/mindorks/understanding-python-dataclasses-part-1-c3ccd4355c34

作者:Shikhar Chauhan[12] 譯者:MjSeven 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

贊(0)

分享創造快樂

© 2024 知識星球   網站地圖