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

独家号 大邓的独家号 作者 邓旭东 原文链接

标题:The Ultimate Guide to Data Classes in Python 3.7
原文链接https://realpython.com/python-data-classes/
作者:Geir Arne Hjelle
译者:大邓

python3.7 的dataclass新特性大大简化了定义类对象的代码量,代码简洁明晰。通过使用@dataclass装饰器来修饰类的设计,例如

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之前的语法类似于这样

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

虽然这种写法并没有使用更多的代码量,但是我们很容易看到为了初始化,仅仅只是为了初始化一个对象,rank和suit已经重复了三次。此外,如果你试图使用这个RegularCard类,你会注意到对象的表示不是很具描述性,并且已有的类与新声明的类是无法比较是否相同的。因为每次声明都会使用一个新的内存地址,而“==”不止比较类存储的信息,还比较内存地址是否相同。

具体请看下面代码

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

运行结果

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

dataclass还在底层给我们做了更多的有用的封装。默认情况下dataclass实现了__repr__方法,可以很好的提供字符串表示;也是了__eq__方法,可以做基本的对象比较。而如果RegularCard想实现上面的功能需要写大量的声明,代码量多的吓人

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)

  • 如何让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')])

往期文章

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

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

哈工大国际人工智能暑期学校招生通知

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

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

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

应用PCA降维加速模型训练

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

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

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

Python圈中的符号计算库-Sympy

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

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

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

美团商家信息采集神器

用chardect库解决网页乱码问题

昨日财报

赞赏、点赞、转发、AD支持都是对大邓的认可和支持,希望大家在阅读后顺便帮大邓转发一下。额,昨天仅有0.14RMB收入!

开发者头条

程序员分享平台