8

I'm learning about decorators and came across an example where the decorator took an argument. This was a little confusing for me though, because I learned that (note: the examples from this question are mostly from this article):

def my_decorator(func):
  def inner(*args, **kwargs):
    print('Before function runs')
    func(*args, **kwargs)
    print('After function ran')
  return inner

@my_decorator
def foo(thing_to_print):
  print(thing_to_print)

foo('Hello')
# Returns:
# Before function runs
# Hello
# After function ran

was equivalent to

foo = my_wrapper(foo)

So, it doesn't make sense to me how something could take an argument, to better explain, here is a decorator example that takes an argument:

def repeat(num_times):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for _ in range(num_times):
                value = func(*args, **kwargs)
            return value
        return wrapper_repeat
    return decorator_repeat

@repeat(num_times=4)
def greet(name):
    print(f"Hello {name}")

greet('Bob')
# Returns:
# Hello Bob
# Hello Bob
# Hello Bob
# Hello Bob

So when I see this I am thinking:

greet = repeat(greet, num_times=4)

I know that that can't be right because num_times is the only argument that should be getting passed. So what is the correct equivalent to @repeat(num_times=4) without the "@-symbol-syntax"? Thanks!

Peterlits Zo
  • 476
  • 1
  • 4
  • 17
ThatComputerGuy
  • 175
  • 1
  • 3
  • 11
  • For an extensive explanation of what decorators are see [this answer](https://stackoverflow.com/a/1594484/9698684) – yatu Apr 11 '20 at 15:48

4 Answers4

5

In this case it would be:

greet = repeat(num_times=4)(greet)

This would explain the two levels of nesting within repeat (you need to call the function "twice," you could say). repeat(num_times=4) returns a decorator, then that decorator wraps around greet.

iz_
  • 15,923
  • 3
  • 25
  • 40
3

From your example code, repeat returns decorator_repeat definition.
So you can call decorator_repeat function and pass in greet function as the following:

def greet(name):
    print(f"Hello {name}")

greet = repeat(num_times=4)(greet)

greet('Bob')
# Hello Bob
# Hello Bob
# Hello Bob
# Hello Bob
ywbaek
  • 2,971
  • 3
  • 9
  • 28
3

That article is the same one that tought me everthing I know about decorators! It's brilliant. In regards to what the non @ symbol syntax looks like:

You can imagine the actual decorator function is decorator_repeat(func), the function within repeat(num_times=4).

@repeat(num_times=4) returns a decorator which is essentially @decorator_repeat except @decorator_repeat now has access to a variable num_times.

Further down the page in the article it shows how to make these arguments optional which may help further clarify it for you.

GTBebbo
  • 1,198
  • 1
  • 8
  • 17
  • Woah, thanks! That makes sense, the parentheses means that it will actually call the function `repeat` with the argument `num_times = 4`, that returns the actual decorator. The `@` symbol is still technically there though, meaning that the decorator that was returned essentially becomes the decorator but as you said, still has the argument that says it should run four times! That clarified a lot, thanks! – ThatComputerGuy Apr 11 '20 at 15:55
1

it will be better if you use

from typing import Callable

def func(*args, **kwargs):
    pass

def repeat(times: int, func: Callable, *args, **kwargs):
    for __ in range(times):
        value = func(*args, **kwargs)
    return value

# usage: repear(4, func, ...)

then you can make it be another form to make repeat return a function:

def repeat(times: int, func: Callable) -> Callable:
    # the function that you really have to have:
    def inner(*args, **kwargs):
        for __ in times:
            value = func(*args, **kwargs)
        return valur
    # return the function that you really want
    return inner

# usage: repeat(4, func)(...)

Fine, but now you think decorator will be better:

def repeat(func: Callable) -> Callable:
    # the function that you really have to have:
    def inner(*args, **kwargs):
        for __ in times:
            value = func(*args, **kwargs)
        return valur
    # return the function that you really want
    return inner

now you can use it like:

times = 4
@repeat
def func(*args, **kwargs):
    pass

but it do ugly, how about making this be a strong function? Good idea.

def outer_repeat(times: int):
    return repeat

now you can work like this:

@outer_repeat(times = 4)
def func(*args, **kwargs):
    pass

so the finally solution will like this:

def out_repeat(times: int):
    def repeat(func: Callable):
        # the function that you want to have
        @functools.wraps(func)
        def inner(*args, **kwargs):
            for __ in range(times):
                value = func(*args, **kwargs)
            return value
        # return the function that you wanna
        return inner
    return repeat
Peterlits Zo
  • 476
  • 1
  • 4
  • 17