Python: Iterating through constructor's argume

2020-05-26 10:10发布

问题:

I often find myself writing class constructors like this:

class foo:
    def __init__(self, arg1, arg2, arg3):
        self.arg1 = arg1
        self.arg2 = arg2
        self.arg3 = arg3

This can obviously become a pain if the number of arguments (and class attributes) gets high. I'm looking for the most pythonic way to loop through the constructor's arguments list and assign attributes accordingly. I'm working with Python 2.7, so ideally I'm looking for help with that version.

回答1:

The most Pythonic way is what you've already written. If you are happy to require named arguments, you could do this:

class foo:
    def __init__(self, **kwargs):
        vars(self).update(kwargs)


回答2:

Provided answers rely on *vargs and **kargs arguments, which might not be convenient at all if you want to restrict to a specific set of arguments with specific names: you'll have to do all the checking by hand.

Here's a decorator that stores the provided arguments of a method in its bound instance as attributes with their respective names.

import inspect
import functools

def store_args(method):
    """Stores provided method args as instance attributes."""
    argspec = inspect.getargspec(method)
    defaults = dict(zip( argspec.args[-len(argspec.defaults):], argspec.defaults ))
    arg_names = argspec.args[1:]
    @functools.wraps(method)
    def wrapper(*positional_args, **keyword_args):
        self = positional_args[0]
        # Get default arg values
        args = defaults.copy()
        # Add provided arg values
        list(map( args.update, ( zip(arg_names, positional_args[1:]), keyword_args.items() ) ))
        # Store values in instance as attributes
        self.__dict__.update(args)
        return method(*positional_args, **keyword_args)

    return wrapper

You can then use it like this:

class A:
    @store_args
    def __init__(self, a, b, c=3, d=4, e=5):
        pass

a = A(1,2)
print(a.a, a.b, a.c, a.d, a.e)

Result will be 1 2 3 4 5 on Python3.x or (1, 2, 3, 4, 5) on Python2.x



回答3:

You can do that both for positional and for keyword arguments:

class Foo(object):
    def __init__(self, *args, **kwargs):
        for arg in args:
            print arg
        for kwarg in kwargs:
            print kwarg

* packs positional arguments into a tuple and ** keyword arguments into a dictionary:

foo = Foo(1, 2, 3, a=4, b=5, c=6) // args = (1, 2, 3), kwargs = {'a' : 4, ...}


回答4:

class foo:
    def __init__(self, **kwargs):
        for arg_name, arg_value in kwargs.items():
            setattr(self, arg_name, arg_value)

This requires arguments to be named:

obj = foo(arg1 = 1, arg2 = 2)


回答5:

How about this?

class foo:
    def __init__(self, arg1, arg2, arg3):
        for _prop in dir():
            setattr(self, _prop, locals()[_prop])

This uses the builtin python dir function to iterate over all local variables. It has a minor side effect of creating an extraneous self reference but you could filter that if you really wanted. Also if you were to declare any other locals before the dir() call, they would get added as constructed object's attributes as well.



回答6:

What about iterating over the explicit variable names?

I.e.

class foo:
    def __init__(self, arg1, arg2, arg3):
        for arg_name in 'arg1,arg2,arg3'.split(','):
            setattr(self, arg_name, locals()[arg_name])

f = foo(5,'six', 7)

Resulting with

print vars(f)

{'arg1': 5, 'arg2': 'six', 'arg3': 7}

My suggestion is similar to @peepsalot, except it's more explicit and doesn't use dir(), which according to the documentation

its detailed behavior may change across releases



回答7:

As others have noted, you should probably stick to your original 'pythonic' method in most cases.

However, if you really want to go the whole nine yards, here's some code that neatly deals with args, keyword args if desired, and avoids boilerplate repetition:

def convert_all_args_to_attribs(self, class_locals):
    class_locals.pop('self')
    if 'kwargs' in class_locals:
        vars(self).update(class_locals.pop('kwargs'))
    vars(self).update(class_locals)

class FooCls:
    def __init__(self, foo, bar):
        convert_all_args_to_attribs(self, locals())

class FooClsWithKeywords:
    def __init__(self, foo, bar, **kwargs):
        convert_all_args_to_attribs(self, locals())

f1 = FooCls(1,2)
f2 = FooClsWithKeywords(3,4, cheese='stilton')


print vars(f1) #{'foo': 1, 'bar': 2}
print vars(f2) #{'cheese': 'stilton', 'foo': 3, 'bar': 4}


回答8:

For python >= 3.7 take a look at the @dataclass class decorator. Among other things, it handles your __init__ boilerplate coding problem.

From the doc:

@dataclass
class InventoryItem:
    name: str
    unit_price: float
    quantity_on_hand: int = 0

Will automatically add an __init__ like that:

def __init__(self, name: str, unit_price: float, quantity_on_hand: int=0):
    self.name = name
    self.unit_price = unit_price
    self.quantity_on_hand = quantity_on_hand


回答9:

the *args is a sequence so you can access the items using indexing:

def __init__(self, *args):
        if args:       
            self.arg1 = args[0]    
            self.arg2 = args[1]    
            self.arg3 = args[2]    
            ...  

or you can loop through all of them

for arg in args:
   #do assignments