1

Following is a useless example to show that I do not manage to decorate my add function with my_decorator, giving some extra arguments:

def my_decorator(fn=None, message=None, **options):
    assert fn is not None, 'fn is not found'
    print('function is found')
    print(message)
    return fn

# This syntax works (but does not make sense...)
@my_decorator
def multiply(a, b):
   return a * b

# This syntax does not work
@my_decorator(message='surprise')
def add(a, b):
   return a + b

# This no sugar-syntax works
add = my_decorator(add, message='surprise')  # Works

In the second example, fn is seen as None, and that raise the AssertionError, but works with "no-sugar" syntax!

What is wrong in my code? (Python 2.7. Yes, we will migrate it ...)

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
Timothé Delion
  • 1,310
  • 12
  • 17
  • 2
    Actually, you don't need a decorator but a decorator factory: a function which creates a decorator. – Laurent LAPORTE Apr 12 '20 at 11:04
  • 1
    @TimothéDelion “the decorator works if I do not use the @- syntax !” — Because that’s just a regular function call, why would that not work? By contrast, the decorator syntax has specific constraints as noted by Laurent. – Konrad Rudolph Apr 12 '20 at 11:18
  • 1
    This article can be really useful : https://stackoverflow.com/q/739654/1513933 – Laurent LAPORTE Apr 12 '20 at 11:38
  • This article, https://hynek.me/articles/decorators/ in the opposite, is very critic about `@wraps` – Timothé Delion Apr 12 '20 at 11:42

2 Answers2

1

my_decorator(message='surprise') returns None because it uses fn=None implicitly. You then try to apply None to add, which as you noticed cannot work.

You need a parametrized function that returns a decorator. Informally, this is called a parametrized decorator.

Here's an example:

def make_decorator(message):
    def decorator(func):
        def new_func(*args, **kwargs):
            print(message)
            return func(*args, **kwargs)
        return new_func
    return decorator

@make_decorator(message='surprise')
def add(a, b):
    return a + b

x = add(1, 2) # prints 'surprise' and sets x to 3

Note that

@make_decorator(message='surprise')
def add(a, b):
    return a + b

is equivalent to

def add(a, b):
    return a + b

decorator = make_decorator(message='surprise')
add = decorator(add)
actual_panda
  • 1,178
  • 9
  • 27
  • Your answer is incomplete, what about `multiply`? See [my answer](https://stackoverflow.com/a/61170648/1513933). – Laurent LAPORTE Apr 12 '20 at 11:26
  • You are right @Laurent LAPORTE. My actual need was to be able to make the two operations `my_decorator_factory(message="something")` # or `my_decorator_factory(function)` with the same decorator. This answer is very good too for its conciseness. – Timothé Delion Apr 12 '20 at 11:30
1

First, the minimum requirement to create a decorator is to implement a function (the decorator) which take a function in parameter.

For instance, you can write:

def my_decorator(f):
    def wrapper(*args, **kwargs):
        print("f() is called")
        return f(*args, **kwargs)

    return wrapper


# You can use your decorator w/o parameter:
@my_decorator
def multiply(a, b):
    return a * b

You get:

multiply(5, 8)
f() is called
40

As you can see, it works but your decorator take no parameter. So you need to create a decorator factory:

def my_decorator_factory(message):
    def my_decorator(f):
        def wrapper(*args, **kwargs):
            print("f() is called, saying: " + message)
            return f(*args, **kwargs)

        return wrapper

    return my_decorator


# You can use your decorator factory with a parameter:
@my_decorator_factory(message="hi")
def multiply(a, b):
    return a * b

You get:

multiply(5, 8)
f() is called, saying: hi
40

But, you want the ability to call you decorator factory like a decorator (w/o parameter).

So you need to change the signature of your decorator factory to allow the calls like:

my_decorator_factory(message="something")  # or
my_decorator_factory(function)

here, you need to check that the first parameter is a function of something else:

import inspect


def my_decorator_factory(message=None):
    if inspect.isfunction(message):
        return my_decorator_factory(message=None)(message)
    else:
        def my_decorator(f):
            def wrapper(*args, **kwargs):
                print("f() is called, saying: " + (message or "(empty)"))
                return f(*args, **kwargs)

            return wrapper

        return my_decorator


@my_decorator_factory
def multiply(a, b):
    return a * b


@my_decorator_factory(message='surprise')
def add(a, b):
    return a + b

You get:

multiply(5, 8)
f() is called, saying: (empty)
40
add(3, 2)
f() is called, saying: surprise
5

You may consider using Wrapt to implement "good" decorators/decorator factories.

Laurent LAPORTE
  • 21,958
  • 6
  • 58
  • 103
  • 1
    I think it would be much wiser to use `my_decorator_factory()` instead of inspect-hacking your way to be able to use `my_decorator_factory` but losing the ability to decorate callable objects that are not functions. – actual_panda Apr 12 '20 at 11:32
  • 1
    Well, there are several ways to check that, you can use the `callable` predicate also. Actually, you can also decorate classes (but it is another discussion). – Laurent LAPORTE Apr 12 '20 at 11:34