你们好,这一期我主要记录一下个人Dataclasses的学习过程。html
上一期简单回顾了attrs的用法,这一期来看更简洁的自带写类神器:dataclasses前端
官方文档连接: Data Classes 下面直接来看例子:python
from dataclasses import dataclass
@dataclass
class Position:
name: str
lon: float
lat: float
复制代码
能够发现,主要起做用的是装饰符@dataclass ,须要注意,若是想要使用dataclass,须要Python 3.7或更高版本 使用dataclass的好处是能够节省书写__init()__等一些经常使用的实例方法git
这里建立一个Position类,用来显示一个地点的位置github
新建一个实例来看看: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类规定了三个属性的类型:优化
如今若是想要开放限制,容许任意的数据类型,能够这么作:
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)
复制代码
如今咱们想要计算两个地点的距离,能够参考以下公式:
根据这个公式为类添加一个.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='♠')])
复制代码
如今简单总结一下dataclass中使用field涉及到的关键参数:
default
: Default value of the fielddefault_factory
: Function that returns the initial value of the fieldinit
: 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'
复制代码
这里指的就是经常使用的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)
复制代码
其实这里和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上,若是感兴趣的朋友能够去看看,连接以下: