小白的Python學習筆記(十五)面向對象編程知識總結《5》

Dataclasses

你們好,這一期我主要記錄一下個人Dataclasses的學習過程。html

上一期簡單回顧了attrs的用法,這一期來看更簡潔的自帶寫類神器:dataclasses前端

官方文檔連接: Data Classes 下面直接來看例子:python

建立Dataclass

from dataclasses import dataclass

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

複製代碼

能夠發現,主要起做用的是裝飾符@dataclass ,須要注意,若是想要使用dataclass,須要Python 3.7或更高版本 使用dataclass的好處是能夠節省書寫__init()__等一些經常使用的實例方法git

這裏建立一個Position類,用來顯示一個地點的位置github

  • name:地點的名字
  • lon:經度
  • lat:緯度

新建一個實例來看看:api

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

除了這種方法,還要一種相似建立namedtuple的方式也能夠:app

from dataclasses import make_dataclass

Position = make_dataclass('Position', ['name', 'lat', 'lon'])
複製代碼

默認值

讓咱們看看如何給類的屬性添加默認值:函數

from dataclasses import dataclass

@dataclass
class Position:
    name: str
    lon: float = 0.0
    lat: float = 0.0
複製代碼

效果和普通的類設定初始值的效果是同樣的:學習

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

複製代碼

輸入提示

你們能夠發現咱們的Positon類規定了三個屬性的類型:優化

  • name:str
  • lon:float
  • lat:float

如今若是想要開放限制,容許任意的數據類型,能夠這麼作:

from dataclasses import dataclass
from typing import Any

@dataclass
class WithoutExplicitTypes:
    name: Any
    value: Any = 42
複製代碼

這樣運行的時候不會報錯,哪怕瞎傳參:

>>> Position(3.14, 'pi day', 2018)
Position(name=3.14, lon='pi day', lat=2018)
複製代碼

添加一個方法

如今咱們想要計算兩個地點的距離,能夠參考以下公式:

The haversine formula

根據這個公式爲類添加一個.distance_to()方法

from dataclasses import dataclass
from math import asin, cos, radians, sin, sqrt

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

    def distance_to(self, other):
        r = 6371  # Earth radius in kilometers
        lam_1, lam_2 = radians(self.lon), radians(other.lon)
        phi_1, phi_2 = radians(self.lat), radians(other.lat)
        h = (sin((phi_2 - phi_1) / 2)**2
             + cos(phi_1) * cos(phi_2) * sin((lam_2 - lam_1) / 2)**2)
        return 2 * r * asin(sqrt(h))
複製代碼

實驗一下:

>>> oslo = Position('Oslo', 10.8, 59.9)
>>> vancouver = Position('Vancouver', -123.1, 49.3)
>>> oslo.distance_to(vancouver)
7181.7841229421165
複製代碼

更加靈活的應用

目前爲止咱們已經看到了dataclass的基礎用法,如今咱們看看根據實際須要,有哪些其餘靈活的應用方式。

如今建立兩個類,紙牌類和牌庫類,紙牌類的屬性包括花色和數字,牌庫是List類型,包含紙牌類的一些實例

from dataclasses import dataclass
from typing import List

@dataclass
class PlayingCard:
    rank: str
    suit: str

@dataclass
class Deck:
    cards: List[PlayingCard]

複製代碼

如今向牌庫添加紅桃Q和黑桃A的操做能夠這樣:

>>> queen_of_hearts = PlayingCard('Q', 'Hearts')
>>> ace_of_spades = PlayingCard('A', 'Spades')
>>> two_cards = Deck([queen_of_hearts, ace_of_spades])
Deck(cards=[PlayingCard(rank='Q', suit='Hearts'),
 PlayingCard(rank='A', suit='Spades')])
複製代碼

如今咱們能夠建立一套完整的撲克牌牌庫,注意這裏使用了符號表示花色,建議在實際環境中改換爲字符串:

RANKS = '2 3 4 5 6 7 8 9 10 J Q K A'.split()
SUITS = '♣ ♢ ♡ ♠'.split()

def make_french_deck():
    return [PlayingCard(r, s) for s in SUITS for r in RANKS]

>>> make_french_deck()
[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ...
 PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')]
複製代碼

理論上,咱們能夠把這個方法做爲Deck類的初始變量,可是根本不行,由於它可變:

from dataclasses import dataclass
from typing import List

@dataclass
class Deck:  # Will NOT work
    cards: List[PlayingCard] = make_french_deck()
複製代碼

面對這種狀況,dataclass的解決方案是使用default_factory函數做爲field的參數來指明:

from dataclasses import dataclass, field
from typing import List

@dataclass
class Deck:
    cards: List[PlayingCard] = field(default_factory=make_french_deck)
複製代碼

如今就沒有任何問題了:

>>> Deck()
Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ...
 PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')])
複製代碼

field

如今簡單總結一下dataclass中使用field涉及到的關鍵參數:

  • default: Default value of the field
  • default_factory: Function that returns the initial value of the field
  • init: Use field in .__init__() method? (Default is True.)
  • repr: Use field in repr of the object? (Default is True.)
  • compare: Include the field in comparisons? (Default is True.)
  • hash: Include the field when calculating hash()? (Default is to use the same as for compare.)
  • metadata: A mapping with information about the field

最後這個metadata有點像前端h5中的那個,就是能夠爲類的一個屬性添加一個額外的描述信息:

from dataclasses import dataclass, field

@dataclass
class Position:
    name: str
    lon: float = field(default=0.0, metadata={'unit': 'degrees'})
    lat: float = field(default=0.0, metadata={'unit': 'degrees'})


複製代碼

能夠發現,傳遞的是一個dict,如今可使用fields來查看一個屬性的附加信息了:

>>> from dataclasses import fields
>>> fields(Position)
(Field(name='name',type=<class 'str'>,...,metadata={}),
 Field(name='lon',type=<class 'float'>,...,metadata={'unit': 'degrees'}),
 Field(name='lat',type=<class 'float'>,...,metadata={'unit': 'degrees'}))
>>> lat_unit = fields(Position)[2].metadata['unit']
>>> lat_unit
'degrees'
複製代碼

描述類(str,repr)

這裏指的就是經常使用的repr(obj)和str(obj) 先看一下剛纔的Deck()描述:

>>> Deck()
Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), PlayingCard(rank='4', suit='♣'), PlayingCard(rank='5', suit='♣')...

複製代碼

有些過長了,讓咱們用傳統的str或者repr來表達,首先從紙牌類PlayingCard開始:

from dataclasses import dataclass

@dataclass
class PlayingCard:
    rank: str
    suit: str

    def __str__(self):
        return f'{self.suit}{self.rank}'
複製代碼
>>> ace_of_spades = PlayingCard('A', '♠')
>>> ace_of_spades
PlayingCard(rank='A', suit='♠')
>>> print(ace_of_spades)
♠A
複製代碼

如今看上去好多了,如今再自定義下牌庫類Deck的描述:

from dataclasses import dataclass, field
from typing import List

@dataclass
class Deck:
    cards: List[PlayingCard] = field(default_factory=make_french_deck)

    def __repr__(self):
        cards = ', '.join(f'{c!s}' for c in self.cards)
        return f'{self.__class__.__name__}({cards})'
複製代碼

這回看上去舒服多了:

>>> Deck()
Deck(♣2, ♣3, ♣4, ♣5, ♣6, ♣7, ♣8, ♣9, ♣10, ♣J, ♣Q, ♣K, ♣A,
 ♢2, ♢3, ♢4, ♢5, ♢6, ♢7, ♢8, ♢9, ♢10, ♢J, ♢Q, ♢K, ♢A,
 ♡2, ♡3, ♡4, ♡5, ♡6, ♡7, ♡8, ♡9, ♡10, ♡J, ♡Q, ♡K, ♡A,
 ♠2, ♠3, ♠4, ♠5, ♠6, ♠7, ♠8, ♠9, ♠10, ♠J, ♠Q, ♠K, ♠A)

複製代碼

不可修改的Dataclass

其實這裏和FrozenSet有點像,無非在裝飾器中添加了一個參數frozen=True:

from dataclasses import dataclass

@dataclass(frozen=True)
class Position:
    name: str
    lon: float = 0.0
    lat: float = 0.0
複製代碼

在這樣一個frozen data class中,咱們不能隨意賦值:

>>> pos = Position('Oslo', 10.8, 59.9)
>>> pos.name
'Oslo'
>>> pos.name = 'Stockholm'
dataclasses.FrozenInstanceError: cannot assign to field 'name'

複製代碼

繼承

使用dataclass的繼承也比較簡單,和普通類的繼承沒有太大區別:

from dataclasses import dataclass

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

@dataclass
class Capital(Position):
    country: str

複製代碼
>>> Capital('Oslo', 10.8, 59.9, 'Norway')
Capital(name='Oslo', lon=10.8, lat=59.9, country='Norway')
複製代碼

這裏能夠發現,建立Capital類的實例時會自動繼承了父類的屬性,咱們只須要額外加入country這個新屬性就能夠了

假如父類初始化時有默認值:

from dataclasses import dataclass

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

@dataclass
class Capital(Position):
    country: str = 'Unknown'
    lat: float = 40.0
複製代碼

簡單優化

能夠用我以前提到的slot方法進行優化:

from dataclasses import dataclass

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

@dataclass
class SlotPosition:
    __slots__ = ['name', 'lon', 'lat']
    name: str
    lon: float
    lat: float

複製代碼

總結

此次我記錄了dataclass的基礎用法,是參考了別人的文章,若是想要了解更多,仍是要到官方文檔去看

個人其餘原創文章已經放到了Github上,若是感興趣的朋友能夠去看看,連接以下:

相關文章
相關標籤/搜索