本文为我在阅读Python’s decorators vs Java’s annotations, same thing?的笔记。
Flask 的路由看起来和 Spring 的机制非常像,比如用 Flask 定义的一个路由:将 Http 请求 GET: /hello
路由到 say_hello() 函数。
app = Flask(__name__)
@app.route('/hello', methods=['GET'])
def say_hello():
return 'hello'
Spring 用注解定义了路由,看起来跟 Flask 类似:
@RequestMapping(value = "/hello", method = GET)
class HelloController{
@ResponseBody
public String getHelloMessage() {
return "hello";
}
}
但实际上,Python 的装饰器 (decorator) 和 Java 的注解(annotation) 是完全不同的概念。Python 的装饰器本质上是返回另一个函数的函数,用于装饰函数的时候,作为语法糖,实际上是函数的调用。
下面我来详细解释一下这句加粗的这句话。我们定义 hello() 函数,返回一个字符串 hello:
def hello():
return 'hello'
if __name__ == '__main__':
print(hello())
假如我们想将字符串变为 html 的标签 heading 1,可以定义一个函数提供这个转换:
def hello():
return 'hello'
def as_h1(message):
return f'<h1>{message}</h1>'
if __name__ == '__main__':
print(as_h1(hello()))
如果使用装饰器实现相同的功能,只需要将 @as_h1 对 hello() 函数进行装饰:
def as_h1(func):
def wrapper():
result = func()
return f'<h1>{result}</h1>'
return wrapper
@as_h1
def hello():
return 'hello'
if __name__ == '__main__':
print(hello())
@as_h1 装饰器用于装饰 hello() 函数,本质是调用 as_h1() 函数。作为装饰器的函数要点:
- 外函数返回内函数 : as_h1() 函数的返回值是内函数 wrapper (函数的 return wrapper 语句)
- 内函数改变从外函数传入函数的行为:func 是传入参数,通过外函数 as_h1 传给 内函数 wrapper,在 wrapper 函数中被改变(func 函数被改变了)
之前写过一篇博文:理解和使用Python装饰器 比较详细的说明了装饰器的机制。
函数大多数是带参数的,所以一个实用的装饰器必须能处理含有参数的函数。下面的代码定义能处理带参数的装饰器,演示了装饰器的典型写法:
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 装饰器的作用是对某个函数提供缓存机制,对某个函数比较耗时的函数,缓存可以提升效率。下面的代码演示了将 cached 装饰器作用于 intensive_task() 函数,第一次调用耗时 1 秒,第二次调用从缓存中获取,时间为 0:
@cached
def intensive_task():
time.sleep(1.0)
return 10
start_time = time.time()
intensive_task()
print("INFO: %.8f seconds first execution" % (time.time() - start_time))
start_time = time.time()
intensive_task()
print("INFO: %.8f seconds second execution" % (time.time() - start_time))
在我的 PC 上运行的结果如下:
python decorator_test2.py
INFO: 1.00221372 seconds first execution
INFO: 0.00000000 seconds second execution
而在 Java 中,注解其实就是一种特殊的注释,相当于对类、方法等贴一个标签,不会改变代码的行为。但为什么本文开头的代码中,@ReqeustMapping 注解实现了 Flask 相同的路由机制呢?其实是因为 Spring 框架发现某个方法被注解了,提供相应的功能实现而已。
比如说,定义一个 @Cached 注解,并且用在 intensiveTask() 方法上,此时与没有注解的代码,行为没有任何不同。
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";
}
}
public static void main(String[] args) throws Exception {
long time = System.currentTimeMillis();
final SomeObject expensiveTaskObject = new SomeObject();
someObject.intensiveTask();
System.out.println("First execution:" + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
someObject.intensiveTask();
System.out.println("Second execution:" + (System.currentTimeMillis() - time));
}
}
如果需要达到与 Python 代码相同的缓存效果,需要编写代码,将 intensiveTask() 方法加到缓存中:
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();
}
}
然后在两次调用调用 intensiveTask() 方法的时候,通过执行 SomeObjectExecutor 类的 execute() 方法进行判断,如果缓存中已经有了 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));
}
总结起来:
Java 的注解本身什么都不做,而 Python 的装饰器是函数,会改变被装饰函数的行为;
如果想改变 Java 被注解方法的行为,需要另外的代码判断某个方法是否被某个注解(名词)注解(动词),对被注解的方法提供不同的实现。