注册回调函数应该是开发中很常见的一种行为。这在 Python 中通常通过装饰器来实现,看起来比较漂亮:
@app.route("/")
def home():
return "It works."
但是这种用法常常带来一种隐藏的“惊讶”,比如说:
@permission(["manager", "developer"])
@app.route("/admin")
def admin_menu():
return render_template("admin/menu.html")
这个 @permission 根本不会生效,因为在它装饰 admin_menu 之前,admin_menu 就已经被注册到 app 里了。最终我们执行的是未经过装饰的函数。
类似的陷阱还有可能出现在“猴子补丁”使用的场景。比如下面这段代码将一系列 filter 注册到一个对象中:
class FilterManager(object):
"""The filter registry center."""
def __init__(self):
self._filters = {}
def register(self, name):
def decorator(func):
self._filters[name] = func
return func
return decorator
def filter(self, type_name, value):
return self._filters[type_name](value)
filter_manager = FilterManager()
@filter_manager.register("datetime")
def datetime_filter(dt):
dt.strftime("%Y-%m-%d %H:%M")
看起来似乎很漂亮,但是如果我在写单元测试的时候,希望用一个 Stub 来取代 datetime_filter ,就会小小惊讶一下:
from datetime import datetime
from mock import patch
from filter_manager import filter_manager, datetime_filter
@patch("filter_manager.datetime_filter")
def test_datetime_filter():
filter_manager.filter("datetime", datetime.utcnow())
assert datetime_filter.called
说到底还是用装饰器注册函数,不是 Lazy Binding 的。
这是我个人感觉 Python 的装饰器使用中比较违反直觉的一个地方。虽然装饰器这种看上去很像“声明”的语法很酷,却也很 implicit 。当然因为装饰器的这种用法实在太流行,很多 Python 开发者已经具备了绕过这个陷阱的技能了(Orz,小白和非小白的区别之一)。但是我还是觉得这种东西需要开发者用经验去绕过的,并不符合我比较认同的最小惊讶原则。
所以…… 我可能还是更喜欢用 Qualified Name 来引用要注册的函数,以实现真正的 Lazy Binding:
def import_object(qualname):
try:
from werkzeug.utils import import_string
except ImportError:
from importlib import import_module
module_name, object_name = qualname.rsplit(".", 1)
module = import_module(module_name)
return getattr(module, object_name)
else:
return import_string(qualname)
def get_qualname(o):
return getattr(o, "__qualname__", "%s.%s" % (o.__module__, o.__name__))
class FilterManager(object):
"""The filter registry center."""
def __init__(self):
self._filters_qualname = {}
def register(self, name):
def decorator(func):
self._filters_qualname[name] = get_qualname(func)
return func
return decorator
def filter(self, type_name, value):
qualname = self._filters_qualname[type_name]
func = import_object(qualname)
return func(value)