Decorators

Everything has a type in Python

 

The functions as well!

>>> class Panda:
...     pass
... 
>>> def foo():
...     return 'bar'
... 
>>> panda = Panda()
>>> type(panda)
<class '__main__.Panda'>
>>> type(Panda)
<class 'type'>
>>> type(foo)
<class 'function'>
>>> type(foo())
<class 'str'>
>>> type(dir)
<class 'builtin_function_or_method'>

The functions in Python are instances of a class and these instances can be called.

How?

Just __call__ me baby

>>> class Panda:
...     def __call__(self):
...         print('What am I!?')
... 
>>> panda = Panda()
>>> panda()
What am I!?

To recap...

  • Everything in Python is an object.
  • Functions are objects.
  • Functions can return everything.

Functions can return other functions

>>> def take_print():
...     print('I am wrapping the print')
...     return print
... 
>>> my_print = take_print()
I am wrapping the print
>>> type(my_print)
<class 'builtin_function_or_method'>
>>> my_print('Marto, WTF?!')
Marto, WTF?!

Scopes

Local and global scope

global_x = 123

def foo():
    global_x = 42
    print('In foo: ', global_x)

print('Before foo: ', global_x)
foo()
print('After foo: ', global_x)
╰─Ѫ py demo.py
Before foo:  123
In foo:  42
After foo:  123

Nesting functions

  1. You can define functions in another function
  2. The nested functions can see the scope of the wrapper function
def foo(x):
    print('Before bar, x is: ', x)

    def bar():
        x = 100
        print('x in bar: ', x)

    bar()
    print('After bar, x is: ')

foo(42)
╰─Ѫ py demo.py
Before bar, x is:  42
x in bar:  100
After bar, x is:  42

Closures

A function that uses non-local variables is called closure

def make_multiplier(n):
    def multiply(x):
        print(x * n)

    return multiply

times_three = make_multiplier(3)
times_five = make_multiplier(5)

times_three(10)
times_five(10)
╰─Ѫ py demo.py
30
50

__closure__

def make_multiplier(n):
    def multiply(x):
        print(x * n)

    return multiply

print(make_multiplier.__closure__)

times_three = make_multiplier(3)
times_three(10)

print(times_three.__closure__)
print(times_three.__closure__[0].cell_contents)

del make_multiplier
print('make_multiplier is deleted')
times_three(10)
╰─Ѫ py demo.py
None
30
(<cell at 0x7f25fcf47210: int object at 0x55980abcca40>,)
3
make_multiplier is deleted
30

A problem

Let's say you offer a software where peaople can pay their bills

def pay_phone(user, money):
    save_to_db('phone', user, money)

def pay_net(user, money):
    save_to_db('internet', user, money)

def pay_tv(user, money):
    save_to_db('tv', user, money)


pay_phone('Marto', 10)
pay_net('Marto', 30)
pay_tv('Marto', 20)

The beginning

def pay_phone(user, money, promo=100):
    save_to_db('phone', user, money * (promo / 100))

def pay_net(user, money, promo=100):
    save_to_db('internet', user, money * (promo / 100))

def pay_tv(user, money, promo=100):
    save_to_db('tv', user, money * (promo / 100))

PROMO = 50

pay_phone('Marto', 10, PROMO)
pay_net('Marto', 30, PROMO)
pay_tv('Marto', 20, PROMO)

Add discounts

def add_discount(func, promo=100):
    def discounted(user, money):
        return func(user, money * (promo / 100))
    return discounted

def pay_phone(user, money):
    save_to_db('phone', user, money)

def pay_net(user, money):
    save_to_db('internet', user, money)

def pay_tv(user, money):
    save_to_db('tv', user, money)

PROMO = 50

pay_phone = add_discount(pay_phone, PROMO)
pay_net = add_discount(pay_net, PROMO)

pay_phone('Marto', 10)
pay_net('Marto', 30)
pay_tv('Marto', 20)

Make it a closure

def add_discount(func, promo=100):
    def discounted(user, money):
        return func(user, money * (50 / 100))  # hardcoded!
    return discounted

@add_discount
def pay_phone(user, money):
    save_to_db('phone', user)

@add_discount
def pay_net(user, money):
    save_to_db('internet', user)

def pay_tv(user, money):
    save_to_db('tv', user)

pay_phone('Marto', 10)
pay_net('Marto', 30)
pay_tv('Marto', 20)

Python has a syntax for this!

Decorators

Essentially, decorators work as wrappers, modifying the behavior of the code before and after a target function execution, without the need to modify the function itself, augmenting the original functionality, thus decorating it.

def add_discount(promo=100):
    def inner(func):
        def discounted(user, money):
            return func(user, money * (promo / 100))
        return discounted
    return inner

@add_discount(promo=50)
def pay_phone(user, money):
    print('money: ', money)
    save_to_db('phone', user)

@add_discount(promo=20)
def pay_net(user, money):
    save_to_db('internet', user)

def pay_tv(user, money):
    save_to_db('tv', user)

pay_phone('Marto', 10)
pay_net('Marto', 30)
pay_tv('Marto', 20)

Decorators with arguments

@send_sms('Phone paid.')
@add_discount(promo=50)
def pay_phone(user, money):
    print('money: ', money)
    save_to_db('phone', user)

@send_email('Internet paid.')
@add_discount(promo=20)
def pay_net(user, money):
    save_to_db('internet', user)

@send_email('TV paid.')
def pay_tv(user, money):
    save_to_db('tv', user)

pay_phone('Marto', 10)
pay_net('Marto', 30)
pay_tv('Marto', 20)

Stacking decorators

Which decorator is going to be called first here?

Python 101 9th Decorators

By Hack Bulgaria

Python 101 9th Decorators

  • 953