无知的 tonyseek

Yet Another Seeker

用装饰器注册 Python 函数

注册回调函数应该是开发中很常见的一种行为。这在 Python 中通常通过装饰器来实现,看起来比较漂亮:

flaskr.py
 @app.route("/")
 def home():
     return "It works."

但是这种用法常常带来一种隐藏的“惊讶”,比如说:

admin_menu.py
 @permission(["manager", "developer"])
 @app.route("/admin")
 def admin_menu():
     return render_template("admin/menu.html")

这个 @permission 根本不会生效,因为在它装饰 admin_menu 之前,admin_menu 就已经被注册到 app 里了。最终我们执行的是未经过装饰的函数。

类似的陷阱还有可能出现在“猴子补丁”使用的场景。比如下面这段代码将一系列 filter 注册到一个对象中:

filter_manager.py
 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 ,就会小小惊讶一下:

test_filter_manager.py
 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)

Comments