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,这也没问题的。

posted @ 2024-12-31 11:49  RolandHe  阅读(396)  评论(0)    收藏  举报