dataclasses -- 数据类

dataclasses – 数据类

模块级装饰器、类和函数

dataclass

@dataclasses.dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)

dataclass时decorator, 用于生成 special method 添加到类中,如下所述、

dataclass() 装饰器会检查类以查找 fieldfield 被定义为具有 类型标注 的类变量。在 dataclass() 中没有什么东西会去检查在变量标注中所指定的类型。

所有生成的方法中的字段顺序是它们在类定义中出现的顺序。

如果 dataclass() 仅用作没有参数的简单装饰器,它就像它具有此签名中记录的默认值一样。也就是说,这三种 dataclass() 用法是等价的:

@dataclass
class C:
    ...

@dataclass()
class C:
    ...

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class C:
   ...

dataclass() 的参数有

  • init: 如果为真值(默认),将生成一个__init__()方法

  • repr: 如果为真值(默认),将生成一个__repr__()方法。生成的repr字符串将具有类名以及每个字段的名称和repr,按照它们在类中的定义顺序。不包括标记为从repr中排除的字段。例如:InventoryItem(name='widget', unit_price=3.0, quantity_on_hand=10)

  • eq: 如果为true(默认值),将生成__eq__()方法。此方法将类作为其字段的元祖按顺序比较。比较中的两个实例必须是相同的类型,

  • order: 如果未真值(默认为False),则__lt__() __le__()__gt__()__ge__() 方法将生成。 这将类作为其字段的元组按顺序比较。比较中的两个实例必须是相同的类型。如果 order 为真值并且 eq 为假值 ,则引发 ValueError

    如果类已经定义了 __lt__()__le__()__gt__() 或者 __ge__() 中的任意一个,将引发 TypeError

  • unsafe_hash :如果为 False (默认值),则根据 eqfrozen 的设置方式生成 __hash__() 方法。

    __hash__() 由内置的 hash() 使用,当对象被添加到散列集合(如字典和集合)时。有一个 __hash__() 意味着类的实例是不可变的。可变性是一个复杂的属性,取决于程序员的意图, __eq__() 的存在性和行为,以及 dataclass() 装饰器中 eqfrozen 标志的值。

    默认情况下, dataclass() 不会隐式添加 __hash__() 方法,除非这样做是安全的。 它也不会添加或更改现有的明确定义的 __hash__() 方法。 设置类属性 __hash__ = None 对 Python 具有特定含义,如 __hash__() 文档中所述。

    如果 __hash__() 没有显式定义,或者它被设为 None,则 dataclass() 可能 会添加一个隐式 __hash__() 方法。 虽然并不推荐,但你可以用 unsafe_hash=True 来强制 dataclass() 创建一个 __hash__() 方法。 如果你的类在逻辑上不可变但却仍然可被修改那么可能就是这种情况。 这是一个特殊用例并且应当被仔细地考虑。

    以下是隐式创建 __hash__() 方法的规则。请注意,你不能在数据类中都使用显式的 __hash__() 方法并设置 unsafe_hash=True ;这将导致 TypeError

    如果 eqfrozen 都是 true,默认情况下 dataclass() 将为你生成一个 __hash__() 方法。如果 eq 为 true 且 frozen 为 false ,则 __hash__() 将被设置为 None ,标记它不可用(因为它是可变的)。如果 eq 为 false ,则 __hash__() 将保持不变,这意味着将使用超类的 __hash__() 方法(如果超类是 object ,这意味着它将回到基于id的hash)。

  • frozen: 如为真值 (默认值为 False),则对字段赋值将会产生异常。 这模拟了只读的冻结实例。 如果在类中定义了 __setattr__()__delattr__() 则将会引发 TypeError。 参见下文的讨论。

field

dataclasses.field(*, default=MISSING, default_factory=MISSING, repr=True, hash=None, init=True, compare=True, metadata=None)

对于常见和简单的用例,不需要其他功能。但是,有些数据类功能需要额外的每个字段信息。为了满足这种对附加信息的需求,你可以通过调用filed()函数来替换默认字段值。例如:

from typing import List
@dataclass
class Person:
  myFriends: List[int] = field(default_factory=list)

c = Person()
print(c)

# output
# Person(myFriends=[])

如上所示, MISSING 值是一个 sentinel 对象,用于检测是否提供了 defaultdefault_factory 参数。 使用此 sentinel 是因为 Nonedefault 的有效值。没有代码应该直接使用 MISSING 值。

field()参数有:

  • default: 如果提供,这将是该字段的默认值。这是必需的,因为field()调用本身会替换一般的默认值。
  • defult_factory: 如果提供,它必须时一个零参数课调用对象,当该字段需要一个默认值时,它将被调用。除了其他目的之外,这可以用于指定具有可以变默认值的字段,如下所述。同时指定default和default_factory将产生错误。
  • init: 如果为true(默认值),则该字段作为参数包含在生成的__init__()方法中。
  • repr:如果为true(默认值),则该字段包含在生成的__repr__()方法返回的字符串中。
  • compare: 如果为true (默认值),则该字段包含在生成的相等性和比较方法中(__eq__(), __gt__()等等)。
  • hash: 这可以时布尔值或None.如果true,则此字段包含在生成的__hash__()方法中。如果为None(默认值),请使用compare的值,这通常是预期的行为。如果字段用于比较,则应在hash中考虑该字段。不鼓励将此值设置为None以为的任何值。
  • metadata: 这可以是映射或None。None被视为一个空的字典。这个值包含在MappingProxyType()中,使其成为只读,并暴露在Field对象熵。数据类根本不使用它,它是作为第三方扩展机制提供的。多个第三方可以各自拥有自己的键值,以用作元数据中的命名空间。

如果通过调用field()指定字段的默认值,则该字段的类属性将替换为指定的default值。如果没有提供default,那么将删除类属性。目的是在dataclass()装饰器运行之后,类属性将包含字段的默认值,就像指定了默认值一样。例如,之后:

@dataclass
class C:
    x: int
    y: int = field(repr=False)
    z: int = field(repr=False, default=10)
    t: int = 20
d = C(1, 5)
print(d)

# output
# C(x=1, t=20)

类属性 C.z 将是 10 ,类属性 C.t 将是 20,类属性 C.xC.y 将不设置。

Field

class dataclasses.Field

Field对象描述每个定义的字段。这些对象在内部创建,并由fields()模块级方法返回。用户永远不应该实例化Field对象。其有文档的属性是:

  • name: 字段的名字
  • type: 字段的类型
  • defaultdefault_factoryinitreprhashcompare 以及 metadata 与具有和 field() 声明中相同的意义和值。

可能存在其他属性,但它们是私有的,不能被审查或依赖。

fields

dataclasses.fields(class_or_instance)

返回Field对象的元组,用于定义此数据类的字段。接受数据类或数据类的实例。如果没有传递一个数据类或实例将引发TypeError。不返回ClassVar或InitVar的伪字段。

asdict

dataclasses.asdict(obj, *, dict_factory=dict)

将dataclass的obj转化为dict类型。示例:

@dataclass
class Point:
  x: int
  y: int

@dataclass
class C:
  mylist: List[Point]

p1 = Point(10, 50)
assert asdict(p1) == {"x": 10, "y": 50}
c = C([Point(10, 50), Point(5, 20)])
d = asdict(c)
print(d["mylist"])

output

[{'x': 10, 'y': 50}, {'x': 5, 'y': 20}]
{'mylist': [{'x': 10, 'y': 50}, {'x': 5, 'y': 20}]}
dataclasses.astuple(obj, *, tuple_factory=tuple)

将dataclass的obj转化为tuple类型这个跟asdict类似。

make_dataclass

dataclasses.make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)

创建一个名为 cls_name 的新数据类,字段为 fields 中定义的字段,基类为 bases 中给出的基类,并使用 namespace 中给出的命名空间进行初始化。 fields 是一个可迭代的元素,每个元素都是 name(name, type)(name, type, Field) 。 如果只提供nametypetyping.Anyinitrepreqorderunsafe_hashfrozen 的值与它们在 dataclass() 中的含义相同。

此函数不是严格要求的,因为用于任何创建带有 __annotations__ 的新类的 Python 机制都可以应用 dataclass() 函数将该类转换为数据类。提供此功能是为了方便。例如:

C = make_dataclass("C", 
                [("x", int),
                  "y",
                  ("z", int, field(default=5)),
                ],
                namespace={"add_one": lambda self: self.x + 1}
                )

等价于

import typing
@dataclass
class C:
  x: int
  y: "typing.Any"
  z: int = 5
  def add_one(self):
    return self.x + 1

replace

dataclasses.replace(obj, / , **changes)

新返回的对象通过调用数据类的__init__()方法创建。这确保了如果存在**post_init()**,其他也被调用

如果存在没有默认值的仅初始化变量,必须在调用 replace() 时指定,以便它们可以传递给 __init__()__post_init__()

changes 包含任何定义为 init=False 的字段是错误的。在这种情况下会引发 ValueError

提前提醒 init=False 字段在调用 replace() 时的工作方式。如果它们完全被初始化的话,它们不是从源对象复制的,而是在 __post_init__() 中初始化。估计 init=False 字段很少能被正确地使用。

is_dataclass

dataclasses.is_dataclass(obj)

如果其形参为dataclass或其实例则返回True,否则返回False

如果你需要知道一个类是否是一个数据类的实例(而不是一个数据类本身),那么再添加一个 not isinstance(obj, type) 检查:

def is_dataclass_instance(obj):
    return is_dataclass(obj) and not isinstance(obj, type)

初始化后处理

生成的 __init__() 代码将调用一个名为 __post_init__() 的方法,如果在类上已经定义了 __post_init__() 。它通常被称为 self.__post_init__() 。但是,如果定义了任何 InitVar 字段,它们也将按照它们在类中定义的顺序传递给 __post_init__() 。 如果没有 __ init__() 方法生成,那么 __post_init__() 将不会被自动调用。

在其他用途中,这允许初始化依赖于一个或多个其他字段的字段值。例如:

@dataclass
class C:
  a:float
  b:float
  c:float = field(init=False)

  def __post_init__(self):
    self.c = self.a + self.b
    
t = C(1.1, 2.1)
print(t)
C(1.1, 2.1, 2.3)

output

C(a=1.1, b=2.1, c=3.2)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[73], line 3
      1 t = C(1.1, 2.1)
      2 print(t)
----> 3 C(1.1, 2.1, 2.3)

TypeError: __init__() takes 3 positional arguments but 4 were given

dataclass() 所生成的 __init__() 方法不会调用基类的 __init__() 方法。

类变量

两个地方 dataclass() 实际检查字段类型的之一是确定字段是否是如 PEP 526 所定义的类变量。它通过检查字段的类型是否为 typing.ClassVar 来完成此操作。如果一个字段是一个 ClassVar ,它将被排除在考虑范围之外,并被数据类机制忽略。这样的 ClassVar 伪字段不会由模块级的 fields() 函数返回。例子:

from typing import ClassVar
@dataclass
class A:
  x:float 
  y:ClassVar

@dataclass
class B:
  x:float
  y:float

a = A(1.1)
print(a)
b = B(1.1)
print(b)

output

A(x=1.1)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[83], line 14
     12 a = A(1.1)
     13 print(a)
---> 14 b = B(1.1)
     15 print(b)

TypeError: __init__() missing 1 required positional argument: 'y'

仅初始化变量

另一个 dataclass() 检查类型注解地方是为了确定一个字段是否是一个仅初始化变量。它通过查看字段的类型是否为 dataclasses.InitVar 类型来实现。如果一个字段是一个 InitVar ,它被认为是一个称为仅初始化字段的伪字段。因为它不是一个真正的字段,所以它不会被模块级的 fields() 函数返回。仅初始化字段作为参数添加到生成的 __init__() 方法中,并传递给可选的 __post_init__() 方法。数据类不会使用它们。

例如,假设一个字段将从数据库初始化,如果在创建类时未提供其值:

@dataclass
class D:
  i: int
  j: int = None
  k: InitVar[float] = None
  
  def __post_init__(self, *arg):
    print(arg)
    k = arg[0]
    if self.j is None and k is not None:
      self.j = self.i * k

D(1, k=2.5)

output

(2.5,)
D(i=1, j=2.5)

冻结的实例

无法创建真正不可变的 Python 对象。但是,通过将 frozen=True 传递给 dataclass() 装饰器,你可以模拟不变性。在这种情况下,数据类将向类添加 __setattr__()__delattr__() 方法。 些方法在调用时会引发 FrozenInstanceError

使用 frozen=True 时会有很小的性能损失: __init__() 不能使用简单的赋值来初始化字段,并必须使用 object.__setattr__()

@dataclass(frozen=True)
class E:
  i:int
  j:int

e = E(1.2, 1.6)
e.i = 5.6
print(e)

output

---------------------------------------------------------------------------
FrozenInstanceError                       Traceback (most recent call last)
Cell In[96], line 7
      4   j:int
      6 e = E(1.2, 1.6)
----> 7 e.i = 5.6
      8 e

File <string>:4, in __setattr__(self, name, value)

FrozenInstanceError: cannot assign to field 'i'

继承

当数组由 dataclass() 装饰器创建时,它会查看反向 MRO 中的所有类的基类(即从 object 开始 ),并且对于它找到的每个数据类, 将该基类中的字段添加到字段的有序映射中。添加完所有基类字段后,它会将自己的字段添加到有序映射中。所有生成的方法都将使用这种组合的,计算的有序字段映射。由于字段是按插入顺序排列的,因此派生类会重载基类。一个例子:

from typing import Any
@dataclass
class Base:
    x: Any = 12.0
    y: int = 0

@dataclass
class C(Base):
    z: int = 10
    x: int = 15

print(C())

output

C(x=15, y=0, z=10)

最后的字段列表依次是 xyzx 的最终类型是 int ,如类 C 中所指定的那样。

C 生成的 __init__() 方法看起来像:

def __init__(self, x: int = 15, y: int = 0, z: int = 10):
    ...

默认工厂函数

如果一个 field() 指定了一个 default_factory ,当需要该字段的默认值时,将使用零参数调用它。例如,要创建列表的新实例,请使用:

mylist: list = field(default_factory=list)

如果一个字段被排除在 __init__() 之外(使用 init=False )并且字段也指定 default_factory ,则默认的工厂函数将始终从生成的 __init__() 函数调用。发生这种情况是因为没有其他方法可以为字段提供初始值。

可变的默认值

Python在类中的存储默认成员变量值。如果不使用数据类:

class C:
    x = []
    def add(self, element):
        self.x.append(element)

o1 = C()
o2 = C()
o1.add(1)
o2.add(2)
assert o1.x == [1, 2]
assert o1.x is o2.x

运行后我们可以观察到c的两个实例共享相同的类变量x,如预期的那样。

其他

exec 函数

在看用法的时候同时看了一下源码的实现,发现其中很多都用到了内置函数exec.

exec语句用来执行储存在字符串或者文件中的python语句。可以生成一个包含python代码的字符串,然后使用exec语句执行这些语句。

比如源码中就有这么一个函数:

def _create_fn(name, args, body, *, globals=None, locals=None,
              return_type=MISSING):
  # Note that we mutate locals when exec() is called.  Caller
  # beware!  The only callers are internal to this module, so no
  # worries about external callers.
  if locals is None:
      locals = {}
  if 'BUILTINS' not in locals:
      locals['BUILTINS'] = builtins
  return_annotation = ''
  if return_type is not MISSING:
      locals['_return_type'] = return_type
      return_annotation = '->_return_type'
  args = ','.join(args)
  body = '\n'.join(f'  {b}' for b in body)

  # Compute the text of the entire function.
  txt = f' def {name}({args}){return_annotation}:\n{body}'

  local_vars = ', '.join(locals.keys())
  txt = f"def __create_fn__({local_vars}):\n{txt}\n return {name}"

  ns = {}
  exec(txt, globals, ns)
  return ns['__create_fn__'](**locals)

通过名字我们可以差不多猜出这个函数是用来,动态创建函数的。在我们编码的时候,我们通常已经将所有的函数写死,比如函数的名字,内部的运行逻辑等。

这里直接使用exec通过字符串的形式创建函数。而dataclass创建数据类的时候需要为我们编写的类中添加各种内置函数,其实就需要借助这种灵活地生成函数的方式。

不详细讲解exec这个函数,可以通过以下例子更好地理解这个函数:

s = _create_fn(
  "test",
  ["a","b"],
  [
    "print(type(a), type(b))",
    "if isinstance(a, int) and isinstance(b, (int, float)):",
    "  return a + b"
  ]
)

print(s(1, 2))

output

{'__create_fn__': <function __create_fn__ at 0x000001BFD411E9D0>}
BUILTINS
<class 'int'> <class 'int'>
3

另外_create_fn这个函数中的globalslocals其实就是将字符串中的内容运行时的上下文环境。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值