Skip to content

Decorators

shamik edited this page Apr 18, 2022 · 11 revisions

Decorators

It's a function which wraps other functions. This definition might not be clear, however, fret not and read on.

Simple Decorators

import functools

def some_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # Doing something before
        value = func(*args, **kwargs)
        # Doing something after
        return value
    return wrapper

@some_decorator
def say_hi():
    """Python says hi to you! 😀"""
    print('Hello there! 😀')
    return {'H':0,'e':1,'l':2,'l':3,'o':4}

The name of the decorator is some_decorator and it's decorating/wrapping another function say_hi with @some_decorator above say_hi. This decorator can be used by using @some_decorator. All that @some_decorator is doing is wrapping say_hi and returning what say_hi is supposed to return. This is just for illustration purposes and might seem very useless at first, but if this decorator boilerplate template is used then you can create pretty impressive decorators.

Digging Deeper

Looking inside some_decorator, there is another function wrapper, which takes in any number of args and kwargs, assigns a value to the function it's wrapping, in our case say_hi, and returns the wrapped function and its values. If we don't return the value parameter then it will not return the dictionary object of say_hi. Try it by commenting return value! Finally we are returning the wrapper object without, which our decorator object would be defunct. Try this too by commenting return wrapper!

Functools as decorator

Importing functools and using it as a decorator is the first use of decorators. @functools.wraps(func) wraps any function passed to some_decorator so that when we introspect func i.e. help(func) or func__name__, it would be pointing to the name and help of func instead of the wrapper function, wrapper, that it's wrapped with. Try removing @functools.wraps(func) and run help(say_hi) and say_hi.__name__.

Standard Decorators

Three main decorators will be looked into:

  1. @property
  2. @classmethod
  3. @staticmethod

The code, which will be used to demonstrate all of them is here and certain sections will be pasted below for illustration.

@property

This decorator is used in the class to instantiate properties of the class. Since the class is a Circle, one of the properties is a radius and the other is an area.

Click to expand the code!
  @property
  def radius(self):
      """Get value of radius"""
      return self._radius

  @radius.setter
  def radius(self, value):
      """Set radius, raise error if negative"""
      if value >= 0:
          self._radius = value
      else:
      raise ValueError("Radius must be positive")

  @property
  def area(self):
      """Calculate area inside circle"""
      return self.pi() * self.radius**2

The radius of a circle must be given to calculate the area, however, one cannot set the area of a circle without the radius. Therefore, the radius is a property of the class circle.

As the radius can have a value, it will be set with a setter method of the property decorator, in our case @radius.setter. More info on the getter, setter, & deleter methods of property here. Note, how the radius method needs to have the same name for setting the @radius.setter. If we were to use the getter & deleter methods for the radius they would have the same method name as setter.

The area doesn't need to be set and therefore doesn't have a setter method like radius.

Since area & radius methods are properties, they can be retrieved as an attribute without (), e.g. Circle.area/Circle.radius.

@classmethod

Classmethods are factory methods, which create a specific instance of the class. The unit_circle is a specific instance of the class circle, and therefore can be instantiated as below.

Click to expand the code!
  @classmethod
  def unit_circle(cls):
      """Factory method creating a circle with radius 1"""
      return cls(1)

unit_circle can access all the methods of Circle and has access to all the other attributes as well.

@staticmethod

This is a method which is independent of the Circle class and can be accessed by other methods in the class. This can be called on either an instance or on the class itself.

Click to expand the code!
@staticmethod
def pi():
    """Value of π, could use math.pi instead though"""
    return 3.1415926535

Cool Decorators

Now that we have an idea about some decorators, let's look at some other cool ones. This directory contains the codes.

Timer Deccorator

The first one is a timer function, which calculates how much time a function takes to execute.

Click to expand the code!
  import functools
  import time


  def timing_func(func):
  """Prints the runtime of any decorated function."""
  @functools.wraps(func)
  def timer_wrapper(*args, **kwargs):
      # perf_counter is used her to measure time intervals
      start_time = time.perf_counter()
      value = func(*args, **kwargs)
      end_time = time.perf_counter()
      # total run time
      run_time = end_time - start_time
      # printing the repr of the function name and
      # the time in seconds
      print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
      return value
  return timer_wrapper


  @timing_func  # wrapping the function
  def timer_testing(some_num_time):
      """Testing the timer decorator."""
      for _ in range(some_num_time):
          # creating a list of cubed numbers
          # and adding them up
          sum([i**3 for i in range(10000)])

Debug decorator

This decorator is useful for checking which function is being called and the value being returned.

Click to the expand the code!
  import math
  import functools


  def debug(func):
  """Print the function arguments and return value."""

      @functools.wraps(func)
      def debug_wrapper(*args, **kwargs):
          # repr to get a list of the function arguments
          args_repr = [repr(a) for a in args]
          # repr to get a list of keyword arguments and their
          # corresponding values
          kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
          signature = ", ".join(args_repr + kwargs_repr)
          # Printing the function name and its arguments
          print(f"Calling {func.__name__}({signature})")
          value = func(*args, **kwargs)
          # the function and their returned values
          print(f"{func.__name__!r} returned {value!r}")
          return value
      return debug_wrapper


  # initialising the debug wrapper
  # for the math.factorial module.
  math.factorial = debug(math.factorial)

  # initialising the debug module
  # for the exp approximation


  @debug
  def approximate_exp(num_terms=10):
      """Approximating exponential by adding
      the inverse of n factorials."""

      # returning the approximated exponential
      return sum(1/math.factorial(num) for num in range(num_terms))

Clone this wiki locally