声明即实现(三) - Python 装饰器的应用

1 前言

古语有云:人生苦短,快用 Python。

众所周知,Python 是一门开发效率非常高的语言。当使用这个以开发效率著称的语言去封装 HTTP API 时,如果还是为每个 API 写一遍实现,就太不 Pythonic 了。

本文将介绍如何将 “声明即实现” 这个方案带入到 Python 中,主要用到的技术是装饰器(Decorator)和 inspect 模块。

2 装饰器

先简单介绍一下装饰器。

装饰器是用来装饰函数的,一个特殊的函数。它只有一个参数,是被装饰的函数对象,返回值应为一个函数或可调用对象(Callable Object):

1
2
3
4
5
6
7
def decorator(func):
def wrapper(*args, **kwargs):
# Do something before function
result = func(*args, **kwargs)
# Do something after function
return result
return wrapper

在使用装饰器时,只需要在目标函数的上一行,加上”@+ 装饰器名称” 即可:

1
2
3
@decorator
def foobar():
return None

是的,看起来就像 Java 的注解一样。

装饰器也是可以配合参数使用,这时候就要多套一层函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
def decorator(*some_args, **some_other_args):
def wrapper_creator(func):
def wrapper(*args, **kwargs):
# Do something before function
result = func(*args, **kwargs)
# Do something after function
return result
return wrapper
return wrapper_creator

@decorator("foo", "bar")
def foobar():
return None

尽管我在装饰器的演示代码中调用了原始函数,实际上开发者可以自行选择是否这样做,甚至可以完全不鸟原始函数。

当调用一个被装饰过的函数时,实际上调用的是装饰器返回的函数对象,因此装饰器返回的函数对象,其形参和返回值应与目标函数相同。

不过在 Python 中,有 (*args, **kwargs) 这样的通用形参写法,而且由于 Python 是弱类型语言,因此对返回值类型的要求也不严格,这就给实现通用装饰器提供了便利。

3 实现

在对装饰器有个基本的认识之后,就可以开始进行 “声明即实现” 这个方案了。

3.1 声明

首先,仍是声明 HTTP API 的封装类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class FoobarClient:

def set_name(self, name):
pass

def set_profile(self,
first_name, last_name, email, bio):
pass

def set_avatar(self, url):
pass

def say_hello(self):
pass

3.2 装饰器

3.2.1 定义

创建带参数的装饰器 http_api,接受一个 path 参数,用于传入 HTTP API 的 URL Path:

1
2
3
4
5
6
7
8
9
def http_api(path):
# The Full API URL
api_url = 'https://api.foo.bar/%s' % path
def wrapper_creator(func):
def wrapper(*args, **kwargs):
# TODO
pass
return wrapper
return wrapper_creator

装饰器的具体实现留在后面,这里先用装饰器把所有 API 函数都装饰一遍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class FoobarClient:

@http_api(path='name/set')
def set_name(self, name):
pass

@http_api(path='profile/set')
def set_profile(self,
first_name, last_name, email, bio):
pass

@http_api(path='avatar/set')
def set_avatar(self, url):
pass

@http_api(path='action/say_hello')
def say_hello(self):
pass

装饰完成后,对每个 API 函数的调用,都会走到装饰器内的 wrapper 函数中。

3.2.2 实现

接下来开始进行装饰器的实现,也就是对 wrapper 函数的实现。

第一个需要解决的问题,就是怎样拿到函数参数和 API 参数的对应关系。在 Java 中,是通过反射实现这一点的,而在 Python 中,一个函数就搞定了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import inspect

def http_api(path):
api_url = 'https://api.foo.bar/%s' % path
def wrapper_creator(func):
def wrapper(*args, **kwargs):
call_args = inspect.getcallargs(func, *args, **kwargs)
# FIXME: debugging code
print('api_url => %s' % api_url)
print('call_args => %r' % call_args)

# TODO
pass
return wrapper
return wrapper_creator

这里先加上两行调试代码,把 api_urlcall_args 打印出来,然后调用几个 API 函数试验一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
client = FoobarClient()

client.set_name('deadblue')
# Output:
# api_url => https://api.foo.bar/name/set
# call_args => {'self': <__main__.FoobarClient object at 0x108dbf070>, 'name': 'deadblue'}

client.set_profile(
first_name='Tomo' ,
last_name='Kunagisa',
email='[email protected]',
bio='This Ugly Yet Beautiful World'
)
# Output:
# api_url => https://api.foo.bar/profile/set
# call_args => {'self': <__main__.FoobarClient object at 0x108dbf070>, 'first_name': 'Tomo', 'last_name': 'Kunagisa', 'email': '[email protected]', 'bio': 'This Ugly Yet Beautiful World'}

可以看到,call_args 就是一个形参名->参数值的字典。也就是说,在定义 API 方法时,只要让函数的形参名和 HTTP API 的参数名保持一致即可。

由于被装饰的 API 函数,都是类(Class)上的方法(Method),因此第一个形参都是 self,即类实例自身。在把 call_args 转换成 HTTP 表单(Form)时,记得过滤掉它。

至此,就拿到了调用 HTTP API 的所有必要信息,最后只要封装一个通用的发送请求方法,再通过装饰器调用即可。这里直接将这个方法定义在 FoobarClient 上:

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
def http_api(path):
api_url = 'https://api.foo.bar/%s' % path
def wrapper_creator(func):
def wrapper(*args, **kwargs):
call_args = inspect.getcallargs(func, *args, **kwargs)
client = call_args['self']
del call_args['self']
return client.call_http_api(api_url, call_args)
return wrapper
return wrapper_creator


class FoobarClient:

api_key = None

def __init__(self, api_key):
# DO NOT forget the API key
self.api_key = api_key

def call_http_api(self, url, args):
# TODO:
# * Send request to FOOBAR API Service
# * Return API result
return None

# Other API methods ...

发送 HTTP 请求和解析 JSON 结果的代码就不用赘述了,这应该是大多数 Python 程序员的基本技能了。

最终,通过封装好的 FoobarClient 调用 HTTP API 的代码为:

1
2
3
client = FoobarClient('you-api-key')
client.set_name('deadblue')
client.say_hello()

4 总结

同样是 AOP 的编程思想,Python 带来了一个完全不同的编程体验。

装饰器是一个设计的非常巧妙的语法糖,配合通用形参,可以演化出多种多样的玩法。

等有空的时候,准备专门写一篇来介绍它。

咕!

5 参考