Статьи

Напишите ваши собственные декораторы Python

В статье Deep Dive Into Python Decorators я представил концепцию декораторов Python, продемонстрировал множество классных декораторов и объяснил, как их использовать.

В этом уроке я покажу вам, как писать собственные декораторы. Как вы увидите, написание собственных декораторов дает вам много контроля и дает много возможностей. Без декораторов эти возможности потребовали бы много подверженных ошибкам и повторяющихся шаблонов, которые загромождают ваш код или полностью внешние механизмы, такие как генерация кода.

Краткий обзор, если вы ничего не знаете о декораторах. Декоратор — это вызываемый объект (функция, метод, класс или объект с методом call ()), который принимает вызываемый элемент в качестве входных данных и возвращает вызываемый элемент в качестве выходных данных. Как правило, возвращаемый вызываемый объект делает что-то до и / или после вызова входного вызываемого объекта. Вы применяете декоратор с помощью @ синтаксис. Много примеров в ближайшее время …

Давайте начнем с «Привет, мир!» декоратор. Этот декоратор полностью заменит любой оформленный вызываемый объект функцией, которая просто печатает «Hello World!».

python def hello_world(f): def decorated(*args, **kwargs): print 'Hello World!' return decorated

Вот и все. Давайте посмотрим на это в действии, а затем объясним различные части и как это работает. Предположим, у нас есть следующая функция, которая принимает два числа и печатает их произведение:

python def multiply(x, y): print x * y

Если вы вызываете, вы получаете то, что ожидаете:

multiply(6, 7) 42

Давайте украсим его с помощью нашего декоратора hello_world, пометив функцию умножения @hello_world .

python @hello_world def multiply(x, y): print x * y

Теперь, когда вы вызываете умножение с любыми аргументами (включая неправильные типы данных или неправильное количество аргументов), результатом всегда будет «Hello World!» распечатаны.

« `python multiply (6, 7) Hello World!

multiply () Привет, мир!

умножить (‘zzz’) Hello World! « `

OK. Как это работает? Оригинальная функция умножения была полностью заменена вложенной декорированной функцией внутри декоратора hello_world . Если мы проанализируем структуру декоратора hello_world, то вы увидите, что он принимает входной вызываемый элемент f (который не используется в этом простом декораторе), он определяет вложенную функцию с именем decor, которая принимает любую комбинацию аргументов и аргументов ключевого слова ( def decorated(*args, **kwargs) ), и, наконец, он возвращает украшенную функцию.

Нет разницы между написанием функции и метода декоратора. Определение декоратора будет таким же. Вызываемый вход будет либо обычной функцией, либо связанным методом.

Давайте проверим это. Вот декоратор, который просто печатает вызываемый ввод и печатает перед его вызовом. Это очень типично для декоратора, который выполняет какое-то действие и продолжает, вызывая исходный вызываемый объект.

python def print_callable(f): def decorated(*args, **kwargs): print f, type(f) return f(*args, **kwargs) return decorated

Обратите внимание на последнюю строку, которая вызывает входной вызов вызываемым образом и возвращает результат. Этот декоратор ненавязчив в том смысле, что вы можете декорировать любую функцию или метод в рабочем приложении, и приложение будет продолжать работать, потому что декорированная функция вызывает оригинал и просто имеет небольшой побочный эффект.

Давайте посмотрим на это в действии. Я украслю нашу функцию умножения и метод.

« `python @print_callable def multiply (x, y): print x * y

класс A (объект): @print_callable def foo (self): выведите ‘foo () здесь’ « `

Когда мы вызываем функцию и метод, вызываемый объект печатается, а затем они выполняют свою первоначальную задачу:

« `python multiply (6, 7) <функция multiply в 0x103cb6398> <тип ‘function’> 42

A (). Foo () <function foo at 0x103cb6410> <тип ‘function’> foo () здесь « `

Декораторы тоже могут принимать аргументы. Эта возможность настроить работу декоратора очень мощна и позволяет использовать один и тот же декоратор во многих контекстах.

Предположим, ваш код слишком быстрый, и ваш начальник просит вас немного его замедлить, потому что вы заставляете других членов команды выглядеть плохо. Давайте напишем декоратор, который измеряет, как долго работает функция, и если она выполняется менее чем за определенное количество секунд t , она будет ждать, пока истечет t секунд, а затем вернется.

В настоящее время отличается то, что сам декоратор принимает аргумент t, который определяет минимальное время выполнения, и разные функции могут быть украшены разными минимальными временами выполнения. Также вы заметите, что при представлении аргументов декоратора требуются два уровня вложенности:

« `время импорта Python

def minimal_runtime (t): def украшенный (f): def wrapper ( args, ** kwargs): start = time.time () result = f ( args, ** kwargs) runtime = time.time () — запустить, если время выполнения <t: time.sleep (t — время выполнения) возвращаемый результат возвращаемая оболочка возвращаемая оформленная « `

Давайте распакуем это. Сам декоратор — функция Minimum_runtime принимает аргумент t , который представляет минимальное время выполнения для оформленного вызываемого объекта. Входной вызываемый f был «передан» во вложенную декорированную функцию, а входные вызываемые аргументы «переданы» в еще одну оболочку вложенной функции.

Фактическая логика происходит внутри функции оболочки . Время начала записывается, исходный вызываемый f вызывается с его аргументами, а результат сохраняется. Затем проверяется время выполнения, и если оно меньше минимального t, то оно спит в течение остального времени, а затем возвращается.

Чтобы проверить это, я создам пару функций, которые вызывают умножение, и украсим их с разными задержками.

« `python @minimum_runtime (1) def slow_multiply (x, y): умножить (x, y)

@minimum_runtime (3) def slower_multiply (x, y): multiply (x, y) « `

Теперь я вызову умножение напрямую, а также более медленные функции и буду измерять время.

« `время импорта Python

funcs = [multiply, slow_multiply, slower_multiply] для f в funcs: start = time.time () f (6, 7) print f, time.time () — start « `

Вот вывод:

plain 42 <function multiply at 0x103cb6b90> 1.59740447998e-05 42 <function wrapper at 0x103d0bcf8> 1.00477004051 42 <function wrapper at 0x103cb6ed8> 3.00489807129

Как вы можете видеть, первоначальное умножение почти не заняло времени, а более медленные версии были действительно отложены в соответствии с предоставленным минимальным временем выполнения.

Еще один интересный факт заключается в том, что выполняемая декорированная функция является оберткой, что имеет смысл, если вы следуете определению декорированной. Но это может быть проблемой, особенно если мы имеем дело с декораторами стека. Причина в том, что многие декораторы также проверяют свои входные данные и проверяют их имя, подпись и аргументы. В следующих разделах будет рассмотрен этот вопрос и предоставлены советы по передовому опыту.

Вы также можете использовать объекты в качестве декораторов или возвращать объекты из ваших декораторов. Единственное требование состоит в том, что у них есть метод __call __ () , поэтому они могут быть вызваны. Вот пример для объектного декоратора, который подсчитывает, сколько раз вызывается его целевая функция:

python class Counter(object): def __init__(self, f): self.f = f self.called = 0 def __call__(self, *args, **kwargs): self.called += 1 return self.f(*args, **kwargs)

Вот оно в действии:

« `python @Counter def bbb (): print ‘bbb’

BBB () BBB

BBB () BBB

BBB () BBB

печать bbb.called 3 « `

В основном это вопрос личных предпочтений. Вложенные функции и замыкания функций обеспечивают все управление состоянием, которое предлагают объекты. Некоторые люди чувствуют себя как дома с классами и предметами.

В следующем разделе я расскажу о хороших поведенческих декораторах, а объектно-ориентированные декораторы требуют немного дополнительной работы, чтобы быть хорошими.

Декораторы общего назначения часто могут быть сложены. Например:

python @decorator_1 @decorator_2 def foo(): print 'foo() here'

При наложении декораторов внешний декоратор (в данном случае decorator_1) получит запрос, возвращаемый внутренним декоратором (decorator_2). Если decorator_1 каким-то образом зависит от имени, аргументов или строки документа исходной функции, а decorator_2 реализован наивно, то decorator_2 увидит, что не увидит правильную информацию из исходной функции, а только вызываемый элемент, возвращаемый decorator_2.

Например, вот декоратор, который проверяет имя своей целевой функции в нижнем регистре:

python def check_lowercase(f): def decorated(*args, **kwargs): assert f.func_name == f.func_name.lower() f(*args, **kwargs) return decorated

Давайте украсим функцию этим:

python @check_lowercase def Foo(): print 'Foo() here'

Вызов Foo () приводит к утверждению:

« `plain В [51]: Foo () ————————————————————————— AssertionError Traceback (последний вызов был последним)

в () —-> 1 Foo () в оформленном (* args, ** kwargs) 1 def check_lowercase (f): 2 def оформленном (* args, ** kwargs): —-> 3 assert f.func_name == f.func_name.lower () 4 return decor « `Но если мы накладываем ** check_lowercase ** декоратор на декоратор, такой как ** hello_world **, который возвращает вложенную функцию под названием ‘decor», результат будет совсем другим: « `python @check_lowercase @hello_world def Foo ( ): выведите ‘Foo () здесь’ Foo () Hello World! « `Декоратор ** check_lowercase ** не выдвинул утверждение, потому что не увидел имя функции ‘Foo’. Это серьезная проблема. Правильное поведение декоратора — сохранить как можно больше атрибутов исходной функции. Посмотрим, как это делается. Теперь я создам декоратор оболочки, который просто вызывает свой входной сигнал, который вызывается, но сохраняет всю информацию из входной функции: имя функции, все ее атрибуты (в случае, если внутренний декоратор добавил некоторые пользовательские атрибуты) и строку документации. « `python def passthrough (f): оформлено def (* args, ** kwargs): оформлено f (* args, ** kwargs) .__ name__ = f .__ name__ оформлено .__ name__ = f .__ module__ оформлено .__ dict__ = f. __dict__ украшен .__ doc__ = f .__ doc__ возврат оформлен « `Теперь декораторы, уложенные поверх ** passthrough ** декоратора, будут работать так же, как если бы они непосредственно декорировали целевую функцию. « `python @check_lowercase @passthrough def Foo (): print ‘Foo () here’` « ### Использование декоратора @wraps Эта функция настолько полезна, что в стандартной библиотеке есть специальный декоратор в модуле functools под названием [ ‘wraps’] (https://docs.python.org/2/library/functools.html#functools.wraps), чтобы помочь написать правильные декораторы, которые хорошо работают с другими декораторами. Вы просто декорируете внутри вашего декоратора возвращенную функцию с помощью ** @ wraps (f) **. Посмотрите, насколько лаконичнее выглядит ** passthrough ** при использовании ** wraps **: « `python из functools import wraps def passthrough (f): @wraps (f) def decor (* args, ** kwargs): f (* args, ** kwargs) возвращать оформленный « `Я настоятельно рекомендую всегда использовать его, если ваш декоратор не предназначен для изменения некоторых из этих атрибутов. ## Написание декораторов классов Декораторы классов были введены в Python 3.0. Они действуют на весь класс. Декоратор класса вызывается, когда класс определен и до создания каких-либо экземпляров. Это позволяет декоратору класса изменять практически все аспекты класса. Обычно вы добавляете или украшаете несколько методов. Давайте сразу перейдем к причудливому примеру: предположим, у вас есть класс с именем AwesomeClass с набором открытых методов (методы, чье имя не начинается с подчеркивания, например __init__), и у вас есть тестовый класс на основе юнит-тестов, называемый AwesomeClassTest ». AwesomeClass не просто великолепен, но и очень важен, и вы хотите убедиться, что если кто-то добавит новый метод в AwesomeClass, он также добавит соответствующий метод тестирования в AwesomeClassTest. Вот класс AwesomeClass: « `класс Python AwesomeClass: def awesome_1 (self): return ‘awesome!’ def awesome_2 (self): вернуть ‘классно! классно!’ « `Вот AwesomeClassTest:` « Python из unittest import TestCase, основной класс AwesomeClassTest (TestCase): def test_awesome_1 (self): r = AwesomeClass (). Awesome_1 () self.assertEqual (‘awesome!’, R) def test_awesome_2 (self): r = AwesomeClass (). awesome_2 () self.assertEqual (‘awesome! awesome!’, r) if __name__ == ‘__main__’: main () « `Теперь, если кто-то добавит ** Метод awesome_3 ** с ошибкой, тесты все равно пройдут, потому что нет теста, который вызывает ** awesome_3 **. Как вы можете гарантировать, что всегда есть метод тестирования для каждого публичного метода? Ну, вы пишете декоратор класса, конечно. Декоратор класса @ensure_tests украсит AwesomeClassTest и убедится, что у каждого публичного метода есть соответствующий тестовый метод. « `python def sure_tests (cls, target_class): test_methods = [m для m в cls .__ dict__ if m.startswith (‘test_’)] public_methods = [k для k, v в target_class .__ dict __. items () если вызывается (v) а не k.startswith (‘_’)] # Убрать префикс ‘test_’ из имен методов тестирования test_methods = [m [5:] для m в test_methods], если установлено (test_methods)! = set (public_methods): повысить RuntimeError (‘Несоответствие тестов / открытых методов!’) Возвращает cls « `Это выглядит довольно хорошо, но есть одна проблема. Декораторы класса принимают только один аргумент: декорированный класс. Декоратору sure_tests требуется два аргумента: класс и целевой класс. Я не мог найти способ иметь декораторы классов с аргументами, похожими на декораторы функций. Не бойся Python имеет функцию [functools.partial] (https://docs.python.org/2/library/functools.html#functools.partial) только для этих случаев. « `python @partial (sure_tests, target_class = AwesomeClass) класс AwesomeClassTest (TestCase): def test_awesome_1 (self): r = AwesomeClass (). awesome_1 () self.assertEqual (‘awesome!’, r) def test_awesome_2 : r = AwesomeClass (). awesome_2 () self.assertEqual (‘awesome! awesome!’, r) if __name__ == ‘__main__’: main () « `Выполнение тестов приводит к успеху, поскольку все открытые методы, * * awesome_1 ** и ** awesome_2 **, имеют соответствующие методы тестирования, ** test_awesome_1 ** и ** test_awesome_2 **. « `———————————————— ———————— Провел 2 теста за 0.000 с ОК. « `Давайте добавим новый метод ** awesome_3 ** без соответствующего теста и запустим тесты снова. « `класс Python AwesomeClass: def awesome_1 (self): вернуть ‘awesome!’ def awesome_2 (self): вернуть ‘классно! классно!’ def awesome_3 (self): вернуть ‘классно! классно! классно!’ « `Повторное выполнение тестов приводит к следующему выводу:` « python3 a.py Traceback (последний вызов был последним): файл «a.py», строка 25, в class AwesomeClassTest (TestCase): файл «a.py», строка 21, в sure_tests поднять RuntimeError (‘Несоответствие тестов / открытых методов!’) RuntimeError: Несовпадение тестовых / открытых методов! « `Декоратор класса обнаружил несоответствие и уведомил вас громко и четко. ## Заключение Написание Python-декораторов очень увлекательно и позволяет инкапсулировать тонны функциональности многократно используемым способом. Чтобы в полной мере воспользоваться преимуществами декораторов и объединить их интересными способами, вам необходимо знать лучшие практики и идиомы. Декораторы классов в Python 3 добавляют совершенно новое измерение, настраивая поведение завершенных классов.