Python 3.7中一个令人兴奋的新特性是 data classes 。 数据类通常是一个主要包含数据的类,尽管实际上没有任何限制。 它是使用新的 @dataclass 装饰器创建的,如下所示:

from dataclasses import dataclass
@dataclass
class DataClassCard:
 rank: str
 suit: str

此代码以及本教程中的所有其他示例仅适用于 Python 3.7 及更高版本。

注意:

当然在 Python 3.6 版本也可以使用这个功能,不过需要安装 dataclasses 这个库,使用 pip install dataclasses 命令就可以轻松安装, Github地址: dataclass (在 Python 3.7 版本中 dataclasses 已经作为一个标准库存在了)

dataclass 类带有已实现的基本功能。 例如,你可以直接实例化,打印和比较数据类实例。

> queen_of_hearts = DataClassCard('Q', 'Hearts')
> queen_of_hearts.rank
'Q'
> queen_of_hearts
DataClassCard(rank='Q', suit='Hearts')
> queen_of_hearts == DataClassCard('Q', 'Hearts')
True

将 dataclass 其与其他普通类进行比较的话。最基本的普通类看起来像这样:

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

虽然没有太多代码需要编写,但是你应该已经看到了不好的地方: 为了初始化一个对象, rank 和 suit 都会重复出现三次。此外,如果你尝试使用这个普通类,你会注意到对象的表示不是很具有描述性,并且由于某种原因, queen_of_hearts 和 DataClassCard('Q', 'Hearts') 会不相等,如下:

> queen_of_hearts = RegularCard('Q', 'Hearts')
> queen_of_hearts.rank
'Q'
> queen_of_hearts
<__main__.RegularCard object at 0x7fb6eee35d30>
> queen_of_hearts == RegularCard('Q', 'Hearts')
False

似乎 dataclass 类在在背后帮我们做了什么。默认情况下, dataclass 实现了一个 __repr__() 方法,用来提供一个比较好的字符串表示方式,并且还实现了 __eq__() 方法,这个方法可以实现基本对象之间的比较。如果要使 RegularCard 类模拟上面的 dataclass 类,还需要添加下面这些方法:

class RegularCard:
 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})')

 def __eq__(self, other):
 if other.__class__ is not self.__class__:
 return NotImplemented
 return (self.rank, self.suit) == (other.rank, other.suit)

在本教程中,你能够确切地了解 dataclass 类提供了哪些便利。除了良好的表示形式和对象比较之外,你还会看到:

dataclass
dataclass
dataclass

接下来,我们将深入研究 dataclass 类的这些特性。或许,你可能认为你以前看到过类似的内容。

1. 先说说 dataclass 的替代方案

对于简单的数据结构,你可能会使用 tuple 或 dict 。你可以用以下两种方式表示 红心Q 扑克牌:

> queen_of_hearts_tuple = ('Q', 'Hearts')
> queen_of_hearts_dict = {'rank': 'Q', 'suit': 'Hearts'}

这样写,是没有问题的。但是,作为一名程序员,你还需要注意:

你需要你记住 红心Q、红心K... 等等,所有的变量所代表的扑克牌
对于上边使用 tuple 的版本,你需要记住元素的顺序。比如,写 ('黑桃','A') ,顺序就乱了,但是程序却可能不会给你一个容易理解的错误信息
如果你使用了 dict 的方式,必须确保属性的名称是一致的。 例如,如果写成 {'value':'A','suit':'Spades'} ,同样无法达到预期的目的。

另外,使用这些结构并不是最好的:

> queen_of_hearts_tuple[0] # 不能通过名称访问
'Q'
> queen_of_hearts_dict['suit'] # 这样的话还不如使用 `.suit` 
'Hearts'

所以,这里有一个更好的替代方案是:使用 namedtuple 。

它长期以来被用于创建可读的小数据结构(用以构建只有少数属性但是没有方法的对象)。 我们可以使用 namedtuple 重新创建上面的 dataclass 类示例:

from collections import namedtuple
NamedTupleCard = namedtuple('NamedTupleCard', ['rank', 'suit'])

NamedTupleCard 的这个定义将与我们之前的的 DataClassCard 示例,有完全相同的输出。

> queen_of_hearts = NamedTupleCard('Q', 'Hearts')
> queen_of_hearts.rank
'Q'
> queen_of_hearts
NamedTupleCard(rank='Q', suit='Hearts')
> queen_of_hearts == NamedTupleCard('Q', 'Hearts')
True

那么,为什么还要使用 dataclass 类呢"htmlcode">

> queen_of_hearts == ('Q', 'Hearts')
True

虽然这似乎是一件好事,但如果缺乏对其自身类型的认识,会导致细微且难以发现的 bug ,特别是因为它也可以友好地比较两个不同的 namedtuple 类,如下:

> Person = namedtuple('Person', ['first_initial', 'last_name']
> ace_of_spades = NamedTupleCard('A', 'Spades')
> ace_of_spades == Person('A', 'Spades')
True

namedtuple 也有一些限制。 例如,很难为 namedtuple 中的某些字段添加默认值。 namedtuple 本质上也是不可变的。也就是说, namedtuple 的值永远不会更改。在某些应用程序中,这是一个很棒的特性,但是在其他设置中,如果有更多的灵活性就更好了。

> card = NamedTupleCard('7', 'Diamonds')
> card.rank = '9'
AttributeError: can't set attribute

dataclass 不会取代 namedtuple 的所有用法。 例如,如果你需要你的数据结构像元组一样,那么 namedtuple 是一个很好的选择!

dataclass 的另一种选择(也是 dataclass 的灵感之一)是 attrs 库。安装了 attrs 之后(可以通过 pip install attrs 命令安装),你可以按如下方式编写 Card 类:

import attr
@attr.s
class AttrsCard:
 rank = attr.ib()
 suit = attr.ib()

可以使用与前面的 DataClassCard 和 NamedTupleCard 示例完全相同的方法。 attrs 非常棒,并且支持了一些 DataClass 不支持的特性,比如转换器和验证器。此外, attrs 已经出现了一段时间,并且支持 Python 2.7 和 Python 3.4 及以上版本。但是,由于 attrs 不在标准库中,所以它确实需要为项目添加了一个外部依赖项。通过 dataclass ,可以在任何地方使用类似的功能。

除了 tuple , dict , namedtuple 和 attrs 之外,还有许多其他类似的项目,包括 yping.NamedTuple , namedlist , attrdict , plumber 和 fields 。虽然 dataclass 是一个很好的新选择,但仍有一些旧版本适合更好的用例。例如,如果需要与期望元组的特定API兼容,或者遇到需要 dataclass 中不支持的功能。

2. dataclass 基本要素

让我们继续回到 dataclass 。例如,我们将创建一个 Position 类,它将使用名称以及纬度和经度来表示地理位置。

from dataclasses import dataclass
@dataclass
class Position:
 name: str
 lon: float
 lat: float

类定义上面的 @dataclass 装饰器定义了 Position 类为 dataclass 类型。在类 Position: 行下面,只需列出 dataclass 类中需要的字段。用于字段的 :表示法 使用了Python 3.6中的一个称为 变量注释 的新特性。我们将很快讨论更多关于这种表示法的内容,以及为什么要指定像 str 和 float 这样的数据类型。

只需几行代码即可。 新创建的类可以使用了:

> 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

你还可以使用类似于创建命名元组的方式创建 dataclass 类。下面的方式(几乎)等价于上面位置的定义:

from dataclasses import make_dataclass
Position = make_dataclass('Position', ['name', 'lat', 'lon'])

dataclass 类是一个普通的Python类。唯一使它与众不同的是,它有一些以及实现的基本数据模型方法,比如: __init__() , __repr__() ,以及 __eq__() 。

2.1 添加默认值

向 dataclass 类的字段添加默认值很容易:

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

这与普通类的 __init__() 方法的定义中指定默认值完全相同:

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

接下来,将了解到 default_factory ,这是一种提供更复杂默认值的方法。

2.2 类型提示

到目前为止,我们还没有对 dataclass 类支持开箱即用的事实大做文章。你可能已经注意到,我们使用类型提示的方式来定义字段, name: str :表示 name 应该是一个文本字符串(str类型)。

实际上,在定义 dataclass 类中的字段时,必须添加某种类型的提示。 如果没有类型提示,该字段将不 dataclass 类的一部分。 但是,如果不想向 dataclass 类添加显式类型,可以使用 typing.Any :

from dataclasses import dataclass
from typing import Any
@dataclass
class WithoutExplicitTypes:
 name: Any
 value: Any = 42

虽然在使用 dataclass 类时需要以某种形式添加类型提示,但这些类型在运行时并不是强制的。下面的代码运行时没有任何问题:

> Position(3.14, 'pi day', 2018)
Position(name=3.14, lon='pi day', lat=2018)

这就是Python进行输入通常的工作方式:Python现在是,将来也永远是一种动态类型语言。要实际捕获类型错误,可以在你的代码中运行 Mypy 之类的类型检查器。

2.3 添加方法

前边已经提到, dataclass 类也只是一个普通类。这意味着你可以自由地将自己的方法添加到 dataclass 类中。举个例子,让我们计算一个位置与另一个位置之间沿地球表面的距离。一种方法是使用 hasrsine公式 :

你可以像使用普通类一样将 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

3.更灵活的 dataclass

到目前为止,你已经看到了 dataclass 类的一些基本特性:它提供了一些方便的方法、可以添加默认值和其他方法。现在,你将了解一些更高级的特性,比如 @dataclass 装饰器的参数和 field() 方法。在创建 dataclass 类时,它们一起给你提供了更多的控制权。

让我们回到你在本教程开始时看到的 playingcard示例 ,并且添加一个包含一副纸牌的类:

from dataclasses import dataclass
from typing import List
@dataclass
class PlayingCard:
 rank: str
 suit: str
@dataclass
class Deck:
 cards: List[PlayingCard]

可以创建一副简单的牌组,这副牌组只包含两张牌,如下所示:

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

3.1 默认值的高级用法

假设你想给牌组提供默认值。例如, Deck() 很方便就可以创建一个由52张扑克牌组成的普通牌组。首先,指定不同的数字( ranks )和花色( suits )。然后,添加一个方法 make french deck() ,该方法创建 PlayingCard 的实例列表:

RANKS = '2 3 4 5 6 7 8 9 10 J Q K A'.split()
SUITS = '"htmlcode">
> make_french_deck()
[PlayingCard(rank='2', suit='"htmlcode">
from dataclasses import dataclass
from typing import List
@dataclass
class Deck: # Will NOT work
 cards: List[PlayingCard] = make_french_deck()

不要这样做!这引入了Python中最常见的反模式之一: 使用可变的默认参数 。

问题在于, Deck 的所有实例都将使用相同的list对象作为 cards 属性的默认值。这意味着,如果一张牌从一副牌中被移走,那么它也将从牌的所有其他实例中消失。 实际上, dataclass 类也会阻止你这样做,上面的代码将引发 ValueError 。

相反, dataclass 类使用称为 default_factory 的东西来处理可变的默认值。 要使用 default_factory (以及 dataclass 类的许多其他很酷的功能),你需要使用 field() 说明符:

from dataclasses import dataclass, field
from typing import List
@dataclass
class Deck:
 cards: List[PlayingCard] = field(default_factory=make_french_deck)

default_factory 的参数可以是任何可调参数的零参数。现在很容易就可以创建一副完整的扑克牌:

> Deck()
Deck(cards=[PlayingCard(rank='2', suit='"htmlcode">
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 (以及关于字段的其他信息,注意 field 是复数)。

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

3.2 更好的表示方式

回想一下,我们可以使用下边的代码创造出一副纸牌:

> Deck()
Deck(cards=[PlayingCard(rank='2', suit='"htmlcode">
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', '"htmlcode">
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 说明符。这意味着我们要显式地使用每个 PlayingCard 的 str() 表示。用新的 __repr__() , Deck 的表示更容易看懂:

> Deck()
Deck("htmlcode">
> queen_of_hearts = PlayingCard('Q', '"htmlcode">
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: 是否增加 __init__() 方法, (默认是True)
repr: 是否增加 __repr__() 方法, (默认是True)
eq: 是否增加 __eq__() 方法, (默认是True)
order: 是否增加 ordering 方法, (默认是False)
unsafe_hash: 是否强制添加 __hash__() 方法, (默认是False )
frozen: 如果为 True ,则分配给字段会引发异常。(默认是False )
有关每个参数的详细信息,请参阅PEP。 设置 order = True 后,就可以比较 PlayingCard 对象了:

> queen_of_hearts = PlayingCard('Q', '"htmlcode">
> ('A', '"htmlcode">
> RANKS = '2 3 4 5 6 7 8 9 10 J Q K A'.split()
> SUITS = '"htmlcode">
from dataclasses import dataclass, field

RANKS = '2 3 4 5 6 7 8 9 10 J Q K A'.split()
SUITS = '"htmlcode">
> queen_of_hearts = PlayingCard('Q', '"htmlcode">
> Deck(sorted(make_french_deck()))
Deck("htmlcode">
> from random import sample
> Deck(sample(make_french_deck(), k=10))
Deck("color: #ff0000">4. 不可变的 dataclass

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

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

在 frozen=True 的 dataclass 中,不能在创建后为字段赋值。

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

但是要注意,如果你的数据类包含可变字段,这些字段可能仍然会更改。这适用于Python中的所有嵌套数据结构。

from dataclasses import dataclass
from typing import List

@dataclass(frozen=True)
class ImmutableCard:
 rank: str
 suit: str

@dataclass(frozen=True)
class ImmutableDeck:
 cards: List[PlayingCard]

尽管 ImmutableCard 和 ImmutableDeck 都是不可变的,但是包含 Card 的列表并不是不可变的。因此你仍然可以换牌。

> queen_of_hearts = ImmutableCard('Q', '"color: #ff0000">5. 继承

你可以非常自由地子类化 dataclass 类。例如,我们将使用 country 字段继承 Position 示例并使用它来记录国家名称:

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 字段被添加在 Position 类的三个原始字段( name , lon , lat )后边。如果基类中的任何字段具有默认值,事情会变得复杂一些:

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

上边这段代码将立即崩溃,并报一个 TypeError : "non-default argument ‘country' follows default argument." 问题是:我们的新字段: country 没有默认值,而 lon 和 lat 字段有默认值。 dataclass 类将尝试编写一个像下面一样的 __init__() 方法:

def __init__(name: str, lon: float = 0.0, lat: float = 0.0, country: str):
 ...

然而,这不是可行的。如果参数具有默认值,则后边的所有参数也必须具有默认值。换句话说,如果基类中的字段具有默认值,那么子类中添加的所有新字段也必须具有默认值。

另一件需要注意的是字段在子类中的排序方式。 从基类开始,字段按照首次定义的顺序排序。 如果在子类中重新定义字段,则其顺序不会更改。 例如,如果你按如下方式定义 Position 和 Capital :

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。

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

6. 优化 dataclass

我将用几个关于 Slot 的内容来结束本教程。 Slot 可用于更快地创建类并使用更少的内存。 dataclass 类没有明确的语法来处理 Slot ,但创建 Slot 的常规方法也适用于 dataclass 类。(他们真的只是普通的类!)

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

本质上, Slot 是用 __slots__ 在类中定义,并列出了变量。对于不在 __slots__ 的变量或属性,将不会被定义。此外, Slot 类可能没有默认值。

添加这些限制的好处是可以进行某些优化。例如, Slot 类占用的内存更少,这个可以使用 Pympler 进行测试:

> from pympler import asizeof
> simple = SimplePosition('London', -0.1, 51.5)
> slot = SlotPosition('Madrid', -3.7, 40.4)
> asizeof.asizesof(simple, slot)
(440, 248)

同样, Slot 类通常处理起来更快。下面的示例中,使用标准库中的 timeit 测试了 slots data class 类和常规 data class 类上的属性访问速度。

> from timeit import timeit
> timeit('slot.name', setup="slot=SlotPosition('Oslo', 10.8, 59.9)", globals=globals())
0.05882283499886398
> timeit('simple.name', setup="simple=SimplePosition('Oslo', 10.8, 59.9)", globals=globals())
0.09207444800267695

在这个特定的例子中, Slot 类的速度提高了约35%。

7. 总结及进一步阅读

data class 类是 Python 3.7 的新特性之一。使用 DataClass 类,你不必编写样板代码来为对象获得适当的初始化、表示和比较。

你已经了解了如何定义自己的 data class 类,以及:

data class
data class
data class
data class

如果你还想深入了解 data class 类的所有细节,请查看PEP 557 以及 GitHub repo 中的讨论。

总结

以上所述是小编给大家介绍的Python3.7 新特性之dataclass装饰器,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

广告合作:本站广告合作请联系QQ:858582 申请时备注:广告合作(否则不回)
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!

稳了!魔兽国服回归的3条重磅消息!官宣时间再确认!

昨天有一位朋友在大神群里分享,自己亚服账号被封号之后居然弹出了国服的封号信息对话框。

这里面让他访问的是一个国服的战网网址,com.cn和后面的zh都非常明白地表明这就是国服战网。

而他在复制这个网址并且进行登录之后,确实是网易的网址,也就是我们熟悉的停服之后国服发布的暴雪游戏产品运营到期开放退款的说明。这是一件比较奇怪的事情,因为以前都没有出现这样的情况,现在突然提示跳转到国服战网的网址,是不是说明了简体中文客户端已经开始进行更新了呢?