Reputation: 1071
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
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
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
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