相关文章推荐
Collectives™ on Stack Overflow

Find centralized, trusted content and collaborate around the technologies you use most.

Learn more about Collectives

Teams

Q&A for work

Connect and share knowledge within a single location that is structured and easy to search.

Learn more about Teams

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])
                It looks like these questions are similar: stackoverflow.com/questions/3884612/…  stackoverflow.com/questions/356718/… stackoverflow.com/questions/1446555/… so it looks like what I want is maybe this-- self.__dict__[key] = kwargs[key]
– fijiaaron
                Nov 18, 2011 at 18:20
                Not really relevant to your question, but you might want to check PEP8 for a few hints on conventional Python styling.
– Thomas Orozco
                Nov 18, 2011 at 18:28
                There is a fantastic library for this called attrs.  simply pip install attrs, decorate your class with @attr.s, and set the args as a = attr.ib(); b = attr.ib() etc.  Read more here.
– Adam Barnes
                Nov 16, 2016 at 12:24
                Am I missing something here? You still need to do self.x =kwargs.get'x']  You open yourself up to typos from caller You have to create client with extra chars instance=Class(**{}) If you don't jump thru hoops with the self.x =kwargs.get'x'] mundaneness, isn't it going to bite you later anyway? i.e. Instead of self.x, you 'll end up with self.__dict__['x'] down the line right? Or getattr() Either more typing than self.
– JGFMK
                May 27, 2018 at 2:58

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

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

I think you'll need this nowadays.. kwargs[0].items() instead of kwargs.iteritems() - (I'm using Python 3.6.5 at time of writing) – JGFMK May 26, 2018 at 15:06 @JGFMK Why kwargs[0] instead of just kwargs? Can kwargs even have an integer key? I'm pretty sure they have to be strings. – wjandrea Apr 22, 2020 at 19:12 def setAllWithKwArgs(self, **kwargs): for key, value in kwargs.items(): setattr(self, key, value)

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

@larsks thanks but any idea how we could unpack only a dictionary key? stackoverflow.com/questions/41792761/… – JinSnow Feb 2, 2017 at 21:09 @StevenVascellaro you can of course just use Foo.attrname. I think I was just pointing out the fact that the getattr method exists. It's also useful if you want to provide a default value for when the named attribute isn't available. – larsks Mar 20, 2018 at 15:37

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)
                I think this is the most complete answer due to the inizialization to False. Good point!
– Kyrol
                Apr 27, 2017 at 13:34

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'}
                This is the most flexible answer, summarizing the other approaches in this thread. It sets the attributes, allows for default values and adds only allowed attribute names. Works fine with python 3.x as shown here.
– squarespiral
                Jul 16, 2019 at 14:38
  • Avoid hardcoding a list of allowed attributes
  • Directly and explicitly set default values for each attributes in the constructor
  • 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.

    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))
        def setAllWithKwArgs(self, **kwargs):
            for key, value in kwargs.items():
                setattr(self, key, value)
    

    my example:

    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 is a dictionary of keyword arguments, and items() is a method that returns a copy of the dictionary's list of (key, value) pairs.
    – harryscholes
                    Oct 28, 2017 at 12:54
        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 most cases I find this short code enough.

    Using setattr with for loop can negatively impact the performance of your code especially if you create a lot of this class. In my test, replacing the __update__ with setattr(self, key, value) in a for loop for the Foo class above, made the class take 1.4 times longer to instantiate. This will be worst if you have more arguments to set. The for loops are slow in python so this is not a surprise.

    Both the setattr() and __dict__.update() methods bypass property @setter functions. The only way I have found to get this to work is by using exec().

    exec is considered a security risk, but we're not using it with any old user input, so I can't see why it would be. If you disagree I'd really like to learn why, so please leave a comment. I don't want to be advocating or committing insecure code.

    My example is mostly borrowed from previous answers for completeness, but the main difference is the line 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)
                    What I'm looking for is to conditionally set attributes based on validation.  I realized that the problem with using kwargs is that it doesn't validate (or document) which attributes are acceptable
    – fijiaaron
                    Nov 18, 2011 at 18:40
    

    I suspect it might be better in most instances to use named args (for better self documenting code) so it might look something like this:

    class Foo:
        def setAll(a=None, b=None, c=None):
            for key, value in (a, b, c):
                if (value != None):
                    settattr(self, key, value)
    

    I landed on this page with a subtly different question, but here's the answer I needed:

    Try namedtuple class (see this answer), or the @dataclass decorator (this question).

    These are built specifically with this kind of functionality in mind, but may be less flexible than the other answers here.

    Thanks for contributing an answer to Stack Overflow!

    • Please be sure to answer the question. Provide details and share your research!

    But avoid

    • Asking for help, clarification, or responding to other answers.
    • Making statements based on opinion; back them up with references or personal experience.

    To learn more, see our tips on writing great answers.

    How to handle constructors or methods with a different set (or type) of arguments in Python? See more linked questions
     
    推荐文章