查看原文
其他

Python 3.7中dataclass的终极指南(一)

大邓 大邓和他的Python 2019-04-26
from dataclasses import dataclass
from typing import List

@dataclass
class PlayingCard:
    rank: str
    suit: str

@dataclass
class Deck:
    #Deck:一副牌。cards参数传入列表,该列表可以含有多个PlayingCard类实例。
    cards: List[PlayingCard]

dataclass类默认值(进阶)

假设我们要给Deck提供默认值。 例如,如果Deck()创建了52张扑克牌的常规(法国)牌组。

首先,指定不同的点数和花色。 然后,添加一个函数make_french_deck(),它创建一个PlayingCard实例列表:

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]

#生成一副52张的扑克牌组合
make_french_deck()

运行

[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), PlayingCard(rank='4', suit='♣'), PlayingCard(rank='5', suit='♣'), PlayingCard(rank='6', suit='♣'), 
...
PlayingCard(rank='J', suit='♠'), PlayingCard(rank='Q', suit='♠'), PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')]

理论上,我们可以使用make_french_deck()为Deck设置默认的属性(Deck.cards)值。

from dataclasses import dataclass
from typing import List

@dataclass
class Deck:
    # Will Not Work!!
    cards: List[PlayingCard] = make_french_deck()

#生成Deck实例
deck = Deck()
print(deck)

但是,千万不要这么做。这引入了Python中最常见的反常模式:使用可变的默认参数。问题是Deck的所有实例都将使用相同的列表对象作为.cards属性的默认值。 这意味着,如果从一个Deck中移除一张卡,那么它也会从Deck的所有其他实例中消失。 代码运行出错,提示如下

ValueError: mutable default <class 'list'> for field cards is not allowed: use default_factory

实际上,dataclass试图阻止您这样做,并且上面的代码将引发ValueError。相反,dataclass会使用default_factory来处理可变默认值。使用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 = Deck()
print(deck)   

运行如我们预期般的结果

[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), PlayingCard(rank='4', suit='♣'), PlayingCard(rank='5', suit='♣'), PlayingCard(rank='6', suit='♣'), 
...
PlayingCard(rank='J', suit='♠'), PlayingCard(rank='Q', suit='♠'), PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')]

注意defualt_factory参数传入的是make_french_deck,不是make_french_deck()。如果传入make_french_deck(),代码运行报错

TypeError: 'list' object is not callable

field()

field()用来定义dataclass类的每个字段。之后,我们会看到其他案例。field()参数及其含义(功能):

  • default: 字段的默认值

  • default_factory:返回字段初始值的函数

  • init: 布尔值,默认为True。相当于使用了init()方法

  • repr:布尔值,默认为True。可以通过print()将实例打印出来

  • compare:布尔值,默认为True。对不同的实例进行字段的比较。

  • hash:布尔值,默认为True。计算包含的字段的hash值

  • metadata:关于该字段的信息的映射

在Position类中,我们之前是通过如

from dataclasses import dataclass

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

使用lat: float = 0.0设置属性(字段)的默认值。然而,如果使用field()方法,且不想print()打印出来,则需要写成lat:float = field(default = 0.0,  repr = False)

参数metadata不是由dataclass类本身使用,但可供您(或第三方软件包)将信息附加到字段。 在Position类中,我们可以指定纬度和经度应以度为单位:

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'})

可以使用fields()函数检索metadata(以及有关字段的其他信息)(注意复数s):

from dataclasses import fields

print(fields(Position))

运行

(Field(name='name',type=<class 'str'>,default=<dataclasses._MISSING_TYPE object at 0x104577198>,default_factory=<dataclasses._MISSING_TYPE object at 0x104577198>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD), Field(name='lon',type=<class 'float'>,default=0.0,default_factory=<dataclasses._MISSING_TYPE object at 0x104577198>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({'unit''degrees'}),_field_type=_FIELD), Field(name='lat',type=<class 'float'>,default=0.0,default_factory=<dataclasses._MISSING_TYPE object at 0x104577198>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({'unit''degrees'}),_field_type=_FIELD))


表示representation

print(Deck())

运行结果

Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ...
PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')])

虽然Deck()的这种表示是明确的和可读的,但它也非常冗长。 我在上面的输出中删除了Deck上52张卡中的48张。 在80列显示器上,只需打印完整的Deck就占用22行! 让我们添加一个更简洁的表示。 通常,Python对象有两种不同的字符串表示形式:

  • repr(obj)由obj ._ repr _()定义,并且应该返回对开发人员友好的obj表示。如果可能,这应该是可以重新创建obj的代码。dataclass类执行此操作。

  • str(obj)由obj._ str__()定义,并应返回obj的用户友好表示。数据类不实现_ str __()方法,因此Python将回退到._ repr _()方法。

让我们实现一个用户友好的PlayCard表示:

from dataclasses import dataclass


@dataclass
class PlayingCard:
    rank: str
    suit: str

    def __str__(self):
        return f'{self.suit}{self.rank}'


playing = PlayingCard('Q''spade')
print(playing)

运行结果

spadeQ


我们将上述应用到Deck类中

from dataclasses import dataclass, field
from typing import List 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]

@dataclass
class PlayingCard:    rank: str    suit: str    

   def __str__(self):        #将显示简化了        return f'{self.suit}{self.rank}'

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


#Deck显示大大简化
print(Deck())

运行结果

Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), PlayingCard(rank='4', suit='♣'), PlayingCard(rank='5', suit='♣'), PlayingCard(rank='6', suit='♣'), PlayingCard(rank='3', suit='♡'), PlayingCard(rank='4', suit='♡'), ...... PlayingCard(rank='8', suit='♠'), PlayingCard(rank='9', suit='♠'), PlayingCard(rank='10', suit='♠'), PlayingCard(rank='J', suit='♠'), PlayingCard(rank='Q', suit='♠'), PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')])

现在我们看看添加自己的.__ repr __()方法,产生更简洁的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})'

注意{c!s}格式字符串中的!s说明符。这意味着我们明确要使用PlayingCards的str()表示。使用新的._ repr _(),使得Deck的表示更简洁:

print(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)

比较卡牌

在许多纸牌游戏中,卡片是可以相互比较。比如斗地主中,K比Q大。但是PlayingCard类不支持这种比较:

>>> queen_of_hearts = PlayingCard('Q''♡')
>>> ace_of_spades = PlayingCard('A''♠')
>>> ace_of_spades > queen_of_hearts
TypeError: '>' not supported between instances of 'Card' and 'Card'

但在dataclass中,是很容易实现的。

from dataclasses import dataclass

@dataclass(order=True)
class PlayingCard:
    rank: str
    suit: str

    def __str__(self):
        return f'{self.suit}{self.rank}'

@dataclass装饰器有两种形式。 到目前为止,您已经看到了指定@dataclass的简单形式,没有任何括号和参数。 但是,您也可以在括号中为@dataclass()装饰器提供参数。 支持以下参数:

  • init: Add.init() 方法,默认为True

  • repr: Add.repr()方法,默认为True

  • eq: Add.eq()方法,默认为True

  • order:顺序,默认为False

  • unsafe_hash:强制添加.hash()方法,默认为False。

  • frozen: 如果为True,则分配给字段会引发异常。 (默认为False。)

queen_of_hearts = PlayingCard('Q''♡')
ace_of_spades = PlayingCard('A''♠')
print(ace_of_spades > queen_of_hearts)

现在可以进行比较,运行结果

False

不可变dataclass类

之前看到的namedtuple的一个定义特征是它是不可变的。 也就是说,其字段的值可能永远不会改变。 对于许多类型的类,这是一个好主意! 要使dataclass类不可变,请在创建时设置frozen = True。 例如,以下是您Position类的不可变版本:

from dataclasses import dataclass

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

pos = Position('Oslo'10.859.9)
print(pos.name)
pos.name = 'Stockholm'

运行结果

'Oslo'
dataclasses.FrozenInstanceError: cannot assign to field 'name'

类的继承

我们可以非常自由的对dataclass进行子类化(继承操作)。例如,我们将使用country字段扩展Position示例:

from dataclasses import dataclass

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

@dataclass
class Capital(Position):
    country: str


print(Capital('Ohio'10.859.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  # Does NOT work

解决办法

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

然后,Capital中字段的顺序仍然是name,lon,lat,country。但是,lat的默认值为40.0。

print(Capital('Madrid', country='Spain'))

运行正常,返回的结果

Capital(name='Madrid', lon=0.0, lat=40.0, country='Spain')

往期文章

100G Python学习资料:从入门到精通! 免费下载    

上百G文本数据集等你来认领|免费领取  

Python 3.7中dataclass的终极指南(一)

2017年度15个最好的数据科学领域Python库   

推荐系统与协同过滤、奇异值分解

机器学习之使用逻辑回归识别图片中的数字

应用PCA降维加速模型训练

使用sklearn做自然语言处理-1 

使用sklearn做自然语言处理-2

机器学习|八大步骤解决90%的NLP问题    

Python圈中的符号计算库-Sympy

Python中处理日期时间库的使用方法 

如何从文本中提取特征信息? 

【视频讲解】Scrapy递归抓取简书用户信息

美团商家信息采集神器 

用chardect库解决网页乱码问题

昨日财报

赞赏、点赞、转发、AD支持都是对大邓的认可和支持,希望大家在阅读后顺便帮大邓转发一下。前天的0.14,昨天20.22,大家真给力!


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存