2012-07-25 59 views
3

尽管已经阅读了很多关于这个主题的文章(包括[这] [1]非常受欢迎的文章),但我仍然很难很好地理解装饰器。我怀疑我一定是愚蠢的,但是随着所有愚蠢带来的固执,我决定尝试弄清楚这一点。用装饰器替换宏样式的类方法?

也就是说,我怀疑我有一个很好的用例...

下面是我的一个项目,从PDF文件中提取文本的一些代码。处理包括三个步骤:

  1. 设置处理PDF文件(样板初始化)所需的PDFMiner对象。
  2. 将处理功能应用于PDF文件。
  3. 无论发生什么情况,关闭文件。

我最近了解了上下文管理器和with声明,这对他们来说似乎是一个很好的用例。因此,我开始通过定义PDFMinerWrapper类:

class PDFMinerWrapper(object): 
    ''' 
    Usage: 
    with PDFWrapper('/path/to/file.pdf') as doc: 
     doc.dosomething() 
    ''' 
    def __init__(self, pdf_doc, pdf_pwd=''): 
     self.pdf_doc = pdf_doc 
     self.pdf_pwd = pdf_pwd 

    def __enter__(self): 
     self.pdf = open(self.pdf_doc, 'rb') 
     parser = PDFParser(self.pdf) # create a parser object associated with the file object 
     doc = PDFDocument() # create a PDFDocument object that stores the document structure 
     parser.set_document(doc) # connect the parser and document objects 
     doc.set_parser(parser) 
     doc.initialize(self.pdf_pwd) # pass '' if no password required 
     return doc 

    def __exit__(self, type, value, traceback): 
     self.pdf.close() 
     # if we have an error, catch it, log it, and return the info 
     if isinstance(value, Exception): 
      self.logError() 
      print traceback 
      return value 

现在,我可以很容易地与PDF文件工作,并确保它会优雅地处理错误。从理论上讲,所有我需要做的是这样的:

with PDFMinerWrapper('/path/to/pdf') as doc: 
    foo(doc) 

这是伟大的,但我需要检查PDF文档提取以前适用于由PDFMinerWrapper返回的对象的功能。我目前的解决方案涉及中间步骤。

我正在和一个叫做Pamplemousse的课程一起工作,它可以作为与PDF一起工作的接口。每次必须在该对象已链接到的文件上执行操作时,它又使用PDFMinerWrapper

下面是一些(有删节)代码,演示了使用方法:

class Pamplemousse(object): 
    def __init__(self, inputfile, passwd='', enc='utf-8'): 
     self.pdf_doc = inputfile 
     self.passwd = passwd 
     self.enc = enc 

    def with_pdf(self, fn, *args): 
     result = None 
     with PDFMinerWrapper(self.pdf_doc, self.passwd) as doc: 
      if doc.is_extractable: # This is the test I need to perform 
       # apply function and return result 
       result = fn(doc, *args) 

     return result 

    def _parse_toc(self, doc): 
     toc = [] 
     try: 
      toc = [(level, title) for level, title, dest, a, se in doc.get_outlines()] 
     except PDFNoOutlines: 
      pass 
     return toc 

    def get_toc(self): 
     return self.with_pdf(self._parse_toc) 

任何时候,我想对PDF文件进行操作,我通过相关功能将with_pdf方法与它的参数一起。反过来,with_pdf方法使用with语句来利用PDFMinerWrapper的上下文管理器(从而确保正常处理异常),并在实际应用已传递的函数之前执行检查。

我的问题如下:

我想简化该代码,这样我就不必显式调用Pamplemousse.with_pdf。我的理解是,装饰可能会有所帮助在这里,所以:

  1. 我将如何实现一个装饰他们的工作是打电话给with语句并执行可提取支票?
  2. 装饰器是否可能是一个类方法,或者我的装饰器是否必须是一个自由形式的函数或类?

回答

1

我理解你的目标的方式,是能够对您的Pamplemousse类中定义多个方法,而不是不停得包起来这一呼吁。这是什么它可能是一个非常简化的版本:

def if_extractable(fn): 
    # this expects to be wrapping a Pamplemousse object 
    def wrapped(self, *args): 
     print "wrapper(): Calling %s with" % fn, args 
     result = None 
     with PDFMinerWrapper(self.pdf_doc) as doc: 
      if doc.is_extractable: 
       result = fn(self, doc, *args) 
     return result 
    return wrapped 


class Pamplemousse(object): 

    def __init__(self, inputfile): 
     self.pdf_doc = inputfile 

    # get_toc will only get called if the wrapper check 
    # passes the extractable test 
    @if_extractable 
    def get_toc(self, doc, *args): 
     print "get_toc():", self, doc, args 

的装饰if_extractable定义只是一个功能,但它希望在您的类的实例方法使用。

用于委托给私有方法的装饰get_toc只需要接收doc对象和参数(如果它通过检查)。否则它不会被调用并且包装返回None。

有了这个,你可以保持定义期待您的操作功能的doc

你甚至可以添加一些类型的检查,以确保其包裹的预期类:

def if_extractable(fn): 
    def wrapped(self, *args): 
    if not hasattr(self, 'pdf_doc'): 
     raise TypeError('if_extractable() is wrapping '\ 
         'a non-Pamplemousse object') 
    ... 
+0

您正确解读了我的目标!这非常合理。只是一个简单的问题 - 是否有可能(也是可取的)将'if_extractable'作为'Pamplemousse'类的方法?为了形式的利益,这个功能将成为“Pamplemousse”类的一部分。无论如何,我会测试你的解决方案,非常感谢你! – blz 2012-07-26 12:34:58

+0

我会说不是真的,因为你甚至不能把它装饰成classmethod,因为它会在同一个类中使用它。再加上它不是一种方法,会被某个使用该类的人称为成员,所以你不需要在对象上看到它。将它作为模块中的一个函数并将其保留在类名称空间之外更容易。它更适合那个班。 – jdi 2012-07-26 14:32:14

0

装饰器只是一个接受函数并返回另一个函数的函数。你可以做任何你喜欢的:

def my_func(): 
    return 'banana' 

def my_decorator(f): # see it takes a function as an argument 
    def wrapped(): 
     res = None 
     with PDFMineWrapper(pdf_doc, passwd) as doc: 
      res = f() 
     return res 
    return wrapper # see, I return a function that also calls f 

现在,如果你申请的装饰:

@my_decorator 
def my_func(): 
    return 'banana' 

wrapped功能将取代my_func,所以额外的代码将被调用。

0

您可能要沿着这个线路尝试:

def with_pdf(self, fn, *args): 
    def wrappedfunc(*args): 
     result = None 
     with PDFMinerWrapper(self.pdf_doc, self.passwd) as doc: 
      if doc.is_extractable: # This is the test I need to perform 
       # apply function and return result 
       result = fn(doc, *args) 
     return result 
    return wrappedfunc 

,当你需要包装的功能,只是这样做:

@pamplemousseinstance.with_pdf 
def foo(doc, *args): 
    print 'I am doing stuff with', doc 
    print 'I also got some good args. Take a look!', args 
+0

我不知道我完全理解第二块代码。 “@ pamplemousseinstance.with_pdf”应该是'self.with_pdf'吗? 'Pamplemousse.with_pdf'?另外,'foo'会是一个类方法,但我想这并不能改变任何东西,对吧? – blz 2012-07-25 23:46:36

+0

我不认为建议使用类实例作为装饰器是有意义的。这需要立即在全局范围内创建一个实例,因为在模块加载时应用了装饰器。 – jdi 2012-07-25 23:47:07

+0

@jdi Hrm,这实际上是一个很好的观点。但是,考虑到他的动态版本无论如何都在访问类字段,最好不要使用技术'@'装饰器。另外,'pamplemousseinstance'是Pamplemousse的一个实例。把它做成“Pamplemousse”和一种分类方法可能会更好。 – Kupiakos 2012-07-26 01:51:58

0

下面是一些演示代码:

#! /usr/bin/python 

class Doc(object): 
    """Dummy PDFParser Object""" 

    is_extractable = True 
    text = '' 

class PDFMinerWrapper(object): 
    ''' 
    Usage: 
    with PDFWrapper('/path/to/file.pdf') as doc: 
     doc.dosomething() 
    ''' 
    def __init__(self, pdf_doc, pdf_pwd=''): 
     self.pdf_doc = pdf_doc 
     self.pdf_pwd = pdf_pwd 

    def __enter__(self): 
     return self.pdf_doc 

    def __exit__(self, type, value, traceback): 
     pass 

def safe_with_pdf(fn): 
    """ 
    This is the decorator, it gets passed the fn we want 
    to decorate. 

    However as it is also a class method it also get passed 
    the class. This appears as the first argument and the 
    function as the second argument. 
    """ 
    print "---- Decorator ----" 
    print "safe_with_pdf: First arg (fn):", fn 
    def wrapper(self, *args, **kargs): 
     """ 
     This will get passed the functions arguments and kargs, 
     which means that we can intercept them here. 
     """ 
     print "--- We are now in the wrapper ---" 
     print "wrapper: First arg (self):", self 
     print "wrapper: Other args (*args):", args 
     print "wrapper: Other kargs (**kargs):", kargs 

     # This function is accessible because this function is 
     # a closure, thus still has access to the decorators 
     # ivars. 
     print "wrapper: The function we run (fn):", fn 

     # This wrapper is now pretending to be the original function 

     # Perform all the checks and stuff 
     with PDFMinerWrapper(self.pdf, self.passwd) as doc: 
      if doc.is_extractable: 
       # Now call the orininal function with its 
       # argument and pass it the doc 
       result = fn(doc, *args, **kargs) 
      else: 
       result = None 
     print "--- End of the Wrapper ---" 
     return result 

    # Decorators are expected to return a function, this 
    # function is then run instead of the decorated function. 
    # So instead of returning the original function we return the 
    # wrapper. The wrapper will be run with the original functions 
    # argument. 

    # Now by using closures we can still access the original 
    # functions by looking up fn (the argument that was passed 
    # to this function) inside of the wrapper. 
    print "--- Decorator ---" 
    return wrapper 


class SomeKlass(object): 

    @safe_with_pdf 
    def pdf_thing(doc, some_argument): 
     print '' 
     print "-- The Function --" 

     # This function is now passed the doc from the wrapper. 

     print 'The contents of the pdf:', doc.text 
     print 'some_argument', some_argument 
     print "-- End of the Function --" 
     print '' 

doc = Doc() 
doc.text = 'PDF contents' 
klass = SomeKlass() 
klass.pdf = doc 
klass.passwd = '' 
klass.pdf_thing('arg') 

我建议运行该代码以了解其工作原理。一些有趣的点看出来的寿:

首先,你会发现,我们只传递一个参数来pdf_thing(),但如果你看一下该方法需要两个参数:

@safe_with_pdf 
def pdf_thing(doc, some_argument): 
    print '' 
    print "-- The Function --" 

这是因为如果你看看我们的所有功能包装:

with PDFMinerWrapper(self.pdf, self.passwd) as doc: 
    if doc.is_extractable: 
     # Now call the orininal function with its 
     # argument and pass it the doc 
     result = fn(doc, *args, **kargs) 

我们生成文档的说法,并传递它,与原来的参数(*args, **kargs)一起。这意味着除了在声明(def pdf_thing(doc, some_argument):)中列出的参数之外,使用此装饰器包装的每个方法或函数还会接收一个额外的doc参数。

另一个要注意的是,包装:

def wrapper(self, *args, **kargs): 
    """ 
    This will get passed the functions arguments and kargs, 
    which means that we can intercept them here. 
    """ 

也抓住了self说法,不把它传递给被调用的方法。你可以改变这种行为,我从修改函数调用:

result = fn(doc, *args, **kargs) 
    else: 
     result = None 

要:

result = fn(self, doc, *args, **kargs) 
    else: 
     result = None 

,然后改变方法本身:

def pdf_thing(self, doc, some_argument): 

希望帮助,随意要求更多的澄清。

编辑:

要回答你的问题的第二部分。

是的,可以是一个类的方法。只需在SomeKlass之内safe_with_pdf以上并打电话给它,例如,班级中的第一种方法。

此处还有上述代码的简化版本,并带有类中的装饰器。

class SomeKlass(object): 
    def safe_with_pdf(fn): 
     """The decorator which will wrap the method""" 
     def wrapper(self, *args, **kargs): 
      """The wrapper which will call the method is a doc""" 
      with PDFMinerWrapper(self.pdf, self.passwd) as doc: 
       if doc.is_extractable: 
        result = fn(doc, *args, **kargs) 
       else: 
        result = None 
      return result 
     return wrapper 

    @safe_with_pdf 
    def pdf_thing(doc, some_argument): 
     """The method to decorate""" 
     print 'The contents of the pdf:', doc.text 
     print 'some_argument', some_argument 
     return '%s - Result' % doc.text 

print klass.pdf_thing('arg')