Продвинутый курс по Python. Урок 4:
В этой лекции мы рассмотрим такие немаловажные вещи, как контейнеры и связанные с ними понятия итераторов и генераторов. В завершении всего, мы рассмотрим декораторы.
Начнём по порядку, контейнер – это тип данных, предназначенный для хранения элементов и предоставляющий набор операций для работы с ними. Сами контейнеры и, как правило, их элементы хранятся в памяти. В Python существует масса разнообразных контейнеров, среди которых всем хорошо знакомые: list, deque, set, frozensets, dict, defaultdict, OrderedDict, Counter, tuple, namedtuple, str.
Технически, объект является контейнером тогда, когда он предоставляет возможность определить наличие или отсутствие в нём конкретного элемента. Обратите внимание, что несмотря на то, что большинство контейнеров предоставляют возможность извлекать из них любой элемент, само по себе наличие этой возможности не делает объект контейнером, а лишь итерируемым объектом.
Как было уже сказано выше, большинство контейнеров в Python являются итерируемыми. Кроме того, множество других типов данных также являются итерируемыми объектами, например, файлы, сокеты и тому подобные. В отличие от контейнеров, обычно содержащих конечное количество элементов, итерируемый объект может по сути представлять собой бесконечный источник данных.
По определению, итерируемым объектом является любой объект, который может предоставить итератор. Этот итератор, в свою очередь, возвращает отдельные элементы. Вообще говоря это звучит немного странновато, но тем не менее очень важно понимать разницу между итератором и интерируемым объектом. Сейчас рассмотрим пример.
Функция iter вернёт итератор, основанный на контейнере.
Здесь х – это итерируемый объект, а y и z это два отдельных экземпляра итератора, производящего значения из итерируемого объекта x. Как мы видим, оба экземпляра y и z сохраняют свое состояние между вызовами next. В данном примере в качестве источника данных для итератора используется список, но строго говоря это не обязательно.
На практике, чтобы сократить объем кода, классы итерируемых объектов имплементируют сразу оба метода: __iter__ и __next__, при этом __iter__ возвращает экземпляр класса. Таким образом класс одновременно является и итерируемым объектом, и итератором самого себя. Однако, лучшей практикой всё же считается в качестве итератора возвращать отдельный объект.
А теперь про сами итераторы. Итак, итератор – это некоторый вспомогательный объект, который возвращает следующий элемент каждый раз, когда вызывается функция __next__ с передачей этого объекта в качестве аргумента. Таким образом, любой объект, предоставляющий метод __next__, является итератором, возвращающим следующий элемент при вызове этого метода, при этом совершенно неважно, как именно это происходит.
Итого, итератор – это некая фабрика по производству значений. Всякий раз, когда вы обращаетесь к ней с просьбой «давай следующее значение», она знает как сделать это, поскольку сохраняет своё внутреннее состояние между обращениями к ней. Существует бесчисленное множество примеров использования итераторов.
Например, все функции библиотеки itertools возвращают итераторы. Некоторые из них генерируют бесконечные последовательности.
Другие создают бесконечные последовательности из конечных. Например, метод cycle из последовательности [‘red’, ‘white’, ‘blue’] генерирует повторяющуюся бесконечную.
Еще один полезный метод – islice. Он возвращает конечный итератор из любой большой или даже бесконечной последовательности.
В целом, генератор – это особый, более изящный случай итератора.
Используя генератор, вы можете создавать итераторы, вроде того, что мы рассмотрели выше, используя более лаконичный синтаксис, и не создавая при этом отдельный класс с методами
Давайте внесём немного ясности. Любой генератор является итератором (но не наоборот). Следовательно, любой генератор является «ленивой фабрикой», возвращающей значения последовательности по требованию.
Вот пример итератора последовательности чисел Фибоначчи в исполнении генератора.
Обратите внимание, что fib является обычной функцией, ничего особенного. Однако же, в ней отсутствует оператор return, возвращающий значение. В данном случае возвращаемым значением функции будет генератор (что и обеспечивает оператор yield). То есть, по сути, итератор – фабрика значений, сохраняющая состояние между обращениями к ней
Теперь, когда происходит вызов функции fib, будет создан и возвращён экземпляр генератора. К данному моменту ещё никакого кода внутри функции не выполняется и генератор ожидает вызова.
Дальше созданный экземпляр генератора можно передать в качестве аргумента функции islice(), которая также возвращает итератор, следовательно также никакого кода функции fib() пока что не выполняется.
И, наконец, можно преобразовать это в список вызовом list с передачей в качестве аргумента итератора, возвращённого функцией
islice(). Чтобы list смог построить объект списка на основе полученного аргумента, ему необходимо получить все значения из этого аргумента. Для этого list выполняет последовательные вызовы метода next итератора, возвращённого вызовом islice, который, в свою очередь, выполняет последовательные вызовы next в экземпляре итератора gen.
Если с предыдущими темами в сегодняшней лекции лучше изучать теорию, то здесь мы начнём сразу с практического примера.
Например, у нас есть словарь user с именем пользователя и его уровнем доступа, который в нашем случае «guest» (гость). Также есть функция get_admin_password, которая возвращает пароль.
Если вызвать функцию, то мы получим пароль админа, но в то же время уровень доступа по идее не должен позволять этого сделать.
Самый простой вариант решения такой задачи – написать условную конструкцию.
Всё бы хорошо, но мы защитили конкретный вызов функции, но далее можем вызвать эту же функцию и получить пароль. Сама функция всё ещё не защищена.
Становится очевидным написать проверку в самой функции. Снова кажется, что всё хорошо, но нет.
Если в дальнейшем понадобится написать ещё «защищенных» функций, то придётся добавлять проверку каждый раз, а это такое себе решение.
В таких ситуациях и нужны декораторы, так как они позволяют модифицировать уже существующие функции без изменения их кода.
Суть декораторов заключается в том, что они принимают на вход функцию и возвращают уже новую, с измененным поведением.
Сейчас напишем универсальную функцию make_secure, которая будет отвечать за всю логику защищенности.
Новая функция будет принимать на вход любую другую функцию в качестве аргумента. Далее прямо в теле этой функции объявляем новую secure_funcrion (да, так делать можно).
Уже в теле новой функции происходит проверка уровня доступа пользователя. Если он равен ‘admin’, то возвращаем результат вызова функции, которую приняла на входа функция make_secure.
И в конце возвращаем новую функцию. Обратите внимание, что круглые скобки не ставим, так как не вызываем её(а возвращаем).
Далее, в предпоследней строчке, переопределяем переменную get_admin_password. Сначала она хранила первоначальное значение функции, а теперь она хранит по сути новую функцию secure_funcrion.
Теперь функция get_admin_password вернёт значение, только если уровень доступа будет соответствующий.
И самое главное, make_secure позволит сделать любую функцию защищенной, как и требовалось.
В целом, примерно так и выглядит самый простой декоратор. Вообще можно было бы вернуть любую другую функцию, но тогда весь смысл декоратора пропал бы.
Давайте сейчас добавим ещё одну небольшую деталь, чтобы понимать, что происходит в программе.
Вместо того, чтобы писать make_secure(get_admin_password), можно сделать ещё проще: использовать синтаксис @, как показано ниже. Такая запись аналогична тому, что мы писали чуть ранее.
Кстати, есть ещё одна проблема. Так как функция новая, то её внутреннее имя тоже уже новое.
к тому же, если функция имела какую-то документацию, то она тоже будет утеряна.
Но решение достаточно простое: можно использовать декоратор wraps из модуля functools. Он подменяет определённые аргументы, docstring и названия так, что функция не меняется.
Оборачиваем этим декоратором ту функцию, которая возвращается в качестве уже новой. В аргументы передаём ту, которую изначально декорируем.
Подобный трюк не необходим, но крайне желателен в каждом декораторе.
Декорируемые функции с параметрами
Предлагаю ещё немного изменить логику первоначальной функции: теперь она будет принимать ещё и панель, от которой запрашивается пароль.
Теперь надо сделать так, чтобы secure_function принимала этот агумент. Но возникает проблема: декоратор make_secure планировался как универсальный. И если другие функции, которые надо будет сделать защищенными, будут принимать другие параметры.
Именно по этой причине принято использовать *args, **kwargs. Это позволяет принимать неограниченные количество аргументов и отдавать их обратно декарируемой функции.
Для начала немного усложним задачу. Допустим, у нас будет две разные функции get_admin_password и get_user_password. Обе должны быть защищёнными, но при этом первая должна быть доступна только админам, а вторая – пользователям.
То есть если уровень доступа пользователя будет ‘admin’, то он сможет получить пароль из первой функции, но не из второй. Если же ‘user’, то ровно наоборот.
А теперь обратите внимание на небольшую деталь. В записе @make_secure мы не пишем круглые скобки, так как вызываем функцию, и поэтому не можем передать никакие параметры. Но при этом эта функция-декоратор применяется к декорируемой функции.
И сейчас мы напишем новую функцию, которая будет принимать какие-то параметры и возвращать непосредственно декоратор.
Здесь мы добавили ещё один уровень функций и принимаемый аргумент.
По сути это тот же декоратор, который при вызове возвращает декоратор. Но фактически, правильнее называть этой фабрикой декораторов. Но если не заморачиваться, то просто запомните этот приём как «декоратор с параметрами».
Далее поменяем слегка условную конструкцию, так как теперь надо сравнивать с переданным access_level, а не просто с ‘admin’.
Теперь крайне важно поставить круглые скобки после применения декораторов и передать соответствующие аргументы.
Запись @make_secure(. ) вернёт по факту новый декоратор, который мы ранее объявляли как decorator.
Изменим ‘access_level’ в словаре и посмотрим на то, как всё работает.
В целом, это уже почти готовый декоратор, который может использоваться даже в реальных проектах. Так что всё не так уж и сложно.
Думаю, что теперь вам всё должно быть понятно с декораторами, так как мы покрыли почти все темы, связанные с ними. Но есть ещё кое-что – применение нескольких декораторов к одной функции сразу.
Давайте же посмотрим, что будет, если применить сразу несколько декораторов.
То же самое, но без синтаксического сахара в виде @.
Видим, что сначала вызвался сначала первый декоратор, потом второй. Разберём это подробнее.
Функция second_decorator возвращает новую функцию wrapped, таким образом, функция подменяется на wrapped внутри second_decorator. После этого вызывается first_decorator, который принимает функцию полученную из second_decorator функции wrapped и возвращает ещё одну функцию wrapped заменяя decorated на неё.
Таким образом, итоговая функция decorated – это функция wrapped из first_decorator вызывающая функцию из second_decorator.
Ещё один пример на применение декораторов. Обратите внимание, что сначала теги идут в том же порядке, что и декораторы, а затем в обратном. Это происходит потому, что декораторы вызываются один внутри другого.
Домашнее задание
К темам итераторов, генераторов и прочего особо нечего дать, так как это просто надо знать. А вот декораторы лучше уметь применять на практике, поэтому именно по ним и будет домашнее задание.
Задание 1. Написать декоратор timing, замеряющий время выполнения функции.
Задание 2. Продвинутое задание. Модифицировать декоратор из прошлого задания таким образом, чтобы он выполнял декорируемую функцию iters раз, а затем выводил среднее время выполнения.