如何在Python中使用可变参数(kwargs)设置类属性

176

假设我有一个带有构造函数(或其他函数)的类,该函数接受可变数量的参数,然后根据条件将它们设置为类属性。

我可以手动设置它们,但似乎在Python中可变参数很常见,应该有一种通用的方法来做到这一点。但是我不确定如何以动态方式实现。

我有一个使用eval的示例,但那显然不安全。我想知道正确的方法是什么--也许使用lambda?

class Foo:
    def setAllManually(self, a=None, b=None, c=None):
        if a!=None: 
            self.a = a
        if b!=None:
            self.b = b
        if c!=None:
            self.c = c
    def setAllWithEval(self, **kwargs):
        for key in **kwargs:
            if kwargs[param] != None
                eval("self." + key + "=" + kwargs[param])

1
看起来这些问题很相似:https://dev59.com/P2865IYBdhLWcg3wTctY https://dev59.com/mXRC5IYBdhLWcg3wROtQ https://dev59.com/EEnSa4cB1Zd3GeqPN2wv,所以我想要的可能是这个——self.__dict__[key] = kwargs[key]。 - fijiaaron
1
不是与您的问题真正相关,但您可能希望检查PEP8,以获取有关传统Python样式的一些提示。 - Thomas Orozco
有一个非常棒的库可以实现这个功能,叫做attrs。只需要简单地运行pip install attrs,然后在你的类上加上修饰符@attr.s,并设置参数如a = attr.ib(); b = attr.ib()即可。更多信息请阅读此处 - Adam Barnes
我在这里漏掉了什么吗? 你仍然需要执行self.x = kwargs.get('x') 这会让调用者容易出现拼写错误 你必须创建带有额外字符的客户端 instance=Class(**{}) 如果你不想通过self.x = kwargs.get('x')这种平凡无奇的方式跳过障碍,那么它最终不是也会咬你一口吗?即使你最终使用self.dict['x']或getattr(),也比self.打字更多。 - JGFMK
13个回答

236
你可以使用关键字参数更新__dict__属性(它以字典形式表示实例属性):
class Bar(object):
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

那么你可以:

>>> bar = Bar(a=1, b=2)
>>> bar.a
1

然后使用类似以下的方式:

allowed_keys = {'a', 'b', 'c'}
self.__dict__.update((k, v) for k, v in kwargs.items() if k in allowed_keys)

你可以事先过滤键(如果你仍在使用Python 2.x,请使用iteritems而不是items)。


7
如果您使用self.__dict__.update(locals())来复制位置参数,那就更好了。 - Giancarlo Sportelli
2
我认为现在你需要使用kwargs[0].items()而不是kwargs.iteritems() - (我正在使用Python 3.6.5编写) - JGFMK
@JGFMK 为什么要用 kwargs[0] 而不是直接用 kwargskwargs 可以有整数键吗?我相当确定它们必须是字符串。 - wjandrea
你的意思是“它代表了实例属性...”吗? - progammer
@jsp99,你说得对,已经修复了。有趣的是,在九年中没有其他人遇到过这个问题 :) - fqxp

181
你可以使用 setattr() 方法:
class Foo:
  def setAllWithKwArgs(self, **kwargs):
    for key, value in kwargs.items():
      setattr(self, key, value)

有一个类似的getattr() 方法可以用来获取属性。


@larsks 谢谢,但您有什么想法可以仅解压字典键吗?http://stackoverflow.com/questions/41792761/calling-and-using-an-attribute-stored-in-variable-using-beautifulsoup-4 - JinSnow
你需要使用 .getattr() 吗?还是可以通过 Foo.key 访问属性? - Stevoisiak
@StevenVascellaro,你当然可以直接使用Foo.attrname。我只是想指出getattr方法的存在。如果你想提供一个默认值来处理未找到的属性时,它也会非常有用。 - larsks
4
被接受的回答相比有什么不同?它们各自的优缺点是什么? - Eduardo Pignatelli
1
在我看来,这样对读者更加清晰明了。 - Jacob Pavlock

20
大多数答案都没有涵盖一种好的方法来将所有允许的属性初始化为一个默认值。因此,补充@fqxp@mmj所提供的答案:
class Myclass:

    def __init__(self, **kwargs):
        # all those keys will be initialized as class attributes
        allowed_keys = set(['attr1','attr2','attr3'])
        # initialize all allowed keys to false
        self.__dict__.update((key, False) for key in allowed_keys)
        # and update the given keys by their given values
        self.__dict__.update((key, value) for key, value in kwargs.items() if key in allowed_keys)

我认为这是最完整的答案,因为初始化为“False”。好观点! - Kyrol
1
实际上,将初始化设置为“False”会使此答案比其他答案不够灵活 - mmj

16

我提议对fqxp的答案进行变化,除了允许的属性之外,还可以为属性设置默认值:

class Foo():
    def __init__(self, **kwargs):
        # define default attributes
        default_attr = dict(a=0, b=None, c=True)
        # define (additional) allowed attributes with no default value
        more_allowed_attr = ['d','e','f']
        allowed_attr = list(default_attr.keys()) + more_allowed_attr
        default_attr.update(kwargs)
        self.__dict__.update((k,v) for k,v in default_attr.items() if k in allowed_attr)

这是 Python 3.x 的代码,如果您使用的是 Python 2.x,则需要至少进行一个调整,items() 需要替换为 iteritems()

非常晚的跟进

我最近将上述代码重写为 类装饰器,以将属性的硬编码降至最低。在某种程度上,它类似于 @dataclass 装饰器 的一些特性,您可能希望使用它。

# class decorator definition
def classattributes(default_attr,more_allowed_attr):
    def class_decorator(cls):
        def new_init(self,*args,**kwargs):
            allowed_attr = list(default_attr.keys()) + more_allowed_attr
            default_attr.update(kwargs)
            self.__dict__.update((k,v) for k,v in default_attr.items() if k in allowed_attr)
        cls.__init__ = new_init
        return cls
    return class_decorator

# usage:
# 1st arg is a dict of attributes with default values
# 2nd arg is a list of additional allowed attributes which may be instantiated or not
@classattributes( dict(a=0, b=None, c=True) , ['d','e','f'] )
class Foo():
    pass # add here class body except __init__

@classattributes( dict(g=0, h=None, j=True) , ['k','m','n'] )
class Bar():
    pass # add here class body except __init__

obj1 = Foo(d=999,c=False)
obj2 = Bar(h=-999,k="Hello")

obj1.__dict__ # {'a': 0, 'b': None, 'c': False, 'd': 999}
obj2.__dict__ # {'g': 0, 'h': -999, 'j': True, 'k': 'Hello'}

1
这是最灵活的答案,总结了本主题中的其他方法。它设置属性,允许默认值,并添加仅允许的属性名称。在此处展示,与Python 3.x完美配合。 - squarespiral

10

基于 mmjfqxp 的优秀答案,这是另一种变体。如果我们想要:

  1. 避免硬编码允许属性列表
  2. 直接并明确设置构造函数中每个属性的默认值
  3. 通过静默拒绝无效参数或者抛出错误来限制kwargs到预定义的属性

这里“直接”指的是避免使用一个多余的 default_attributes 字典。

class Bar(object):
    def __init__(self, **kwargs):

        # Predefine attributes with default values
        self.a = 0
        self.b = 0
        self.A = True
        self.B = True

        # get a list of all predefined values directly from __dict__
        allowed_keys = list(self.__dict__.keys())

        # Update __dict__ but only for keys that have been predefined 
        # (silently ignore others)
        self.__dict__.update((key, value) for key, value in kwargs.items() 
                             if key in allowed_keys)

        # To NOT silently ignore rejected keys
        rejected_keys = set(kwargs.keys()) - set(allowed_keys)
        if rejected_keys:
            raise ValueError("Invalid arguments in constructor:{}".format(rejected_keys))

没有什么重大突破,但可能对某些人有用...

编辑: 如果我们的类使用@property装饰器来封装带有getter和setter的“protected”属性,并且如果我们希望能够在构造函数中设置这些属性,则我们可能需要扩展allowed_keys列表,使用dir(self)中的值,如下所示:

allowed_keys = [i for i in dir(self) if "__" not in i and any([j.endswith(i) for j in self.__dict__.keys()])]

上述代码排除:

  • 任何在dir()中以双下划线("__")开头的隐藏变量,以及
  • 任何方法,在其名称未出现在__dict__.keys()的属性名(无论是保护还是其他类型)末尾时从dir()中排除,因此可能只保留用@property修饰的方法。

这个修改可能只适用于Python 3及以上版本。


4
以下解决方案 vars(self).update(kwargs)self.__dict__.update(**kwargs) 不够健壮,因为用户可以输入任何字典而不会出现错误消息。如果我需要检查用户是否插入了以下签名('a1','a2','a3','a4','a5'),这个解决方案将无法工作。此外,用户应该能够通过传递“位置参数”或“键值对参数”来使用对象。

因此,建议使用元类来实现以下解决方案。

from inspect import Parameter, Signature

class StructMeta(type):
    def __new__(cls, name, bases, dict):
        clsobj = super().__new__(cls, name, bases, dict)
        sig = cls.make_signature(clsobj._fields)
        setattr(clsobj, '__signature__', sig)
        return clsobj

def make_signature(names):
    return Signature(
        Parameter(v, Parameter.POSITIONAL_OR_KEYWORD) for v in names
    )

class Structure(metaclass = StructMeta):
    _fields = []
    def __init__(self, *args, **kwargs):
        bond = self.__signature__.bind(*args, **kwargs)
        for name, val in bond.arguments.items():
            setattr(self, name, val)

if __name__ == 'main':

   class A(Structure):
      _fields = ['a1', 'a2']

   if __name__ == '__main__':
      a = A(a1 = 1, a2 = 2)
      print(vars(a))

      a = A(**{a1: 1, a2: 2})
      print(vars(a))

3

这是最简单的方法之一,通过larsks提供。

class Foo:
    def setAllWithKwArgs(self, **kwargs):
        for key, value in kwargs.items():
            setattr(self, key, value)

我的例子:

class Foo:
    def __init__(self, **kwargs):
        for key, value in kwargs.items():
            setattr(self, key, value)

door = Foo(size='180x70', color='red chestnut', material='oak')
print(door.size) #180x70

有人能解释一下kwargs.items()是什么吗? - Oleg_Kornilov
kwargs 是一个关键字参数的字典,而 items() 则是一个返回该字典 (key, value) 对列表副本的方法。 - harryscholes

3

这是我通常要做的事情:

class Foo():
    def __init__(self, **kwrgs):
        allowed_args = ('a', 'b', 'c')
        default_values = {'a':None, 'b':None} 
        self.__dict__.update(default_values)
        if set(kwrgs.keys()).issubset(allowed_args):
            self.__dict__.update(kwrgs)
        else:
            unallowed_args = set(kwrgs.keys()).difference(allowed_args)
            raise Exception (f'The following unsupported argument(s) is passed to Foo:\n{unallowed_args}')

对于大多数情况,我发现这个简短的代码已经足够了。

注意

使用setattr和for循环可以对您的代码性能产生负面影响,特别是如果您创建了许多此类。在我的测试中,将上面Foo类中的__update__替换为setattr(self, key, value),使该类实例化时间增加了1.4倍。如果你有更多的参数需要设置,那么情况会变得更糟。在Python中,for循环速度较慢,所以这并不令人惊讶。


2
setattr()__dict__.update()方法都会绕过属性的@setter函数。我发现唯一能让它们起作用的方法是使用exec()

exec被认为是一种安全风险,但我们并没有使用任何旧的用户输入,所以我不知道为什么会有风险。如果您不同意,请留言告诉我原因。我不想倡导或承担不安全的代码。

我的示例主要是从以前的答案中借鉴而来,但主要区别在于exec(f"self.{key} = '{value}'")这行代码。

class Foo:
    def __init__(self, **kwargs):
        # Predefine attributes with default values
        self.a = 0
        self.b = 0
        self.name = " "
        

        # get a list of all predefined attributes
        allowed_keys = [attr for attr in dir(self) if not attr.startswith("_")]
        for key, value in kwargs.items():
            # Update properties, but only for keys that have been predefined 
            # (silently ignore others)
            if key in allowed_keys:
                exec(f"self.{key} = '{value}'")

    @property
    def name(self):
        return f"{self.firstname} {self.lastname}"
    
    @name.setter
    def name(self, name):
        self.firstname, self.lastname = name.split(" ", 2)


2
class SymbolDict(object):
  def __init__(self, **kwargs):
    for key in kwargs:
      setattr(self, key, kwargs[key])

x = SymbolDict(foo=1, bar='3')
assert x.foo == 1

我将这个类称为SymbolDict,因为它本质上是一个使用符号而不是字符串操作的字典。换句话说,你可以用x.foo代替x['foo'],但在底层实际上是相同的操作。


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