dataclasses(数据类)模块
python通过dataclasses模块提供了dataclass(数据类)对象,适合我们想定义一些类,并且让他们主要用于存放数据。
dataclass: 是一个函数,用做装饰器,把一个类变成数据类。 数据类可以让我们通过简单的方法定义实例属性以及对其赋值,并使用类型提示标明其类型。 通过一些元类的定制化,数据类会自动生成__init__方法,并将类体中定义的属性转变成实例属性。
简单例子如下:
from dataclasses import dataclass
@dataclass
class InventoryItem:
"""Class for keeping track of an item in inventory."""
name: str
unit_price: float
quantity_on_hand: int = 0
def total_cost(self) -> float:
return self.unit_price * self.quantity_on_hand
bread_inv = InventoryItem("面包", 5, 3)
milk_inv = InventoryItem("牛奶", 4, 2)
print(bread_inv)
print('面包 total cost:', bread_inv.total_cost())
print('-' * 25, '分隔线', '-' * 25)
print(milk_inv)
print('牛奶 total cost:', milk_inv.total_cost())
输出结果:
InventoryItem(name='面包', unit_price=5, quantity_on_hand=3)
面包 total cost: 15
------------------------- 分隔线 -------------------------
InventoryItem(name='牛奶', unit_price=4, quantity_on_hand=2)
牛奶 total cost: 8
前面dataclass中定义的属性都会被转成实例属性。 而这些实例属性会被保存为dataclasses.Field对象。
例如,我们用实例对象访问这些属性是可以的,但用类去访问属性则会报错:
print(bread_inv.name) # 面包
print(InventoryItem.name) # 报错: AttributeError: type object 'InventoryItem' has no attribute 'name'
dataclasses.field: 用于定义Filed对象一些其他属性。
正常我们如果不用这个filed函数,定义的Filed就只能简单加个类型提示,以及赋个初值。 但使用filed函数,我们不但可以赋初值,还可以控制其是否放传递给__init__方法进行初始化,以及是否显示在__repr__方法的输出结果中。
例如,下面dataclass使用field定义了两个属性,分别使用了不同的init和repr参数值。
from dataclasses import dataclass, field
@dataclass
class Foo:
name: str
age: int
gender: bool = field(default='man',init=False,repr=True)
score: float = field(default=95, init=True,repr=False)
foo = Foo('Roland', 40, 100)
print(foo) # Foo(name='Roland', age=40, gender='man')
print(foo.score) # 100
KW_ONLY: 用于定义属性时做为位置参数与关键字参数的分隔点。也就是在内部执行__init__实例化方法时传参的方式。 KW_ONLY对应的属性名没有具体意义及用途。
from dataclasses import dataclass, KW_ONLY
@dataclass
class Point:
x: float
_: KW_ONLY
y: float
z: float
p = Point(0, y=1.5, z=2.0)
# p2 = Point(0, 1.5, 2.0) # 会报错 TypeError: Point.__init__() takes 2 positional arguments but 4 were given
类变量(Class variables): 通过类型提示typing.ClassVar来标记该变量不是一个Field,而是类属性。
dataclass中的类属性要在定义时就赋初值,要么干脆不定义,后是使用普通的类.属性的方式对其直接赋值。
from dataclasses import dataclass, fields
from typing import ClassVar
@dataclass
class MyDataClass:
x: int
cls_var: ClassVar[int] # 未赋初值,下面使用时就会报错
# 实例化数据类
instance = MyDataClass(x=10)
# 输出实例
print(instance) # MyDataClass(x=10)
print(instance.cls_var) # AttributeError: type object 'MyDataClass' has no attribute 'cls_var'
print(MyDataClass.cls_var) # AttributeError: type object 'MyDataClass' has no attribute 'cls_var'
正确的用法:
from dataclasses import dataclass, fields
from typing import ClassVar
@dataclass
class MyDataClass:
x: int
cls_var: ClassVar[int] = 42
# 实例化数据类
instance = MyDataClass(x=10)
# 输出实例
print(instance) # MyDataClass(x=10)
print(instance.cls_var) # 42
print(MyDataClass.cls_var) # 42
print(fields(instance)) # 输出的元组中没有cls_var field
__post_init__方法: 这个特殊方法是在__init__被执行完后被自动调用的
看下面简单的例子。
from dataclasses import dataclass, field, fields
@dataclass
class Foo:
a: float
b: float
c: float = field(init=False)
def __post_init__(self):
self.c = self.a + self.b
foo = Foo(100, 200)
print(foo) # Foo(a=100, b=200, c=300)
print(fields(Foo)) # 函数返回的元组里包含 c Field
Init-only variables: 只用于初始化的变量,仅在构造实例的时候使用。不会成为实例的Field。某些特殊场景可能会需要这种类型的变量。一般会在__post_init__方法中接收这些init-only变量并做些逻辑处理。
from dataclasses import dataclass, fields, InitVar
@dataclass
class Foo:
a: int
b: str
c: InitVar[dict]
d: str = 'hello world'
foo = Foo(5, 'abc', {"name": 'Roland', "age": 40}, 'this is InitVar')
print(vars(Foo))
print(foo.a)
print(foo.b)
# print(Foo.c) # AttributeError: type object 'Foo' has no attribute 'c'
# print(foo.c) # AttributeError: 'Foo' object has no attribute 'c'
print(foo.d)
print(fields(foo)) # Field c 不会出现在此函数返回的filed tuple中。
在存在InitVar变量的情况下,如果定义__post_init__方法,则方法参数中要包含所有的InitVar变量, 否则会报错。
例如:
from dataclasses import dataclass, fields, InitVar
@dataclass
class Foo:
a: int
b: str
c: InitVar[dict]
d: InitVar[str]
e: str = 'hello world'
# 方法签名要接收所有的InitVar变量
def __post_init__(self, c, d):
print('__post_init__ 在执行')
print(c)
print(d)
foo = Foo(5, 'abc', {"name": 'Roland', "age": 40}, 'this is InitVar')
print(vars(foo))
Default factory functions: 是指在dataclass中定义Field时,可以指定default_factory参数为一个函数。 一般用做当缺省值的产生需要一段比较复杂的逻辑,或者是一个空的可变类型时。
在进一步说明这个参数的作用时,我想先展示一个现象,通过下面一段简单代码:
print(id([]), id([]))
print('-' * 25, '分隔线', '-' * 25)
a = []
b = []
print(id(a), id(b))
输出结果:
2216998883776 2216998883776
------------------------- 分隔线 -------------------------
2216998883776 2216999679872
从这个简单的例子来看,空列表在使用时不点不可靠,就是不知道它什么时候代表的是同一个空列表。
在搞不清楚python内部复杂的机制的情况下,我们可以使用list()函数来显示创建一个空列表,而每次执行list()都会创建不同的空列表对象。
再看下面的示例:
def add_item(item, item_list=[]):
item_list.append(item)
return item_list
lst = add_item('abc')
print(lst) # ['abc']
lst2 = add_item('gdk')
print(lst2) # ['abc', 'gdk']
print(lst) # ['abc', 'gdk']
当定义函数的参数默认值为空列表时, 如果调用时不传这个参数,那么这个参数默认值其实就是定义函数时创建好的。 后面对该函数调用时就会使用同一个空列表。
为了避免这样意外的共享,改进方法如下:
def add_item(item, item_list=None):
if item_list is None:
item_list = []
item_list.append(item)
return item_list
了解到上述情况后,dataclasses模块为了避免定义dataclass时会产生这种意外的共享现象,会限定我们创建初始值如果为可变类型时,不能直接给空列表,空字典等。 而是要提供一个函数来创建这种空列表或空字典对象,从而避免意外共享。 而这种函数就被称为工厂函数。
一个简单示例如下:
from dataclasses import dataclass, field
@dataclass
class C:
a: int
b: str = 'abc'
mylist: list = field(default_factory=list)
# mylist: list = [] # 会报错:ValueError: mutable default <class 'list'> for field mylist is not allowed: use default_factory
# mylist: list = field(default=[]) # ValueError: mutable default <class 'list'> for field mylist is not allowed: use default_factory
foo = C(1)
foo.mylist.append('foo')
bar = C(2)
bar.mylist.append('bar')
print(foo)
print(bar)
输出结果:
C(a=1, b='abc', mylist=['foo'])
C(a=2, b='abc', mylist=['bar'])
Frozen instances: 冻结实例。 就是在初始化对象后,不能再修改对象里的field。 使用方法是,在dataclass函数装饰时传递参数frozen=True。 举例如下:
from dataclasses import dataclass
@dataclass(frozen=True)
class Foo:
a: int
b: str = 'abc'
foo = Foo(1)
print(foo)
# foo.a = 5 # dataclasses.FrozenInstanceError: cannot assign to field 'a'
Inheritance: dataclass也可以继承,并且子类的field会从父类中获得,并加上子类自己的field。 field的顺序会按照基类到子类的顺序填加,也就是子类mro的相反顺序。子类中若覆盖了基类的field,则按照其在子类中的顺序追加。另外,从基类到子类所有的field要满足带缺省值的要放在后面。
下面示例中子类的定义会报错,因为基类中后面都是kw-only的field,而子类却有不是kw-only的field。
from dataclasses import dataclass
from typing import Any
@dataclass
class Base:
s: int
x: Any = 15.0
y: int = 0
@dataclass
class C(Base):
z: int = 10
x: int = 20
# 下面子类的定义会报错: TypeError: non-default argument 'a' follows default argument
@dataclass
class Foo(C):
a: str
b: str = 'hello'
对子类做下修改,将其都改为kw-only filed,就可以正常使用。
from dataclasses import dataclass
from typing import Any
@dataclass
class Base:
s: int
x: Any = 15.0
y: int = 0
@dataclass
class C(Base):
z: int = 10
x: int = 20
@dataclass
class Foo(C):
a: str = 'hello'
b: str = 'world'
foo = Foo(100)
print(foo) # Foo(s=100, x=20, y=0, z=10, a='hello', b='world')
如果基类中使用了KW_ONLY提示符,子类中即便有非kw-only的field,仍然可以成功继承,并且自动重排所有field,以确保非kw-only的filed排在前面。
@dataclass
class Base:
x: Any = 15.0
_: KW_ONLY
y: int = 0
w: int = 1
@dataclass
class D(Base):
z: int = 10
t: int = field(kw_only=True, default=0)
上面的子类会在内部自动构造__init__方法: def __init__(self, x: Any = 15.0, z: int = 10, *, y: int = 0, w: int = 1, t: int = 0)
dataclasses.replace: replace函数对一个dataclass实例进行浅拷贝,同时可以传参来修改原field的值。
看下面简单的示例:
from dataclasses import dataclass, replace, field
@dataclass
class Rectangle:
width: int
height: int
lst: list = field(default_factory=list)
color: str = "blue"
rec = Rectangle(100, 200)
rec_new = replace(rec, width=300, color='red')
rec_new.lst.append('abc')
print(rec)
print(rec_new)
输出结果:
Rectangle(width=100, height=200, lst=['abc'], color='blue')
Rectangle(width=300, height=200, lst=['abc'], color='red')
从输出结果可以看到,由于类定义中有list类型的field,使用replace浅拷贝后,新对象对list field的修改会影响到原对象。
is_dataclass: 用于判断一个类或实例是不是dataclass。
from dataclasses import dataclass, field, is_dataclass
@dataclass
class Rectangle:
width: int
height: int
lst: list = field(default_factory=list)
color: str = "blue"
rec = Rectangle(100, 200)
print(is_dataclass(rec)) # True
print(is_dataclass(Rectangle)) # True
asdict和astuple:这两个函数用于将dataclass转换成字典或元组。如果dataclass里的field的类型为其他dataclass,也就是说存在数据类的嵌套,那么转换成的字典或元组相当于深拷贝,会递规到最里层。
from dataclasses import dataclass, asdict, astuple
@dataclass
class Point:
x: int
y: int
@dataclass
class C:
mylist: list[Point]
name: str='my name'
p = Point(10, 20)
c = C([Point(0, 0), Point(10, 4)])
print(asdict(p))
print(asdict(c))
print('-' * 25, '分隔线', '-' * 25)
print(astuple(p))
print(astuple(c))
输出结果:
{'x': 10, 'y': 20}
{'mylist': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}], 'name': 'my name'}
------------------------- 分隔线 -------------------------
(10, 20)
([(0, 0), (10, 4)], 'my name')
其实,在转换成字典或元组时,我们还可以传递自定义的工厂函数。这样,我们可以在自定义的工厂函数里写些复杂的逻辑,甚至,我们可以返回我们想要的类型,而不一定是字典或者元组。
from dataclasses import dataclass, asdict, astuple
@dataclass
class Point:
x: int
y: int
@dataclass
class C:
mylist: list[Point]
name: str='my name'
p = Point(10, 20)
c = C([Point(0, 0), Point(10, 4)])
def custome_dict(items):
print('custome_dict is executing')
print(items)
print(type(items))
rtn_dic = {}
for item in items:
if item[0] == 'x':
rtn_dic['x'] = item[1]
print('custome_dict is completed')
return rtn_dic
def custom_tuple(items):
print('custom_tuple is executing')
print(items)
print(type(items))
rtn_tup = []
rtn_tup.append(items[0])
print('custom_tuple is completed')
return rtn_tup
print(asdict(p, dict_factory=custome_dict))
print('-' * 25, '分隔线', '-' * 25)
print(astuple(p, tuple_factory=custom_tuple)) # 其实将其转换成了列表
输出结果:
custome_dict is executing
[('x', 10), ('y', 20)]
<class 'list'>
custome_dict is completed
{'x': 10}
------------------------- 分隔线 -------------------------
custom_tuple is executing
[10, 20]
<class 'list'>
custom_tuple is completed
[10]
从上面的输出结果中可以看到,dict_factory函数接收的参数是list[tuple[str, Any]]类型,tuple_factory接收到的参数是list[Any]类型(即普通的list类型)。 而且我还让custom_tuple函数返回一个列表,而不是tuple,这也没问题的。

浙公网安备 33010602011771号