如何用另一类的方法装饰(猴子补丁)一个Python类?

1 投票
2 回答
1536 浏览
提问于 2025-04-16 12:28

这段内容主要讲的是两个类,httplib.HTTPMessageemail.message.Message,它们都能处理RFC822格式的头信息,但实现方式不同,功能也不一样。

让我来举几个例子,看看它们之间的区别:

  • httplib.HTTPMessage没有get_filename这个方法,而email.Message有。这个方法可以让你很方便地从像Content-disposition: attachment; filename="fghi.xyz"这样的头信息中获取文件名。

  • httplib.HTTPMessage有getparamgetplistparseplist这些方法,但据我所知,它们只能在解析content-type头信息时使用,不能在其他地方用。

  • email.Message有一个通用的get_param方法,可以解析任何带参数的RFC822头信息,比如content-dispositioncontent-type

所以,我希望能把email.message.Message中的get_filenameget_param方法加到httplib.HTTPMessage里,但我不能修改httplib.HTTPMessage,因为它是标准库的一部分... :-q

接下来,我想说说装饰器的事情... :-)

我成功创建了一个叫monkeypatch_http_message的函数,用来给httplib.HTTPMessage添加我缺少的解析方法:

def monkeypatch_http_message(obj):
    from email import utils
    from email.message import (
        _parseparam,
        _unquotevalue,
    )
    cls = obj.__class__

    # methods **copied** from email.message.Message source code
    def _get_params_preserve(self, failobj, header): ...
    def get_params(self, failobj=None, header='content-type', 
                   unquote=True): ...
    def get_param(self, param, failobj=None, header='content-type', 
                  unquote=True): ...
    def get_filename(self, failobj=None): ...

    # monkeypatching httplib.Message
    cls._get_params_preserve = _get_params_preserve
    cls.get_params = get_params
    cls.get_param = get_param
    cls.get_filename = get_filename

现在我可以这样做:

import mechanize
from some.module import monkeypatch_http_message
browser = mechanize.Browser()

# in that form, browser.retrieve returns a temporary filename 
# and an httplib.HTTPMessage instance
(tmp_filename, headers) = browser.retrieve(someurl) 

# monkeypatch the httplib.HTTPMessage instance
monkeypatch_http_message(headers)

# yeah... my original filename, finally
filename = headers.get_filename()

问题是,我实际上是把源类中的装饰方法的代码直接复制过来了,我想避免这样做。

所以,我尝试通过引用源方法来进行装饰:

def monkeypatch_http_message(obj):
    from email import utils
    from email.message import (
        _parseparam,
        _unquotevalue,
        Message    # XXX added
    )
    cls = obj.__class__

    # monkeypatching httplib.Message
    cls._get_params_preserve = Message._get_params_preserve
    cls.get_params = Message.get_params
    cls.get_param = Message.get_param
    cls.get_filename = Message.get_filename

但这样做给我带来了:

Traceback (most recent call last):
  File "client.py", line 224, in <module>
    filename = headers.get_filename()
TypeError: unbound method get_filename() must be called with Message instance as first argument (got nothing instead)

我现在有点困惑... 我该如何在不直接复制源方法的情况下装饰我的类呢?

有什么建议吗?:-)

祝好,

Georges Martin


  1. 我在使用Python 2.6,不能在生产环境中使用2.7或3.x。

  2. httplib.HTTPMessage是从mimetools.Messagerfc822.Message继承而来的,而email.Message有自己的实现。

2 个回答

1

@ncoghlan: 我不能在评论里放缩进的代码,所以我再发一次:

def monkeypatch_http_message(obj):
    import httplib
    assert isinstance(obj, httplib.HTTPMessage)
    cls = obj.__class__

    from email import utils
    from email.message import (_parseparam, _unquotevalue, Message)
    funcnames = ('_get_params_preserve', 'get_params', 'get_param', 'get_filename')
    for funcname in funcnames:
        cls.__dict__[funcname] = Message.__dict__[funcname]

谢谢!:-)

2

在Python 3.x中,所谓的“未绑定方法”不再存在,所以在这种情况下你只会得到文件对象,你的第二个例子就可以正常工作了:

>>> class C():
...   def demo(): pass
... 
>>> C.demo
<function demo at 0x1fed6d8>

而在Python 2.x中,你可以通过未绑定方法来访问底层函数,或者直接从类字典中获取它(这样就绕过了正常的查找过程,不会变成未绑定方法):

>>> class C():
...   def demo(): pass
... 
>>> C.demo.im_func                  # Retrieve it from the unbound method
<function demo at 0x7f463486d5f0>
>>> C.__dict__["demo"]              # Retrieve it directly from the class dict
<function demo at 0x7f463486d5f0>

后者的方法有个好处,就是在Python 3.x中也能继续使用。

撰写回答