How can you set class attributes from variable arguments (kwargs) in python

Posted on

Question :

How can you set class attributes from variable arguments (kwargs) in python

Suppose I have a class with a constructor (or other function) that takes a variable number of arguments and then sets them as class attributes conditionally.

I could set them manually, but it seems that variable parameters are common enough in python that there should be a common idiom for doing this. But I’m not sure how to do this dynamically.

I have an example using eval, but that’s hardly safe. I want to know the proper way to do this — maybe with 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])

Answer #1:

You could update the __dict__ attribute (which represents the instance attributes in the form of a dictionary) with the keyword arguments:

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

then you can:

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

and with something like:

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

you could filter the keys beforehand (use iteritems instead of items if you’re still using Python 2.x).

Answered By: fijiaaron

Answer #2:

You can use the setattr() method:

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

There is an analogous getattr() method for retrieving attributes.

Answered By: fqxp

Answer #3:

Most answers here do not cover a good way to initialize all allowed attributes to just one default value.
So, to add to the answers given by @fqxp and @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)
Answered By: larsks

Answer #4:

I propose a variation of fqxp’s answer, which, in addition to allowed attributes, lets you set default values for attributes:

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)

This is Python 3.x code, for Python 2.x you need at least one adjustment, iteritems() in place of items().

VERY LATE FOLLOW UP

I recently rewrote the above code as a class decorator, so that hard coding of attributes is reduced to a minimum. In some way it resembles some features of the @dataclass decorator, which is what you might want to use instead.

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

Answer #5:

Yet another variant based on the excellent answers by mmj and fqxp. What if we want to

  1. Avoid hardcoding a list of allowed attributes
  2. Directly and explicitly set default values for each attributes in the constructor
  3. Restrict kwargs to predefined attributes by either
    • silently rejecting invalid arguments or, alternatively,
    • raising an error.

By “directly”, I mean avoiding an extraneous default_attributes dictionary.

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

Not a major breakthrough, but maybe useful to someone…

EDIT:
If our class uses @property decorators to encapsulate “protected” attributes with getters and setters, and if we want to be able to set these properties with our constructor, we may want to expand the allowed_keys list with values from dir(self), as follows:

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

The above code excludes

  • any hidden variable from dir() (exclusion based on presence of “__”), and
  • any method from dir() whose name is not found in the end of an attribute name (protected or otherwise) from __dict__.keys(), thereby likely keeping only @property decorated methods.

This edit is likely only valid for Python 3 and above.

Answered By: mmj

Answer #6:

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

I called the class SymbolDict because it essentially is a dictionary that operates using symbols instead of strings. In other words, you do x.foo instead of x['foo'] but under the covers it’s really the same thing going on.

Answered By: billjoie

Answer #7:

The following solutions vars(self).update(kwargs) or self.__dict__.update(**kwargs) are not robust, because the user can enter any dictionary with no error messages. If I need to check that the user insert the following signature (‘a1’, ‘a2’, ‘a3’, ‘a4’, ‘a5’) the solution does not work. Moreover, the user should be able to use the object by passing the “positional parameters” or the “kay-value pairs parameters”.

So I suggest the following solution by using a metaclass.

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))
Answered By: wberry

Answer #8:

Their might be a better solution but what comes to mind for me is:

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}
Answered By: antonjs

Leave a Reply

Your email address will not be published. Required fields are marked *