Замена метода класса в стиле макросов декоратором?
У меня много проблем с тем, чтобы хорошо понять декораторов, несмотря на то, что я прочитал много статей на эту тему (включая [эту][1] очень популярную статью о SO). Я подозреваю, что я, должно быть, глуп, но со всем этим упрямством, которое приходит с глупостью, я решил попытаться понять это.
Это, и я подозреваю, что у меня есть хороший пример использования...
Ниже приведен код моего проекта, который извлекает текст из файлов PDF. Обработка включает три этапа:
- Настройте объекты PDFMiner, необходимые для обработки файла PDF (шаблонные инициализации).
- Примените функцию обработки к файлу PDF.
- Что бы ни случилось, закройте файл.
Я недавно узнал о менеджерах контекста и 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
, Насколько я понимаю, здесь могут помочь декораторы, поэтому:
- Как бы я реализовал декоратор, работа которого состояла бы в том, чтобы вызвать
with
заявление и выполнить проверку извлекаемости? - Возможно ли, чтобы декоратор был методом класса, или мой декоратор должен быть функцией или классом произвольной формы?
4 ответа
То, как я интерпретировал вашу цель, состояло в том, чтобы иметь возможность определить несколько методов на вашем 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')
...
Вот некоторый демонстрационный код:
#! /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)
Мы генерируем аргумент doc и передаем его вместе с исходными аргументами (*args, **kargs
). Это означает, что каждый метод или функция, обернутые этим декоратором, получают дополнение doc
аргумент в дополнение к аргументам, указанным в его объявлении (def pdf_thing(doc, some_argument):
).
Еще одна вещь, которую стоит отметить, это то, что обертка:
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):
Надеюсь, что это помогает, не стесняйтесь просить больше разъяснений.
РЕДАКТИРОВАТЬ:
Чтобы ответить на вторую часть вашего вопроса.
Да, это может быть метод класса. Просто место safe_with_pdf
Внутри SomeKlass
выше и вызывает его, например, первый метод в классе.
Также здесь приведена уменьшенная версия приведенного выше кода с декоратором в классе.
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')
Вы можете попробовать в соответствии с этим:
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
Декоратор - это просто функция, которая берет функцию и возвращает другую. Вы можете делать все что угодно:
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
, поэтому будет вызван дополнительный код.