Monday 21 November 2011

Decorators in Python (@ annotations)

In Java, there is a feature called annotations which allows the developer to add metadata to source code (members variables, methods, classes, etc). This metadata is written using the @annotation syntax and can then be read using reflection. Python has a similar syntax that can be used. However, it's not called annotations and it is not really the same thing. In Java for example, this only adds a piece of metadata to the annotated code, whereas in Python we call this syntax Decorators. They are named appropriately because it essentially is the decorator pattern. Here are a few examples of built-in decorators in Python:
@staticmethod
@classmethod
@property
The really cool thing about python's decorator is to create your own custom ones. Basically, when you decorate a function or a class, this is what happens behind the scene:
@myDecorator
def myfunction():
   return "Hello"
The decorator is read by the parser and this is what it's translated to:
myfunction = myDecorator(myfunction)
Therefore, you can do whatever you'd like with this annotated function! The "@" really is only syntactic sugar, it doesn't add anything more than calling the method directly and assigning to the decorated name. However, the code becomes clearer and more readable by using the @decorator syntax. A decorator can be any callable, it will simply be called with the decorated name (class, method, etc). An example here would be to add "world" to the returned string:
def myDecorator(decorated):
   def f():
      return decorated() + " world"
   return f
There you go, myfunction now returns "Hello world".

Update:
You can also pass arguments to the decorator, i.e.:
@mydecorator(arg1, arg2)
The best way to understand this is to know what happens behind the scene:
myfunction = mydecorator(arg1, arg2)(myfunction)
Pretty straightforward isn't it? The decorator is called with the arguments, the return value is then called with the function (or class or whatever) to be decorated. You would implement a decorator that takes arguments as such:
def myDecorator(a1, a2):
   def wrapper(f):
      def decorated():
         return f() + a1 + a2
      return decorated
   return wrapper
Essentially, decorating the "myfunction()" above (the one that returns "Hello" with this decorator as such:
@myDecorator(" World", " Weee!")
def myfunction():
   return "Hello"
would now return "Hello World Weee!". The decorator function takes arguments and returns a function that will be called with the "thing" to be decorated. Because of closure, the arguments can be applied inside the second call and return the decorated function. You can also do it with a class (may be easier to understand):
class myDecorator(object):
   def __init__(self, a1, a2):
      self.a1 = a1
      self.a2 = a2
   def __call__(self, f):
      def decorated():
         return f() + self.a1 + self.a2
      return decorated
remember how it's done under the hood!
myfunction = myDecorator(a1,a2)(myfunction)

2 comments: