[Python!] - Decorator(2)

Hailey Park·2021년 11월 8일
0

Python!

목록 보기
7/11
post-thumbnail

[Use cases for decorators]

1. Checking Arguments with a Decorator

In our chapter about recursive functions we introduced the factorial function. We wanted to keep the function as simple as possible and we didn't want to obscure the underlying idea, so we didn't incorporate any argument checks. So, if somebody called our function with a negative argument or with a float argument, our function would get into an endless loop.

The following program uses a decorator function to ensure that the argument passed to the function factorial is a positive integer:

def argument_test_natural_number(f):
    def helper(x):
        if type(x) == int and x > 0:
            return f(x)
        else:
            raise Exception("Argument is not an integer")
    return helper
@argument_test_natural_number
def factorial(n):
    if n == 1:
        return 1
    else:
        return n * factorial(n-1)
for i in range(1,10):
	print(i, factorial(i))
print(factorial(-1v))

OUTPUT:

1 1
2 2
3 6
4 24
5 120
6 720
7 5040
8 40320
9 362880

2. Counting Function Calls with Decorators

The following example uses a decorator to count the number of times a function has been called. To be precise, we can use this decorator solely for functions with exactly one parameter:

def call_counter(func):
    def helper(x):
        helper.calls += 1
        return func(x)
    helper.calls = 0
    return helper
@call_counter
def succ(x):
    return x + 1
print(succ.calls)
for i in range(10):
    succ(i)
print(succ.calls)

OUTPUT:

0
10

We pointed out that we can use our previous decorator only for functions, which take exactly one parameter. We will use the *args and **kwargs notation to write decorators which can cope with functions with an arbitrary number of positional and keyword parameters.

def call_counter(func):
    def helper(*args, **kwargs):
        helper.calls += 1
        return func(*args, **kwargs)
    helper.calls = 0
    return helper
@call_counter
def succ(x):
    return x + 1
@call_counter
def mul1(x, y=1):
    return x*y + 1
print(succ.calls)
for i in range(10):
    succ(i)
mul1(3, 4)
mul1(4)
mul1(y=3, x=2)
print(succ.calls)
print(mul1.calls)

OUTPUT:

0
10
3

[Decorators with parameters]

We define two decorators in the following code:

def evening_greeting(func):
    def function_wrapper(x):
        print("Good evening, " + func.__name__ + " returns:")
        return func(x)
    return function_wrapper
def morning_greeting(func):
    def function_wrapper(x):
        print("Good morning, " + func.__name__ + " returns:")
        return func(x)
    return function_wrapper
@evening_greeting
def foo(x):
    print(42)
foo("Hi")

OUTPUT:

Good evening, foo returns:
42

These two decorators are nearly the same, except for the greeting. We want to add a parameter to the decorator to be capable of customizing the greeting, when we do the decoration. We have to wrap another function around our previous decorator function to accomplish this. We can now easily say "Good Morning" in Greek:

def greeting(expr):
    def greeting_decorator(func):
        def function_wrapper(x):
            print(expr + ", " + func.__name__ + " returns:")
            func(x)
        return function_wrapper
    return greeting_decorator
@greeting("καλημερα")
def foo(x):
    print(42)
foo("Hi")

OUTPUT:

καλημερα, foo returns:
42

If we don't want or cannot use the "at" decorator syntax, we can do it with function calls:

def greeting(expr):
    def greeting_decorator(func):
        def function_wrapper(x):
            print(expr + ", " + func.__name__ + " returns:")
            return func(x)
        return function_wrapper
    return greeting_decorator
def foo(x):
    print(42)
greeting2 = greeting("καλημερα")
foo = greeting2(foo)
foo("Hi")

OUTPUT:

καλημερα, foo returns:
42

Of course, we don't need the additional definition of "greeting2". We can directly apply the result of the call "greeting("καλημερα")" on "foo":

foo = greeting("καλημερα")(foo)

[Using wraps from functools]

The way we have defined decorators so far hasn't taken into account that the attributes

  • __name__ (name of the function),
  • __doc__ (the docstring) and
  • __module__ (The module in which the function is defined) of the original functions will be lost after the decoration.

The following decorator will be saved in a file greeting_decorator.py:

    def greeting(func):
    def function_wrapper(x):
        """ function_wrapper of greeting """
        print("Hi, " + func.__name__ + " returns:")
        return func(x)
    return function_wrapper 

We call it in the following program:

from greeting_decorator import greeting
@greeting
def f(x):
    """ just some silly function """
    return x + 4
f(10)
print("function name: " + f.__name__)
print("docstring: " + f.__doc__)
print("module name: " + f.__module__) 

OUTPUT:

Hi, f returns:
function name: function_wrapper
docstring:  function_wrapper of greeting 
module name: greeting_decorator
We get the "unwanted" results above.

We can save the original attributes of the function f, if we assign them inside of the decorator. We change our previous decorator accordingly and save it as greeting_decorator_manually.py:

def greeting(func):
def function_wrapper(x):
    """ function_wrapper of greeting """
    print("Hi, " + func.__name__ + " returns:")
    return func(x)
    function_wrapper.__name__ = func.__name__
    function_wrapper.__doc__ = func.__doc__
    function_wrapper.__module__ = func.__module__
    return function_wrapper 

In our main program, all we have to do is change the import statement.

from greeting_decorator_manually import greeting

Fortunately, we don't have to add all this code to our decorators to have these results. We can import the decorator "wraps" from functools instead and decorate our function in the decorator with it:

from functools import wraps
def greeting(func):
    @wraps(func)
    def function_wrapper(x):
        """ function_wrapper of greeting """
        print("Hi, " + func.__name__ + " returns:")
        return func(x)
    return function_wrapper

[Classes instead of function]

1. The call method

So far we used functions as decorators. Before we can define a decorator as a class, we have to introduce the __call__ method of classes. We mentioned already that a decorator is simply a callable object that takes a function as an input parameter. A function is a callable object, but lots of Python programmers don't know that there are other callable objects. A callable object is an object which can be used and behaves like a function but might not be a function. It is possible to define classes in a way that the instances will be callable objects. The __call__ method is called, if the instance is called "like a function", i.e. using brackets.

class A:
    def __init__(self):
        print("An instance of A was initialized")
    def __call__(self, *args, **kwargs):
        print("Arguments are:", args, kwargs)
x = A()
print("now calling the instance:")
x(3, 4, x=11, y=10)
print("Let's call it again:")
x(3, 4, x=11, y=10)

OUTPUT:

An instance of A was initialized
now calling the instance:
Arguments are: (3, 4) {'x': 11, 'y': 10}
Let's call it again:
Arguments are: (3, 4) {'x': 11, 'y': 10}

We can write a class for the fibonacci function by using the __call__ method:

class Fibonacci:
    def __init__(self):
        self.cache = {}
    def __call__(self, n):
        if n not in self.cache:
            if n == 0:
                self.cache[0] = 0
            elif n == 1:
                self.cache[1] = 1
            else:
                self.cache[n] = self.__call__(n-1) + self.__call__(n-2)
        return self.cache[n]
fib = Fibonacci()
for i in range(15):
    print(fib(i), end=", ")

OUTPUT:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377,

You can find further information on the __call__ method in the chapter Magic Functions of our tutorial.

2. Using a Class as a Decorator

We will rewrite the following decorator as a class:

def decorator1(f):
    def helper():
        print("Decorating", f.__name__)
        f()
    return helper
@decorator1
def foo():
    print("inside foo()")
foo()

OUTPUT:

Decorating foo
inside foo()

The following decorator implemented as a class does the same "job":

class decorator2:
    def __init__(self, f):
        self.f = f
    def __call__(self):
        print("Decorating", self.f.__name__)
        self.f()
@decorator2
def foo():
    print("inside foo()")
foo() 

OUTPUT:

Decorating foo
inside foo()

Both versions return the same output.

Resources
https://python-course.eu/advanced-python/decorators-decoration.php

profile
I'm a deeply superficial person.

0개의 댓글