python class decorators

Enabling Code Reuse with Decorators – Part 2

Welcome to the 2nd part of my python decorator series. In my first post, I introduced you to decorators.
In this post, I am going to show you how to write class decorators and decorate class methods.

OUTLINE OF DECORATOR SERIES
i. Introduction to decorators and decorating python functions.
ii.Writing class decorators and decorating class methods (this article)
iii. Decorators that take arguments.

Defining Decorators as classes

To define a class decorator, we need to make sure it implements the__call__() and __get__() method.
The __call__ method enables us to write classes where the instances behave like functions and can be called like a function.
The __get__ method is used to get the attribute of the owner class or instance of that class.

Now let’s write our class decorator

Example.
Here, we’ll implement a decorator that checks if all the characters in the user name are alphabetic and if they are and capitalize the first letter.

import functools
import types

class StringFormatter:

    def __init__(self, func):
        self.func = func
        # functools.wraps for classes...         
        functools.update_wrapper(self, func)

    def __call__(self, *args, **kwargs):
        function_result = self.func(*args, **kwargs)
        if function_result.isalpha():
            return function_result.capitalize()
        else:
            return "incorrect text format"
        
    def __get__(self, instance,cls):
        if instance is None:
            return self
        else:
            return types.MethodType(self, instance)


The class decorator above can be used either with a python function or a class.
The only notable difference between functions and classes is that functools.wraps is now replaced with  functools.update_wrapper in the __init__ method.

@StringFormatter
def input_name(first_name):
    return first_name

class UserRegistration: 
    @StringFormatter
    def print_name(self,name):
        return name

print(input_name("GH567"))
print(input_name("linda"))

user = UserRegistration()
user.print_name("thomas")
'incorrect text format'
'Linda'
'Thomas'

Decorating class functions (methods)

Decorating class functions is very similar to regular functions, but you need to be aware of the required first argument, self— the class instance.

We would test our first class decorator (StringFormatter) and also create a new function decorator to test our class methods.
Design a simple decorator that checks if the specified user age is of the specified type and returns the output.

def check_type(function):
        @functools.wraps(function)  
        def wrapper( *args,**kwargs):
            result = function(*args,**kwargs)
            if isinstance(result, int):
                return result
            else:
                return "Your age is incorrect"
        return wrapper
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
    @check_type   
    def input_age(self):
        return self.age
    
    @StringFormatter
    def input_name(self):
        return self.name

person = Person("john", 56)
print(person.input_age())
print(person.input_name())
56
John

Applying Decorators to Class and Static Methods

The difference between a @classmethod and a @staticmethod is fairly simple. The classmethod passes a class object instead of a class instance (self), and staticmethod skips both the class and the instance entirely. This effectively makes staticmethod very similar to a regular function outside of a class.
I recently wrote an article on decoding the staticmethod and classmethod, please check it here.

Applying decorators to class and static methods is straightforward, but make sure that your decorators are applied before @classmethod or @staticmethod because If you get the order of decorators wrong, you’ll get an error
For example:  we have a decorator called @deco

def deco(function):
    return function

class A:
        
    @deco   
    def instance_method(self):
        pass
    
    @classmethod
    @deco
    def class_method(cls):
        pass
    
    @staticmethod
    @deco
    def static_method():
        pass

Let us design a simple application to get started and apply our already created @check_type decorator.

from datetime import datetime as dt

class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
        
    @staticmethod
    @check_type
    def deduce_birthday(name, year):
        age= dt.now().year - year
        return age
        

    @classmethod
    @check_type
    def compute_age_diff(cls,name, year):
        age= dt.now().year - year
        return age


user_1 = User.compute_age_diff("Jerry", 1998)
print(user_1)
user_2 = User.compute_age_diff("Jerry", 1209.98)
print(user_2)
22
Your age is incorrect

You can see clearly the decorator (@checktype checks if the age is an int) because our age was 810.02 and is a float type object, our output was “Your age is incorrect”

user_3 = User.deduce_birthday("Jerry", 1998)
print(user_3)
user_4 = User.deduce_birthday("Jerry", 1209.98)
print(user_4)
22
Your age is incorrect

In my next post, we look at creating decorators that take arguments

Thanks for reading, please share with your network, if you found this post helpful, comment, and share your thoughts in the comment section.

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *