一、引入数据类
形容一个人的信息,例如,姓名、年龄、性别、地址等。用什么数据类型来实现。
可以是字典,也可以是元组。
# 字典
user= {'name': '张三', 'age': 30, 'address':'china beijing'}
# 元组
userTuple=('张三',30,'china beijing')
也可以使用类
class User:
def __init__(self,name,age,address):
self.name=name
self.age=age
self.address=address
不管使用哪种方式,都感觉有太多样板代码的感觉。对于类而言,在__init__
方法中编写的代码,当属性比较多的时候,就比较麻烦了。
如果这个类,仅仅只是这些字段的容器,几乎没有额外的功能,那写这么多样板代码就有一些不合适了。
此时,就有了数据类的概念。
二、典型的具名元组
collections.namedtuple
是一个工厂函数,用于构建增强的tuple
子类,具有字段名称、类名和提供有用信息的__repr__
方法。namedtuple
构建的类可在任何需要元组的地方使用。其实,为了方便,以前 Python 标准库中返回元组的很多函数,现在都返回具名元组,这对用户的代码没有任何影响。namedtuple
构建的类,其实例占用的内存量与元组相同,因为字段名称存储在类中。
# 定义并使用一个具名元组类型
from collections import namedtuple
# 创建具名元组需要指定两个参数:一个类名和一个字段名称列表。后一个参数可以是产生字符串的可迭代对象,也可以是一整个以空格分隔的字符串
User = namedtuple('User', 'name age address love')
# 字段的值必须以单个位置参数传给构造函数(而 tuple 构造函数接受单个可迭代对象)
zs = User('zhangsan', 30, 'beijing', ('篮球', '羽毛球'))
print(zs) # User(name='zhangsan', age=30, address='beijing', love=('篮球', '羽毛球'))
# 可以通过名称或位置访问字段。
print(zs.name) # zhangsan
作为 tuple 的子类,User 继承了一些有用的方法,例如__eq__
,以及比较运算符背后的特殊方法__lt__
等,可用于排序 User 实例构成的列表。
除了从tuple
继 承,具名元组还有几个额外的属性和方法。下面代码 演示了几个最有用的属性和方法:类属性_fields
、类方法 _make(iterable)
和实例方法 _asdict()
。
# 定义并使用一个具名元组类型
from collections import namedtuple
# 创建具名元组需要指定两个参数:一个类名和一个字段名称列表。后一个参数可以是产生字符串的可迭代对象,也可以是一整个以空格分隔的字符串
User = namedtuple('User', 'name age address love')
# 字段的值必须以单个位置参数传给构造函数(而 tuple 构造函数接受单个可迭代对象)
zs = User('zhangsan', 30, 'beijing', ('篮球', '羽毛球'))
# ._fields 属性的值是一个元组,存储类的字段名称。
print(User._fields) # ('name', 'age', 'address', 'love')
lisi_data = ('lisi', 20, 'nanjing', ('足球', '羽毛球'))
# ._make() 方法根据可迭代对象构建 User 实例,与 User(*lisi_data) 作用相同
lisi = User._make(lisi_data)
# ._asdict() 方法返回根据具名元组实例构建的 dict 对象
print(lisi._asdict()) # {'name': 'lisi', 'age': 20, 'address': 'nanjing', 'love': ('足球', '羽毛球')}
import json
# ._asdict() 方法可把数据序列化成 JSON 格式。
print(json.dumps(
lisi._asdict())) # {"name": "lisi", "age": 20, "address": "nanjing", "love": ["u8db3u7403", "u7fbdu6bdbu7403"]}
在 Python 3.7 之前,asdict 方法返回一个 OrderedDict 对象。从 Python 3.8 开始,它返回一个简单的 dict 对象,因为现在键的插入顺序得以保留,所以影响不大。如果你还想得OrderedDict 对象,_asdict 文档建议根据返回结果自行构建,即OrderedDict(x._asdict())
。 从 Python 3.7 开始,namedtuple 接受 defaults 关键字参数,值为一个产生 _N 项的可迭代对象,为从右数的 N 个字段指定默认值。示例 5-6 定义具名元组 Coordinate,为 reference 字段指定默认值。
例如:构建一个具名元组,为字段指定默认值
from collections import namedtuple
# defaults中的列表,从右到左,为字段进行附魔认知
User = namedtuple('User', 'name age address love', defaults=['上海', ('football', 'pingpang')])
ww = User('wangwu', 11)
print(ww) # User(name='wangwu', age=11, address='上海', love=('football', 'pingpang'))
具名元组也能用于增加方法,只是过程有点曲折。
# 定义并使用一个具名元组类型
from collections import namedtuple
# 定义一个具名元组
User = namedtuple('User', 'name age address')
# 定义一个方法,第一个参数不必命名为 self,但是调用时指代的就是接收方
def show_info(user):
return user.name + " " + user.address
# 把方法依附到User类型上
User.showInfo = show_info
# 实现一个对象,并调用依附的方法,这样就成功了
zs = User('zhangsan', 20, 'beijing')
print(zs.showInfo())
三、带类型的具名元组
from typing import NamedTuple
# 定义一个类,父类为NamedTuple
class User(NamedTuple):
# 每个实例字段都要注解类型
name: str
age: int
address: str
# 最后一个可以写默认值
love: tuple = ('okok')
zs = User('zhangsan', 20, 'beijing', ('football', 'baskball'))
print(zs) # User(name='zhangsan', age=20, address='beijing', love=('football', 'baskball'))
lisi = User('lisi', 20, 'nanjing')
print(lisi) # User(name='lisi', age=20, address='nanjing', love='okok')
使用typing.NamedTuple
构建的类,拥有的方法并不比collections.namedtuple
生成的更多,而且同样也从tuple
继承方法。唯一的区别是多了类属性__annotations__
,而在运行时,Python 完全忽略该属性。
四、类型提示入门
类型提示(也叫类型注解)声明函数参数、返回值、变量和属性的预期类型。
关于类型提示,首先你要知道,Python 字节码编译器和解释器根本不强制你提供类型信息。
4.1 运行时没有作用
Python 类型提示可以看作“供 IDE 和类型检查工具验证类型的文档”。 这是因为,类型提示对 Python 程序的运行时行为没有影响。参考如下代码
import typing
class User(typing.NamedTuple):
name: str
age: int
zs = User("hello", ('zs', 'lisi'))
print(zs) # User(name='hello', age=('zs', 'lisi')) 看到这个输出,就说明,运行时,是不检查类型的
类型提示主要为第三方类型检查工具提供支持,例如 Mypy 和 PyCharm IDE 内置的类型检查器。这些是静态分析工具,在“静止”状态下检查 Python 源码,不运行代码。
4.2 变量注解句法
typing.NamedTuple
和@dataclass
使用 PEP 526 定义的句法注解变量。本节简要介绍在 class 语句中定义属性的注解句法。
变量注解的基本句法如下所示。
var_name: some_type
允许使用的类型在 PEP 484 中的“Acceptable type hints”一节规定,不过定义数据类时,最常使用以下类型。
-
一个具体类,例如
str
或FrenchDeck
。 -
一个参数化容器类型,例如
list[int]
、tuple[str, float]
等。 -
typing.Optional
,例如Optional[str]
,声明一个字段的类型可以是str
或None
。
另外,还可以为变量指定初始值。在typing.NamedTuple
和@dataclass
声明中,指定的初始值作为属性的默认值,防止调用构造函数时没有提供对应的参数。
var_name: some_type = a_value
4.3 变量注解的意义
类型提示在运行时没有作用。然而,Python 在导入时(加载模块时)会读取类型提示,构建__annotations__
字典,供typing.NamedTuple
和@dataclass
使用,增强类的功能。
class DemoPlainClass:
# a 出现在 __annotations__ 中,但被抛弃了,因为该类没有名为 a 的属性
a: int
# b 作为注解记录在案,而且是一个类属性,值为 1.1。
b: float = 1.1
# ❸ c 是普通的类属性,没有注解
c = 'spam'
print(DemoPlainClass.__annotations__) # {'a': <class 'int'>, 'b': <class 'float'>}
# 下面代码会报错 AttributeError: type object 'DemoPlainClass' has no attribute 'a'
# print(DemoPlainClass.a)
print(DemoPlainClass.b) # 1.1
print(DemoPlainClass.c) # spam
注意,特殊属性__annotations__
由解释器创建,记录源码中出现的类型提示,即使是普通的类。 a
只作为注解存在,不是类属性,因为没有绑定值。 b
和 c
存储为类属性,因为它们绑定了值。
这 3 个属性都不出现在DemoPlainClass
的实例中。使用 o = DemoPlainClass()
创建一个对象,o.a 抛出 AttributeError,而 o.b 和 o.c 检索类属性,值分别为 1.1 和 ‘spam’,行为与常规的 Python 对象相同。
研究一个 typing.NamedTuple 类
现在来研究一个使用 typing.NamedTuple 构建的类
import typing
class DemoNTClass(typing.NamedTuple):
# a 是注解,也是实例属性
a: int
# 同样,b 是注解,也是实例属性,默认值为 1.1。
b: float = 1.1
# c 是普通的类属性,没有注解
c = 'spam'
print(DemoNTClass.__annotations__) # {'a': <class 'int'>, 'b': <class 'float'>}
# 下面代码会报错 AttributeError: type object 'DemoPlainClass' has no attribute 'a'
print(DemoNTClass.a) # _tuplegetter(0, 'Alias for field number 0')
print(DemoNTClass.b) # _tuplegetter(1, 'Alias for field number 1')
print(DemoNTClass.c) # spam
可以看到,a 和 b 的注解与示例 5-10 一样。但是,typing.NamedTuple 创建了类属性 a 和 b。c 是
普通的类属性,值为 ‘spam’。类属性 a 和 b 是描述符。这是高级功能。现在可以把描述符理解为特性
(property)读值(getter)方法,即不带调用运算符()
的方法,用于读取实例属性。实际上,这意味着 a 和 b 是只读实例属性。这一点不难理解,因为 DemoNTClass 实例是某种高级的元组,而元组是不可变的。
研究一个使用 dataclass 装饰的类
from dataclasses import dataclass
@dataclass
class DemoDataClass:
# a 是注解,也是受描述符控制的实例属性。
a: int
# 同样b 是注解,也是受描述符控制的实例属性,默认值为 1.1
b: float = 1.1
# c 是普通的类属性,没有注解
c = 'spam'
print(DemoDataClass.__annotations__) # {'a': <class 'int'>, 'b': <class 'float'>}
# 下面代码会报错 AttributeError: type object 'DemoDataClass' has no attribute 'a'
# print(DemoDataClass.a)
print(DemoDataClass.b) # 1.1
print(DemoDataClass.c) # spam
print(DemoDataClass.__doc__) # DemoDataClass(a: int, b: float = 1.1)
__annotations__
和 __doc__
没什么让人意外的。然而,DemoDataClass 没有名为 a 的属性。
相比之下,上面例子中 DemoNTClass 有可从实例中获取只读属性 a 的描述符(那个神秘的<_collections._tuplegetter>
)。这是因为,a 属性只在 DemoDataClass 实例中存在。如果冻结 DemoDataClass 类,那么 a 就变成可获取和设定的公开属性。但是,b 和 c 作为类属性存在,b 存储实例属性 b 的默认值,而 c 本身就是类属性,不绑定到实例上。
下面来看 DemoDataClass 实例的情况。
from dataclasses import dataclass
@dataclass
class DemoDataClass:
# a 是注解,也是受描述符控制的实例属性。
a: int
# 同样b 是注解,也是受描述符控制的实例属性,默认值为 1.1
b: float = 1.1
# c 是普通的类属性,没有注解
c = 'spam'
dc = DemoDataClass(9)
print(dc.a) # 9
print(dc.b) # 1.1
print(dc.c) # spam
同样,a 和 b 是实例属性,而 c 是通过实例获取的类属性。
前文说过,DemoDataClass 实例是可变的,而且运行时不检查类型。
dc.a = 10
dc.b = "china"
甚至还可以为不存在的属性赋值。
dc.c='hello'
dc.z='no error'
# 这样都不报错
现在,dc 实例有 c 属性,这对类属性 c 没有影响。我们还可以新增一个 z 属性。这是 Python 正常的行为:常规实例自身可以有未出现在类中的属性。
五、`@dataclass` 详解
@dataclass
这个装饰器接受多个关键字参数,完整签名如下
@dataclass(*, init=True, repr=True, eq=True, order=False,unsafe_hash=False, frozen=False)
第一个参数位置上的*
表示后面都是关键字参数。下面简要说明这些关键字参数。
以上参数的默认值适用于多数情况。不过,你可能会更改以下参数的值,不使用默认值。
-
frozen=True 防止意外更改类的实例。
-
order=True 允许排序数据类的实例。
Python 对象是动态的,只要愿意,程序员还是可以绕过frozen=True
这道防线。不过,在代码评审阶段很容易发现这种小伎俩。
如果 eq
和 frozen
参数的值都是True
,那么 @dataclass
将生成一个合适的__hash__
方法,确保实例是可哈希的。生成的·hash方法使用所有字段的数据,通过字段选项(见 5.6.1 节)也不能排除。对于frozen=False
(默认值),@dataclass
把__hash__
设为None
,覆盖从任何超类继承的__hash__
方法,表明实例不可哈希。
5.1 字段选项
我们已经见过最基本的字段选项,即在提供类型提示的同时设定默认值。声明的字段将作为参数传给生成的__init__
方法。Python 规定,带默认值的参数后面不能有不带默认值的参数。因此,为一个字段声明默认值之后,余下的字段都要有默认值。
对初级 Python 开发人员来说,可变的默认值往往导致 bug。如果在函数定义中使用可变默认值,调用函数时很容易破坏默认值,则导致后续调用的行为发生变化。类属性通常用作实例属性的默认值,数据类也是如此。@dataclass
使用类型提示中的默认值生成传给__init__
方法的参数默认值。为了避免 bug,@dataclass
拒绝像下面代码这样定义类。
from dataclasses import dataclass
@dataclass
class ClubMember:
name: str
guests: list = [] # ValueError: mutable default <class 'list'> for field guests is not allowed:
ValueError 消息指出了问题所在,还提供了一个解决方案:使用default_factory
。下面代码给出
了纠正 ClubMember 的方法。
from dataclasses import dataclass, field
@dataclass
class ClubMember:
name: str
guests: list = field(default_factory=list)
上面代码中,guests
字段的默认值不是一个列表字面量,而是调用dataclasses.field
函数,把参数设为default_factory=list
,以此设定默认值。 default_factory
参数的值可以是一个函数、一个类,或者其他可调用对象,在每次创建数据类的实例时调用(不带参数),构建默认值。这样,每个 ClubMember 实例都有自己的一个 list,而不是所有实例共用同一个 list。
浏览 dataclasses 模块文档,你会发现有一个 list 字段使用的句法比较新奇,
from dataclasses import dataclass, field
@dataclass
class ClubMember:
name: str
# list[str] 的意思是由字符串构成的列表
guests: list[str] = field(default_factory=list)
新句法 list[str]
是一种参数化泛型。从 Python 3.9 开始,内置类型 list 可以使用方括号表示法指定列表中项的类型。
这两种声明方式是有区别的,guests: list
表示 guests 列表可以由任何类型的对象构成,而 guests: list[str]
的意思是 guests 列表中的每一项都必须是字符串。因此,如果在列表中存储无效的项,或者读取到无效的项,则类型检查工具将报错。 default_factory
应该是 field 函数最常使用的参数,不过除此之外还有其他参数可用
之所以有default
参数,是因为在字段注解中设置默认值的位置被field
函数调用占据了。假如我们想创建一个athlete
字段,把默认值设为False
,而且不提供给 __repr__
方法使用,那么要像下面这样编写。
from dataclasses import dataclass, field
@dataclass
class ClubMember:
name: str
guests: list = field(default_factory=list)
athlete: bool = field(default=False, repr=False)
5.2 初始化后处理
@dataclass
生成的__init__
方法只做一件事:把传入的参数及其默认值(如未指定值)赋值给实例属性,变成实例字段。可是,有些时候初始化实例要做的不只是这些。为此,可以提供一个__post_init__
方法。如果存在这个方法,则@dataclass
将在生成的__init__
方法最后调用__post_init__
方法。 __post_init__
经常用于执行验证,以及根据其他字段计算一个字段的值。
5.3 带类型的类属性
为了让类型检查工具和@dataclass
满意,应当像下面这样声明 all_handles。
all_handles: ClassVar[set[str]] = set()
这里,类型提示的意思如下: all_handles
是一个类属性,类型为字符串构成的集合,默认值是一个空集合。
编写这个注解之前,必须从typing
模块中导入ClassVar
。 @dataclass
装饰器不关心注解中的类型,但有两种例外情况,这是其中之一,即类型为 ClassVar 时,
不为属性生成实例字段。
5.4 初始化不作为字段的变量
有时,我们需要把不作为实例字段的参数传给__init__
方法。按照dataclasses
文档的说法,这种参数叫“仅作初始化的变量”(init-only variable)。为了声明这种参数,dataclasses 模块提供了伪类型InitVar
,句法与 typing.ClassVar
一样。文档中给出的例子定义一个数据类,包含一个使用数据库初始化的字段,因此必须把数据库对象传给构造方法。
from dataclasses import dataclass, field, InitVar
@dataclass
class C:
i: int
j: int = None
database: InitVar[DatabaseType] = None
def __post_init__(self, database):
if self.j is None and database is not None:
self.j = database.lookup('j')
c = C(10, database=my_database)
注意 database 属性的声明方式。InitVar
阻止 @dataclass
把 database
视为常规的字
段。database
不会被设为实例属性,也不会出现在dataclasses.fields
函数返回的列表中。然而,对于生成的__init__
方法,database 是参数之一,同时也传给 __post_init__
方法。如果你想自己编写__post_init__
方法,那就必须像上面代码那样,在方法签名中增加相应的参数。
六、模式匹配类实例
类模式通过类型和属性(可选)匹配类实例。类模式的匹配对象可以是任何类的实例,而不仅仅是数据类的实例。
类模式有 3 种变体:简单类模式、关键字类模式和位置类模式。
6.1 简单类模式
case [str(name), _, _, (float(lat), float(lon))]:
那个模式匹配项数为 4 的序列,第一项必须是 str 实例,最后一项必须是二元组,两项均为 float 实例。 类模式的句法看起来与构造函数调用差不多。下面的类模式匹配 float 值,未绑定变量(在 case 主体中,如果需要可以直接引用 x)。
match x:
case float():
do_something_with(x)
但是,像下面这样做可能导致 bug。
match x:
case float: # 危险!!!
do_something_with(x)
这里,case float: 可以匹配任何对象,因为 Python 把 float 看作匹配对象绑定的变量。
float(x) 这种简单模式句法只适用于 9 种内置类型(在“PEP 634—Structural Pattern Matching: Specification”中“Class Patterns”一节的末尾列出)。
bytes dict float frozenset int list set str tuple
对这些类来说,看上去像构造函数的参数的那个变量,例如 float(x) 中的 x,绑定整个匹配的实例。如果是子模式,则绑定匹配对象的一部分,例如前例中序列模式内的 str(name)。
case [str(name), _, _, (float(lat), float(lon))]:
除 9 种内置类型之外,看上去像参数的那个变量表示模式匹配的类实例的属性。
6.2 关键字类模式
import typing
class City(typing.NamedTuple):
continent: str
name: str
country: str
cities = [
City('Asia', 'Tokyo', 'JP'),
City('Asia', 'Delhi', 'IN'),
City('North America', 'Mexico City', 'MX'),
City('North America', 'New York', 'US'),
City('South America', 'São Paulo', 'BR'),
]
那么,以下函数返回的列表中都是位于亚洲的城市。
def match_asian_cities():
results = []
for city in cities:
match city:
case City(continent='Asia'):
results.append(city)
return results
与前面一样,City(continent=’Asia’, country=cc) 也匹配位于亚洲的城市,不过现在把变量 cc绑定到了实例的 country 属性上。模式变量叫 country 也没关系。
match city:
case City(continent='Asia', country=country):
results.append(country)
关键字类模式的可读性非常高,适用于任何有公开的实例属性的类,不过有点烦琐。
有时候,使用位置类模式更方便,不过匹配对象所属的类要显式支持。
6.3 位置类模式
以下函数使用位置类模式获取亚洲城市列表
def match_asian_cities_pos():
results = []
for city in cities:
match city:
case City('Asia'):
results.append(city)
return results
City(‘Asia’) 匹配的 City 实例,第一个属性的值是 ‘Asia’,其他属性的值不考虑。如果你想收集 country 属性的值,可以像下面这样写。
def match_asian_countries_pos():
results = []
for city in cities:
match city:
case City('Asia', _, country):
results.append(country)
return results
与前面一样,City(‘Asia’, _, country) 也匹配位于亚洲的城市,不过现在把变量 country 绑定到了实例的第三个属性上。
可是,“第一个属性”和“第三个属性”是什么意思呢?
City 或其他类若想使用位置模式,要有一个名为__match_args__
的特殊类属性。本章讲到的类构建器会自动创建这个属性。对于 City 类,__match_args__
属性的值如下所示。
City.__match_args__ # ('continent', 'name', 'country')
可以看到,位置模式中属性的顺序就是__match_args__
声明的顺序。
注意:
一个模式可以同时使用关键字参数和位置参数。
__match_args__
列出的是可供匹配的实例属性,不是全部属性。因此,有时候除了位置参数之外可能还需要使用关键字参数。
原文始发于微信公众号(Python之家):Python深入-5-数据类
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/198407.html