Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

daisy-chaining Python/Django custom decorators

Is it good style to daisy-chain Python/Django custom decorators? And pass different arguments than received?

Many of my Django view functions start off with the exact same code:

@login_required
def myView(request, myObjectID):
    try:
        myObj = MyObject.objects.get(pk=myObjectID)
    except:
        return myErrorPage(request)       

    try:
        requester = Profile.objects.get(user=request.user)
    except:
        return myErrorPage(request)

    # Do Something interesting with requester and myObj here

FYI, this is what the corresponding entry in urls.py file looks like:

url(r'^object/(?P<myObjectID>\d+)/?$', views.myView, ),

Repeating the same code in many different view functions is not DRY at all. I would like to improve it by creating a decorator that would do this repetitive work for me and make the new view functions much cleaner and look like this:

@login_required
@my_decorator
def myView(request, requester, myObj):        
    # Do Something interesting with requester and myObj here

So here are my questions:

  1. Is this a valid thing to do? Is it good style? Notice that I will be changing the signature of the myView() function. That feels a bit strange and risky to me. But I'm not sure why
  2. If I make multiple such decorators that do some common function but each call the wrapped function with different arguments than the decorator received, is it OK if I daisy-chain them together?
  3. If it is OK to #1 and #2 above, what is the best way to indicate to the users of this myView what the set of arguments are that they should pass in (because just looking at the parameters in the function definition is no longer really valid)
like image 385
Saqib Ali Avatar asked Mar 26 '14 03:03

Saqib Ali


Video Answer


2 Answers

That's a very interesting question ! Another one has already been answered in depth on the basic usage of decorators. But it does not provide much insight on modifying arguments

Stackable decorators

You can find on that other question an example of stacked decorators with the following piece of explanation hidden in a very, very long and detailed answer :

Yes, that’s all, it’s that simple. @decorator is just a shortcut to:

another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function)

And that's the magic. As python documentation states : a decorator is a function returning another function.

That means you can do :

from functools import wraps

def decorator1(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        do_something()
        f(*args, **kwargs)
    return wrapper


def decorator2(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        do_something_else()
        f(*args, **kwargs)
    return wrapper

@decorator1
@decorator2
def myfunc(n):
    print "."*n

#is equivalent to 

def myfunc(n):
    print "."*n
myfunc = decorator1(decorator2(myfunc))

Decorators are not Decorators

Python decorators might be puzzling for developpers who learned OOP with a language where GoF has already used half of the dictionary to name the patterns who fix the failures of the language is the de-facto design pattern shop.

GoF's decorators are subclasses of the component (interface) they're decorating, therefore sharing this interface with any other subclass of that component.

Python decorators are functions returning functions (or classes).

Functions all the way down

A python decorator is a function returning a function, any function.

Most decorators out there are designed to extend the decorated function without getting in the way of it's expected behavior. They are shaped after GoF's definition of the Decorator pattern, which describes a way to extend an object while keeping it's interface.

But GoF's Decorator is a pattern, whereas python's decorator is a feature.

Python decorators are functions, these functions are expected to return functions (when provided a function).

Adapters

Let's take another GoF pattern : Adapter

An adapter helps two incompatible interfaces to work together. This is the real world definition for an adapter.

[An Object] adapter contains an instance of the class it wraps. In this situation, the adapter makes calls to the instance of the wrapped object.

Take for example an object — say a dispatcher, who would call a function which takes some defined parameters, and take a function who would do the job but provided another set of parameters. Parameters for the second function can be derived from those of the first.

A function (which is a first-class object in python) who would take the parameters of the first and derive them to call the second and return a value derived from its result would be an adapter.

A function returning an adapter for the function it is passed would be an adapter factory.

Python decorators are functions returning functions. Including adapters.

def my_adapter(f):
    def wrapper(*args, **kwargs):
        newargs, newkwargs = adapt(args, kwargs)
        return f(*newargs, **newkwargs)

@my_adapter # This is the contract provider
def myfunc(*args, **kwargs):
    return something()

Oooooh, I see what you did there… is it good style ?

I'd say, hell yeah, yet another built-in pattern ! But you'd have to forget about GoF Decorators and simply remember that python decorators are functions which return functions. Therefore, the interface you're dealing with is the one of the wrapper function, not the decorated one.

Once you decorate a function, the decorator defines the contract, either telling it's keeping the interface of the decorated function or abstracting it away. You don't call that decorated function anymore, it's even tricky to try it, you call the wrapper.

like image 182
ddelemeny Avatar answered Sep 29 '22 05:09

ddelemeny


First of all, this block of code:

try:
    myObj = MyObject.objects.get(pk=myObjectID)
except:
    return myErrorPage(request)

can be replaced with:

from django.shortcuts import get_object_or_404
myObj = get_object_or_404(MyObject, pk=myObjectID)

The same applies with the second block of code you have.

That in and of itself makes this a lot more elegant.

If you'd like to go further and implement your own decorator, your best bet is to subclass @login_required. If you're passing different arguments or don't want to do that, then you can indeed make your own decorator and it wouldn't be wrong.

like image 26
ubadub Avatar answered Sep 29 '22 05:09

ubadub