带有**kwargs(星号)的python3数据类

utugiqy6  于 2023-02-02  发布在  Python
关注(0)|答案(8)|浏览(161)

目前我使用的DTO(数据传输对象)如下所示。

class Test1:
    def __init__(self, 
        user_id: int = None,
        body: str = None):
        self.user_id = user_id
        self.body = body

示例代码很小,但是当对象规模变大时,我必须定义每个变量。
在深入研究时,发现python3.7支持dataclass
下面的代码是DTO使用的数据类。

from dataclasses import dataclass

@dataclass
class Test2:
    user_id: int
    body: str

在这种情况下,我怎么能允许传递更多的参数,没有定义到class Test2
如果我用Test1的话,很简单,只要把**kwargs(asterisk)加到__init__里面就行了

class Test1:
    def __init__(self, 
        user_id: int = None,
        body: str = None,
        **kwargs):
        self.user_id = user_id
        self.body = body

但是使用数据类,还没有找到实现的方法.
有什么解决办法吗?
谢谢。

    • 编辑**
class Test1:
    def __init__(self,
        user_id: str = None, 
        body: str = None):
        self.user_id = user_id
        self.body = body

if __name__ == '__main__':
    temp = {'user_id': 'hide', 'body': 'body test'}
    t1 = Test1(**temp)
    print(t1.__dict__)

结果:{'user_id': 'hide', 'body': 'body test'}
如您所知,我想插入字典类型为-〉**temp的数据
在数据类中使用星号的原因是相同的。
我必须将字典类型传递给类init。
有什么想法吗?

mklgxw1f

mklgxw1f1#

数据类的基本用例是提供一个将参数Map到属性的容器,如果你有未知的参数,你就不能在类创建过程中知道相应的属性。
如果在初始化过程中知道哪些参数是未知的,可以通过手动将它们发送到一个捕获所有属性来解决这个问题:

from dataclasses import dataclass, field

@dataclass
class Container:
    user_id: int
    body: str
    meta: field(default_factory=dict)

# usage:
obligatory_args = {'user_id': 1, 'body': 'foo'}
other_args = {'bar': 'baz', 'amount': 10}
c = Container(**obligatory_args, meta=other_args)
print(c.meta['bar'])  # prints: 'baz'

但是在这种情况下,您仍然需要查看字典,并且无法通过参数名称访问参数,即c.bar不起作用。
如果您关心通过名称访问属性,或者在初始化过程中无法区分已知参数和未知参数,那么不重写__init__(这几乎完全违背了使用dataclasses的初衷)的最后一个办法是编写@classmethod

from dataclasses import dataclass
from inspect import signature

@dataclass
class Container:
    user_id: int
    body: str

    @classmethod
    def from_kwargs(cls, **kwargs):
        # fetch the constructor's signature
        cls_fields = {field for field in signature(cls).parameters}

        # split the kwargs into native ones and new ones
        native_args, new_args = {}, {}
        for name, val in kwargs.items():
            if name in cls_fields:
                native_args[name] = val
            else:
                new_args[name] = val

        # use the native ones to create the class ...
        ret = cls(**native_args)

        # ... and add the new ones by hand
        for new_name, new_val in new_args.items():
            setattr(ret, new_name, new_val)
        return ret

用法:

params = {'user_id': 1, 'body': 'foo', 'bar': 'baz', 'amount': 10}
Container(**params)  # still doesn't work, raises a TypeError 
c = Container.from_kwargs(**params)
print(c.bar)  # prints: 'baz'
tyu7yeag

tyu7yeag2#

Dataclass只依赖于__init__方法,因此您可以自由地在__new__方法中更改您的类。

from dataclasses import dataclass

@dataclass
class Container:
    user_id: int
    body: str

    def __new__(cls, *args, **kwargs):
        try:
            initializer = cls.__initializer
        except AttributeError:
            # Store the original init on the class in a different place
            cls.__initializer = initializer = cls.__init__
            # replace init with something harmless
            cls.__init__ = lambda *a, **k: None

        # code from adapted from Arne
        added_args = {}
        for name in list(kwargs.keys()):
            if name not in cls.__annotations__:
                added_args[name] = kwargs.pop(name)

        ret = object.__new__(cls)
        initializer(ret, **kwargs)
        # ... and add the new ones by hand
        for new_name, new_val in added_args.items():
            setattr(ret, new_name, new_val)

        return ret

if __name__ == "__main__":
    params = {'user_id': 1, 'body': 'foo', 'bar': 'baz', 'amount': 10}
    c = Container(**params)
    print(c.bar)  # prints: 'baz'
    print(c.body)  # prints: 'baz'`
41ik7eoe

41ik7eoe3#

这是我用过的一个简洁的变体。

from dataclasses import dataclass, field
from typing import Optional, Dict

@dataclass
class MyDataclass:
    data1: Optional[str] = None
    data2: Optional[Dict] = None
    data3: Optional[Dict] = None

    kwargs: field(default_factory=dict) = None

    def __post_init__(self):
        [setattr(self, k, v) for k, v in self.kwargs.items()]

其工作原理如下:

>>> data = MyDataclass(data1="data1", kwargs={"test": 1, "test2": 2})
>>> data.test
1
>>> data.test2
2

但是,请注意,数据类似乎并不知道它具有这些新属性:

>>> from dataclasses import asdict
>>> asdict(data)
{'data1': 'data1', 'data2': None, 'data3': None, 'kwargs': {'test': 1, 'test2': 2}}

这意味着键必须是已知的。这对我的用例和其他可能的用例都有效。

nkhmeac6

nkhmeac64#

from dataclasses import make_dataclass
Clas = make_dataclass('A', 
                      ['d'], 
                      namespace={
                                 '__post_init__': lambda self: self.__dict__.update(self.d)
                      })
d = {'a':1, 'b': 2}
instance = Clas(d)
instance.a
pwuypxnk

pwuypxnk5#

answer from Trian Svinit的变化:
您可以使用以下方法:
1.额外属性通过kwargs参数添加,如下所示:(x月1日至1x日)

  1. kwargs是一个dataclasses.InitVar,随后将在数据类的__post_init__中进行处理
    1.可以使用instance.__dict__访问所有值(因为asdict不会检测通过kwargs=...添加的属性
    这将只使用数据类的原生特性,并且继承该类仍然有效。
from dataclasses import InitVar, asdict, dataclass
from typing import Dict, Optional

@dataclass
class MyDataclass:
    data1: Optional[str] = None
    data2: Optional[Dict] = None
    data3: Optional[Dict] = None

    kwargs: InitVar[Optional[Dict[str, Any]]] = None

    def __post_init__(self, kwargs: Optional[Dict[str, Any]]) -> None:
        if kwargs:
            for k, v in kwargs.items():
                setattr(self, k, v)

data = MyDataclass(data1="data_nb_1", kwargs={"test1": 1, "test2": 2})
print(data, "-", data.data1, "-", data.test1)
# MyDataclass(data1='data_nb_1', data2=None, data3=None) - data1 - 1
print(asdict(data))
# {'data1': 'data_nb_1', 'data2': None, 'data3': None}
print(data.__dict__)
# {'data1': 'data_nb_1', 'data2': None, 'data3': None, 'test1': 1, 'test2': 2}

如果你真的需要使用asdict来获取作为kwargs传递的属性,你可以开始使用dataclass中的私有属性来破解asdict

from dataclasses import _FIELD, _FIELDSInitVar, asdict, dataclass, field
from typing import Dict, Optional

@dataclass
class MyDataclass:
    data1: Optional[str] = None
    data2: Optional[Dict] = None
    data3: Optional[Dict] = None

    kwargs: InitVar[Optional[Dict[str, Any]]] = None

    def __post_init__(self, kwargs: Optional[Dict[str, Any]]) -> None:
        if kwargs:
            for k, v in kwargs.items():
                setattr(self, k, v)
                self._add_to_asdict(k)

    def _add_to_asdict(self, attr:str) -> None:
        """Add an attribute to the list of keys returned by asdict"""
        f = field(repr=True)
        f.name = attr
        f._field_type = _FIELD
        getattr(self, _FIELDS)[attr] = f

data = MyDataclass(data1="data_nb_1", kwargs={"test1": 1, "test2": 2})
print(asdict(data))
# {'data1': 'data_nb_1', 'data2': None, 'data3': None, 'test1': 1, 'test2': 2}
5n0oy7gb

5n0oy7gb6#

基于Arnes Answer,我创建了一个类装饰器,它使用from_kwargs方法扩展了数据类装饰器。

from dataclasses import dataclass
from inspect import signature

def dataclass_init_kwargs(cls, *args, **kwargs):
    cls = dataclass(cls, *args, **kwargs)

    def from_kwargs(**kwargs):
        cls_fields = {field for field in signature(cls).parameters}
        native_arg_keys = cls_fields & set(kwargs.keys())
        native_args = {k: kwargs[k] for k in native_arg_keys}
        ret = cls(**native_args)
        return ret

    setattr(cls, 'from_kwargs', from_kwargs)
    return cls
oalqel3c

oalqel3c7#

所有这些改变都是善意的,但很明显违背了数据类的精神,即避免编写一堆样板文件来设置类。
Python 3.10引入了match语句,数据类通过它在构造器(即装饰器)中获得了一个match_args=True默认参数。
这意味着您会得到一个dunder属性__match_args__,它存储了init(kw)参数的元组,重要的是没有运行时检查。
所以你可以创建一个类方法

from dataclasses import dataclass

@dataclass
class A:
    a: int
    b: int = 0
    
    def from_kwargs(cls, **kwargs: dict) -> A:
        return cls(**{k: kwargs[k] for k in kwargs if k in cls.__match_args__})

它的工作原理是:

>>> A.from_kwargs(a=1, b=2, c=3)
A(a=1, b=2)
>>> A.from_kwargs(a=1)
A(a=1, b=0)

然而,由于__dataclass_fields__,我们在Python 3.9中也可以访问这些相同的键,如果不能依赖Python 3.10运行时,__dataclass_fields__是次佳选择。

def from_kwargs(cls, **kwargs: dict) -> A:
        return cls(**{k: kwargs[k] for k in kwargs if k in cls.__dataclass_fields__})

这给出了相同的结果。
对于问题中的用例(不寻常但合理!),您可以在构建init_kw dict时将类方法更改为pop,而不是访问kwargs dict,这样剩下的键将留在kwargs中,并可以作为它们自己的kwarg rest传递。

from dataclasses import dataclass

@dataclass
class A:
    a: int
    b: int = 0
    rest: dict = {}
    
    def from_kwargs(cls, **kwargs: dict) -> A:
        init_kw = {k: kwargs.pop(k) for k in dict(kwargs) if k in cls.__match_args__}
        return cls(**init_kw, rest=kwargs)

请注意,您必须将kwargs Package 在对dict的调用中(进行复制),以避免"dict size changed during iteration"错误

mrfwxfqh

mrfwxfqh8#

对于这个问题,应该使用default_factory,正如数据类文档中所述。

@dataclass
class Foo:
    a: Dict = field(default_factory=dict)

相关问题