In this blog, I would like to introduce the concepts that I believe are the most powerful in programming languages, and mastering them will give you solid programming thinking. I will explain them one by one with examples in Java and Python. After introducing the concepts of object-oriented, recursion, concurrency, functional programming, data structure, and design patterns in earlier articles, I will elucidate the concept of annotations and decorators in this article.
1. Object-oriented
2. Recursion
3. Concurrency
4. Functional Programming and Lambda Expressions
5. Data Structure
6. Software Design Patterns
7. Annotations and Decorators
8. Machine Learning
1. Annotations and Decorators
It’s a strong concept that allows you to think on a more abstract and higher level. Annotations reduce boilerplate code so that you don’t need to write it explicitly but implement it using annotations.
Not all programming languages support this concept. Sometimes they also use different terminology to perform a similar implementation of this concept. In Java, this is called an annotation, but in Python, a similar mechanism is called a decorator.
When an element (class, method, etc.) is annotated, the corresponding annotation does not affect or modify the annotated code. Annotations are used to produce informative data or documentation. They add new resources or new code that will be processed by the compiler and executed at compile or run time.
For instance, the Java annotation ‘@Override’ processes that a method is an override of another method in one of its super-classes or implemented interfaces. If this is not the case, the compiler throws an error.
In some programming languages like Java, the contents of an annotation can be retrieved at runtime using the provided reflection mechanism. The latter helps review code elements, including annotations. As you save data in annotations, you can use this data to make decisions and adapt your code as you wish.
Frameworks like Spring make extensive use of annotations, which help developers, when they want to implement a new behavior, to wrap it in an annotation. They don’t need to write explicit code for this. An example of this behavior is the ‘@Autowired’ annotation, which annotates a field declaration in a class.
We assume that we have a class ‘Service’ and inside this class, we need to declare an object of a class named ‘Product’. The ‘@Autowired’ annotation, which is applied to the product variable, allows the product instance to be automatically created when the ‘Service’ is created. You don’t need to instantiate the ‘Product’ class using explicit code.
class Service {
@Autowired
private Product product;
//...
}
2. Example in Java
To confirm the robustness of the concept of annotations, let’s take an example of Hibernate, which is a Java framework that heavily uses annotations. Hibernate is an object-oriented mapping (ORM) that maps Java persistent objects to database entities (tables).
Instead of performing different operations like retrieving rows from the database or saving new ones to the database, you can achieve the same goal by performing these operations on Java objects. They will have the same effect on the corresponding entities in the database.
Suppose we have a table ‘USER’ with columns, ‘FIRST_NAME’, ‘LAST_NAME’, ‘EMAIL’, and the ‘BIRTH’. The first three columns are of type ‘VARCHAR’ and the ‘BIRTH’ column is of type ‘DATE’. To use hibernate, you must set up a configuration between your database and Java application.
But here, we only focus on how to map between a Java class and a database table to show the strength of the annotation concept. We create the ‘User’ class to represent the ‘USER’ table. We can achieve this goal using ‘@Entity’ and ‘@Table’ annotations at the class level.
Using the ‘@Entity’ annotation persists instances of the annotated class into the database by creating a mapping from a Java class to a database table. To add more details to this mapping, you can use the ‘@Table’ annotation to specify the table name, its schema, and other information.
When you use these annotations, Hibernate will automatically generate the SQL statements necessary to perform the various operations on the corresponding table. The ‘@Id’ annotation placed on a field is used to mark the annotated field as a primary key of the entity.
The ‘@GeneratedValue’ annotation describes how the primary key is generated. In our example, this is set to ‘GenerationType.AUTO’. This means that Hibernate decides the most appropriate primary key generation strategy depending on the type of database used. That could be an auto-incrementing column strategy in the case of MySQL databases.
To map between a database column and an entity field, we need the ‘@Column’ annotation. This annotation includes many attributes to add additional detail to this column-field relationship, including the column name. In our example, the field name and column name are identical.
However, they can be different, and specifying the column name is required. Another attribute you can specify is ‘nullable’. If this attribute is set to false, the corresponding database column cannot have null values.
import jakarta.persistence.*;
import java.sql.Date;
import java.time.LocalDate;
import java.time.Period;
@Entity
@Table(name="USER")
public class User {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Long id;
@Column(name="FIRST_NAME", nullable=false)
private String firstName;
@Column(name="LAST_NAME", nullable=false)
private String lastName;
@Column(name="EMAIL", nullable=false)
private String email;
@Column(name="BIRTH", nullable=false)
private Date birth;
@Transient
private int age;
@PostLoad
public void calculateAge() {
age = Period.between(birth.toLocalDate(), LocalDate.now()).getYears();
}
}
You can also define additional fields that you need, but these are not columns in the database. For example, to store temporary values or values calculated from the column values. To do this, you can use the ‘@Transient’ annotation. In our example, we use this annotation to declare a new ‘age’ field that does not correspond to any column in the ‘USER’ table.
Suppose we need to calculate the age value whenever an instance of the ‘User’ class is created. That is possible if a row of the ‘USER’ table is loaded from the database and mapped to this instance. The ‘@PostLoad’ annotation allows us to execute the annotated method (the ‘calculateAge’ method in our example) immediately after creating a user object to calculate the age as the difference between the date of birth retrieved from the database and Today.
3. Example in Python
Decorators in Python allow you to extend or change the behavior of a function or method without explicitly modifying its code. A decorator is implemented using a function that takes another function as input and returns a new function with a modified code of the input function.
The decorator is a powerful concept that maintains the single responsibility principle, meaning that a class should only perform one function and do it well. Decorators ensure that code unrelated to the decorated function remains isolated from that function's code. Some common tasks you can use decorators for are logging and authentication. Another benefit of decorators is code reuse. The same decorator can be used for many functions. Let’s illustrate this with an example.
A good example of using decorators in Python is to calculate the execution time of a function. Instead of writing such code in each function or method, we can define a function as a decorator for all functions whose execution time we want to calculate. So, we define ‘exec_time’ as a decorator function that takes another function ‘func’ as input.
The decorator code consists of a nested function that surrounds the ‘func’ function. It will perform certain actions before and after executing the ‘func’ function. The action we perform before calling ‘func’ is to start counting the execution time of the function. Here we use ‘time.perf_counter()’ to do this.
Then, we execute the function and get the result. After the execution of the ‘func’ function is finished, we stop the time and get the difference between the end time and start time, which is the execution time of the wrapped function. We display the obtained time with a few information about the function and return its result in the final statement.
import time
from functools import wraps
from functools import reduce
def exec_time(func):
@wraps(func)
def exec_time_wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
total_time = end_time - start_time
print(f'Function {func.__name__}{args} {kwargs} executed in {total_time:.2f} seconds')
return result
return exec_time_wrapper
We can apply this decorator to several functions. Let’s apply it to two different functions ‘calc_sum’ and ‘calc_average’. The first one returns the sum of all numbers squared in a range of ‘n’ numbers. The ‘calc_average’ returns the average of a ‘ls’ list of numbers.
Using the ‘@’ symbol, we add ‘exec_time’ as a decorator for both functions. Let’s now call the functions with different parameters and see what will happen. The decorator will intercept the execution of each function and run its own code.
@exec_time
def calc_sum(n):
total = sum((x * x for x in range(0, n)))
return total
@exec_time
def calc_average(ls):
return reduce(lambda a, b: a + b, ls) / len(ls)
# Define a list
lst = [15, 9, 55, 41, 35, 20, 62, 49]
# Calculate the average of the elements of lst
avg = calc_average(lst)
# Printing average of the list
print("Average of the list =", int(avg))
# Printing the result of calling the function ‘calc_sum’ with different parameters
print("Sum of 3 squared items =", calc_sum(3))
print("Sum of 10 squared items =", calc_sum(10))
print("Sum of 5000 squared items =", calc_sum(5000))
print("Sum of 10000 squared items =", calc_sum(10000))
Running the above code gives us the following result:
Function calc_average([15, 9, 55, 41, 35, 20, 62, 49],) {} executed in 0.00 seconds
Average of the list = 35
Function calc_sum(3,) {} executed in 0.00 seconds
Sum of 3 squared items = 5
Function calc_sum(10,) {} executed in 0.00 seconds
Sum of 10 squared items = 285
Function calc_sum(5000,) {} executed in 0.00 seconds
Sum of 5000 squared items = 41654167500
Function calc_sum(10000,) {} executed in 0.00 seconds
Sum of 10000 squared items = 333283335000
No comments:
Post a Comment