如何在 python 中从变量参数(kwargs)设置类属性

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

我可以手动设置它们,但是在 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])
132371 次浏览

你可以使用 setattr()方法:

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

有一个类似的 getattr()方法用于检索属性。

他们可能是更好的解决方案,但我想到的是:

class Test:
def __init__(self, *args, **kwargs):
self.args=dict(**kwargs)


def getkwargs(self):
print(self.args)


t=Test(a=1, b=2, c="cats")
t.getkwargs()




python Test.py
{'a': 1, 'c': 'cats', 'b': 2}

我怀疑在大多数情况下,使用命名 args (为了更好地自我记录代码)可能会更好,因此它可能看起来像这样:

class Foo:
def setAll(a=None, b=None, c=None):
for key, value in (a, b, c):
if (value != None):
settattr(self, key, value)
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'],但是在表面下其实是一样的。

您可以使用关键字参数更新 __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)。

我提出了 Fqxp 的回答的一个变体,除了 允许的属性之外,它还允许您为属性设置 < strong > 默认值 :

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,您至少需要一个调整,即 iteritems()代替 items()

很晚才跟进

我最近将上面的代码重写为 < strong > 类装饰器 ,以便将属性的硬编码减少到最低限度。在某种程度上,它类似于 @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'}

这是最容易通过 云雀

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

这里的大多数答案都没有涵盖将所有允许的属性初始化为一个默认值的好方法。 因此,为了补充 @ 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)

下列解决方案 vars(self).update(kwargs)self.__dict__.update(**kwargs)不健壮,因为用户可以输入任何没有错误消息的字典。如果我需要检查用户是否插入以下签名(‘ a1’,‘ a2’,‘ a3’,‘ a4’,‘ a5’) ,则解决方案无法工作。此外,用户应该能够通过传递“位置参数”或“ kay-value 对参数”来使用对象。

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

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

还有一个变体是基于 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”属性,如果我们希望能够用构造函数设置这些属性,我们可能希望用来自 dir(self)的值展开 allowed_keys列表,如下所示:

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

以上代码不包括

  • dir()中的任何隐藏变量(基于存在“ _ _”的排除) ,以及
  • dir()中的任何方法,其名称不在 __dict__.keys()的属性名(protected 或 other)的末尾,因此可能只保留@property 修饰的方法。

此编辑可能只对 Python3及以上版本有效。

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)


我带着一个微妙的不同的问题来到这一页,但这是我需要的答案:

尝试使用 namedtuple类(看看这个答案)或 @dataclass装饰器(这个问题)。

它们是专门针对这种功能构建的,但是可能没有这里的其他答案灵活。

我通常都是这么做的:

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

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

注意

在 for 循环中使用 setattr 可能会对代码的性能产生负面影响,特别是在创建大量此类的情况下。在我的测试中,在上面的 Foo 类的 for 循环中用 setattr(self, key, value)替换 __update__,使该类的实例化时间延长了1.4倍。如果你有更多的参数要设置,这将是最糟糕的。在 python 中,for 循环的速度很慢,所以这并不奇怪。