Книга: Чистый Python. Тонкости программирования для профи
Назад: 2. Шаблоны для чистого Python
Дальше: 4. Классы и ООП

3. Эффективные функции

3.1. Функции Python — это объекты первого класса

Функции Python относятся к объектам первого класса. Их можно присваивать переменным, хранить их в структурах данных, передавать их в качестве аргументов другим функциям и даже возвращать их в качестве значений из других функций.

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

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

Чтобы осмыслить понятия, которые мы будем здесь рассматривать, потребуется немного больше времени, чем вы ожидаете. Не волнуйтесь — это абсолютно нормально. Со мной было точно так же. Вполне возможно, что вы будете ощущать, как бьетесь головой о стену, а затем в один прекрасный момент, когда вы будете готовы, вас внезапно осенит, и все встанет на свои места.

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

def yell(text):

     return text.upper() + '!'

 

>>> yell('привет')

'ПРИВЕТ!'

Функции — это объекты

Все данные в программе Python представляются объектами или связями между объектами. Символьные последовательности (строки), списки, модули и функции — все эти явления языка представляют собой объекты. Что касается функций, то в Python они ничем не отличаются. Они — тоже объекты.

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

>>> bark = yell

Эта строка кода не вызывает функцию. Она берет объект-функцию, на который ссылается имя yell, и создает второе имя, bark, которое на него указывает. Теперь вы можете исполнить тот же самый объект-функцию, который лежит в его основе, вызвав bark:

>>> bark('гав')

'ГАВ!'

Объекты-функции и их имена — это две отдельные компетенции. Вот еще одно доказательство. Вы можете удалить первоначальное имя функции (yell), и, поскольку еще одно имя (bark) по-прежнему указывает на лежащую в основе функцию, вы все так же можете через него вызвать эту функцию:

>>> del yell

 

>>> yell('Привет?')

NameError: "name 'yell' is not defined"

 

>>> bark('эй')

'ЭЙ!'

Кстати, Python прикрепляет к каждой функции строковый идентификатор. Это делается для отладочных целей во время создания функции. К этому внутреннему идентификатору можно получить доступ посредством атрибута __name__:

>>> bark.__name__

'yell'

Нужно сказать, что хотя атрибут __name__ функции по-прежнему «yell», это не влияет на то, каким образом вы получаете доступ к объекту-функции из вашего программного кода. Идентификатор имени является просто средством отладки. Указывающая на функцию переменная и сама функция обладают совершенно разными компетенциями.

Функции могут храниться в структурах данных

Поскольку функции — это объекты первого класса, их можно хранить в структурах данных точно так же, как это делается с другими объектами. Например, вы можете добавить функции в список:

>>> funcs = [bark, str.lower, str.capitalize]

>>> funcs

[<function yell at 0x10ff96510>,  

<method 'lower' of 'str' objects>,

<method 'capitalize' of 'str' objects>]

Доступ к объектам-функциям, хранящимся внутри списка, осуществляется точно так же, как это происходит с объектом любого другого типа:

>>> for f in funcs:

...     print(f, f('всем привет'))

<function yell at 0x10ff96510> 'ВСЕМ ПРИВЕТ!'

<method 'lower' of 'str' objects> 'всем привет'

<method 'capitalize' of 'str' objects> 'Всем привет'

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

>>> funcs[0]('приветище')

'ПРИВЕТИЩЕ!'

Функции могут передаваться другим функциям

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

def greet(func):

     greeting = func('Привет! Я — программа Python')

     print(greeting)

На результирующее приветствие можно влиять, передавая различные функции. Ниже показано, что происходит, если в функцию greet передать функцию bark:

>>> greet(bark)

'ПРИВЕТ! Я — ПРОГРАММА PYTHON'

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

def whisper(text):

     return text.lower() + '...'

 

>>> greet(whisper)

'Привет! Я — программа Python...'

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

Функции, которые в качестве аргументов могут принимать другие функции, также называются функциями более высокого порядка (higher-order functions). Они являются непременным условием функционального стиля программирования.

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

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

>>> list(map(bark, ['здравствуй', 'эй', 'привет']))

['ЗДРАВСТВУЙ!', 'ЭЙ!', 'ПРИВЕТ!']

Как видите, функция более высокого порядка map обошла весь список и применила функцию bark к каждому его элементу. В результате у нас теперь новый объект-список с измененными приветственными строковыми значениями.

Функции могут быть вложенными

Быть может, вы удивитесь, но Python допускает определение функций внутри других функций. Такие функции нередко называются вложенными функциями (nested functions), или внутренними функциями (inner functions). Приведем пример:

def speak(text):

     def whisper(t):

         return t.lower() + '...'

     return whisper(text)

 

>>> speak('Привет, Мир')

'привет, мир...'

Итак, что же тут происходит? Всякий раз, когда вы вызываете функцию speak, она определяет новую внутреннюю функцию whisper и затем после этого немедленно ее вызывает. В этом месте мой мозг начинает испытывать легкий зуд, но в целом пока материал относительно последовательный.

Правда, вот вам неожиданный поворот — функция whisper не существует за пределами функции speak:

>>> whisper('Йоу')

NameError:

"name 'whisper' is not defined"

 

>>> speak.whisper

AttributeError:

"'function' object has no attribute 'whisper'"

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

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

def get_speak_func(volume):

     def whisper(text):

         return text.lower() + '...'

     def yell(text):

         return text.upper() + '!'

     if volume > 0.5:

         return yell

     else:

         return whisper

Обратите внимание на то, как функция get_speak_func фактически не вызывает ни одну из своих внутренних функций — она просто выбирает соответствующую внутреннюю функцию на основе аргумента volume и затем возвращает объект-функцию:

>>> get_speak_func(0.3)

<function get_speak_func.<locals>.whisper at 0x10ae18>

 

>>> get_speak_func(0.7)

<function get_speak_func.<locals>.yell at 0x1008c8>

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

>>> speak_func = get_speak_func(0.7)

>>> speak_func('Привет')

'Привет!'

Только подумайте… Это означает, что функции не только могут принимать линии поведения через аргументы, но и возвращать линии поведения. Здорово, правда?

И знаете что? С этого места дела приобретают несколько безумный характер. И прежде чем я продолжу писать, мне срочно требуется перерыв на кофе (я предлагаю вам сделать то же самое).

Функции могут захватывать локальное состояние

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

Сейчас лучше всего пристегнуть ремень безопасности, потому что все становится еще безумнее — мы собираемся зайти на территорию функционального программирования еще дальше. (У вас ведь был перерыв на кофе, правда?)

Мало того что функции могут возвращать другие функции, эти внутренние функции также могут захватывать и уносить с собой часть состояния родительской функции. И что же это означает?

Чтобы это проиллюстрировать, я собираюсь немного переписать предыдущий пример функции get_speak_func. Новая версия сразу принимает аргументы «volume» и «text», чтобы немедленно сделать возвращаемую функцию вызываемой:

def get_speak_func(text, volume):

     def whisper():

         return text.lower() + '...'

     def yell():

         return text.upper() + '!'

     if volume > 0.5:

         return yell

     else:

         return whisper

>>> get_speak_func('Привет, Мир', 0.7)()

'ПРИВЕТ, МИР!'

Теперь взгляните на внутренние функции whisper и yell. Обратили ­внимание на то, что у них больше нет параметра text? Но каким-то непостижимым образом они по-прежнему могут получать доступ к этому параметру text, определенному в родительской функции. На самом деле они, похоже, захватывают и «запоминают» значение этого аргумента.

Функции, которые это делают, называются лексическими замыканиями (lexical closures) (или, для краткости, просто замыканиями). Замыкание помнит значения из своего лексического контекста, даже когда поток управления программы больше не находится в этом контексте.

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

def make_adder(n):

     def add(x):

         return x + n

     return add

>>> plus_3 = make_adder(3)

>>> plus_5 = make_adder(5)

>>> plus_3(4)

7

>>> plus_5(4)

9

В данном примере make_adder служит фабрикой для создания и конфигурирования функций-«сумматоров». Обратите внимание на то, что функции-«сумматоры» по-прежнему могут получать доступ к аргументу n функции make_adder (объемлющему контексту).

Объекты могут вести себя как функции

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

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

class Adder:

     def __init__(self, n):

         self.n = n

 

    def __call__(self, x):

         return self.n + x

 

>>> plus_3 = Adder(3)

>>> plus_3(4)

7

За кадром «вызов» экземпляра объекта в качестве функции сводится к исполнению метода __call__ этого объекта.

Безусловно, не все объекты будут вызываемыми. Вот почему существует встроенная функция callable, которая проверяет, является объект вызываемым или нет:

>>> callable(plus_3)

True

>>> callable(yell)

True

>>> callable('привет')

False

Ключевые выводы

• В Python абсолютно все является объектом, включая функции. Их можно присваивать переменным, хранить в структурах данных и передавать или возвращать в другие функции и возвращать из них (функции первого класса).

• Функции первого класса позволяют абстрагироваться и раздавать линии поведения в ваших программах.

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

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

3.2. Лямбды — это функции одного выражения

Ключевое слово lambda в Python предоставляет краткую форму для объявления небольших анонимных функций. Лямбда-функции ведут себя точно так же, как обычные функции, объявляемые ключевым словом def. Они могут использоваться всякий раз, когда требуются объекты-функции.

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

>>> add = lambda x, y: x + y

>>> add(5, 3)

8

Та же самая функция add может быть определена при помощи ключевого слова def, но она была бы чуть-чуть многословнее:

>>> def add(x, y):

...     return x + y

>>> add(5, 3)

8

Сейчас вы, вероятно, задаетесь вопросом: «Что за шум вокруг этих лямбд? Если они нечто иное, чем слегка укороченная версия объявления функций при помощи ключевого слова def, то что тут такого-то?»

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

>>> (lambda x, y: x + y)(5, 3)

8

Ладно, и что же здесь произошло? Я просто использовал lambda, чтобы определить однострочную функцию «add», а затем немедленно вызвал ее с аргументами 5 и 3.

Концептуально: лямбда-выражение lambda x, y: x + y аналогично объявлению функции при помощи ключевого слова def, только записывается в одну строку. Основное отличие здесь в том, что перед его использованием мне не пришлось связывать объект-функцию с именем. Я просто сформулировал выражение, которое хотел вычислить как часть лямбды, и затем немедленно его вычислил, вызвав лямбда-выражение как обычную функцию.

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

Существует еще одно синтаксическое различие между определениями лямбд и обычных функций. Лямбда-функции ограничены одним-единственным выражением. Это означает, что в лямбда-функциях не могут применяться инструкции или аннотации — и даже инструкция return.

Тогда каким образом возвращать значения из лямбд? При исполнении лямбда-функции ее выражение вычисляется и затем результат выражения автоматически возвращается, поэтому всегда существует неявная инструкция return. Именно поэтому некоторые разработчики называют лямбды функциями одного выражения.

Лямбды в вашем распоряжении

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

Этот факт обеспечивает удобную и «небюрократическую» краткую форму для определения функции в Python. Мой самый частый вариант применения лямбд состоит в написании кратких и сжатых функций для сортировки итерируемых объектов по альтернативному ключу:

>>> tuples = [(1, 'd'), (2, 'b'), (4, 'a'), (3, 'c')]

>>> sorted(tuples, key=lambda x: x[1])

[(4, 'a'), (2, 'b'), (3, 'c'), (1, 'd')]

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

>>> sorted(range(-5, 6), key=lambda x: x * x)

[0, -1, 1, -2, 2, -3, 3, -4, 4, -5, 5]

Оба показанных мною примера имеют в Python более сжатые реализации с использованием встроенных функций operator.itemgetter() и abs(). Но, надеюсь, вы заметили, как применение лямбды обеспечивает вам гораздо большую гибкость. Хотите отсортировать последовательность по некоему произвольно вычисленному ключу? Без проблем. Теперь вы знаете, как это сделать.

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

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

>>> def make_adder(n):

...     return lambda x: x + n

 

>>> plus_3 = make_adder(3)

>>> plus_5 = make_adder(5)

>>> plus_3(4)

7

>>> plus_5(4)

9

В приведенном выше примере лямбда x + n по-прежнему может получать доступ к значению n, несмотря на то что она была определена в функции make_adder (объемлющем контексте).

Иногда применение лямбда-функции вместо вложенной функции, объявленной при помощи ключевого слова def, может яснее выражать намерение программиста. Но, честно говоря, это случается редко — по крайней мере, не в том программном коде, который мне нравится писать. Поэтому давайте поговорим об этом подробнее.

А может, не надо…

С одной стороны, я надеюсь, что эта глава заинтересовала вас исследованием лямбда-функций Python. С другой стороны, я чувствую, что пора озвучить еще одно предостережение: лямбда-функции следует применять экономно и с необычайной осторожностью.

Уж я-то знаю. Я написал изрядную долю исходного кода с использованием лямбд, которые выглядели «круто», но на самом деле оказались помехой для меня и моих коллег. Если вы испытываете желание применить лямбда-функцию, потратьте несколько секунд (или минут) и обдумайте, будет ли этот способ достижения нужного результата и вправду самым чистым и максимально удобным в сопровождении.

Например, делать что-то подобное для экономии пары строк кода просто глупо. Несомненно, технически все работает, и это вполне приличный «трюк». Но он порядочно смутит очередную девчонку или парня, которые в жесткие сроки должны отправить багфикс:

# Вредно:

>>> class Car:

...     rev = lambda self: print('Бум!')

...     crash = lambda self: print('Бац!')

 

>>> my_car = Car()

>>> my_car.crash()

'Бац!'

Похожие чувства у меня и по поводу запутанных конструкций map() и filter() с использованием лямбд. Обычно гораздо лучше использовать конструкцию включения в список или в выражение-генератор:

# Вредно:

>>> list(filter(lambda x: x % 2 == 0, range(16)))

[0, 2, 4, 6, 8, 10, 12, 14]

 

# Лучше:

>>> [x for x in range(16) if x % 2 == 0]

[0, 2, 4, 6, 8, 10, 12, 14]

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

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

Ключевые выводы

• Лямбда-функции — это функции одного-единственного выражения, которые не обязательно привязаны к имени (анонимны).

• В лямбда-функциях нельзя использовать обычные инструкции Python, и в них всегда содержится неявная инструкция возврата return.

• Всегда спрашивайте себя: обеспечит ли применение обычной (именованной) функции либо конструкции включения в список большую ясность?

3.3. Сила декораторов

В своей основе декораторы Python позволяют расширять и изменять поведение вызываемых объектов (функций, методов и классов) без необратимой модификации самих вызываемых объектов.

Любая достаточно универсальная функциональность, которую можно прикрепить к существующему классу или поведению функции, является отличным кандидатом для декорирования. Сюда входят:

• ведение протокола операций (журналирование);

• обеспечение контроля за доступом и аутентификацией;

• функции инструментального оформления и хронометража;

• ограничение частоты вызова API;

• кэширование и др.

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

Предположим, в вашей программе составления отчетности есть 30 функций с бизнес-логикой. Одним дождливым утром понедельника ваш босс подходит к вашему столу и заявляет: «Доброго понедельника! Помните ту отчетность по TPS? Мне нужно, чтобы вы в каждый шаг генератора отчетов добавили ведение протокола входных и выходных операций. Компании XYZ это нужно для аудиторских целей. Да, и еще. Я им сказал, что к среде мы сможем все отправить».

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

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

А если вы знаете свои декораторы, вы спокойно улыбнетесь своему боссу и скажете: «Не беспокойся, Джим. Я сделаю это сегодня к 14:00».

Сразу после этого вы наберете исходный код для универсального декоратора @audit_log (всего порядка 10 строк кода) и быстро вставите его перед каждым определением функции. Затем вы зафиксируете код в GitHub и перехватите очередную чашечку кофе…

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

Уверен, что награда за понимание того, как в Python работают декораторы, может быть огромной.

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

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

• функции являются объектами — их можно присваивать переменным, передавать в другие функции и возвращать из других функций;

• функции могут быть определены внутри других функций — и дочерняя функция может захватывать локальное состояние родительской функции (лексические замыкания).

Готовы? Отлично! Тогда приступим.

Основы декораторов Python

Итак, что же такое декораторы на самом деле? Они «украшают», или «обер­тывают», другую функцию и позволяют исполнять программный код до и после того, как обернутая функция выполнится.

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

Как могла бы выглядеть реализация простого декоратора? В общих чертах декоратор — это вызываемый объект, который на входе принимает один вызываемый объект, а на выходе возвращает другой вызываемый объект.

Приведенная ниже функция имеет это свойство и может считаться самым простым декоратором, который вы могли когда-либо написать:

def null_decorator(func):

     return func

Как вы видите, null_decorator является вызываемым объектом (это функция). На входе он принимает еще один вызываемый объект и на выходе возвращает тот же самый вызываемый объект без его изменения.

Давайте его применим, чтобы декорировать (или обернуть) еще одну функцию:

def greet():

    return 'Привет!'

 

greet = null_decorator(greet)

 

>>> greet()

'Привет!'

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

Вместо того чтобы явным образом вызывать null_decorator с функцией greet и затем по-новому присваивать его переменной, удобнее воспользоваться синтаксисом Python @ для декорирования функции:

@null_decorator

def greet():

    return 'Привет!'

 

>>> greet()

'Привет!'

Размещение строки @null_decorator перед определением функции аналогично тому, что функция сначала определяется и затем уже прогоняется через декоратор. Синтаксис @ является всего лишь синтаксическим сахаром (syntactic sugar) и краткой формой для этого широко применяемого шаблона.

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

Декораторы могут менять поведение

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

Вот чуть более сложный декоратор, который преобразовывает результат декорированной функции в буквы верхнего регистра:

def uppercase(func):

     def wrapper():

         original_result = func()

         modified_result = original_result.upper()

         return modified_result

     return wrapper

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

Замыкание wrapper имеет доступ к недекорированной входной функции, и оно свободно может выполнить дополнительный программный код до и после ее вызова. (Технически замыканию вообще не нужно вызывать входную функцию.)

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

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

Самое время, чтобы взглянуть на декоратор uppercase в действии. Что произойдет, если продекорировать им оригинальную функцию greet?

@uppercase

def greet():

     return 'Привет!'

>>> greet()

'ПРИВЕТ!'

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

>>> greet

<function greet at 0x10e9f0950>

 

>>> null_decorator(greet)

<function greet at 0x10e9f0950>

 

>>> uppercase(greet)

<function uppercase.<locals>.wrapper at 0x76da02f28>

И как вы видели чуть раньше, ему это нужно, чтобы изменить поведение декорированной функции, когда он в итоге будет вызван. Декоратор uppercase сам является функцией. И единственный способ повлиять на «будущее поведение» входной функции, которую он декорирует, состоит в том, чтобы подменить (или обернуть) входную функцию замыканием.

Вот почему декоратор uppercase определяет и возвращает еще одну функцию (замыкание), которая затем может быть вызвана в дальнейшем, выполняет оригинальную входную функцию и модифицирует ее результат.

Декораторы изменяют поведение вызываемого объекта посредством обертки-замыкания, в результате чего вам не приходится необратимо модифицировать оригинал. Оригинальный вызываемый объект не изменяется необратимо — его поведение меняется, только когда он декорирован.

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

Короткая пауза

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

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

Уверен, что у вас получится!

Применение многочисленных декораторов к функции

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

Приведем пример. Представленные ниже два декоратора обертывают выходную строку декорированной функции в HTML-теги. Глядя на то, как теги вложены, вы видите, в каком порядке Python применяет многочисленные декораторы:

def strong(func):

     def wrapper():

         return '<strong>' + func() + '</strong>'

     return wrapper

 

def emphasis(func):

     def wrapper():

         return '<em>' + func() + '</em>'

     return wrapper

Теперь давайте возьмем эти два декоратора и одновременно применим их к нашей функции greet. Для этого вы можете использовать обычный синтаксис @ и просто «уложить» многочисленные декораторы вертикально поверх одной-единственной функции:

@strong

@emphasis

def greet():

     return 'Привет!'

Какой результат вы ожидаете увидеть, если выполнить декорированную функцию? Сначала декоратор @emphasis добавит тег <em>? Или же прио­ритет имеет тег @strong? Когда вы вызываете декорированную функцию, происходит вот что:

>>> greet()

'<strong><em>Привет!</em></strong>'

Этот результат ясно показывает, в каком порядке декораторы были применены: снизу вверх. Сначала входная функция была обернута декоратором @emphasis, и затем результирующая (декорированная) функция снова была обернута декоратором @strong.

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

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

decorated_greet = strong(emphasis(greet))

И снова вы видите, что сначала применяется декоратор emphasis и затем результирующая обернутая функция снова обертывается декоратором strong.

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

Декорирование функций, принимающих аргументы

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

Если применить один из этих декораторов к функции, которая принимает аргументы, то она не заработает правильно. Тогда как декорировать функцию, которая принимает произвольные аргументы?

Вот где на помощь приходят функциональные средства языка Python *args и **kwargs для работы с неизвестными количествами аргументов. Ниже приведен декоратор proxy, в котором задействуется их преимущество:

def proxy(func):

     def wrapper(*args, **kwargs):

         return func(*args, **kwargs)

     return wrapper

С этим декоратором происходят две вещи, заслуживающие внимания:

• В определении замыкания wrapper он использует операторы * и **, чтобы собрать все позиционные и именованные аргументы, и помещает их в переменные (args и kwargs).

• Замыкание wrapper затем переадресует собранные аргументы в оригинальную входную функцию, используя операторы «распаковки аргументов» * и **.

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

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

def trace(func):

     def wrapper(*args, **kwargs):

         print(f'ТРАССИРОВКА: вызвана {func.__name__}() '

               f'с {args}, {kwargs}')

 

        original_result = func(*args, **kwargs)

 

        print(f'ТРАССИРОВКА: {func.__name__}() '

               f'вернула {original_result!r}')

        return original_result

     return wrapper

При декорировании функции с использованием декоратора trace и последующем ее вызове, будут выведены переданные в декорированную функцию аргументы и возвращаемое ею значение. Этот пример по-прежнему остается несколько «игрушечным» — но в случае крайней необходимости он становится отличным средством отладки:

@trace def say(name, line):

     return f'{name}: {line}'

 

>>> say('Джейн', 'Привет, Мир')

'ТРАССИРОВКА: вызвана say() с ("Джейн", "Привет, Мир"), {}'

'ТРАССИРОВКА: say() вернула "Джейн: Привет, Мир"'

'Джейн: Привет, Мир'

Если говорить об отладке, то существует две вещи, которые при отладке декораторов следует иметь в виду.

Как писать «отлаживаемые» декораторы

При использовании декоратора вы на самом деле только подменяете одну функцию другой. Оборотной стороной этого процесса является то, что он «скрывает» некоторые метаданные, закрепленные за оригинальной (недекорированной) функцией.

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

def greet():

     """Вернуть дружеское приветствие."""

     return 'Привет!'

 

decorated_greet = uppercase(greet)

При попытке получить доступ к каким-либо из этих метаданных функции вместо них вы увидите метаданные замыкания-обертки:

>>> greet.__name__

'greet'

>>> greet.__doc__

'Вернуть дружеское приветствие.'

 

>>> decorated_greet.__name__

'wrapper'

>>> decorated_greet.__doc__

None

Это делает отладку и работу с интерпретатором Python неуклюжей и трудоемкой. К счастью, существует быстрое решение этой проблемы: декоратор functools.wraps, включенный в стандартную библиотеку Python.

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

import functools

def uppercase(func):

     @functools.wraps(func)

     def wrapper():

         return func().upper()

     return wrapper

Применение декоратора functools.wraps к замыканию-обертке, возвращаемому декоратором, переносит в него строку документации и другие метаданные входной функции:

@uppercase def greet():

     """Вернуть дружеское приветствие."""

     return 'Привет!'

>>> greet.__name__

'greet'

>>> greet.__doc__

'Вернуть дружеское приветствие.'

В качестве оптимального практического приема я порекомендовал бы использовать декоратор functools.wraps во всех декораторах, которые вы пишете сами. Это не займет много времени и уменьшит головную боль вам (и другим) в будущем при отладке.

Да, и поздравляю — вы успешно добрались до самого конца этого сложного раздела и многое узнали о декораторах в Python. Отличная работа!

Ключевые выводы

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

• Синтаксис @ является всего-навсего сокращенной записью для вызова декоратора с входной функцией. Многочисленные декораторы, размещенные над одной-единственной функцией, применяются снизу-вверх (стековая укладка декораторов).

• В качестве оптимального практического приема отладки используйте в своих собственных декораторах вспомогательный декоратор functools.wraps, чтобы переносить метаданные из недекорированного вызываемого объекта в декорированный.

• Точно так же, как и любой другой инструмент в комплекте инструментов разработки программного обеспечения, декораторы не являются панацеей, и ими не стоит злоупотреблять. Важно уравновесить необходимость «довести дело до конца» с целевой установкой «не увязнуть в ужасной и неудобной в обслуживании мешанине кодовой базы».

3.4. Веселье с *args и **kwargs

Однажды я программировал в паре с суровым питонистом, который то и дело восклицал «арррг!» и «кварг!» всякий раз, когда набирал определение функции с необязательными или именованными параметрами. Во всем остальном мы отлично поладили. М-да, вот что в итоге делает с людьми программирование в академии...

Нужно сказать, что сколько бы ни потешались над параметрами *args и **kwargs, тем не менее они являются очень полезным функциональным средством языка Python. И понимание их потенциала сделает из вас более эффективного разработчика.

Итак, для чего же используются параметры *args и **kwargs? Они позволяют функции принимать необязательные аргументы, благодаря чему вы можете создавать гибкие API в модулях и классах:

def foo(required, *args, **kwargs):

     print(required)

     if args:

         print(args)

     if kwargs:

         print(kwargs)

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

Если мы вызовем функцию с дополнительными аргументами, то args соберет дополнительные позиционные аргументы в кортеж, потому что имя параметра имеет префикс *.

Аналогичным образом, kwargs соберет дополнительные именованные аргументы в словарь, потому что имя параметра имеет префикс **.

Как args, так и kwargs могут быть пустыми, если никакие дополнительные аргументы в функцию не переданы.

Когда мы вызываем функцию с различными комбинациями аргументов, вы видите, как Python собирает их в параметрах args и kwargs в соответствии с тем, являются они позиционными или именованными аргументами:

>>> foo()

TypeError:

"foo() missing 1 required positional arg: 'required'"

 

>>> foo('привет')

привет

 

>>> foo('привет', 1, 2, 3)

привет

(1, 2, 3)

 

>>> foo('привет', 1, 2, 3, key1='значение', key2=999)

привет

(1, 2, 3)

{'key1': 'значение', 'key2': 999}

Сразу хочу прояснить. Название параметров args и kwargs принято по договоренности, как согласованное правило именования. Приведенный выше пример будет работать точно так же, если вы назовете их *parms и **argv. Фактическим синтаксисом является, соответственно, просто звездочка (*) или двойная звездочка (**).

Однако чтобы избежать недоразумений, я рекомендую придерживаться общеринятого согласованного правила именования. (И иметь возможность время от времени рычать «арррг!» и «кваррррг!».)

Переадресация необязательных или именованных аргументов

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

Это также дает вам возможность модифицировать аргументы перед тем, как вы передадите их дальше. Вот пример:

def foo(x, *args, **kwargs):

     kwargs['имя'] = 'Алиса'

     new_args = args + ('дополнительный', )

     bar(x, *new_args, **kwargs)

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

class Car:

     def __init__(self, color, mileage):

         self.color = color

         self.mileage = mileage

class AlwaysBlueCar(Car):

     def __init__(self, *args, **kwargs):

         super().__init__(*args, **kwargs)

         self.color = 'синий'

>>> AlwaysBlueCar('зеленый', 48392).color

'синий'

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

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

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

Но это всегда опасная территория, поэтому лучше соблюдать осторожность (иначе вскоре у вас, возможно, появится еще одна причина воскликнуть «аррррг!»).

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

И если мы можем сделать это без необходимости копипастить сигнатуру оригинальной функции, то, возможно, сопровождение станет удобнее:

def trace(f):

     @functools.wraps(f)

     def decorated_function(*args, **kwargs):

         print(f, args, kwargs)

         result = f(*args, **kwargs)

         print(result)

     return decorated_function

@trace

def greet(greeting, name):

   return '{}, {}!'.format(greeting, name)

 

>>> greet('Привет', 'Боб')

<function greet at 0x1031c9158> ('Привет', 'Боб') {}

'Привет, Боб!'

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

Ключевые выводы

• В Python переменные *args и **kwargs позволяют писать функции с неизвестным количеством аргументов.

• Переменная *args собирает дополнительные позиционные аргументы в кортеж. Переменная **kwargs собирает дополнительные именованные аргументы в словарь.

• Фактическим синтаксисом является * и **. Названия args и kwargs — это просто договоренность (которой следует придерживаться).

3.5. Распаковка аргументов функции

Действительно крутым, но немного загадочным функциональным средством языка является способность «распаковывать» аргументы функции из последовательностей и словарей при помощи операторов * и **.

Давайте определим простую функцию для работы в качестве примера:

def print_vector(x, y, z):

     print('<%s, %s, %s>' % (x, y, z))

Как вы видите, эта функция принимает три аргумента (x, y и z) и печатает их в приятно отформатированном виде. Мы можем применить эту функцию в нашей программе для структурной распечатки трехмерных векторов:

>>> print_vector(0, 1, 0)

<0, 1, 0>

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

>>> tuple_vec = (1, 0, 1)

>>> list_vec = [1, 0, 1]

>>> print_vector(tuple_vec[0],

                  tuple_vec[1],

                  tuple_vec[2])

<1, 0, 1>

Обычный вызов функции с отдельными аргументами кажется излишне многословным и громоздким. Не лучше ли будет просто развернуть векторный объект на три его компонента и передать их все одним разом в функцию print_vector?

(Разумеется, вы могли бы просто переопределить функцию print_vector так, чтобы она принимала один-единственный параметр, представляющий векторный объект, но ради того, чтобы иметь простой пример, мы этот вариант пока проигнорируем.)

К счастью, в Python имеется более подходящий способ справиться с этой ситуацией — при помощи распаковки аргументов функции с использованием оператора *:

>>> print_vector(*tuple_vec)

<1, 0, 1>

>>> print_vector(*list_vec)

<1, 0, 1>

Размещение звездочки * перед итерируемым объектом в вызове функции его распакует и передаст его элементы как отдельные позиционные аргументы в вызванную функцию.

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

>>> genexpr = (x * x for x in range(3))

>>> print_vector(*genexpr)

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

>>> dict_vec = {'y': 0, 'z': 1, 'x': 1}

Этот объект-словарь можно передать в функцию print_vector практически таким же образом, использовав оператор ** для распаковки:

>>> print_vector(**dict_vec)

<1, 0, 1>

Поскольку словари не упорядочены, этот оператор соотносит значения словаря и аргументы функции на основе ключей словаря: аргумент x получает значение, связанное в словаре с 'x'.

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

>>> print_vector(*dict_vec)

<y, x, z>

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

Ключевые выводы

• Операторы * и ** могут использоваться для «распаковки» аргументов функции, поступающих из последовательностей и словарей.

• Эффективное применение распаковки аргументов будет способствовать написанию более гибких интерфейсов для ваших модулей и функций.

3.6. Здесь нечего возвращать

В конец любой функции Python добавляет неявную инструкцию return None. По этой причине, если в функции не указано возвращаемое значение, по умолчанию она возвращает None.

Это означает, что инструкции return None можно заменять на пустые инструкции return или даже пропускать их полностью и по-прежнему получать тот же самый результат:

def foo1(value):

     if value:

         return value

     else:

         return None

 

def foo2(value):

     """Пустая инструкция return подразумевает `return None`"""

     if value:

         return value

     else:

         return

def foo3(value):

     """Пропущенная инструкция return подразумевает `return None`"""

     if value:

         return value

Все три функции правильно возвращают None, если передать им в качестве единственного аргумента фиктивное значение:

>>> type(foo1(0))

<class 'NoneType'>

 

>>> type(foo2(0))

<class 'NoneType'>

 

>>> type(foo3(0))

<class 'NoneType'>

Итак, когда же лучше всего использовать это функциональное средство языка Python в своем собственном программном коде?

Мое эмпирическое правило заключается в следующем: если функция не имеет возвращаемого значения (в других языках такая функция называется процедурой), то я исключаю инструкцию return. Добавлять эту инструкцию было бы лишним и вносило бы путаницу. Примером процедуры является встроенная в Python функция печати print, которая вызывается только ради ее побочных эффектов (распечатки текста) и никогда — ради ее возвращаемого значения.

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

С одной стороны, вы можете утверждать, что исключение явной инструкции return None делает программный код более сжатым и, следовательно, более легким для чтения и понимания. Субъективно вы отметили бы, что это делает программный код «симпатичнее».

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

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

В итоге я начал получать непрекращающийся поток электронных писем, указывающих мне на «пропущенную» в том примере кода инструкцию возврата. Неявное поведение инструкции return в Python отнюдь не было очевидным для всех и в данном случае отвлекало от сути. Я добавил примечание, чтобы прояснить, что происходит, и электронные письма прекратились.

Не поймите меня превратно: я люблю писать чистый и «красивый» программный код так же, как и любой другой разработчик. И раньше я тоже был твердо убежден, что программисты должны знать всю подноготную языка, с которым они работают.

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

Ключевые выводы

• Если в функции не указано возвращаемое значение, то она возвращает None. Возвращать None явным образом или неявным, решается стилистически.

• Это ключевое функциональное средство языка Python, однако ваш программный код может передавать свое намерение четче при помощи явной инструкции return None.

См. документацию Python «Объекты, значения и типы»:

Начиная с Python 3.3, также имеется атрибут __qualname__, который служит для такой же цели и обеспечивает строку с квалифицированным именем для устранения неоднозначности между именами функций и классов (см. PEP 3155: /).

См. раздел 3.4 «Веселье с *args и **kwargs».

См. документацию Python «functools»:

См. раздел 3.5 «Распаковка аргументов функции».

DRY (от англ. Don’t Repeat Yourself, то есть «не повторяйся») — это принцип разработки программного обеспечения, нацеленный на снижение повторения информации различного рода. См. . — Примеч. пер.

Назад: 2. Шаблоны для чистого Python
Дальше: 4. Классы и ООП