Ruby 中的 OpenClass 是个非常方便的特性,我们可以扩充一个已有的类,往里面添加方法。甚至还能脱离类,向实例中添加该实例独有的方法,称为“特异方法”。这种做法的前提是语言具有足够的动态特性,能够运行时更改语言结构,所谓“ 元编程 ”(Meta-Programming)能力。
Python 也有类似的能力,但是不像 Ruby 有原生的语法支持。在 Python 中实现 OpenClass 和特异方法基本原理与 Ruby 中的原理类似——“函数、方法也是一种对象”。
本文只讨论 Python 的 新式类 ,对于 old-style class 保持无视。
Ruby 中的 OpenClass 与特异方法
OpenClass 即在一个类已经完成定义之后,再次向其中添加方法。在 Ruby 中的实现方法就是定义同名类。在 Ruby 中定义同名类不会像 Java 一样被认为是编译错误,也不会像 Python 一样“新定义的类覆盖旧类”,而是把同名类中定义的方法全部附加到已定义的旧类中。以下为示例代码:
特异方法和 OpenClass 有点类似,不过附加的方法不是附加到类中,而是附加到特定的实例中。被附加的方法仅仅在目标实例中存在,不会影响该类的其他实例。示例代码:
Python 方法探幽
从函数到绑定方法
Python 中定义方法,和写一个函数是大致相同的,唯一的不同之处有两点:第一,作为方法的函数必须嵌套在类的结构里;第二,作为方法的函数第一个参数必须为 self。
在一个 Python 类实例化生产出一个实例的时候,类中的所有“函数”都会被包装一层再放到实例的成员字典里(装饰器模式 [1] )。包装层除了添加了一些元信息之外,还对原始的函数做了“装饰”,装饰的效果为“偏函数”。
通过为已经存在的某个函数指定数个参数,生成一个新的函数,这个函数只需要传入剩余未指定的参数就能实现原函数的全部功能,这被称为偏函数。—— AstralWind [2]
也就是说,对于类中有 n 个参数的函数,实例中会有对应的可调用(callable)对象,接受 n-1 个参数的调用。被省略的那个参数就是 self —— 在偏函数化的过程中被赋予了当前实例。
概念上来说,可以把这个包装过程用如下代码大致模拟(仅仅是概念上):
掌握了 Python 的这个特点,我们就可以模拟语言内置的“方法绑定”过程,实现模拟 Ruby 的“特异方法”。
未绑定方法
如果使用的是 py3k,嵌套定义在类中的函数和定义在全局作用域的函数是没有区别的。类实例化出实例的时候,“方法绑定”的过程也是基于这种 raw function。但是 Python 2.x 的处理方式有所区别,尝试一下便知:
这点似乎阻碍了我们以直接向类中添加函数的方式添加函数,其实不然,如果我们写一个自由函数,然后添加到类中,这个函数就会被自动包装成未绑定方法对象。
我认为是类的 __setattr__ 产生了这样的作用。无论是什么原因,我们确定了一点—— Python 2.x 的“未绑定方法”不会干扰我们以正常思维模式实现 OpenClass。
当然,如果用的是 Python 3.x,未绑定方法问题就不存在了。我们使用属性访问的方式直接向类中添加方法,是可以同时兼容 Python 2.x 和 Python 3.x 的。
实现
有了上述基础,我们可以开始编写一个 utils.py 模块,实现 OpenClass 和特异方法。
上述代码中的 attach_method 装饰器在内部对装饰目标做出装饰之前,先用 isinstance(target, type) 判断目标是否是一个“类”,即“元类” type 的实例。如果是类,则视为 OpenClass,将被装饰函数用 setattr 放置到类中;如果不是类,则视为特异方法,手动构造一个偏函数去掉 self 参数,在放到实例中。
然后我们就可以编写简单的测试:
实际用途
有了这个数行的简单工具,我们就可以在其他地方展现 OpenClass 和特异方法的便捷了。例如对于 SQLAlchemy 的 Session,我们一般的用法是操作结束领域对象,然后调用 commit 方法提交工作单元。我们希望能够用 Python 的 with 上下文管理方式完成这个工作,但是 SQLAlchemy 的 Session 默认并没有实现 __enter__ 和 __exit__ 协议,我们就可以通过 OpenClass 加入该实现。
加入了 __enter__ 和 __exit__ 协议后,使用起来就便捷的多了。
特异方法也是类似的使用场景,不过特异方法针对的是特定实例而不是整个类。
[1] | 维基百科上关于装饰器模式的词条 |
[2] | AstralWind 关于 Python 函数介绍的博客 |