Skip to content

Annotations

Python’s decorators vs Java’s annotations, same thing?

If you know Java and happened to work with Python, or the other way, you could see some @ symbols with names above the function in both languages. If you thought they’re similar, congratulations, you’ll be surprised by their behavior in the most unsuitable moment 😏

What the hell are those @ signs above function anyway?

In Java, they called annotations. Annotation is, in simple words, metadata. You can pass data into the annotation, and, with some work, read this metadata. Here’s a small example of annotation usage from Spring framework(Class was omitted for simplicity):

1
2
3
4
5
6
@RequestMapping(
  value = "/hello", method = GET)
@ResponseBody
public String getHelloMessage() {
    return "hello";
}

You can see that ‘@RequestMapping’ used to specify that we want to execute this function when the client calls the GET method /hello endpoint.

In Python, oppositely, they called decorators. Here’s an example from Flask application, as in Java’s example, our decorator will make so our function will serve /hello endpoint with GET method:

1
2
3
4
5
app = Flask(__name__)

@app.route('/hello', methods=['GET'])
def say_hello():
    return 'hello'

A decorator is a design pattern that allows a user to add new functionality to an existing object without modifying its structure.

Ah okay, problem solved. So Java’s annotation just adds metadata, and Python’s decorators modify usage of the function.

Thank you for reading this… wait for a second! So if Java’s annotation just adds metadata, how our method became endpoint server as in Python? In other words, how they both do the same thing?? Well, let’s start by implementing our own decorator in Python.

Decorators in Python, how they work?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def as_html(func):
    def wrapper():
        result = func()
        return f'<html>{result}</html>'

    return wrapper

@as_html
def say_hello():
    return 'Hello'

print(say_hello())

Key takeaways from code above:

  • Decorator is just a function that returns another function
  • If applied with @ sign above the function, it’ll change it’s behavior as specified in wrapper() function
  • They can change input or output of a wrapped function, or even don’t call it at all.

What is going on in the code above? Firstly we create simple function as_html(func). In this function we create another function called wrapper(), because in Python function is just another object that you can use. Inside wrapper() we will call this func object from as_html(func), which, as you can think, another function. And so basically we return our wrapper() function. Take a cup of coffee and try to structure what’s going on in your head.

So now, we annotate this function with @ above the say_hello(), and, as a result, you can see that when we call say_hello(), we get different result than ‘Hello’. We get ‘Hello’ because our as_html(func) wrapped our say_hello() function. So func is the say_hello() function itself. Maybe if I put it like as_html(say_hello) makes it clearer to you. Technically, you can re-write the previous example like this, and it still works the same way:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def as_html(func):
    def wrapper():
        result = func()
        return f'<html>{result}</html>'

    return wrapper


def say_hello():
    return 'Hello'

print(as_html(say_hello)())

Cached decorator for functions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import time
cached_items = {}

def cached(func):
    def wrapper(*args, **kwargs):
        global cached_item
        if func.__name__ not in cached_items:
            cached_items[func.__name__] = func(*args, **kwargs)
        return cached_items[func.__name__]
    return wrapper

@cached
def intensive_task():
    time.sleep(1.0)
    return 10

start_time = time.time()
intensive_task()
print("--- %.8f seconds first execution ---" % (time.time() - start_time))
start_time = time.time()
intensive_task()
print("--- %.8f seconds second execution ---" % (time.time() - start_time))

This is (kinda) more practical example, imagine that you have some function that should execute only once, and all other times return previous result. You can, of course, implement it within the function itself, but what if you’ll have 10 of them? 100? That’s a decorator’s work.

So, in wrapper(*args, **kwargs) we check if we already contain this function name in the global cached_items dictionary. If not, we execute the function, if yes, we only return the value of dictionary for a key with the function name. After it, we wrap intensive_task() with our @cached decorator and execute function two times with benchmarking. Here’s what I get on my pc:

1
2
3
python3 decorators_example.py 
--- 1.00484562 seconds first execution ---
--- 0.00000787 seconds second execution ---

You can clearly see that second time execution time took under a 7E-06 second even though in our intensive_task() we execute sleep command. So the body of function was executed only once, as we can see by logic in wrapper()

Now you can call yourself a junior expert in decorators in Python. So what about Java?

Annotations in Java, are they decorators?

When I first encountered annotations in Java, they were really astonishing to me. So later when I encountered anything with @ sign in other languages I said: “oh, that’s an annotation, I know that!”. Only now I realized how different those two things are.

In Java, instead of writing the logic for “decorator” in annotation itself, this responsibility falls onto executor. So, technically speaking, annotations in Java doesn’t contain any logic at all*, only some data that can be put as a variable. So how it happens that we can do the same logic in Java as in Python? Fairly easy, we just inverse our code a little bit.(be ready for large piece of code due to Java ;)

There can be annotation pre-processor like Lombok, but main idea is still the same. Unless defined(even if in pre-processing), annotations won’t do any changes to behavior. More discussion on it in responses of this post.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Main {

    @Retention(RetentionPolicy.RUNTIME)
    @interface Cached { } // Nothing inside our annotation

    static class SomeObject {
        @Cached
        public String intensiveTask() throws InterruptedException {
            Thread.sleep(1000);
            return "expensive task result";
        }
    }

    static class SomeObjectExecutor {

        Map<String, String> cache = new HashMap<>();

        public String execute(SomeObject task) throws Exception {
            final Method intensiveTaskMethod = task.getClass().getDeclaredMethod("intensiveTask");
            if (intensiveTaskMethod.isAnnotationPresent(Cached.class)) {
                String className = task.getClass().getName();
                if (!cache.containsKey(className)) {
                    cache.put(className, task.intensiveTask());
                }
                return cache.get(className);
            }
            return task.intensiveTask();
        }
    }

    public static void main(String[] args) throws Exception {
        final SomeObjectExecutor someObjectExecutor = new SomeObjectExecutor();
        long time = System.currentTimeMillis();
        final SomeObject expensiveTaskObject = new SomeObject();
        someObjectExecutor.execute(expensiveTaskObject);
        System.out.println("First execution:" + (System.currentTimeMillis() - time));
        time = System.currentTimeMillis();
        someObjectExecutor.execute(expensiveTaskObject);
        System.out.println("Second execution:" + (System.currentTimeMillis() - time));
    }
}

So, what happened here? Here are a few points:

  1. The annotation doesn’t do anything at all. As the name says, it only annotates.
  2. Instead of wrapping functions, we check if it contains some annotation and execute it differently.
  3. We should have some “executor” object which will hold all the logic that could be in python’s decorator.

So, first thing first, we created @Cached annotation, which is just empty, nothing special here except ‘@Retention(RetentionPolicy.RUNTIME)’. This thing says that we can access this annotation when the program is running. Next, we have our SomeObject, because in Java functions are not object, and we can’t pass them into other functions. In this object, we have intensiveTask(), which will sleep when executed. The next thing that we made is SomeObjectExecutor, which takes our SomeObject and executes its function via Object Reflection.

To combine it all, we use main() method, where we create SomeObjectExecutor and SomeObject and then pass SomeObject two times with benchmarking, here’s my output:

1
2
First execution:1012
Second execution:0

As expected, the First time we execute an object it takes 1 second, next time — almost 0, because the object was cached.

What about other retention policy?

If you work with Java for a while, you can know such thing as aspect-oriented programming(AOP), AspectJ and similar tools. I even made an article about it a few years ago. So AspectJ kinda allows you to convert your annotations into decorators. But there’s still a small gap between the AOP and decorators, and you need to apply more efforts in Java with AOP then in Python. I may cover this topic of how pre-compilers work in the next article.

Conclusion

If you read through the article up to this point, you can see that both approaches have their benefits and disadvantages. Because in Python functions are first-class citizens, it’s really easy to juggle with them and pass one function into another, etc. Java’s approach, on the other hand, gives you more control over what’s going on.