【问题标题】:How does one ignore extra arguments passed to a data class?如何忽略传递给数据类的额外参数?
【发布时间】:2019-07-07 18:25:27
【问题描述】:

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

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'

【问题讨论】:

    标签: python python-3.x python-dataclasses


    【解决方案1】:

    在将参数列表传递给构造函数之前清理它可能是最好的方法。不过,我建议您不要编写自己的 __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')
    

    【讨论】:

    • 不要使用cls.__annotations__,使用dataclass.fields(),这样你就可以反省他们的配置(例如忽略init=False字段)。
    • 但是在这种情况下你会想要InitVars,不是吗? dataclasses.fields() 也跳过了它们,因此我可能需要在这里修复更多内容。
    • @MartijnPieters cls.__dataclass_fields__InitVar 包含并可以访问 init 字段。
    • 不幸的是,映射还包括ClassVar 字段,并且init 标志未设置为False
    • 我不是这个意思。 inspect.signature() 会给你一个 Signature 实例,它可以让你轻松地创建一组可接受的参数名称。
    【解决方案2】:

    我只会提供一个明确的__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})
    

    【讨论】:

    • 是的,这是我关心的问题,看起来这个功能有点复杂,有一些检查等(但我只看了一秒钟)。有什么办法可以撕掉自动生成的函数并包装它?我也真的不想要那里的其他环境变量。
    • 你不想包装自动生成的函数;你想更换它。也就是说,您始终可以在调用默认 __init__:c = Config({k:v for k,v in kwargs if k in set(f.name for f in dataclasses.fields(Config))})之前过滤环境映射
    • 在初始化实例之前过滤参数效果很好!如果你把它变成一个单独的答案,我会接受它。我最终得到的代码: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))}
    【解决方案3】:

    我使用了两个答案的组合; 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})
    

    【讨论】:

      猜你喜欢
      • 2014-10-29
      • 2014-12-18
      • 2020-03-08
      • 1970-01-01
      • 2020-03-15
      • 2018-10-07
      • 1970-01-01
      • 2018-07-30
      相关资源
      最近更新 更多