Python 3.7中dataclass的終極指南(一)

python3.7 的dataclass新特性大大簡化了定義類對象的代碼量,代碼簡潔明晰。經過使用@dataclass裝飾器來修飾類的設計,例如python

from dataclasses import dataclass

@dataclass
class DataClassCard:
    rank: str
    suit: str

#生成實例
queen_of_hearts = DataClassCard('Q', 'Hearts')
print(queen_of_hearts.rank)
print(queen_of_hearts)
print(queen_of_hearts == DataClassCard('Q', 'Hearts'))

運行結果數據結構

Q
DataClassCard(rank='Q', suit='Hearts')
True

而常規的類,按照3.7以前的語法相似於這樣ide

class RegularCard
    def __init__(self, rank, suit):
        self.rank = rank
        self.suit = suit

雖然這種寫法並無使用更多的代碼量,可是咱們很容易看到爲了初始化,僅僅只是爲了初始化一個對象,rank和suit已經重複了三次。此外,若是你試圖使用這個RegularCard類,你會注意到對象的表示不是很具描述性,而且已有的類與新聲明的類是沒法比較是否相同的。由於每次聲明都會使用一個新的內存地址,而「==」不止比較類存儲的信息,還比較內存地址是否相同。函數

具體請看下面代碼post

queen_of_hearts = RegularCard('Q', 'Hearts')
print(queen_of_hearts.rank)
print(queen_of_hearts)
print(queen_of_hearts == RegularCard('Q', 'Hearts'))

運行結果ui

'Q'
<__main__.RegularCard object at 0x7fb6eee35d30>
False

dataclass還在底層給咱們作了更多的有用的封裝。默認狀況下dataclass實現了repr方法,能夠很好的提供字符串表示;也是了eq方法,能夠作基本的對象比較。而若是RegularCard想實現上面的功能須要寫大量的聲明,代碼量多的嚇人spa

class RegularCard(object):
    def __init__(self, rank, suit):
        self.rank = rank
        self.suit = suit

    def __repr__(self):
        #能夠將類的信息打印出來
        return (f'{self.__class__.__name__}'  
                f'(rank={self.rank!r}, suit={self.suit!r})')  

         #你們能夠試着將「!r」去掉或者將其中的r改變爲s或a,看看輸出結果會有什麼變化
         #conversion character: expected 's', 'r', or 'a'

    def __eq__(self, other):
        #能夠比較類是否相同(不考慮內存地址)
        if other.__class__ is not self.__class__:
            return NotImplemented
        return (self.rank, self.suit) == (other.rank, other.suit

在本教程中,您將準確瞭解dataclass類所帶來的便利性。除了很好的表示(當使用print時能夠在很好的打印出來)和比較(是否相同),你會看到:設計

  • 如何給dataclass對象添加默認的字段(field)code

  • 如何讓dataclass容許對象進行排序對象

  • 如何讓dataclass表示不可更改數據

  • 如何讓dataclass處理繼承

dataclass類的替代方案
可能你接觸過nametuple,它經常用來創造可讀的輕量級數據結構。實際上咱們能夠經過nametuple重複創造數據實例:

from collections import namedtuple

NamedTupleCard = namedtuple('NamedTupleCard', ['rank', 'suit'])

NamedTupleCard能夠作到DataClassCard能過的事情:

queen_of_hearts = NamedTupleCard('Q', 'Hearts')
print(queen_of_hearts.rank)
print(queen_of_hearts)
print(queen_of_hearts == NamedTupleCard('Q', 'Hearts'))

運行結果跟 DataClassCard同樣

'Q'
NamedTupleCard(rank='Q', suit='Hearts')
True

可是nametuple也有一些限制和不足。例如,咱們不能對nametuple實例的屬性值進行更改,由於從根本上將nametuple是元組類,是不可更改數據類型。在某些應用中,這多是很棒的功能,但在其餘應用場景中,擁有更多靈活性會更好:

card = NamedTupleCard('7', 'Diamonds')
card.rank = '9'

因爲nametuple不可更改性,運行結果報錯以下

AttributeError: can't set attribute

dataclass基礎
如今返回到dataclass,咱們要建立一個Position類,包含名字和經緯度信息的地理位置信息類:

from dataclasses import dataclass

@dataclass
class Position:
    name: str
    lon: float
    lat: float

pos= Position('Oslo', 10.8, 59.9)
print(pos)
print(pos.lat)
print(f'{pos.name} is at {pos.lat}°N, {pos.lon}°E')

咱們解讀下Position類代碼含義。首先使用@dataclass放在Position上方起到裝飾器語法做用。

經過@dataclass裝飾後的Position,咱們能夠給Position增長一些默認的字段,而且聲明這些字段的類型。

運行結果以下:

Position(name='Oslo', lon=10.8, lat=59.9)
59.9
Oslo is at 59.9°N, 10.8°E

咱們也可使用相似於nametuple語法的make_dataclass來建立Position類。代碼以下

from dataclasses import make_dataclass

pos = make_dataclass('Position', ['name', 'lat', 'lon'])
print(pos)

#打印結果:<class 'types.Position.

dataclass實際上也是普通的python對象,只不過dataclass幫咱們將 init()、 repr()和eq()封裝,更簡潔的提供給咱們使用。

dataclass類的默認屬性值
在dataclass中很方便的給屬性值添加默認值

from dataclasses import dataclass

@dataclass
class Position:
    name: str
    lon: float = 0.0
    lat: float = 0.0

dataclass默認值設置相似於init()方法

print(Position('Null Island'))
print(Position('Greenwich', lat=51.8))
print(Position('Vancouver', -123.1, 49.3))

默認經緯度均爲0.運行結果以下

Position(name='Null Island', lon=0.0, lat=0.0)
Position(name='Greenwich', lon=0.0, lat=51.8)
Position(name='Vancouver', lon=-123.1, lat=49.3)

稍後咱們會講到默認工廠(default factory),從而爲咱們默認值設置提供了更多更復雜的功能。

類型提示(Type Hints)
您可能已經注意到咱們使用類型提示定義的字段:name:str表示名稱應該是文本字符串(str類型)。

實際上,在數據類中定義字段時,必須添加某種類型提示。 若是沒有類型提示,該字段將不是dataclass類的一部分。 可是,若是您不想向dataclass類添加顯式類型,請使用typing.Any:

from dataclasses import dataclass
from typing import Any

@dataclass
class WithoutExplicitTypes:
    name: Any
    value: Any = 42

雖然在使用數據類時須要以某種形式添加類型提示,但這些類型在運行時不會強制執行。

withoutexplicittypes = WithoutExplicitTypes(name=38, value='29')
print(withoutexplicittypes)

上面的代碼運行沒有任何問題,運行結果。咱們發現name是任意類型,而values也是任意類型,雖然默認設置爲整數42,可是在這裏咱們輸入的是字符串29,也能正常運行。

WithoutExplicitTypes(name=38, value='29')

添加方法
dataclass類就是普通的python類,因此咱們能夠像給類定義方法同樣給dataclass類定義方法。

這裏咱們定義距離計算方法,爲了方便演示,咱們這裏假設地球是二維平面,經緯度表明座標軸中的位置,使用歐幾里得方法計算距離便可。

from dataclasses import dataclass

@dataclass
class Position:
    name: str
    lon: float = 0.0
    lat: float = 0.0

    def distance(self, newpostion):
        return sqrt((newpostion.lon - self.lon)**2 + (newpostion.lat - self.lat)**2)

pos1 = Position('A', 0, 0)
pos2 = Position('B', 0.0, 2.0)
pos3 = Position('C', 3.0, 4.0)
print(pos1.distance(pos2))
print(pos1.distance(pos3))

A點是座標原點,B點(0, 2), C點(3, 4)。運行結果

2.0  #AB = 2
5.0 #AC = 5

靈活的dataclass
到如今位置,咱們已經瞭解了dataclass的基本特性,如今咱們接觸些dataclass的高級用法,如參數和field()函數。將這二者結合能讓咱們更方便的 控制咱們創造的類。

讓咱們回到您在本教程開頭看到的撲克牌示例,並在咱們處理時添加一個包含一副牌的類:

from dataclasses import dataclass
from typing import List

@dataclass
class PlayingCard:
    rank: str
    suit: str

@dataclass
class Deck:
    #Deck:一副牌。cards參數傳入列表,該列表中含有多個PlayingCard類實例。
    cards: List[PlayingCard]

queen_of_hearts = PlayingCard('Q', 'Hearts')
ace_of_spades = PlayingCard('A', 'Spades')
two_cards = Deck([queen_of_hearts, ace_of_spades])
print(two_cards)

上面的two_cards是最簡單的一副牌(Deck類),運行結果以下

Deck(cards=[PlayingCard(rank='Q', suit='Hearts'), PlayingCard(rank='A', suit='Spades')])
相關文章
相關標籤/搜索