Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python decorator best practice, using a class vs a function

As I've understood it there are two ways to do a Python decorator, to either use the __call__ of a class or to define and call a function as the decorator. What's the advantages/disadvantages of these methods? Is there one preferred method?

Example 1

class dec1(object):     def __init__(self, f):         self.f = f     def __call__(self):         print "Decorating", self.f.__name__         self.f()  @dec1 def func1():     print "inside func1()"  func1()  # Decorating func1 # inside func1() 

Example 2

def dec2(f):     def new_f():         print "Decorating", f.__name__         f()     return new_f  @dec2 def func2():     print "inside func2()"  func2()  # Decorating func2 # inside func2() 
like image 985
olofom Avatar asked Apr 24 '12 08:04

olofom


People also ask

Should I use functions or classes in Python?

You should use classes only if you have more than 1 function to it and if keep a internal state (with attributes) has sense. Otherwise, if you want to regroup functions, just create a module in a new . py file.

When would you use a function decorator?

You'll use a decorator when you need to change the behavior of a function without modifying the function itself. A few good examples are when you want to add logging, test performance, perform caching, verify permissions, and so on. You can also use one when you need to run the same code on multiple functions.

Can a class be a decorator Python?

In Python, decorators can be either functions or classes.

How are decorators different from functions?

Decorators provide a simple syntax for calling higher-order functions. By definition, a decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it.


1 Answers

It is rather subjective to say whether there are "advantages" to each method.

However, a good understanding of what goes under the hood would make it natural for one to pick the best choice for each occasion.

A decorator (talking about function decorators), is simply a callable object that takes a function as its input parameter. Python has its rather interesting design that allows one to create other kinds of callable objects, besides functions - and one can put that to use to create more maintainable or shorter code on occasion.

Decorators were added back in Python 2.3 as a "syntactic shortcut" for

def a(x):    ...  a = my_decorator(a) 

Besides that, we usually call decorators some "callables" that would rather be "decorator factories" - when we use this kind:

@my_decorator(param1, param2) def my_func(...):    ... 

the call is made to "my_decorator" with param1 and param2 - it then returns an object that will be called again, this time having "my_func" as a parameter. So, in this case, technically the "decorator" is whatever is returned by the "my_decorator", making it a "decorator factory".

Now, either decorators or "decorator factories" as described usually have to keep some internal state. In the first case, the only thing it does keep is a reference to the original function (the variable called f in your examples). A "decorator factory" may want to register extra state variables ("param1" and "param2" in the example above).

This extra state, in the case of decorators written as functions is kept in variables within the enclosing functions, and accessed as "nonlocal" variables by the actual wrapper function. If one writes a proper class, they can be kept as instance variables in the decorator function (which will be seen as a "callable object", not a "function") - and access to them is more explicit and more readable.

So, for most cases it is a matter of readability whether you will prefer one approach or the other: for short, simple decorators, the functional approach is often more readable than one written as a class - while sometimes a more elaborate one - especially one "decorator factory" will take full advantage of the "flat is better than nested" advice fore Python coding.

Consider:

def my_dec_factory(param1, param2):    ...    ...    def real_decorator(func):        ...        def wraper_func(*args, **kwargs):            ...            #use param1            result = func(*args, **kwargs)            #use param2            return result        return wraper_func    return real_decorator 

against this "hybrid" solution:

class MyDecorator(object):     """Decorator example mixing class and function definitions."""     def __init__(self, func, param1, param2):         self.func = func         self.param1, self.param2 = param1, param2      def __call__(self, *args, **kwargs):         ...         #use self.param1         result = self.func(*args, **kwargs)         #use self.param2         return result  def my_dec_factory(param1, param2):     def decorator(func):          return MyDecorator(func, param1, param2)     return decorator 

update: Missing "pure class" forms of decorators

Now, note the "hybrid" method takes the "best of both Worlds" trying to keep the shortest and more readable code. A full "decorator factory" defined exclusively with classes would either need two classes, or a "mode" attribute to know if it was called to register the decorated function or to actually call the final function:

class MyDecorator(object):    """Decorator example defined entirely as class."""    def __init__(self, p1, p2):         self.p1 = p1         ...         self.mode = "decorating"     def __call__(self, *args, **kw):         if self.mode == "decorating":              self.func = args[0]              self.mode = "calling"              return self          # code to run prior to function call          result = self.func(*args, **kw)          # code to run after function call          return result  @MyDecorator(p1, ...) def myfunc():     ... 

And finally a pure, "white colar" decorator defined with two classes - maybe keeping things more separated, but increasing the redundancy to a point one can't say it is more maintainable:

class Stage2Decorator(object):     def __init__(self, func, p1, p2, ...):          self.func = func          self.p1 = p1          ...     def __call__(self, *args, **kw):          # code to run prior to function call          ...          result = self.func(*args, **kw)          # code to run after function call          ...          return result  class Stage1Decorator(object):    """Decorator example defined as two classes.        No "hacks" on the object model, most bureacratic.    """    def __init__(self, p1, p2):         self.p1 = p1         ...         self.mode = "decorating"     def __call__(self, func):        return Stage2Decorator(func, self.p1, self.p2, ...)   @Stage1Decorator(p1, p2, ...) def myfunc():     ... 

2018 update

I wrote the text above a couple years ago. I came up recently with a pattern I prefer due to creating code that is "flatter".

The basic idea is to use a function, but return a partial object of itself if it is called with parameters before being used as a decorator:

from functools import wraps, partial  def decorator(func=None, parameter1=None, parameter2=None, ...):     if not func:         # The only drawback is that for functions there is no thing         # like "self" - we have to rely on the decorator          # function name on the module namespace         return partial(decorator, parameter1=parameter1, parameter2=parameter2)    @wraps(func)    def wrapper(*args, **kwargs):         # Decorator code-  parameter1, etc... can be used          # freely here         return func(*args, **kwargs)    return wrapper 

And that is it - decorators written using this pattern can decorate a function right away without being "called" first:

@decorator def my_func():     pass 

Or customized with parameters:

@decorator(parameter1="example.com", ...): def my_func():     pass                   

2019 - With Python 3.8 and positional only parameters this last pattern will become even better, as the func argument can be declared as positional only, and require the parameters to be named;

def decorator(func=None, *, parameter1=None, parameter2=None, ...): 
like image 113
jsbueno Avatar answered Sep 18 '22 13:09

jsbueno