11

I've got fed up of continually typing the same, repetitive commands over and over again in my __init__ function. I was wondering if I could write a decorator to do the work for me. Here's an example of my question:

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

Is there some way in which I can automatically have all arguments passed into the function become instance variables with the same names? For example:

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

Where @instance_variables would automatically set self.x = x and self.y = y. How could I do this?
Thanks!

EDIT: I should mention that I use CPython 2.7.

QuantumFool
  • 609
  • 3
  • 10
  • 29
  • This seems like it would be such a useful tool I imagine that there is some 3rd party library on PyPI that provides this functionality. – SethMMorton Feb 10 '15 at 23:01
  • 1
    @SethMMorton I disagree that this is useful. I find that forcing developers (including myself) to type out the painful boilerplate of member variable initiation is a good way to deter people from having `__init__` methods that accept a ridiculous number of arguments, which are turned into a ridiculous number of member variables. It's like a tax on class bloatedness. If you find yourself accepting so many args in `__init__` that you need this feature, it's usually a good indicator that you should refactor your design with smaller, compartmentalized classes, maybe a MixIn design. – ely Feb 10 '15 at 23:36
  • Still willing to go this way? [As Ashwin said](http://stackoverflow.com/questions/28443527/python-decorator-to-automatically-define-init-variables/28443934?noredirect=1#comment45218565_28443934), a reliable decorator-type solution turns out to be rather ugly and ineffective in comparison with a few lines of code. – ivan_pozdeev Feb 11 '15 at 00:40

7 Answers7

4

Here is my first try at the decorator:

[EDIT second try: I added handling defaults for variables and checking for valid keywords. Thanks ivan_pozdeev ]

[EDIT 3: Added check for defaults 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 a 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
Community
  • 1
  • 1
ashwinjv
  • 2,787
  • 1
  • 23
  • 32
  • I see a side effect: a collision between meta-variables and `kwargs` is not checked for until `func(*args, **kwargs)`, i.e. when the data is already in the object. – ivan_pozdeev Feb 10 '15 at 23:49
  • @ivan_pozdeev, you are correct, personally I would not like to have `**kwargs` supported because then the code creates variable names based on initialization of the object which in turn makes code difficult to read. Let me see if I can work on that and also add defaults to variables. – ashwinjv Feb 10 '15 at 23:56
  • I'd even say importing `kwargs` directly without any sanitizing is a security hazard. – ivan_pozdeev Feb 11 '15 at 00:02
  • @ivan_pozdeev, yup! that I completely agree. I could just remove the support for `**kwargs` confirm that the variables in the `**kwargs` in the decorator are present in the signature of the func. – ashwinjv Feb 11 '15 at 00:05
  • Just don't handle them in the decorator, let the wrapped fn do the pest control. – ivan_pozdeev Feb 11 '15 at 00:11
  • @ivan_pozdeev I have to account for `args` being passed as `kwargs`. I made those changes in my second edit – ashwinjv Feb 11 '15 at 00:22
  • This is crazy. Now you need to handle `defaults==None`... You're basically duplicating the stock subroutine invocation code. – ivan_pozdeev Feb 11 '15 at 00:23
  • 1
    @ivan_pozdeev Done. It does feel like an awful lot of code just so that we dont have to write `self.x = x` and `self.y = y` – ashwinjv Feb 11 '15 at 00:30
  • 1
    This is much easier in Python 3.3+, where you can use [`Signature` objects](https://docs.python.org/3/library/inspect.html#introspecting-callables-with-the-signature-object) to inspect and bind the parameters. I think it'd be just `sig = inspect.signature(func); bound = sig.bind(*args, **kwargs); for param in sig.parameters.values(): setattr(selfobj, param.name, bound.get(param.name, param.default))` or something very similar. – Blckknght Feb 11 '15 at 01:16
3

For those who might find this post, but are interested in a Python 3.7+ solution (and as Python 2 End Of Life is the Jan 1, 2020 ;-), you can use the Python standard lib dataclasses.

from dataclasses import dataclass

@dataclass
class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

Will add, among other things, a __init__() that looks like:

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
Samuel Phan
  • 4,218
  • 2
  • 17
  • 18
  • This doesn't really shorten the code though, does it? Instead of `self.x = x` you now do `x: float` for each input. – Daniel May 23 '20 at 21:48
1

You could do this:

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

But this is probably not a real improvement, readability-wise.

kindall
  • 178,883
  • 35
  • 278
  • 309
1

I disagree that this is useful. I find that forcing developers (including myself) to type out the painful boilerplate of member variable initiation is a good way to deter people from having __init__ methods that accept a ridiculous number of arguments, which are turned into a ridiculous number of member variables.

This happens a lot when someone wants to extend the features available in a class through the use of extra arguments, feature flags, and boolean switch variables that control customized instantiation. I consider all of those to be deficient ways to handle the need for accommodating new or optional extended complexity.

Being required to type out this particular kind of boilerplate is like a tax on class bloatedness. If you find yourself accepting so many args in __init__ that you need this feature, it's usually a good indicator that you should refactor your design with smaller, compartmentalized classes, maybe a MixIn design.

Nonetheless, here is one way to do it without the misdirection of the decorator. I didn't make an attempt to handle *args but then again in this particular case you would have to define special logic for what unnamed positional arguments meant 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 f.x, f.y, f.z
print f.__dict__

Prints:

1 2 3
{'x': 1, 'y': 2, 'z': 3}
ely
  • 74,674
  • 34
  • 147
  • 228
  • Though I disagree this is strictly detrimental, it does come to my mind the "complete arglist"-based approach is rather inflexible. If you only need to handle a part of the arglist in this way, you're stuck with a hand-written list in any case. – ivan_pozdeev Feb 10 '15 at 23:59
  • One place I can see it being useful is in machine-generated code. For example, maybe you've got a DSL written in JSON or XML, and from it, a Python class is automatically created on the fly with as many parameters as needed from something in the DSL. The intention must be that the code is not meant to be read or edited by humans though, or it loses the benefit promptly. – ely Feb 11 '15 at 00:05
  • That is another reason for DON'T use the @autoinit hack. PyCharm doesn't like it. It can't recognize the object variables – hevangel Aug 30 '18 at 21:01
  • "I consider all of those to be deficient ways to handle the need for accommodating new or optional extended complexity." What's a better way to do this then? I'm trying to learn more about this, but I'm just starting out with using classes. – Daniel May 23 '20 at 21:51
  • 1
    @Daniel use MixIn patterns if you are committed to object oriented patterns. If you are free to avoid OOP, you should. In that case, use decorators and structure modules with functions that operate on data structures rather than data structures that also contain (or inherit) methods that operate on their internal structure. If you find you're creating functions (whether constructor functions or just utility functions) with very large argument lists, you're probably designing things wrong and should step back and consider how to split it up into more isolated components. – ely May 24 '20 at 22:59
0

You can either use reflection to reduce code duplication

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

(or almost equivalent (v must not be a meta-variable name))

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

Or use CPython's implementation details to retrieve argument names from runtime as per Getting 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 2nd approach appears more "automated" but, as I've said, it turns out to be rather inflexible. If your argument list is longer than 3-4, you probably only need to handle some of the arguments this way, in which case, you have no other options but to construct their list by hand.

Community
  • 1
  • 1
ivan_pozdeev
  • 33,874
  • 19
  • 107
  • 152
0

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)
>>> p.x, p.y, p.z, p.m, p.r, p.u
('foo', 'bar', 1, 'meh', 100, 200)

A non-decorator 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')
>>> p.x
'foo'
>>> p.y
'bar'
Ashwini Chaudhary
  • 244,495
  • 58
  • 464
  • 504
0

I am searching for an autoinit decorator and come across this thread. I couldn't find any @autoinit in the web that handles variable arguments, variable keywords and the keyword-only arguments. Inspired by the other solutions, I wrote my own version that support everything.

I did some testing, it seems to work fine in all scenarios, but I haven't exhaustively test the code. Let me know if it has a bug. Thanks.

def autoinit(func):
"""
This decorator function auto initialize class variables from __init__() arguments
"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
    if func.__name__ != '__init__':
        return func(*args, **kwargs)

    self = args[0]
    func_spec = inspect.getfullargspec(func)

    # initialize default values
    nargs = dict()
    if func_spec.kwonlydefaults is not None:
        for k,v in func_spec.kwonlydefaults.items():
            nargs[k] = v
    if func_spec.defaults is not None:
        for k,v in zip(reversed(func_spec.args), reversed(func_spec.defaults)):
            nargs[k] = v
    if func_spec.varargs is not None:
        nargs[func_spec.varargs] = []
    if func_spec.varkw is not None:
        nargs[func_spec.varkw] = {}
    # fill in positional arguments
    for index, v in enumerate(args[1:]):
        if index+1 < len(func_spec.args):
            nargs[func_spec.args[index+1]] = v
        elif func_spec.varargs is not None:
            # variable argument
            nargs[func_spec.varargs].append(v)
    # fill in keyword arguments
    for k,v in kwargs.items():
        if k in itertools.chain(func_spec.args, func_spec.kwonlyargs):
            nargs[k] = v
        elif func_spec.varkw is not None:
            # variable keywords
            nargs[func_spec.varkw][k] = v

    # set values to instance attributes
    for k,v in nargs.items():
        setattr(self, k, v)
    return func(*args, **kwargs)
return wrapper
hevangel
  • 95
  • 5