Python decoder to automatically detect __init__ variables

I'm tired of constantly typing the same duplicate commands over and over in my __init__ function. I was wondering if I could write a decorator to do the work for me. Here is an example of my question:

 class Point: def __init__(self, x, y): self.x = x self.y = y 

Is there a way by which I can automatically pass all the arguments passed to the function to become instance variables with the same names? For instance:

 class Point: @instance_variables def __init__(self, x, y): pass 

Where @instance_variables will automatically set self.x = x and self.y = y . How can i do this? Thanks!

EDIT: I should mention that I am using CPython 2.7.

+6
source share
5 answers

Here is my first attempt at a decorator:

[EDIT second attempt: I added handling default values ​​for variables and checking for valid keywords. Thanks ivan_pozdeev ]

[EDIT 3: Added default check is not None]

 def instanceVariables(func): def returnFunc(*args, **kwargs): selfVar = args[0] argSpec = inspect.getargspec(func) argumentNames = argSpec[0][1:] defaults = argSpec[3] if defaults is not None: defaultArgDict = dict(zip(reversed(argumentNames), reversed(defaults))) selfVar.__dict__.update(defaultArgDict) argDict = dict(zip(argumentNames, args[1:])) selfVar.__dict__.update(argDict) validKeywords = set(kwargs) & set(argumentNames) kwargDict = {k: kwargs[k] for k in validKeywords} selfVar.__dict__.update(kwargDict) func(*args, **kwargs) return returnFunc 

Here is an example:

 class Test(): @instanceVariables def __init__(self, x, y=100, z=200): pass def printStr(self): print(self.x, self.y, self.z) a = Test(1, z=2) a.printStr() >>> 1 100 2 
+3
source

You can do it:

 def __init__(self, x, y): self.__dict__.update(locals()) del self.self # redundant (and a circular reference) 

But this is probably not a real readable improvement.

+1
source

I do not agree that this is useful. I find that forcing developers (including myself) to exit the painful member variable initiation pattern is a good way to keep people from __init__ methods that take an absurd amount of arguments that turn into a ridiculous number of member variables,

This happens very often when someone wants to extend the functions available in a class using additional arguments, function flags, and switch logical variables that control a custom copy. I believe that all these problems are insufficient ways to cope with the need to accommodate new or additional advanced complexity.

It is required to introduce this special type of template, as a class inflation tax. If you think you accept so many arguments in __init__ that you need this function, this is usually a good indicator that you should reorganize your project with smaller, separated classes, possibly with a MixIn design.

However, here is one way to do this without the decorator pointing in the wrong direction. I did not try to process *args , but then again in this particular case you would need to define a special logic for which the indicated positional arguments made sense anyway.

 def init_from_map(obj, map): for k,v in map.iteritems(): if k not in ["self", "kwargs"]: setattr(obj, k, v) elif k == "kwargs": for kk, vv in v.iteritems(): setattr(obj, kk, vv) class Foo(object): def __init__(self, x, y, **kwargs): init_from_map(self, locals()) f = Foo(1, 2, z=3) print fx, fy, fz print f.__dict__ 

Print

 1 2 3 {'x': 1, 'y': 2, 'z': 3} 
+1
source

You can use reflection to reduce code duplication.

 self.__dict__.update(v,locals()[v] for v in 'x','y') 

(or almost equivalent ( v should not be the name of a meta variable))

 for v in 'x','y': setattr(self,v,locals()[v]) 

Or use CPython implementation details to extract argument names from the runtime; Get method parameter names in python

 cur_fr = sys._getframe().f_code self.__dict__.update(v,locals()[v] for v in cur_fr.co_varnames[1:cur_fr.co_argcount]) # cur_fr.f_locals is the same as locals() 

The second approach looks more "automated", but as I said , it turns out to be rather inflexible. If your argument list is longer than 3-4, you will probably need to handle some of the arguments this way, in which case you have no other options but to create their list manually.

0
source

For Python 3.3+:

 from functools import wraps from inspect import Parameter, signature def instance_variables(f): sig = signature(f) @wraps(f) def wrapper(self, *args, **kwargs): values = sig.bind(self, *args, **kwargs) for k, p in sig.parameters.items(): if k != 'self': if k in values.arguments: val = values.arguments[k] if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY): setattr(self, k, val) elif p.kind == Parameter.VAR_KEYWORD: for k, v in values.arguments[k].items(): setattr(self, k, v) else: setattr(self, k, p.default) return wrapper class Point(object): @instance_variables def __init__(self, x, y, z=1, *, m='meh', **kwargs): pass 

Demo:

 >>> p = Point('foo', 'bar', r=100, u=200) >>> px, py, pz, pm, pr, pu ('foo', 'bar', 1, 'meh', 100, 200) 

A no-decoration approach for both Python 2 and 3 using frames:

 import inspect def populate_self(self): frame = inspect.getouterframes(inspect.currentframe())[1][0] for k, v in frame.f_locals.items(): if k != 'self': setattr(self, k, v) class Point(object): def __init__(self, x, y): populate_self(self) 

Demo:

 >>> p = Point('foo', 'bar') >>> px 'foo' >>> py 'bar' 
0
source

Source: https://habr.com/ru/post/982298/


All Articles