Reputation: 2188
I have several functions that I want to give default arguments. Since these defaults should be newly-initialized objects, I can't just set them as defaults in the argument list. It introduces a lot of clunky code to default them to None and check in every single function
def FunctionWithDefaults(foo=None):
if foo is None:
foo = MakeNewObject()
To reduce repeated code, I made a decorator to initialize parameters.
@InitializeDefaults(MakeNewObject, 'foo')
def FunctionWithDefaults(foo=None):
# If foo wasn't passed in, it will now be equal to MakeNewObject()
The decorator I wrote is as such:
def InitializeDefaults(initializer, *parameters_to_initialize):
"""Returns a decorator that will initialize parameters that are uninitialized.
Args:
initializer: a function that will be called with no arguments to generate
the values for the parameters that need to be initialized.
*parameters_to_initialize: Each arg is the name of a parameter that
represents a user key.
"""
def Decorator(func):
def _InitializeDefaults(*args, **kwargs):
# This gets a list of the names of the variables in func. So, if the
# function being decorated takes two arguments, foo and bar,
# func_arg_names will be ['foo', 'bar']. This way, we can tell whether or
# not a parameter was passed in a positional argument.
func_arg_names = inspect.getargspec(func).args
num_args = len(args)
for parameter in parameters_to_initialize:
# Don't set the default if parameter was passed as a positional arg.
if num_args <= func_arg_names.index(parameter):
# Don't set the default if parameter was passed as a keyword arg.
if parameter not in kwargs or kwargs[parameter] is None:
kwargs[parameter] = initializer()
return func(*args, **kwargs)
return _InitializeDefaults
return Decorator
That works great if I only need to decorate it once, but it breaks with multiple decorations. Specifically, suppose I want to initialize foo and bar to separate variables:
@InitializeDefaults(MakeNewObject, 'foo')
@InitializeDefaults(MakeOtherObject, 'bar')
def FunctionWithDefaults(foo=None, bar=None):
# foo should be MakeNewObject() and bar should be MakeOtherObject()
That breaks because in the second decorator, func_arg_defaults gets the arguments of the first decorator rather than FunctionWithDefaults. As a result, I can't tell whether or not a parameter has already been passed in positionally.
Is there any clean way to solve this problem?
Things that don't work:
__name__
of the function the decorated function, but it doesn't change the arguments (unless there's a different way to do it).I could just use one decorator, but that seems less clean, and it feels like there should be a cleaner solution.
Thanks!
Upvotes: 0
Views: 2410
Reputation: 3503
Why not change your decorator to accept a dictionary of arguments and the default value that you want? I don't see why it would be less cleaner than multiple decorations with different arguments. Here is a sample that works fine for me. I made minor changes to your code to reflect dictionary as an argument: import inspect
class Person:
def __init__(self):
self.name = 'Personality'
class Human(Person):
def __init__(self):
self.name = 'Human'
class Animal(Person):
def __init__(self):
self.name = 'Animal'
#def InitializeDefaults(**initializerDict):
def InitializeDefaults(**initializerDict):
"""Returns a decorator that will initialize parameters that are uninitialized.
Args:
initializer: a function that will be called with no arguments to generate
the values for the parameters that need to be initialized.
*parameters_to_initialize: Each arg is the name of a parameter that
represents a user key.
"""
def Decorator(func):
def _InitializeDefaults(*args, **kwargs):
# This gets a list of the names of the variables in func. So, if the
# function being decorated takes two arguments, foo and bar,
# func_arg_names will be ['foo', 'bar']. This way, we can tell whether or
# not a parameter was passed in a positional argument.
func_arg_names = inspect.getargspec(func).args
num_args = len(args)
for parameter in initializerDict:
# Don't set the default if parameter was passed as a positional arg.
if num_args <= func_arg_names.index(parameter):
# Don't set the default if parameter was passed as a keyword arg.
if parameter not in kwargs or kwargs[parameter] is None:
kwargs[parameter] = initializerDict[parameter]()
return func(*args, **kwargs)
return _InitializeDefaults
return Decorator
#@InitializeDefaults(Human, 'foo')
@InitializeDefaults(foo = Human, bar = Animal)
def FunctionWithDefaults(foo=None, bar=None):
print foo.name
print bar.name
#test by calling with no arguments
FunctionWithDefaults()
#program output
Human
Animal
Upvotes: 1
Reputation: 64308
This might be the best advice you'd get about decorators in python: use wrapt.decorator
.
It would make the task of writing decorators much simpler (you don't need to write a function inside a function inside a function), and would avoid all the errors which everybody makes when writing even simple decorators. To learn what those are, you might want to read Graham Dumpleton's blog posts titled "How you implemented your Python decorator is wrong" (he is very convincing)
Upvotes: 2
Reputation: 2630
You could add a variable to the wrapper function containing the argspec (e.g. __argspec
):
def Decorator(func):
# Retrieve the original argspec
func_arg_names = getattr(func, '__argspec', inspect.getargspec(func).args)
def _InitializeDefaults(*args, **kwargs):
# yada yada
# Expose the argspec for wrapping decorators
_InitializeDefaults.__argspec = func_arg_names
return _InitializeDefaults
Upvotes: 1