简体   繁体   中英

Using metaclass to automatically assign member variables of a class

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?

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM