在 Python 中实现 Ruby 的 Open Class 和特异方法
2015-04-15 10:05
459 查看
原文抄袭自 https://blog.tonyseek.com/post/open-class-in-python/
Ruby 中的 OpenClass 是个非常方便的特性,我们可以扩充一个已有的类,往里面添加方法。甚至还能脱离类,向实例中添加该实例独有的方法,称为“特异方法”。这种做法的前提是语言具有足够的动态特性,能够运行时更改语言结构,所谓“ 元编程 ”(Meta-Programming)能力。
Python 也有类似的能力,但是不像 Ruby 有原生的语法支持。在 Python 中实现 OpenClass 和特异方法基本原理与 Ruby 中的原理类似——“函数、方法也是一种对象”。
本文只讨论 Python 的 新式类 ,对于 old-style class 保持无视。
OpenClass 即在一个类已经完成定义之后,再次向其中添加方法。在 Ruby 中的实现方法就是定义同名类。在 Ruby 中定义同名类不会像 Java 一样被认为是编译错误,也不会像 Python 一样“新定义的类覆盖旧类”,而是把同名类中定义的方法全部附加到已定义的旧类中。以下为示例代码:
特异方法和 OpenClass 有点类似,不过附加的方法不是附加到类中,而是附加到特定的实例中。被附加的方法仅仅在目标实例中存在,不会影响该类的其他实例。示例代码:
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__ 协议后,使用起来就便捷的多了。
特异方法也是类似的使用场景,不过特异方法针对的是特定实例而不是整个类。
Ruby 中的 OpenClass 是个非常方便的特性,我们可以扩充一个已有的类,往里面添加方法。甚至还能脱离类,向实例中添加该实例独有的方法,称为“特异方法”。这种做法的前提是语言具有足够的动态特性,能够运行时更改语言结构,所谓“ 元编程 ”(Meta-Programming)能力。
Python 也有类似的能力,但是不像 Ruby 有原生的语法支持。在 Python 中实现 OpenClass 和特异方法基本原理与 Ruby 中的原理类似——“函数、方法也是一种对象”。
本文只讨论 Python 的 新式类 ,对于 old-style class 保持无视。
Ruby 中的 OpenClass 与特异方法
OpenClass 即在一个类已经完成定义之后,再次向其中添加方法。在 Ruby 中的实现方法就是定义同名类。在 Ruby 中定义同名类不会像 Java 一样被认为是编译错误,也不会像 Python 一样“新定义的类覆盖旧类”,而是把同名类中定义的方法全部附加到已定义的旧类中。以下为示例代码:class Foo def m1() puts "m1 has been called" end end class Foo def m2() puts "m2 has been called" end end foo = Foo.new foo.m1 foo.m2
特异方法和 OpenClass 有点类似,不过附加的方法不是附加到类中,而是附加到特定的实例中。被附加的方法仅仅在目标实例中存在,不会影响该类的其他实例。示例代码:
foo = Foo.new def foo.m3() puts "m3 has been called" end foo.m3
Python 方法探幽
从函数到绑定方法
Python 中定义方法,和写一个函数是大致相同的,唯一的不同之处有两点:第一,作为方法的函数必须嵌套在类的结构里;第二,作为方法的函数第一个参数必须为 self。在一个 Python 类实例化生产出一个实例的时候,类中的所有“函数”都会被包装一层再放到实例的成员字典里(装饰器模式 [1] )。包装层除了添加了一些元信息之外,还对原始的函数做了“装饰”,装饰的效果为“偏函数”。
通过为已经存在的某个函数指定数个参数,生成一个新的函数,这个函数只需要传入剩余未指定的参数就能实现原函数的全部功能,这被称为偏函数。—— AstralWind [2]
也就是说,对于类中有 n 个参数的函数,实例中会有对应的可调用(callable)对象,接受 n-1 个参数的调用。被省略的那个参数就是 self —— 在偏函数化的过程中被赋予了当前实例。
概念上来说,可以把这个包装过程用如下代码大致模拟(仅仅是概念上):
def unbound_method(self, arg1, arg2): pass # 对于 obj1 中的绑定方法 def bound_method(arg1, arg2): return unbound_method(obj1, arg1, arg2)
掌握了 Python 的这个特点,我们就可以模拟语言内置的“方法绑定”过程,实现模拟 Ruby 的“特异方法”。
未绑定方法
如果使用的是 py3k,嵌套定义在类中的函数和定义在全局作用域的函数是没有区别的。类实例化出实例的时候,“方法绑定”的过程也是基于这种 raw function。但是 Python 2.x 的处理方式有所区别,尝试一下便知:# in python 2.x class Spam(object): def egg(self): pass print egg # output: <function egg at 0x02105630> print Spam.egg # output: <unbound method Spam.egg> print Spam.egg.im_func # output: <function egg at 0x02105630>
这点似乎阻碍了我们以直接向类中添加函数的方式添加函数,其实不然,如果我们写一个自由函数,然后添加到类中,这个函数就会被自动包装成未绑定方法对象。
# in python 2.x def egg2(self): pass Spam.egg2 = egg2 print Spam.egg2 # output: <unbound method Spam.egg2>
我认为是类的 __setattr__ 产生了这样的作用。无论是什么原因,我们确定了一点——
Python 2.x 的“未绑定方法”不会干扰我们以正常思维模式实现 OpenClass。
当然,如果用的是 Python 3.x,未绑定方法问题就不存在了。我们使用属性访问的方式直接向类中添加方法,是可以同时兼容 Python 2.x 和 Python 3.x 的。
实现
有了上述基础,我们可以开始编写一个 utils.py 模块,实现 OpenClass 和特异方法。from functools import partial def attach_method(target): if isinstance(target, type): def decorator(func): setattr(target, func.__name__, func) else: def decorator(func): setattr(target, func.__name__, partial(func, target)) return decorator
上述代码中的 attach_method 装饰器在内部对装饰目标做出装饰之前,先用 isinstance(target,
type) 判断目标是否是一个“类”,即“元类” type 的实例。如果是类,则视为
OpenClass,将被装饰函数用 setattr 放置到类中;如果不是类,则视为特异方法,手动构造一个偏函数去掉
self 参数,在放到实例中。
然后我们就可以编写简单的测试:
class Spam(object): pass @attach_method(Spam) def egg1(self, name): print((self, name)) spam1 = Spam() # OpenClass 加入的方法 egg1 可用 spam1.egg1("Test1") spam2 = Spam() @attach_method(spam2) def egg2(self, name, num): print((self, name, num)) # 因为是 OpenClass,所以 egg1 对 spam2 实例也有效 spam2.egg1("Test2") # 特异方法 egg2 spam2.egg2("Test3", 3) # egg2 是 spam2 的特异方法,在 spam1 中不可用 # 这里会抛出一个 AttributeError 异常 spam1.egg2("Test3", 3)
实际用途
有了这个数行的简单工具,我们就可以在其他地方展现 OpenClass 和特异方法的便捷了。例如对于 SQLAlchemy 的 Session,我们一般的用法是操作结束领域对象,然后调用 commit 方法提交工作单元。我们希望能够用 Python 的 with 上下文管理方式完成这个工作,但是 SQLAlchemy 的 Session 默认并没有实现 __enter__ 和 __exit__ 协议,我们就可以通过OpenClass 加入该实现。
#!/usr/bin/env python #-*- coding:utf-8 -*- from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from utils import attach_method engine = create_engine('sqlite:///:memory:', echo=True) Session = sessionmaker(bind=engine) @attach_method(Session) def __enter__(self): return self @attach_method(Session) def __exit__(self, err, err_type, err_tb): if not err: self.commit() else: self.rollback()
加入了 __enter__ 和 __exit__ 协议后,使用起来就便捷的多了。
#!/usr/bin/env python #-*- coding:utf-8 -*- with Session() as db: post = db.query(Post).get(post_id) new_comment = Comment(comment_content_text) post.add_comment(new_comment) db.add(new_comment)
特异方法也是类似的使用场景,不过特异方法针对的是特定实例而不是整个类。
相关文章推荐
- Python实现__metaclass__实现方法运行时间统计
- python实现class对象转换成json/字典的方法
- python实现class对象转换成json/字典的方法
- Python使用metaclass实现Singleton模式的方法
- Python使用metaclass实现Singleton模式的方法
- python实现在每个独立进程中运行一个函数的方法
- 在Win7环境下,利用SWIG实现Python调用C的方法
- Python_cmd的各种实现方法及优劣(subprocess.Popen, os.system和commands.getstatusoutput)
- python实现在windows下操作word的方法
- Python实现基于权重的随机数2种方法
- python实现从网络下载文件并获得文件大小及类型的方法
- python3实现短网址和数字相互转换的方法
- 〖Android〗arm-linux-androideabi-gdb报 libpython2.6.so.1.0: cannot open shared object file错误的解决方法
- python实现将pvr格式转换成pvr.ccz的方法
- Python实现对PPT文件进行截图操作的方法
- python实现向ppt文件里插入新幻灯片页面的方法
- python实现根据ip地址反向查找主机名称的方法
- python实现通过代理服务器访问远程url的方法
- python实现带错误处理功能的远程文件读取方法
- python使用fileinput模块实现逐行读取文件的方法