Python decorators

Python Decorators Tutorial

Decorators in Python are used to add new functionality to an object without changing a code. If a part of the program modifies another part of the program at compile time, it is also called metaprogramming. Everything in Python is an object (even classes) so functions are also first class objects. This means that functions can be used or passed as arguments. For classes, decorators are useful because they allow you to dynamically add functionality without creating subclasses and affecting other objects of the class.

Python 3 decorators are one of the most powerful design possibilities, but not that easy. It can be simple to learn how to use decorators, but writing decorators can get complicated. So you can write more powerful code thanks to the usage of decorators.

 

At first, you need to understand the basic concept that a function is an object. For this, you should know how it is used.

 

  1. A function can be written to a variable. Therefore, the function can be accessed from this variable.

def func():
    print('KoderShop')
 
variable = func
variable()
 
# Output:
# KoderShop

  1. You can declare a function within another function. But then you won’t be able to access the function outside the outer function.

 

def outer_func():
def inner_func():
print('KoderShop')
inner_func()

outer_func()

# Output:
# KoderShop

3. A function can be returned by another function.

def outer_func():
text = 'KoderShop'
def inner_func():
print(text)
return inner_func

variable = outer_func()
variable()

# Output:
# KoderShop

  1. A function can be used as arguments in another function.

def argument_func():
print('KoderShop')

def func(function):
print('Welcome to')
function()

func(argument_func)

# Output:
# Welcome to
# KoderShop

There are two different kinds of decorators:

  • Python function decorators
  • Class decorator python

Syntax of Decorator in Python

Decorator is a function that as an argument takes another function, adds additional functionality, and then returns a new version of that function.

A Python function decorator usually is called first and then a definition of a function you want to modify. It is similar to the function inside another function that you saw earlier.

def decorator_func(function):
def wrapper_func():
# Something before the function
function()
# Something after the function
return wrapper_func

return wrapper

Python decorator takes a function as an argument, therefore you should define a function and pass it to the decorator. Also you can set the function to a variable.

def func():
return 'KoderShop'

variable = decorator_func(func)
variable()

However, in Python you can use the @ symbol before the function you would like to decorate. With this decorator function in Python becomes much easier.

@decorator_func
def func():
return 'KoderShop'

func()

Python decorator example:

def example_power(function):
def temp(a, b):
print(a, "powered by", b)
if b == 0:
print("Whoops!")
return
return function(a, b)
return temp

@example_power
def power(a, b):
print(a**b)

power (2, 3)

Python Decorator with Arguments

Arguments can be passed to decorators. If you want to add arguments to decorators you need to add *args and **kwargs to the inner functions.

  • *args can take arguments of any type, such as True, 14 or ‘KoderShop’.
  • **kwargs can take keyword arguments, such as count = 100 or name = ‘KoderShop’.

Syntax of decorator:

def decorator_func(func):
def wrapper(*args, **kwargs):
# Something before the function.
func(*args, **kwargs)
# Something after the function.
return wrapper

@decorator_func
def func(arg):
pass

Python Decorator Order

You do not need to use only one decorator. You can use both simultaneously. Or not only two… Anyway, here you can see the simple Python decorator tutorial:

def row_of_symbols1(function):
def limits():
print('*' * 30)
function()
print('*' * 30)
return limits
def row_of_symbols2(function):
def limits():
print('#' * 20)
function()
print('#' * 20)
return limits

@row_of_symbols1
@row_of_symbols2
def example():
print("Hello KoderShop!")
example()

# Output:
# ******************************
# ####################
# Hello KoderShop!
# ####################
# ******************************

As we can see the octothorpes (#) are sandwiched by asterisks(*). So it leads us to the only conclusion that in Python the order of decorators that are used in the pie looking way matters. Row_of_symbols1 is first so the asterisks are first.

Built-in Decorators in Python

Does Python have built-in decorators? Factually, yes, it has. But, decorators don`t differ from general functions. Decorators are the only fancier way to call functions.

def f(): 
# ...

f = method(f) 

@method 
def f():

So any built-in function can be used as a decorator.

@property Decorator Python

One of the built-in external decorators is @property. It is similar to the property() function in Python. We can use it to have special functionality to methods and make them act as getters, setters or even deleters in defining properties in a class.

 

For instance, we are modeling a class with the name PC (Personal Computer):

class PC:
def __init__(self, price):

self.price = price

As we can see, this attribute is public as it doesn`t have an underscore. So that means anyone in the developer team can access and change the attribute anywhere in the program:

element.price
element.price = 1000

Here we have ‘element’ that is a reference for instance in class ‘PC’. Anyway it works greatly. But if you want to make this attribute protected, you will make it in a different way. Here getters and setters come.

 

You should change the whole look of the code to make it done. The code will be like this:

 

element.get_price()
element.set_price = 1000

And exactly now the @property Python class decorator can be used.

@property Syntax

class PC:
def __init__(self, price):
self._price = price

@property
def price(self):
return self._price

@price.setter
def price(self, example_price):
if example_price > 0 :
self._price = example_price
else:
print("Please enter another price")

@price.deleter
def price(self):
del self._price

def __init__(self, price):
self._price = price

Here we can see that initialization we have written with underline. It means that our price attribute is protected and it shows other developers that it can`t be modified directly outside the class.

 

For having access with the decorator in class Python to this attribute`s value we used @price.getter, for setting our value we used @price.setter and @price.deleter to delete this instance of attribute. And the question is: why have we done that?

Using @property we do not need to change the value of the class attribute directly inside the code of the class. We can change it a lot of times as we want and without any changes of the class syntax.

@property
def price(self):
return self._price

Getter Syntax Explanation

It’s our method that gets the value. @property text is used to identify for us that we are making properties. def price(self) we use to access and modify the attribute outside of the class. It takes the parameter self that is used for reference to the instance. return self._price is used for returning the value of a protected attribute.

Decorator example Python:

pc = PC(1000)
print(pc.price)
# Output:
# 1000

As we can see, now we have access to the price as if it was public. But we are not changing any syntax of the class.

Setter Syntax Explanation

@price.setter
def price(self, example_price):
if example_price > 0 :
self._price = example_price
else:
print("Please enter another price")

If we need to change the value we need to write the setter class method Python decorator. As the name says @price.setter is the setter. It writes using the name of the attribute and ‘.setter’. def price(self, example_price): again we use self parameter and we have a second one – example_price. It is the new value that we are assigning to the price.

In the body actually we see the simple if function to check if the new value is more than 0.

Example:

pc = PC(1000)
pc.price = 970
print(pc.price)
# Output:
# 970

 

Againly, we can see that we don`t change any syntax

Deleter Syntax

@price.deleter
def price(self):
del self._price

Deleting value we can by ‘.deleter’. Here @price.deleter is used to indicate actually that this is the deleter method for the price. Explanation to def price(self) same as to getter. And by using del self._price we delete the instance attribute.

After all these methods we can see that all of them have used the same name of our property.

Example:

pc = PC(1000)
pc.price = 970
print(pc.price)
del pc.price
print(pc.price)
# Output:
# 970
# AttributeError: 'PC' object has no attribute '_price'

We can see that the error message comes up when we try to access the price property.

@staticmethod Python Decorators Explained

In Python a built-in decorator that defines a static method is one of the Python class decorators and is called @staticmethod. A static method is called by an instance of a class or by the class itself. Also it cannot access the properties of the class itself, but It can return an object of the class.  A static method can be called using class_name.method_name(). @staticmethod can be useful if you need a function that doesn’t access any properties of a class but it belongs to this class, you can use a static function.

Function Decorations with @staticmethod

class Class_name:
@staticmethod
def method_name(arg1, arg2, arg3, ...): ...

Example:

class PC:
def __init__(self):
self.price = 1000

@staticmethod
def example():
print("Buy PCs in KoderShop!")

PC.example()

PC().example()

pc = PC()
pc.example()

# Output:
# Buy PCs in KoderShop!
# Buy PCs in KoderShop!
# Buy PCs in KoderShop!

@classmethod

In Python a built-in decorator, which returns a class method for a function is called @classmethod. This function is a method that is bound to a class rather than its object. Also class methods can be called with an object or with a class (class_name.method_name() or class_name().method_name()). Unlike a static method, a class method is attached to a class with the first argument as the class itself cls, so @classmethod always works with the class.

Function Decoration with @classmethod

@classmethod
def method_name(cls, arg1, arg2, ...): ...

Example:

class PC:
videocard = 'Nvidia RTX 4080'
cpu = 'AMD Ryzen 9'
def __init__(self):
self.price = 1000

@classmethod
def example(cls):
print("PC attributes are:", cls.videocard, ',', cls.cpu )

PC.example()

# Output:
# PC attributes are: Nvidia RTX 4080 , AMD Ryzen 9

Use of Decorators in Python

To conclude, we can say that a decorator function python can be used when we need to change the behavior of a function without modifying it. The answer to the question “where can we use decorators?” can be very wide, but a few good examples are when you want to add logging, test performance, perform caching, verify something, and so on. You can also use one when you want to repeat the code on multiple functions.

 

Sometimes decorators can be used to short your code a bit. For example instead of:

def example(ID, name):
if not (exampletype(ID, 'uint') and exampletype(name, 'utf8string')):
raise exampleexception() ...

You just use:

@accepts(uint, utf8string)
def example(ID, name):
...

And the accepts() decorator does all the work for us. So the conclusion is: choose your own decorator that will help you solve the exercise.

Debugging Decorators Python

Decorator is a very useful feature that can be used to modify different functions. However, some errors can appear in this process. When you change a function to a decorator the metadata of this function gets lost.

Lets see the program below but here we have decorate used in another way than usual:

 

def example_question(func):
def wrapper():
"""Returns a question message"""
neutral_message = func()
happy_message = neutral_message + " Where are you from?"
return happy_message
return wrapper

def speak():
"""Returns a Hi! message"""
return "Hi!"

example_message = example_question(speak)

print(example_message(),'\n')
print(speak.__name__, '->', speak.__doc__) 
print(example_message.__name__, '->', example_message.__doc__)
#Output:
#Hi! Where are you from? 
#
#speak -> Returns a Hi! message
#wrapper -> Returns a question message

When you try to access metadata of the example_message function it returns the metadata of the function wrapper that is inside the decorator.

Python provides a @functools.wraps decorator to solve this problem. This decorator helps you to copy the lost metadata of the undecorated function:

import functools
def example_question(func):
@functools.wraps(func)
def wrapper():
"""Returns a question message"""
neutral_message = func()
happy_message = neutral_message + " Where are you from?"
return happy_message
return wrapper

def speak():
"""Returns a Hi! message"""
return "Hi!"

example_message = example_question(speak)

print(example_message(),'\n')

print(speak.__name__, '->', speak.__doc__) 
print(example_message.__name__, '->', example_message.__doc__)
#Output:
#Hi! Where are you from? 
#
#speak -> Returns a Hi! message
#speak -> Returns a Hi! message

@wrapper Python

Function wrappers are also known as decorators, which are a very useful tool in Python because they allow to change the behavior of a function or a class. Decorators allow us to wrap functions to extend the behavior of the wrapped function. In decorators, functions are taken as arguments to another function and then called inside the Python wrapper function.

@wrapper
def function(func1):
example(func2)

This example is also similar to:

def function(func1):
example(func2)

function = wrapper(function)