Some while ago, I found out that it’s actually quite easy to write your own decorators in Python. That thing that is called a “pie” syntax, available since Python 2.4 but still is mostly only the stuff of some frameworks like Django, Flask or PyTango. As to the “what” and especially “why??”, one might benefit from some thoughts I recently verified.
Motivation: As several of our software projects help our customers in supporting their experimental research with Supervistory Control and Data Acquisition (SCADA) applications, we have build a considerable knowledge base around the development with the open-source Tango Controls system. For me as a developer, its Python implementation PyTango makes my life significantly easier, because I can sketch out Tango applications, like Tango Device Servers, clients for data analysis, plotting live data etc. with little to none overhead. Less than an hour of development time usually gets you started.
PyTango actually makes it that easy to code the basic functionality of a device in a network structure with all the easy Tango features – well, you have to set up Tango once. But then the programming becomes easy. E.g. a current measurement device which is accessible from anywhere in your network could basically only need:
class PowerSupply(Device): def init_device(self): Device.init_device(self) self._current = 0 @attribute(label="Current", unit="mA", dtype=float) def current(self): return self._current @current.write def current(self, current): self._current = current
Compare that to the standard C++ Tango implementation, where is all that would be loads of boilerplote code.
Now, this not unusual for Python. Hiding away the ugly implementation in its libraries or frameworks so you can focus on the content. However, you need to trust it, and depending on what kind of Python code you usually enjoy to read, this can make you think a bit. What is this usage of @ doing there? Isn’t this Java code??
But to the contrary: in Java, the @annotations are a way of basically interfering with the way the compiler itself behaves. For Python, the pie is pure syntactical sugar. In a way, they both address a similar way of your source code telling you “the next thing is treated in a specific way”, but whereas you would dig quite deep in Java to produce useful custom annotations (if you know otherwise, tell me), decorating pies in Python can easily help you to dynamically abstract layers of code away to places where it’s nicer and cleaner.
It turns out: A Python decorator is any function that maps a function to a function; i.e. it takes a function as an argument and returns the same or a different function. Easy enough; as in Python, functions are first-class objects. They can be passed around and extended accordingly.
But… Every programming pattern comes with the question: “What do I gain for learning such a new habit?”
Can and should you write your own decorators?
Under the hood, something like this happens. You define a decorator e.g. like
def my_decorator(func): print("the decorator is initialized") def whatever(): print("the function is decorated") return func() return whatever
giving you instant access to your own decorator magic:
@my_decorator def my_func(): print("the decorated function is called")
A function call of my_func() now results in the output
the function is decorated the decorated function is called
while the first “the decorator is initialized” in only executed at the declaration of the @my_decorator syntax itself.
This pie syntax is pure syntactical sugar. It does nothing more than if you instead had defined:
def my_anonymous_func(): print("the decorated function is called") my_func = my_decorator(my_anonymous_func)
But it appears somehow cleaner, avoiding the need of the two “my_anonymous_func” that are not useful anywhere else in your code. You stash away more irrelevant implementation details from the actualy meaning of “what is this method/function actually for?”. Now you can pack my_decorator in your own set utility functions as you would with any re-usable code block. To make the example more sophisticated, you can also process function arguments (*args, **kwargs) this way; or extend previously decorated functions with further functionality… and while the Decorator itself might be a bit more complex to read, you can test that separatly, and from then on, the actual __main__ code is tidy and easy to extend.
def my_decorator(func): # do setup stuff here def whatever(*args, **kwargs): print("The keyword argument par is:", kwargs.get('par', None)) return func(*args, **kwargs) def whatever2(func2): def some_inner_function(): print("Call the second function and do something with its result") # note that the _actual_ function call of the decorated function would happen in the very core of the decorating function return func2() return some_inner_function result = whatever result.other_thing = whatever2 return result if __name__ == '__main__': print("START") @my_decorator def my_func(*args, **kwargs): print("original my_func", kwargs.get('par', None)) @my_func.other_thing def my_func2(): pass my_func(par='Some keyword argument') my_func2()
So really, we are equipped to do anything. Any scenario, in which a function call should actually be guided by one or more extra actions, might be nicely packaged in a decorator. Which gives you a number of good use cases as e.g.
- Logging (if only for debug reasons)
- Additional metrics (e.g. performance evaluation)
- Type checks (making even simple scripts more robust)
- Error Handling
- Registering of any service or entity e.g. in one centralized list structure
- Updates on a user interface
- Caching, e.g. to ease some expensive operation or asynchronous action
- or any such meta-pattern that you find yourself repeating in otherwise unrelated scenarios.
And more than that, a good name choice of your decorator gives your code a nice self-explaining semantic structure. You describe more “what”, less “how” of a block of code.
I like that pattern, but I always thought the @ looks somewhat cryptical. If you have any objection, want to warn me about unforeseen implications or generally think this was always super-obvious, let me know 🙂
2 thoughts on “Pie-Decoration with Python”
> want to warn me about unforeseen implications
If you write decorators by hand you will lose the original name, docstring etc. of the wrapped function. To avoid that without unnecessary boilerplate one should almost always use functools.wraps (yes, a decorator itself) to write the decorator. Greetings to Miq 😉
Good point, considering that it’s always just a minor inconvenience to always write your code as if you were writing a library for the public. But of course, I didn’t know wraps(). Functools has quite some secrets… thanks.
– Also Matthias
(Greetings to Miq delivered.)