Flickr Badge

Tuesday, December 19, 2006

Using python decorators to implement guards

One cool technique that I learnt while going through the Django code was using python decorators to implement guards.

What are guards?

Take a look at this bit of pseudo-code
if condition1:
if condition2:
{do_something}
else:
return error2
else:
return error1
This is a common pattern where you do something provided condition1 and condition2 are false. The problem with this code is that it is difficult to seperate out the core logic of the function contained in {do_something} and the error handling code in the rest of the function. Another disadvantage is that the condition is at the top of the function, while the failure action is at the bottom. This makes it difficult to correlate the condition with the failure action.

The solution is to refactor the code to use guards.
if not condition1:
return error1

if not contidion2:
return error2

{do_something}
Guards are the conditions at the top of the function. They act like security guards — If the condition passes you go through, otherwise you leave the function. It is now a lot easier to see the conditions and the failure actions, and you can easily identify the code logic block by just skipping past the guards.

Python decorators

Python has a decorator feature that allows you to modify the function that it is applied to. Here is an example:
def decorate(fn):
def _decorate():
print "before calling"
fn()
print "after calling"
return _decorate

@decorate
def myfunction():
print "in function"
What we have is a function myfunction that prints the string "in function". To this, we apply the decorator 'decorate' (denoted by the @ symbol). decorate is itself a function that takes one function as a parameter and returns another function. In this case, it takes fn as a parameter and returns _decorate. Everytime myfunction is called, it will actually call the returned function, in this case _decorate. _decorate prints "before calling", then it calls the original function, then prints "after calling". Here is the output
>>> myfunction()
before calling
in function
after calling

Implementing guards using decorators

We can now see how guards can be implemented using decorators. Let me take a real example that I've encountered — my admin page. My tool has an admin page. In order to access this page, you must be logged in, and you must be an admin. If you are not logged in, you need to be redirected to the login page. If you are not an admin, an error message should be displayed. This is how the code would normally have looked
# check for login
try:
user = request.session["user"]
except KeyError:
# redirect user to login page

# check for admin
if not user.isAdmin():
# display error page

# show admin page
...
Here is a version using decorators
# login decorator checks whether the user is logged in
def login_required(fn):
def _check(request, *args, **kwargs):
try:
user = request.session["user"]
except KeyError:
# redirect to login page
# user is logged in, call the function
return fn(args, kwargs)
return _check

# admin decorator checks whether the user is an admin
def admin_required(fn):
def _check(request, *args, **kwargs):
user = request.session["user"]
if not user.isAdmin():
# return the error page
# user is admin, call the function
return fn(args, kwargs)
return _check

@login_required
@admin_required
def admin(request):
# show admin page
...
This is how it works. We first have the admin function. All it does is implement the admin code. We decorate it with the admin_required and login_required decorators. When the admin function is called, it first enters the login_required decorator function which checks for login. If the user is not logged in, it redirects to the login page, else it calls the function. The function passed to login_required is the admin_required decorated function. So if login passes, it calls the admin_required decorator, checks for admin. If the user is not an admin, it displays an error message, else calls the function, which in this case is the original admin function.

See how neatly the guards are separated from the core logic. The admin function contains only the core logic, while the list of guards is neatly arranged as decorators. This method also has another advantage — it is easy to apply the same guards to other functions. Take a look at this
# no decorators
def login(request):
...

@login_required
@admin_required
def admin(request):
...

@login_required
def dashboard(request):
...

@login_required
@project_permission_required
def view_project(request, project):
...
Now each function only implements the core logic, while all the guard logic is taken care of by the decorators. It is easy to see the core logic and easy to see the guard conditions applied for the function. If I want some other function to have a guard, I can just add a decorator to it without touching the core logic. Best of all, the code is self descriptive and very easy to read.

This post is a part of the selected archive.

21 comments:

Sachin said...

Hey this Guards pattern is realy amazing. How simple yet so powerful. I am maintaining an application which has at times 5-6 levels of nested if-else conditions and trying to figure out what actually is happening in that complex structure is always a pain.

Cool Stuff

Siddhi said...

Yeah, the guards refactoring is really nice, and python decorators make it even better.

Anonymous said...

This is really nice, thanks for the tip.

One thing, though. Wouldn't it make more sense to decorate admin_required with login_required?

Currently, you have to remember to decorate with login_required first, and then admin_required. This seems prone to errors.

Anonymous said...

Very nice simple and straight forward explanation for decorator wrappers.

These are indeed very clean and simple looking guards. It made me start thinking about Zope's method of using class based security wrappers which can feel really confusing and klunky at times. However, on thinking about it I realised that zope's method is probably the better large-scale design; though i may be ignorant of some of the technicalities of decorators. The advantages of the class based wrapper/registry it seems to me are: 1) entire classes are protected with default policies. 2) individual methods/attributes can override the default policy. 3) the wrapper system makes introspection/reporting easy to find out what objects have what protection.... just query the wrapper/registry (though there must be some way to find out what decorators apply to a function somehow).

Now meta-classes... that might be another story.

This is just off the top of my head. I'm not a Zope guru.

Siddhi said...

Randall: Nice. Some of the Django decorators in the default auth framework also do something like that

Johannes: You're right, it does make sense to decorate admin_required with login_required

T.Middleton: I've never used zope either, but it does sound a lot more flexible and fine grained than using access control via decorators. For one thing, decorators, once applied, remain applied. There is no way that I'm aware of where you can remove a decorator at runtime. However, this method certainly scores in simplicity, so it may be a matter of balancing simplicity with flexibility.

Anonymous said...

Sidd,

Was looking for a starter on Decorators as I have to give a talk about it. Just got your blog link from your mail. This is a great piece of explanation of Decorators. The python.org pep-0318 has a good explanation but your examples are very easy to understand. Thanks and write more like this one :)

(If you can guess who I am... ;) )

Anonymous said...

In the example
@login_required
@admin_required
def admin(request):
# show admin page

I think the admin_required decorator is applied first and then the login_required decorator is applied to the already decorated admin function.

login_required(admin_required(admin))
Is it right?

Siddhi said...

Yes thats right. So, during execution, it will enter the login_required function first.

Anonymous said...

Thank you for this write up! It was exactly what I needed to help me get my head wrapped around decorators. This is also the first information I found illustrating decorators as guards which I knew Django was doing, and I knew I wanted to also, and will get a lot of mileage out of this knowledge.

Also, I enjoy your Django screencasts when you do them. Keep up the great work!

rocketmonkeys said...

Hey, minor typo in your code. In the admin_required() function, the 2nd to last line:
return fn(args, kwargs)

This calls the fn() function *without* the 'request' var, and with two dict's as parameters. To handle this properly, you should use this:
return fn(request, *args, **kwargs)

This will call the fn() as if it were directly called, preserving all params including the request parameter. It's a minor typo, but it caught me up while trying out your code.

Thanks so much for this howto! I was looking through django's login_required() decorator source code, which is unnecessarily complex for something like this, and obscures the issue with extra layers. This was simpler to understand, and I now actually have a working permission-checking decorator finally. Thanks again!

sam said...

Its a very informative article.Thanks for sharing.

Austere Technologies provide best cloud services. For anyone wants the best cloud services and any other IT services please visit Mobile app development Services

Unknown said...

Nice interesting article to read your blog. we provide best serices of mobile application development read more information mobile-application-development

Deepika said...

Very good informative article. Thanks for sharing such nice article, keep on up dating such good articles.

TOP CLOUD SERVICES | ORACLE CLOUD SERVICES FOR APPLICATION DEVELOPMENT | MASSIL TECHNOLOGIES

Unknown said...

Very interesting, informative article, Thanks for sharing such a nice article.

Best Digital Transformation Services | DM Services | Austere Technologies

Austere said...

Excellent blog. Thanks for sharing.

Best Mobility Services | Austere Technologies

Deepika said...

REALLY VERY EXCELLENT INFORMATION. I AM VERY GLAD TO SEE YOUR BLOG FOR THIS INFORMATION. THANKS FOR SHARING. KEEP UPDATING.
Best IT Security Services | Austere Technologies

Deepika said...

Great article, really very helpful content you made. Thank you, keep sharing.

Software Testing | Austere Technology

Unknown said...

Excellent information you made in this blog, very helpful information. Thanks for sharing.

chartered accountant | Avinash college of commerce

sandy said...

Thank you for sharing this valuable information. But get out this busy life and find some peace with a beautiful trip. book Andaman family tour packages

Deepika said...

Excellent informative blog, keep for sharing.

Best System Integration services | Massil Technologies

markson said...

The computerized twist suitably named Python has crawled and slides during the profundities of opportunity and arrived out fruitful.
machine learning certification