Education 1.1 Help

Переопределение функций и декораторы

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

Переопределение функций

Как вы уже знаете, функцию можно записать в переменную. Оказывается, помимо этого, с именем функции можно работать как с самой обыкновенной переменной. И значение, на которое указывает переменная с именем функции, можно изменить. Рассмотрим пример. У нас есть функция input(), которая читает данные из стандартного потока ввода. Давайте сделаем так, чтобы она не беспокоила пользователя и всегда давала один и тот же ввод. Для начала создадим свою функцию, которая будет работать вместо input(), а затем подменим значение input этой функцией.

def main_answer_in_the_universe(): return 42 input = main_answer_in_the_universe x = input() print(x) # 42

Инструкция pass. Согласованность аргументов

Теперь, чтобы совсем не отвлекать пользователя работой программы, давайте подменим еще и функцию print() так, чтобы она ничего не печатала на экран. На одном из прошлых занятий мы писали функцию nop(), которая не делала ничего, но принимала в себя любой набор аргументов. К ней и обратимся.

def nop(*rest, **kwargs): pass print = nop print("(шепотом) Потише, пожалуйста!", end='')

Теперь функция print() связана с объектом функции, которая при вызове не делает ничего. Привычная команда print() изменила поведение.

Снова обращаем ваше внимание на то, что в аргументах функции nop() указано произвольное число аргументов (и произвольное число именованных аргументов). Благодаря этому мы можем передавать ей разное число аргументов, как и в старую функцию print(), и разные наборы опций, описываемые именованными аргументами (sep, end и т. п.).

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

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

Инструкция def

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

def input(): return 42 x = input() print(x) # 42

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

language = 'fr' if language == 'ru': def hello(name): print('Привет, ', name) else: def hello(name): print('Hi, ', name) hello('Joe') # => Hi, Joe

Дальше мы будем использовать более многословный способ.

Предосторожности при переопределении функций

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

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

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

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

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

Функция внутри функции

Аналогия функций с переменными имеет продолжение. Бывают функции, которые определены глобально, а бывают — определенные локально. Если внутри функции переопределить print(), это будет локальная функция print(), а глобальная не изменится. Когда мы выйдем из области видимости, в которой был объявлен локальный print(), слово print вновь будет ссылаться на функцию в глобальной области видимости и будет работать как прежде.

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

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

def answer(question): return 'В разработке' def dialog(): def answer(question): if question.lower().startswith('когда') : return 'Никогда!' else: return 'Разоблачили.' question = input() while question != '': print(answer(question)) question = input() dialog() # <= Когда станет тепло? # => Никогда! # <= Когда смогу найти богатство? # => Никогда! # <= Какие в Чили существуют города? # => Разоблачили

Этот пример служит исключительно иллюстрацией, ни в коем случае не рекомендацией. Ведь мы могли не переименовывать функцию, а просто определить новую функцию. Задача. Подмените функцию print() так, чтобы она ПЕЧАТАЛА ВЕСЬ ТЕКСТ В ВЕРХНЕМ РЕГИСТРЕ. Реализовывать работу с именованными аргументами (sep, end, ...) не нужно.

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

old_print = print def print_uppercase(*args): args_upcased = [str(arg).upper() for arg in args] old_print(*args_upcased) print = print_uppercase print('Нельзя ли потише?')

Декораторы

def use_uppercased_arguments(old_func): def new_func(*args, **kwargs): args_upcased = [str(arg).upper() for arg in args] old_func(*args_upcased, **kwargs) return new_func print = use_uppercased_arguments(print)

В коде выше произошло следующее: мы определили функцию use_uppercased_arguments(), которая принимает одну функцию как аргумент и возвращает новую функцию, созданную на основе старой. Функция, которая занимается этими превращениями, называется декоратором. Имя new_func, которое мы определили внутри декоратора, локальное. Как только мы выполним return new_func, это будет просто функция без имени.

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

Этой новой функции мы даем имя, которое было у старой функции.

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

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

Например:

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

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

  • Чтобы функция использовалась для ответа на запросы к веб-серверу

  • Чтобы перед запуском проверялось какое-то условие (например, что пользователь имеет право доступа к выполнению функции)

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

def logged(func): count = 0 def decorated_func(*args, **kwargs): nonlocal count count += 1 print(count, '>>', 'Arguments:', args, 'Named arguments:', kwargs) result = func(*args, **kwargs) print('--', 'Result:', result) return result return decorated_func @logged def make_burger(type_of_meat, with_onion=False, with_tomato=True): print('Булочка') if with_onion: print('Луковые колечки') if with_tomato: print('Ломтик помидора') print('Котлета из', type_of_meat) print('Булочка') @logged def drinking_type(type): return 'У нас есть только чай'

Сначала посмотрите на определения функций make_burger() и drinking_type(). Тело функций представляет мало интереса, это очень простые функции. А вот @logged перед определением функции — новый для вас объект. Эта строчка говорит, что функция, которая идет дальше, будет задекорирована с помощью декоратора logged. Обратите внимание: задекорированы обе функции одним декоратором. И никаких промежуточных функций и вспомогательных переменных мы не использовали.

Нелокальные переменные

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

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

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

При поиске переменной или функции с указанными именем приоритет (или, как говорят, правило разрешения имен) следующий:

  1. Сначала ищем локальную переменную (функцию).

  2. Если не нашли локальную, ищем нелокальную.

  3. Затем — глобальную.

  4. И в самом конце — встроенную в язык.

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

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

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

Типичные детали реализации декоратора

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

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

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

def logged(func): count = 0 def decorated_func(*args, **kwargs): nonlocal count count += 1 current_index = count print(current_index, '>>', 'Arguments:', args, 'Named arguments:', kwargs) result = func(*args, **kwargs) print(current_index, '--', 'Result:', result) return result return decorated_func @logged def fib(n): if n <= 2: return 1 else: return fib(n - 1) + fib(n - 2) fib(5)

Почему мы сделали current_index = count, а не использовали сам count, ведь count нигде в этой функции не изменяется? Но это иллюзия. countнелокальная переменная, поэтому ее может изменить вызов другой функции, имеющей доступ к этой переменной. У нас как раз такая ситуация: функция вызывает саму себя и в процессе этого вызова переменная count изменяется. Чтобы такого не было, мы заранее сохраняем значение счетчика в отдельную переменную, которую не меняют вызовы других функций. А так как числа неизменяемы, «изменение» (присваивание нового значения) значения переменной count никак не повлияет на current_index.

Захват значения из аргумента

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

def power(degree): def func(x): return x ** degree return func square = power(2) cube = power(3) print(square(5)) # --> 25

Внутренняя функция взяла параметр degree из аргумента функции power(). Этот прием часто используется для того, чтобы зафиксировать какой-то набор параметров и сделать функцию с более коротким списком аргументов. Рассмотрим еще один пример. Пусть у нас есть функция send_invitation(email, name, text, date, city). Мы хотим использовать ее, не перечисляя всякий раз весь этот внушительный список параметров.

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

def invitation_sender(city, date, text): def sender(email, name): return send_invitation(email, name, text, date, city) return sender send_mail = invitation_sender('Москва', '1 апреля 2017 г', 'Приглашаем вас на встречу') send_mail('vasiliy-petrov@yandex.ru', 'Василий Петров') send_mail('petr-alekseev@yandex.ru', 'Петр Алексеев') send_mail('vasiliy-vasilyev@yandex.ru', 'Василий Васильев')

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

Немного о терминологии

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

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

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

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

Last modified: 12 June 2024