Python3.7 新特性之dataclass装饰器
Python3.7中一个令人兴奋的新特性是dataclasses。数据类通常是一个主要包含数据的类,尽管实际上没有任何限制。它是使用新的@dataclass装饰器创建的,如下所示:
fromdataclassesimportdataclass @dataclass classDataClassCard: rank:str suit:str
此代码以及本教程中的所有其他示例仅适用于Python3.7及更高版本。
注意:
当然在Python3.6版本也可以使用这个功能,不过需要安装dataclasses这个库,使用pipinstalldataclasses命令就可以轻松安装,Github地址:dataclass(在Python3.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其与其他普通类进行比较的话。最基本的普通类看起来像这样:
classRegularCard: 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__.RegularCardobjectat0x7fb6eee35d30> >>>queen_of_hearts==RegularCard('Q','Hearts') False
似乎dataclass类在在背后帮我们做了什么。默认情况下,dataclass实现了一个__repr__()方法,用来提供一个比较好的字符串表示方式,并且还实现了__eq__()方法,这个方法可以实现基本对象之间的比较。如果要使RegularCard类模拟上面的dataclass类,还需要添加下面这些方法:
classRegularCard: 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): ifother.__class__isnotself.__class__: returnNotImplemented 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类示例:
fromcollectionsimportnamedtuple 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类呢?
首先,dataclass类具有的特性比目前看到的要多得多。与此同时,namedtuple还有其他一些不一定需要的功能。
按照设计,namedtuple是一个普通的元组。这一点可以从如下代码的比较中看出:
>>>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'tsetattribute
dataclass不会取代namedtuple的所有用法。例如,如果你需要你的数据结构像元组一样,那么namedtuple是一个很好的选择!
dataclass的另一种选择(也是dataclass的灵感之一)是attrs库。安装了attrs之后(可以通过pipinstallattrs命令安装),你可以按如下方式编写Card类:
importattr @attr.s classAttrsCard: rank=attr.ib() suit=attr.ib()
可以使用与前面的DataClassCard和NamedTupleCard示例完全相同的方法。attrs非常棒,并且支持了一些DataClass不支持的特性,比如转换器和验证器。此外,attrs已经出现了一段时间,并且支持Python2.7和Python3.4及以上版本。但是,由于attrs不在标准库中,所以它确实需要为项目添加了一个外部依赖项。通过dataclass,可以在任何地方使用类似的功能。
除了tuple,dict,namedtuple和attrs之外,还有许多其他类似的项目,包括yping.NamedTuple,namedlist,attrdict,plumber和fields。虽然dataclass是一个很好的新选择,但仍有一些旧版本适合更好的用例。例如,如果需要与期望元组的特定API兼容,或者遇到需要dataclass中不支持的功能。
2.dataclass基本要素
让我们继续回到dataclass。例如,我们将创建一个Position类,它将使用名称以及纬度和经度来表示地理位置。
fromdataclassesimportdataclass @dataclass classPosition: name:str lon:float lat:float
类定义上面的@dataclass装饰器定义了Position类为dataclass类型。在类Position:行下面,只需列出dataclass类中需要的字段。用于字段的:表示法使用了Python3.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}isat{pos.lat}°N,{pos.lon}°E') Osloisat59.9°N,10.8°E
你还可以使用类似于创建命名元组的方式创建dataclass类。下面的方式(几乎)等价于上面位置的定义:
fromdataclassesimportmake_dataclass Position=make_dataclass('Position',['name','lat','lon'])
dataclass类是一个普通的Python类。唯一使它与众不同的是,它有一些以及实现的基本数据模型方法,比如:__init__(),__repr__(),以及__eq__()。
2.1添加默认值
向dataclass类的字段添加默认值很容易:
fromdataclassesimportdataclass @dataclass classPosition: name:str lon:float=0.0 lat:float=0.0
这与普通类的__init__()方法的定义中指定默认值完全相同:
>>>Position('NullIsland') Position(name='NullIsland',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:
fromdataclassesimportdataclass fromtypingimportAny @dataclass classWithoutExplicitTypes: name:Any value:Any=42
虽然在使用dataclass类时需要以某种形式添加类型提示,但这些类型在运行时并不是强制的。下面的代码运行时没有任何问题:
>>>Position(3.14,'piday',2018) Position(name=3.14,lon='piday',lat=2018)
这就是Python进行输入通常的工作方式:Python现在是,将来也永远是一种动态类型语言。要实际捕获类型错误,可以在你的代码中运行Mypy之类的类型检查器。
2.3添加方法
前边已经提到,dataclass类也只是一个普通类。这意味着你可以自由地将自己的方法添加到dataclass类中。举个例子,让我们计算一个位置与另一个位置之间沿地球表面的距离。一种方法是使用hasrsine公式:
你可以像使用普通类一样将distance_to()方法添加到数据类中:
fromdataclassesimportdataclass frommathimportasin,cos,radians,sin,sqrt @dataclass classPosition: name:str lon:float=0.0 lat:float=0.0 defdistance_to(self,other): r=6371#Earthradiusinkilometers 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) return2*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示例,并且添加一个包含一副纸牌的类:
fromdataclassesimportdataclass fromtypingimportList @dataclass classPlayingCard: rank:str suit:str @dataclass classDeck: 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)。然后,添加一个方法makefrenchdeck(),该方法创建PlayingCard的实例列表:
RANKS='2345678910JQKA'.split() SUITS='♣♢♡♠'.split() defmake_french_deck(): return[PlayingCard(r,s)forsinSUITSforrinRANKS]
这里为了直观展示,使用了Unicode符号指定了四种不同的花色。
注意:上面,我们在源代码中直接使用了像♠这样的Unicode字形。我们能这样做,是因为Python支持默认以UTF-8编写源代码。有关如何在你的系统中输入这些内容的信息,请参阅:Unicodeinput。你还可以使用\N命名字符转义(如\N{BLACKSPADESUIT})或\uUnicode转义(如\u2660)为花色输入Unicode符号。
为了以后简化纸牌的比较,也按通常的顺序列出了数字和花色。
>>>make_french_deck() [PlayingCard(rank='2',suit='♣'),PlayingCard(rank='3',suit='♣'),... PlayingCard(rank='K',suit='♠'),PlayingCard(rank='A',suit='♠')]
理论上,现在可以使用这个方法为Deck.cards指定一个默认值:
fromdataclassesimportdataclass fromtypingimportList @dataclass classDeck:#WillNOTwork cards:List[PlayingCard]=make_french_deck()
不要这样做!这引入了Python中最常见的反模式之一:使用可变的默认参数。
问题在于,Deck的所有实例都将使用相同的list对象作为cards属性的默认值。这意味着,如果一张牌从一副牌中被移走,那么它也将从牌的所有其他实例中消失。实际上,dataclass类也会阻止你这样做,上面的代码将引发ValueError。
相反,dataclass类使用称为default_factory的东西来处理可变的默认值。要使用default_factory(以及dataclass类的许多其他很酷的功能),你需要使用field()说明符:
fromdataclassesimportdataclass,field fromtypingimportList @dataclass classDeck: cards:List[PlayingCard]=field(default_factory=make_french_deck)
default_factory的参数可以是任何可调参数的零参数。现在很容易就可以创建一副完整的扑克牌:
>>>Deck() Deck(cards=[PlayingCard(rank='2',suit='♣'),PlayingCard(rank='3',suit='♣'),... PlayingCard(rank='K',suit='♠'),PlayingCard(rank='A',suit='♠')])
field()说明符用于单独自定义dataclass类的每个字段。后面你还会看到其他一些示例。下面有一些field()支持的参数,可以供你作为参考:
default:字段的默认值
default_factory:该函数返回字段的初始值
init:是否在__init__()方法中使用字段(默认为True。)
repr:是否在对象的repr中使用字段(默认为True。)
compare:是否在比较时包含这个字段(默认为True。)
hash:在计算hash()时是否包含该字段(默认值是使用与比较相同的值)
metadata:包含有关该字段的信息的映射
在上边的Position示例中,你了解了如何通过编写lat:float=0.0来添加简单的默认值。但是,如果你还想自定义字段,例如将其隐藏在repr中,则需要使用默认参数:lat:float=field(default=0.0,repr=False)。
你不能同时指定default和default_factory。参数metadata不被dataclass类本身使用,但是你(或第三方包)可以将信息附加到字段中。例如,在Position示例中,你可以指定纬度和经度应该用度数表示。
fromdataclassesimportdataclass,field @dataclass classPosition: name:str lon:float=field(default=0.0,metadata={'unit':'degrees'}) lat:float=field(default=0.0,metadata={'unit':'degrees'})
可以使用fields()函数检索metadata(以及关于字段的其他信息,注意field是复数)。
>>>fromdataclassesimportfields >>>fields(Position) (Field(name='name',type=,...,metadata={}), Field(name='lon',type= ,...,metadata={'unit':'degrees'}), Field(name='lat',type= ,...,metadata={'unit':'degrees'})) >>>lat_unit=fields(Position)[2].metadata['unit'] >>>lat_unit 'degrees'
3.2更好的表示方式
回想一下,我们可以使用下边的代码创造出一副纸牌:
>>>Deck() Deck(cards=[PlayingCard(rank='2',suit='♣'),PlayingCard(rank='3',suit='♣'),... PlayingCard(rank='K',suit='♠'),PlayingCard(rank='A',suit='♠')])
尽管Deck的这种表示形式是显式的、可读的,但它也非常冗长。(在上面的输出中,我已经删除了52张牌中的48张。如果在80列显示器上,只打印完整的Deck就占用22行!)
让我们来一个更简洁的表示。通常,Python对象有两种不同的字符串表示形式:
repr(obj)由obj.__repr__()定义,并且应该返回对开发人员友好的obj表示。如果可能,这应该是可以重新创建obj的代码。dataclass类就是这样做的。
str(obj)由obj.__str__()定义,并且应该返回一个对用户友好的obj表示。dataclass类不实现__str__()方法,因此Python将返回到__repr__()方法。
让我们实现一个对用户友好的PlayCard表示:
fromdataclassesimportdataclass @dataclass classPlayingCard: rank:str suit:str def__str__(self): returnf'{self.suit}{self.rank}'
现在看起来好多了,但是还和以前一样冗长:
>>>ace_of_spades=PlayingCard('A','♠') >>>ace_of_spades PlayingCard(rank='A',suit='♠') >>>print(ace_of_spades) ♠A >>>print(Deck()) Deck(cards=[PlayingCard(rank='2',suit='♣'),PlayingCard(rank='3',suit='♣'),... PlayingCard(rank='K',suit='♠'),PlayingCard(rank='A',suit='♠')])
为了表示你可以添加你自己的__repr__()方法。同样,我们也违反了它应该返回能够重新创建对象的代码的原则。毕竟,实用性胜过简洁。以下代码添加了更简洁的Deck表示:
fromdataclassesimportdataclass,field fromtypingimportList @dataclass classDeck: cards:List[PlayingCard]=field(default_factory=make_french_deck) def__repr__(self): cards=','.join(f'{c!s}'forcinself.cards) returnf'{self.__class__.__name__}({cards})'
请注意这里的{c!s}格式字符串中的!s说明符。这意味着我们要显式地使用每个PlayingCard的str()表示。用新的__repr__(),Deck的表示更容易看懂:
>>>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)
3.3比较Cards
在许多纸牌游戏中,纸牌是相互比较的。例如,在一个典型的取牌游戏中,最高的牌取牌。目前实现的那样,PlayingCard类不支持这种比较,如下:
>>>queen_of_hearts=PlayingCard('Q','♡') >>>ace_of_spades=PlayingCard('A','♠') >>>ace_of_spades>queen_of_hearts TypeError:'>'notsupportedbetweeninstancesof'Card'and'Card'
然而,这(似乎)很容易纠正:
fromdataclassesimportdataclass @dataclass(order=True) classPlayingCard: rank:str suit:str def__str__(self): returnf'{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','♡') >>>ace_of_spades=PlayingCard('A','♠') >>>ace_of_spades>queen_of_hearts False
那么,这两张牌是如何比较的呢?这里还没有说明应该如何进行排序,就有了结果?由于某些原因,Python似乎认为Queen应该大于Ace。事实证明,dataclass类比较对象时就好像它们是字段的元组一样。换句话说,之所以Queen比Ace大,是因为在字母表中,Q出现A的后面。
>>>('A','♠')>('Q','♡') False
这对我们来说并不适用。相反,我们需要定义某种使用RANKS和SUITS顺序的排序索引。类似下面:
>>>RANKS='2345678910JQKA'.split() >>>SUITS='♣♢♡♠'.split() >>>card=PlayingCard('Q','♡') >>>RANKS.index(card.rank)*len(SUITS)+SUITS.index(card.suit) 42
要让PlayingCard使用此排序索引进行比较,我们需要在类中添加一个sort_index字段。但是,此字段应自动从其他字段rank和suit计算。这正是特殊方法__post_init__()的用途。它允许在调用__init__()方法后进行特殊处理:
fromdataclassesimportdataclass,field RANKS='2345678910JQKA'.split() SUITS='♣♢♡♠'.split() @dataclass(order=True) classPlayingCard: sort_index:int=field(init=False,repr=False) rank:str suit:str def__post_init__(self): self.sort_index=(RANKS.index(self.rank)*len(SUITS) +SUITS.index(self.suit)) def__str__(self): returnf'{self.suit}{self.rank}'
注意:sort_index作为类的第一个字段添加。这样,才能首先使用sort_index进行比较,并且只有在还有其他字段的情况时才能生效。使用field(),还必须指定sort_index不应作为参数包含在__init__()方法中(因为它是根据rank和suit字段计算的)。为避免让使用者对此实现细节感到困惑,从类的repr中删除sort_index可能也是个好主意。
>>>queen_of_hearts=PlayingCard('Q','♡') >>>ace_of_spades=PlayingCard('A','♠') >>>ace_of_spades>queen_of_hearts True
现在你可以轻松地创建一个排序的牌组了:
>>>Deck(sorted(make_french_deck())) Deck(♣2,♢2,♡2,♠2,♣3,♢3,♡3,♠3,♣4,♢4,♡4,♠4,♣5, ♢5,♡5,♠5,♣6,♢6,♡6,♠6,♣7,♢7,♡7,♠7,♣8,♢8, ♡8,♠8,♣9,♢9,♡9,♠9,♣10,♢10,♡10,♠10,♣J,♢J,♡J, ♠J,♣Q,♢Q,♡Q,♠Q,♣K,♢K,♡K,♠K,♣A,♢A,♡A,♠A)
或者,如果你不关心排序,下面介绍了如何随机抽取10张牌:
>>>fromrandomimportsample >>>Deck(sample(make_french_deck(),k=10)) Deck(♢2,♡A,♢10,♣2,♢3,♠3,♢A,♠8,♠9,♠2)
当然,此处你不需要配置order=True。
4.不可变的dataclass
前面看到的namedtuple的定义特性之一是:它是不可变的。也就是说,它的字段的值可能永远不会改变。对于许多类型的dataclass,这是一个好主意!要使dataclass不可变,请在创建时设置frozen=True。比如,下面是你前面看到的Position类的不可变版本:
fromdataclassesimportdataclass @dataclass(frozen=True) classPosition: 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:cannotassigntofield'name'
但是要注意,如果你的数据类包含可变字段,这些字段可能仍然会更改。这适用于Python中的所有嵌套数据结构。
fromdataclassesimportdataclass fromtypingimportList @dataclass(frozen=True) classImmutableCard: rank:str suit:str @dataclass(frozen=True) classImmutableDeck: cards:List[PlayingCard]
尽管ImmutableCard和ImmutableDeck都是不可变的,但是包含Card的列表并不是不可变的。因此你仍然可以换牌。
>>>queen_of_hearts=ImmutableCard('Q','♡') >>>ace_of_spades=ImmutableCard('A','♠') >>>deck=ImmutableDeck([queen_of_hearts,ace_of_spades]) >>>deck ImmutableDeck(cards=[ImmutableCard(rank='Q',suit='♡'),ImmutableCard(rank='A',suit='♠')]) >>>deck.cards[0]=ImmutableCard('7','♢') >>>deck ImmutableDeck(cards=[ImmutableCard(rank='7',suit='♢'),ImmutableCard(rank='A',suit='♠')])
要避免这种情况,请确保不可变dataclass类的所有字段都使用不可变类型(但请记住,在运行时不强制执行类型)。应该使用元组而不是列表来实现ImmutableDeck。
5.继承
你可以非常自由地子类化dataclass类。例如,我们将使用country字段继承Position示例并使用它来记录国家名称:
fromdataclassesimportdataclass @dataclass classPosition: name:str lon:float lat:float @dataclass classCapital(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)后边。如果基类中的任何字段具有默认值,事情会变得复杂一些:
fromdataclassesimportdataclass @dataclass classPosition: name:str lon:float=0.0 lat:float=0.0 @dataclass classCapital(Position): country:str#DoesNOTwork
上边这段代码将立即崩溃,并报一个TypeError:"non-defaultargument‘country'followsdefaultargument."问题是:我们的新字段:country没有默认值,而lon和lat字段有默认值。dataclass类将尝试编写一个像下面一样的__init__()方法:
def__init__(name:str,lon:float=0.0,lat:float=0.0,country:str): ...
然而,这不是可行的。如果参数具有默认值,则后边的所有参数也必须具有默认值。换句话说,如果基类中的字段具有默认值,那么子类中添加的所有新字段也必须具有默认值。
另一件需要注意的是字段在子类中的排序方式。从基类开始,字段按照首次定义的顺序排序。如果在子类中重新定义字段,则其顺序不会更改。例如,如果你按如下方式定义Position和Capital:
fromdataclassesimportdataclass @dataclass classPosition: name:str lon:float=0.0 lat:float=0.0 @dataclass classCapital(Position): country:str='Unknown' lat:float=40.0
Capital中字段的顺序仍然是namelonlatcountry。但是,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类。(他们真的只是普通的类!)
fromdataclassesimportdataclass @dataclass classSimplePosition: name:str lon:float lat:float @dataclass classSlotPosition: __slots__=['name','lon','lat'] name:str lon:float lat:float
本质上,Slot是用__slots__在类中定义,并列出了变量。对于不在__slots__的变量或属性,将不会被定义。此外,Slot类可能没有默认值。
添加这些限制的好处是可以进行某些优化。例如,Slot类占用的内存更少,这个可以使用Pympler进行测试:
>>>frompymplerimportasizeof >>>simple=SimplePosition('London',-0.1,51.5) >>>slot=SlotPosition('Madrid',-3.7,40.4) >>>asizeof.asizesof(simple,slot) (440,248)
同样,Slot类通常处理起来更快。下面的示例中,使用标准库中的timeit测试了slotsdataclass类和常规dataclass类上的属性访问速度。
>>>fromtimeitimporttimeit >>>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.总结及进一步阅读
dataclass类是Python3.7的新特性之一。使用DataClass类,你不必编写样板代码来为对象获得适当的初始化、表示和比较。
你已经了解了如何定义自己的dataclass类,以及:
dataclass
dataclass
dataclass
dataclass
如果你还想深入了解dataclass类的所有细节,请查看PEP557以及GitHubrepo中的讨论。
总结
以上所述是小编给大家介绍的Python3.7新特性之dataclass装饰器,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!