SoFunction
Updated on 2024-10-29

Python Tips for Writing Decorators Using Classes

I recently learned an interesting way to write a decorator, so I'll document it.

A decorator is a function that returns a function. To write a decorator, in addition to the most common definition of a function within a function, Python allows a decorator to be defined using a class.

1. Write decorators with classes

The following implements a cache decorator in a common writeup.

def cache(func):
  data = {}
  def wrapper(*args, **kwargs):
    key = f'{func.__name__}-{str(args)}-{str(kwargs)})'
    if key in data:
      result = (key)
      print('cached')
    else:
      result = func(*args, **kwargs)
      data[key] = result
      print('calculated')
    return result
  return wrapper

Look at the effect of caching.

@cache
def rectangle_area(length, width):
  return length * width
rectangle_area(2, 3)
# calculated
# 6
rectangle_area(2, 3)
# cached
# 6

The @cache of the decorator is a syntactic sugar equivalent to thefunc = cache(func)What if cache is not a function, but a class? Define a class Cache, then callfunc = Cache(func)An object will be obtained, and the func returned at this point is actually an object of Cache. Defining the __call__ method turns an instance of the class into a callable object that can be called like a function. Then call the original func function in the __call__ method to realize the decorator. So the Cache class can also be used as a decorator and can be used as @Cache.

Next rewrite the cache function as a Cache class:

class Cache:
  def __init__(self, func):
     = func
     = {}
  def __call__(self, *args, **kwargs):
    func = 
    data = 
    key = f'{func.__name__}-{str(args)}-{str(kwargs)})'
    if key in data:
      result = (key)
      print('cached')
    else:
      result = func(*args, **kwargs)
      data[key] = result
      print('calculated')
    return result

Look at the cached results again, it works the same.

@Cache
def rectangle_area(length, width):
  return length * width
rectangle_area(2, 3)
# calculated
# 6
rectangle_area(2, 3)
# cached
# 6

2. Methods for decorating classes

Decorators can decorate more than just functions and are often used to decorate methods of classes, but I've found that decorators written in classes can't be used directly to decorate methods of classes. (A bit of a detour...)

Let's first look at how a function-written decorator decorates the methods of a class.

class Rectangle:
  def __init__(self, length, width):
     = length
     = width
  @cache
  def area(self):
    return  * 
r = Rectangle(2, 3)
()
# calculated
# 6
()
# cached
# 6

But if you switch directly to the Cache class you will get an error, the reason for this error is that the area is decorated and becomes a property of the class, not a method.

class Rectangle:
  def __init__(self, length, width):
     = length
     = width
  @Cache
  def area(self):
    return  * 
r = Rectangle(2, 3)
()
# TypeError: area() missing 1 required positional argument: 'self'

# <__main__.Cache object at 0x0000012D8E7A6D30>

# <__main__.Cache object at 0x0000012D8E7A6D30>

Going back to the case without decorators, Python turns functions into methods after instantiating the object.

class Rectangle:
  def __init__(self, length, width):
     = length
     = width

  def area(self):
    return  * 


# <function  at 0x0000012D8E7B28C8>
r = Rectangle(2, 3)

# <bound method  of <__main__.Rectangle object

So the solution is simple, to decorate the methods of a class with class-written decorators, just wrap the callable objects as functions.

# Define a simple decorator that does nothing but wrap the callable object as a function
def method(call):
  def wrapper(*args, **kwargs):
    return call(*args, **kwargs)
  return wrapper
class Rectangle:
  def __init__(self, length, width):
     = length
     = width
  @method
  @Cache
  def area(self):
    return  * 
r = Rectangle(2, 3)
()
# calculated
# 6
()
# cached
# 6

Or with @property you can also turn methods into properties directly.

class Rectangle:
  def __init__(self, length, width):
     = length
     = width
  @property
  @Cache
  def area(self):
    return  * 
r = Rectangle(2, 3)

# calculated
# 6

# cached
# 6

summarize

Writing decorators with classes is not a special trick, and it's really not necessary to do so in general, but this allows you to use some class features to write decorators, such as class inheritance, which kind of provides another way of thinking about it.