0

In a Python class, I would like to automatically assign member variables to be the same as the __init__ function arguments, like this:

class Foo(object):
    def __init__(self, arg1, arg2 = 1):
        self.arg1 = arg1
        self.arg2 = arg2
  • I would like to explicitly have argument names in the init function for the sake of code clarity.
  • I don't want to use decorators for the same reason.

Is it possible to achieve this using a custom metaclass?

7
  • honestly if you just want to assign attributes directly passed to __init__ then you could just use a collections.namedtuple. Commented Oct 25, 2016 at 14:45
  • Why would you like to do this ? Depending on the concrete use case there might be quite a few different (and more or less appropriate) solutions. Commented Oct 25, 2016 at 15:11
  • I have a long list of argument for my classes (say around 10) and I don't want to explicitly type them one by one. Later in the class I used these argument for further calculations. Commented Oct 25, 2016 at 15:13
  • @motam79 do you realize that any solution involving a custom metaclass, inspection etc will require more code - and a code that's much more complex to write and maintain - than those ten straight assignments ? Commented Oct 25, 2016 at 15:31
  • A small voice of Alan J. Perlis inside me wants to say that if you need to pass 10 arguments to a class, you probably missed some. :P Commented Oct 25, 2016 at 15:50

1 Answer 1

1

First, a disclaimer. Python object creation and initialization can be complicated and highly dynamic. This means that it can be difficult to come up with a solution that works for the corner cases. Moreover, the solutions tend to use some darker magic, and so when they inevitably do go wrong they can be hard to debug.

Second, the fact that your class has so many initialization parameters might be a hint that it has too many parameters. Some of them are probably related and can fit together in a smaller class. For example, if I'm building a car, it's better to have:

class Car:

    def __init__(self, tires, engine):
        self.tires = tires
        self.engine = engine

class Tire:

    def __init__(self, radius, winter=False):
        self.radius = radius
        self.winter = winter

class Engine:

    def __init__(self, big=True, loud=True):
        self.big = big
        self.loud = loud

as opposed to

class Car:

    def __init__(self, tire_radius, winter_tires=False, 
                 engine_big=True, engine_loud=True):
        self.tire_radius = tire_radius
        self.winter_tires winter_tires
        self.engine_big = engine_big
        self.engine_loud = engine_loud

All of that said, here is a solution. I haven't used this in my own code, so it isn't "battle-tested". But it at least appears to work in the simple case. Use at your own risk.

First, metaclasses aren't necessary here, and we can use a simple decorator on the __init__ method. I think this is more readable, anyways, since it is clear that we are only modifying the behavior of __init__, and not something deeper about class creation.

import inspect
import functools

def unpack(__init__):
    sig = inspect.signature(__init__)

    @functools.wraps(__init__)
    def __init_wrapped__(self, *args, **kwargs):
        bound = sig.bind(self, *args, **kwargs)
        bound.apply_defaults()

        # first entry is the instance, should not be set
        # discard it, taking only the rest
        attrs = list(bound.arguments.items())[1:]

        for attr, value in attrs:
            setattr(self, attr, value)

        return __init__(self, *args, **kwargs)

    return __init_wrapped__

This decorator uses the inspect module to retrieve the signature of the __init__ method. Then we simply loop through the attributes and use setattr to assign them.

In use, it looks like:

class Foo(object):

    @unpack
    def __init__(self, a, b=88):
        print('This still runs!')

so that

>>> foo = Foo(42)
This still runs!
>>> foo.a
42
>>> foo.b
88

I am not certain that every introspection tool will see the right signature of the decorated __init__. In particular, I'm not sure if Sphinx will do the "right thing". But at least the inspect module's signature function will return the signature of the wrapped function, as can be tested.

If you really want a metaclass solution, it's simple enough (but less readable and more magic, IMO). You need only write a class factory that applies the unpack decorator:

def unpackmeta(clsname, bases, dct):
    dct['__init__'] = unpack(dct['__init__'])
    return type(clsname, bases, dct)

class Foo(metaclass=unpackmeta):

    def __init__(self, a, b=88):
        print('This still runs!')

The output will be the same as the above example.

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks, Is there an equivalent to "inspect.signature" in python 2.7?
Yes, getargspec, but it isn't a drop-in equivalent. You'll have to do some fiddling around. I'll leave it as an exercise for the reader.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.