Dubraven
Dubraven

Reputation: 1071

Defining a class' __init__ arguments dynamically (using a previously defined dictionary)

Is there a way to define a class's arguments using a dictionary?

For example, imagine I have a dictionary with a bunch of key-value pairs. Each key should be an argument of the class and each value should be the default value for that argument.

In terms of code I'm looking for something like this:

d = {'apples':0,'bananas':1}
class A:
    def __init__(self,k=v for k,v in d.items())
        print('apples',apples)
        print('bananas',bananas)
A(4)

which should output:

-> apples 4
-> bananas 1

Upvotes: 1

Views: 1912

Answers (3)

martineau
martineau

Reputation: 123481

You asked:

Is there a way to define a class's arguments using a dictionary?

Indeed this is. Building a class dynamically is often done using what is known as a metaclass, i.e. a class whose instances are other classes.

Below is an example of one that does what you want. Besides defining the class' __init__() method's arguments using a dictionary, it also generates the body of the method which just prints out all its arguments and their values. I added this because you never answered the question I asked you in a comment about whether you wanted this done or not.

In a nutshell what it does is generate the source code needed to define a function that has the arguments desired followed by lines of code to print them. Once that's done, the code is executed using the built-in exec() function to obtain the result of executing it (i.e. the executable byte-code of a function object). Then lastly, a dictionary entry with a key of "__init__" and a value of the executable byte-code is added to the class' dictionary (classdict) before passing it on to the built-in type() function for construction into a usable class object.

Phew! The explanation's more involved than the code. ;¬)

class MyMetaClass(type):
    def __new__(cls, name, bases, classdict, **kwargs):
        initargs = classdict.pop('initargs', None)
        if not initargs:
            INIT_METHOD = f"def __init__(self):\n\tprint('{name}.__init__() called')\n"
        else:
            args = ", ".join(f"{k}={v}" for (k, v) in initargs.items())
            body = "\n".join(f"\tprint({k!r}, {k})" for k in initargs.keys())
            INIT_METHOD = (f'def __init__(self, ' + f'{args}' + '):\n'
                           f'\tprint("{name}.__init__() called")\n'
                           f'{body}\n')
        result = {'__builtins__' : None, 'print': print}  # Must explicitly list any builtins.
        exec(INIT_METHOD, result)
        init_method = result['__init__']
        classdict.update({'__init__': init_method})
        return type.__new__(cls, name, bases, classdict, **kwargs)


class A(metaclass=MyMetaClass):
    initargs = {'apples': 0, 'bananas': 1}


A(4)

Output:

A.__init__() called
apples 4
bananas 1

Upvotes: 1

match
match

Reputation: 11060

One option is to define the defaults, then pick the key from either kwargs or defaults and push it onto the class __dict__

That way any parameters passed to the class which aren't keys in the dict will be ignored. Something like:

class A():
    def __init__(self, **kwargs):
        defaults = {'apples': 0, 'bananas': 1}
        for key, val in defaults.items():
            # Add value from kwargs if set, otherwise use default value
            self.__dict__[key] = kwargs.get(key, val)
        print(self.apples, self.bananas)

A()
# 0, 1

A(apples=5, bananas=6)
# 5, 6

A(apples=5, carrots=10)
# 5, 1

The only downside is that params must be passed to the class as keyword args - plain args won't work.

EDIT It is possible to use the fact that dicts are ordered to do the same thing with *args but it's a bit more of a hack:

class A():
    def __init__(self, *args):
        defaults = {'apples': 0, 'bananas': '1'}
        for i, key in enumerate(defaults.keys()):
          try:
            # Get the value from args by index
            self.__dict__[key] = args[i]
          except IndexError:
            # Use the default value
            self.__dict__[key] = defaults[key]
        print(self.apples, self.bananas)

A()
# 0, 1

A(5)
# 5, 1

A(5, 6, 7)
# 5, 6

Upvotes: 1

Tawy
Tawy

Reputation: 629

I am not sure something like this exists natively, though I like the concept and would love to learn about it.

Anyway, here's a manual solution:

d = {'apples':0,'bananas':1}
class A:
    def __init__(self, *args, **kwargs):
        for arg, k in zip(args, d):
            setattr(self, k, arg)

        for k, v in kwargs.items():
            if hasattr(self, k):
                raise TypeError("multiple values for argument '{}'".format(k))
            elif k not in d:
                raise TypeError("unexpected keyword argument '{}'".format(k))
            setattr(self, k, v)

        for k, default_v in d.items():
            if not hasattr(self, k):
                setattr(self, k, default_v)

        print('apples', self.apples)
        print('bananas', self.bananas)


A(4)

# apples 4
# bananas 1

A(bananas=5)

# apples 0
# bananas 5

A(oranges=18)

# TypeError: unexpected keyword argument 'oranges'

It might require python >= 3.6 (not sure the version) for this to work. I think the dict were not guaranteed to be ordered before.

Upvotes: 0

Related Questions