Iterating through constructor's arguments
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.
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)
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
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, ...}
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.
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)
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
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}
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
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
精彩评论