класс данных python3 с ** kwargs (звездочка)

В настоящее время я использовал DTO (объект передачи данных), как это.

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

Код примера очень маленький, но когда масштаб объекта растет, мне приходится определять каждую переменную.

Копаясь в нем, обнаружил, что python 3.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

Причина использования звездочки в классе данных та же.

Я должен передать тип dictinary классу init.

Любая идея здесь?


person Hide    schedule 11.03.2019    source источник
comment
Передайте init=False декоратору класса данных и реализуйте __init__ вручную. Вид поражения цели, и без явного определения атрибутов ваш класс данных не будет использовать эти переменные. По сути, вам не нужен класс данных.   -  person juanpa.arrivillaga    schedule 11.03.2019
comment
@juanpa.arrivillaga Если я реализую __init__ вручную, использование dataclass не даст никаких преимуществ. Это единственный способ?   -  person Hide    schedule 11.03.2019
comment
Как вы ожидаете, что класс данных узнает, какие переменные использовать, если вы не перечисляете их явно? Если вам нужны динамические атрибуты (как правило, плохой выбор дизайна IMO), вам действительно не нужен класс данных.   -  person juanpa.arrivillaga    schedule 11.03.2019
comment
@juanpa.arrivillaga Я добавил дополнительную информацию по своему вопросу. Пожалуйста, обратитесь к нему.   -  person Hide    schedule 11.03.2019
comment
Да, это ничего не меняет, хотя заметьте, для этого не нужно **kwargs, т.е. передавать дикт   -  person juanpa.arrivillaga    schedule 11.03.2019


Ответы (3)


Основной вариант использования классов данных — предоставление контейнера, который отображает аргументы в атрибуты. Если у вас есть неизвестные аргументы, вы не можете знать соответствующие атрибуты во время создания класса.

Вы можете обойти это, если вы знаете во время инициализации, какие аргументы неизвестны, отправив их в универсальный атрибут вручную:

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 typing import ClassVar


@dataclass
class Container:
    user_id: int
    body: str

    @classmethod
    def from_kwargs(cls, **kwargs):
        # identify the constructor's signature
        cls_fields = {
            name for
            name, f in cls.__dataclass_fields__.items() if
            f.type != ClassVar and getattr(f.type, "__origin__", None) != ClassVar
        }

        # 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'
person Arne    schedule 11.03.2019
comment
Я добавил дополнительную информацию по моему вопросу. Пожалуйста, обратитесь к нему. - person Hide; 11.03.2019
comment
Я боюсь, что нет способа передать неизвестные аргументы конструктору класса данных, не переписав его, и в этом случае, вероятно, вообще не стоит использовать классы данных. Единственным другим вариантом было бы предоставить метод класса, я могу добавить это к своему ответу. - person Arne; 11.03.2019
comment
Также стоит отметить, что мы должны исключить ClassVar-аннотированные атрибуты, поскольку они не включаются в сгенерированный __init__. Что-то вроде if getattr(cls.__annotations__.get(name, None), "__origin__", None) is not ClassVar:. - person Anakhand; 30.06.2021
comment
@ Анакханд хороший улов, спасибо. Я добавил аналогичную проверку. - person Arne; 06.07.2021

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'`
person Adam Haskell    schedule 06.08.2020
comment
Но initializer(ret, *args, **kwargs) - person Sole Sensei; 22.12.2020

Вот аккуратный вариант этого, который я использовал.

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}}

Это означает, что ключи должны быть известны. Это сработало для моего варианта использования и, возможно, для других.

person Traian Svinti    schedule 12.11.2020