简体   繁体   中英

Optional args, kwargs to decorator

I am using the following approach to pass in an optional argument to a decorator:

def wait(func=None, delay=1.0):
    def decorator_wait(func):
        def wrapper_wait(*args, **kwargs):
            time.sleep(delay)
            return func(*args, **kwargs)
        return wrapper_wait
    return decorator_wait(func) if func is not None else decorator_wait


@wait
def print_something(something):
    print (something)

@wait(delay=0.2)
def print_something_else(something):
    print (something)

The above code looks pretty difficult to follow though with all the nesting. Is there another approach to do the above, or is this the only method available for something like this?

You can write classes having a __call__ method, instead of writing a bunch of nested def s.

It sounds like you want a decorator Wait which haults program execution for a few seconds. If you don't pass in a Wait-time then the default value is 1 seconds. Use-cases are shown below.

##################################################
@Wait
def print_something(something):
    print(something)

##################################################
@Wait(3)
def print_something_else(something_else):
    print(something_else)

##################################################
@Wait(delay=3)
def print_something_else(something_else):
    print(something_else)

When Wait has an argument, such as @Wait(3) , then the call Wait(3) is executed before anything else happens.

That is, the following two pieces of code are equivalent

@Wait(3)
def print_something_else(something_else):
    print(something_else)
    
###############################################
return_value = Wait(3)
@return_value
def print_something_else(something_else):
    print(something_else)
    

This is a problem.

if `Wait` has no arguments:
    `Wait` is the decorator.
else: # `Wait` receives arguments
    `Wait` is not the decorator itself.
    Instead, `Wait` ***returns*** the decorator
    

One solution is shown below:

Let us begin by creating the following class, DelayedDecorator :

import io
class DelayedDecorator:
    def __init__(i, cls, *args, **kwargs):
        print("Delayed Decorator __init__", cls, args, kwargs)
        i._cls = cls
        i._args = args
        i._kwargs = kwargs
    def __call__(i, func):
        print("Delayed Decorator __call__", func)
        if not (callable(func)):
            import io
            with io.StringIO() as ss:
                print(
                    "If only one input, input must be callable",
                    "Instead, received:",
                    repr(func),
                    sep="\n",
                    file=ss
                )
                msg = ss.getvalue()
            raise TypeError(msg)
        return i._cls(func, *i._args, **i._kwargs)
    

Now we can write things like:

 dec = DelayedDecorator(Wait, delay=4)
 @dec
 def delayed_print(something):
    print(something)

Note that:

  • dec does not not accept multiple arguments.
  • dec only accepts the function to be wrapped.
import inspect
class PolyArgDecoratorMeta(type):
    def __call__(Wait, *args, **kwargs):
        try:
            arg_count = len(args)
            if (arg_count == 1):
                if callable(args[0]):
                    SuperClass = inspect.getmro(PolyArgDecoratorMeta)[1]
                    r = SuperClass.__call__(Wait, args[0])
                else:
                    r = DelayedDecorator(Wait, *args, **kwargs)
            else:
                r = DelayedDecorator(Wait, *args, **kwargs)
        finally:
            pass
        return r
    
import time
class Wait(metaclass=PolyArgDecoratorMeta):
    def __init__(i, func, delay = 2):
        i._func = func
        i._delay = delay
        
    def __call__(i, *args, **kwargs):
        time.sleep(i._delay)
        r = i._func(*args, **kwargs)
        return r 

The following two pieces of code are equivalent:

@Wait
def print_something(something):
     print (something)

##################################################

def print_something(something):
    print(something)
print_something = Wait(print_something)

We can print "something" to the console very slowly, as follows:

print_something("something")

#################################################
@Wait(delay=1)
def print_something_else(something_else):
    print(something_else)

##################################################
def print_something_else(something_else):
    print(something_else)

dd = DelayedDecorator(Wait, delay=1)
print_something_else = dd(print_something_else)

##################################################

print_something_else("something") 
     

Final Notes

It may look like a lot of code, but you don't have to write the classes DelayedDecorator and PolyArgDecoratorMeta every-time. The only code you have to personally write something like as follows, which is fairly short:

from PolyArgDecoratorMeta import PolyArgDecoratorMeta
import time
class Wait(metaclass=PolyArgDecoratorMeta):
 def __init__(i, func, delay = 2):
     i._func = func
     i._delay = delay

 def __call__(i, *args, **kwargs):
     time.sleep(i._delay)
     r = i._func(*args, **kwargs)
     return r

You can avoid having to remember "do I need to call this or not?" by removing the func argument from the wait function, and remembering to always call your decorator-returner.

It would look like this:

def wait(delay=1.0):
    def decorator_wait(func):
        def wrapper_wait(*args, **kwargs):
            time.sleep(delay)
            return func(*args, **kwargs)
        return wrapper_wait
    return decorator_wait


@wait()
def print_something(something):
    print (something)

@wait(delay=0.2)
def print_something_else(something):
    print (something)

print_something("hello")
# 1 second delay, then 'hello'
print_something_else("there")
# 0.2 second delay, then 'there'

You just have to remember that wait will always return the decorator, so you have to use () when decorating your functions.

I think it is a little bit better:

import functools
import time

def wait(func=None, delay=1.0):
    if func is None:
        return lambda func: wait(func=func, delay=delay)

    @functools.wraps(func) # this is good practice to use it see: https://stackoverflow.com/questions/308999/what-does-functools-wraps-do
    def _wrapper(*args, **kwargs):
        time.sleep(delay)
        return func(*args, **kwargs)
    return _wrapper


@wait
def test():
    return

@wait(delay=3)
def test2():
    return

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