如何忽略传递给数据类的额外参数?

56

我想创建一个config dataclass,以简化对特定环境变量的白名单和访问(相对于os.environ['VAR_NAME']来说,输入config.VAR_NAME更为简便)。因此,在我的dataclass__init__函数中,我需要忽略未使用的环境变量,但我不知道如何提取默认的__init__函数,以便用它包装,例如,一个还包括*_作为其中一个参数的函数。

import os
from dataclasses import dataclass

@dataclass
class Config:
    VAR_NAME_1: str
    VAR_NAME_2: str

config = Config(**os.environ)

运行此代码会出现 TypeError: __init__() got an unexpected keyword argument 'SOME_DEFAULT_ENV_VAR' 错误。

5个回答

65

在将参数列表传递给构造函数之前清理它可能是最好的方法。尽管如此,我建议不要编写自己的__init__函数,因为数据类的__init__还做了一些其他方便的事情,如果覆盖它,你将会失去这些功能。

此外,由于参数清理逻辑与类的行为非常紧密地绑定并返回实例,因此将其放入classmethod中可能是有意义的:

from dataclasses import dataclass
import inspect

@dataclass
class Config:
    var_1: str
    var_2: str

    @classmethod
    def from_dict(cls, env):      
        return cls(**{
            k: v for k, v in env.items() 
            if k in inspect.signature(cls).parameters
        })


# usage:
params = {'var_1': 'a', 'var_2': 'b', 'var_3': 'c'}
c = Config.from_dict(params)   # works without raising a TypeError 
print(c)
# prints: Config(var_1='a', var_2='b')

1
不要使用 cls.__annotations__,而是使用 dataclass.fields(),这样您就可以检查它们的配置(例如忽略 init=False 字段)。 - Martijn Pieters
4
我并不是这个意思。inspect.signature()会返回一个Signature实例,让你轻松地创建一组可接受的参数名称。 - Martijn Pieters
如果性能是一个问题,直接检查cls.__dataclass_fields__会更快。性能也可以通过将inspect.signature(cls).parameters分配给字典推导外的变量来提高。 - tboschi
@tboschi请查看我帖子的第5个版本,这并不容易做到。不过你关于将条件放入变量中是正确的。 - Arne
@Arne 哦,太好了,谢谢你分享你的修订版本链接! - tboschi
显示剩余9条评论

33

我建议提供一个明确的__init__而不是使用自动生成的方法。循环体只设置已识别的值,忽略意外的值。

需要注意的是,这种情况直到后面才会报错,而且不会有默认值。

@dataclass(init=False)
class Config:
    VAR_NAME_1: str
    VAR_NAME_2: str

    def __init__(self, **kwargs):
        names = set([f.name for f in dataclasses.fields(self)])
        for k, v in kwargs.items():
            if k in names:
                setattr(self, k, v)

或者,您可以将过滤后的环境传递给默认的Config.__init__函数。

field_names = set(f.name for f in dataclasses.fields(Config))
c = Config(**{k:v for k,v in os.environ.items() if k in field_names})

是的,这正是我担心的,看起来该函数有一些检查等更复杂的操作(但我只看了一眼)。有没有办法只摘出自动生成的函数并进行包装?我也不想在其中放入其他环境变量。 - Californian
2
你不想包装自动生成的函数;你想要替换它。话虽如此,在调用默认的 __init__ 之前,你总是可以过滤环境映射:c = Config({k:v for k,v in kwargs if k in set(f.name for f in dataclasses.fields(Config))}) - chepner
2
在初始化实例之前过滤参数效果很好!如果你把它变成一个单独的答案,我会接受它。最终的代码: from dataclasses import dataclass, fields ... config = Config(**{k:v for k,v in os.environ.items() if k in set(f.name for f in fields(Config))}. - Californian
遵循“优先使用组合而非继承”的原则,您可能希望将此作为辅助函数进行迭代调用(例如,在提取已连接查询时,可能需要对基本数据类进行适当分离以避免繁琐的拼接)。 - Elysiumplain
1
你正在失去dataclass在'init'中所做的所有魔力。这不是解决问题的方法! - jonathan

7
我使用了两种方法的结合;setattr 可能会影响性能。 当字典中没有数据类记录时,您需要为这些记录设置字段默认值。
from __future__ import annotations
from dataclasses import field, fields, dataclass

@dataclass()
class Record:
    name: str
    address: str
    zip: str = field(default=None)  # won't fail if dictionary doesn't have a zip key

    @classmethod
    def create_from_dict(cls, dict_) -> Record:
        class_fields = {f.name for f in fields(cls)}
        return Record(**{k: v for k, v in dict_.items() if k in class_fields})

2
使用dacite Python库,通过字典值填充数据类会忽略字典中存在的额外参数/值(以及该库提供的所有其他优点)。
from dataclasses import dataclass
from dacite import from_dict


@dataclass
class User:
    name: str
    age: int
    is_active: bool


data = {
    'name': 'John',
    'age': 30,
    'is_active': True,
    "extra_1": 1000,
    "extra_2": "some value"
}

user = from_dict(data_class=User, data=data)
print(user)
# prints the following: User(name='John', age=30, is_active=True)

0

我是基于之前的答案做的:

import functools
import inspect

@functools.cache
def get_dataclass_parameters(cls: type):
    return inspect.signature(cls).parameters


def instantiate_dataclass_from_dict(cls: type, dic: dict):
    parameters = get_dataclass_parameters(cls)
    dic = {k: v for k, v in dic.items() if k in parameters}
    return cls(**dic)

由于 inspect.signature(cls).parameters 花费的时间比实际的实例化/初始化还要多,因此我使用 functools.cache 来为每个类缓存结果。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接