Comments 15
Как мы можем видеть, тело декоратора сократилось в 8 раз. Profit!
Зато само использование декоратора неявно зависит от наличия другого декоратора и, видимо, от порядка их применения (что очень плохо) и добавление декоратора занимает в два раза больше строк, если уж решили обращать на это внимание
Декоратор пишется один раз и пусть он будет максимально прозрачным внутри, а не как у Вас в итоге
А вот использовать декоратор я буду n раз и везде нужно тащить Вашу магию с собой
Во-первых, стоило бы, для начала, залить код на GitHub. На PyPi вы распростряняете .tar.gz, поэтому исходник все равно можно вытащить, но можно было и .whl собрать.
Во-вторых, типовая операция внутри декоратора — обернуть, например, в try/except
. Ну или в контекстный менеджер, например. В вашем случае, судя по описанию, придется использовать contextlib.ExitStack
и придумывать какой-то признак неуспешного завершения функции, НО, внезапно, пользователя поджидает неприятный сюрприз — в случае исключения post_function()
вообще не вызывается!
Добавьте это себе в тесты что ли. Вы же написали тесты для модуля перед тем как писать статью на Хабр?
Ссылки по теме: https://github.com/lord63/awesome-python-decorator
Например, посмотрите на wrapt
from DecoratorHelper ...
Повеяло началом 2000-ых — тогда часто называли питонячие модули в таком стиле. А потом появилась рекомендация не использовать заглавные буквы в названиях пакетов и модулей (для разделения слов можно использовать подчёркивание).
В вашем случае это кроме всего будет также источником ошибок. Например если я или IDE сделает вот так:
import DecoratorHelper
...
@fictive
@DecoratorHelper('Hello, ')
def hello_world(text):
print(text)
Придётся потом некоторое время "долбить глаза" в декорируемую функцию и не понимать, почему питон ругается что DecoratorHelper — not callable.
@fictive @DecoratorHelper('Hello, ') def hello_world(text):
Неужели нельзя было сделать наоборот — декоратор для "декоратора"? Т.е. вот так:
@DecoratorHelper
def fictive(object):
object.pre_function = lambda : print(*object.decorator_args[:-1], end='')
return object
И тогда не пришлось бы "пользователям" декоратора fictive
помнить про DecoratorHelper
.
def fictive(object):
object — это ключевое слово питона. Не рекомендуется ключевые слова использовать в качестве "свободных" переменных.
object — это ключевое слово питона.
Это не ключевое слово, а название класса в builtin пространстве имён
Python 3.9.5 (default, May 24 2021, 12:50:35)
[GCC 11.1.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import keyword
>>> keyword.iskeyword('object')
False
>>> import builtins
>>> builtins.object
<class 'object'>
</зануда mode>
Если совсем просто, то это удобный способ передать одну функцию в другую и получить третью. В этом определении нет ни одного слова правды, но мы вернёмся к этому позже.
Я всегда считал, что wrapper, это способ выполнить одну функцию из обертки, которая может что-то делать дополнительно, а не передавать функцию в функцию. Поэтому ваше определение и неправда, что смысл похож, но «есть нюанс»…
Декоратор — это функция (и класс?), которая принимает что-то callable на вход. И, в принципе, остальное — это уже по желанию. Она может как вызывать эту функцию так и заменить на другую или вообще вернуть None.
def my_fake_decorator(func):
return None
@my_fake_decorator
def very_important_code(password):
print(password)
print(very_important_code is None) # True
wrapt в официальной документации такие случаи с опционально-параметризированным декоратором тоже предлагает решать в три этажа; ну или в два: https://wrapt.readthedocs.io/en/latest/decorators.html#decorators-with-optional-arguments. Так что про "всё реализовано" — не совсем верное утверждение.
Подход интересный. Стоит упомянуть, что помимо функций довольно часто декорируют классы. Но не всё можно передать в декоратор. Точнее, передать можно всё — если вызывать как функцию — но вот именно как декоратор Python позволяет его использовать только с def
& class
— на уровне синтаксиса; на переменные и, например, лямбды, наложить декоратор не получится (а хочется иногда).
Но увы, я лично не понял, так ли проблематична проблема, которую решает эта библиотека. Добавляют ли лишние парочка строк кода с типовым паттерном больше сложности, чем дополнительная 3rd-party зависимость, этот паттерн реализующая?
А если я хочу декоратор тройной вложенности, то это не просто так. Например, на первом уровне я проверяю валидность аргументов декоратора и выкидываю ошибку если что-то не сходится — ещё на этапе импорта модуля. На втором уровне я хочу сделать inspect оборачиваемой функции и что-то сохранить в памяти чтобы это не вычислять на каждом вызове; например, понять, sync она или async, returning или yielding. И на третьем уровне я не просто вызываю ту функцию, но оборачиваю её в другую логику, например, try-except или пост-обработку результата или вставку дополнительных аргументов. Каждый уровень делает своё дело — его нельзя пропустить.
Если эту задачу реального мира решить с таким хелпером, то код будет ничуть не проще, мне думается. Может, даже сложнее.
Гораздо более болючей болью декораторов является типизация — пока нет ParamSpec + Concatenate из PEP-612 для Python 3.10. Да и с ним не сильно легче — особенно когда в оборачиваемую функцию добавляются/удаляются аргументы (hello, partials). А также боль управления пачкой однотипных декораторов с огромным списком почти одинаковых аргументов каждый, но так, чтобы это переваривал и mypy, и IDE (ну хотя бы PyCharm). Не уверен что тут вообще есть решение.
По возможности лучше вообще избегать написания декораторов, которые меняют аргументы функции. Это не только для IDE и линтеров сложно, но и человеку, который не знает детально работу декоратора, будет "больно".
Декоратор может легко получить список аргументов декорируемой функции (и даже их типы, если проставить аннотации) и использовать эту информацию, что бы вернуть функцию с такой же сигнатурой. Или срайзить исключение если, что-то в сигнатуре его не устраивает.
Но это работает только в runtime. В type-checking time (e.g., mypy) декоратор не выполняется. Лишь type-checker решает там какие-то свои уравнения на основе одних лишь аннотаций.
Декораторы Python: хватит это терпеть