⛱️ Как правильно ничего не делать в Python: инструкция pass
Говорят, что в Python нет ничего проще ключевого слова pass – всего лишь инструкция «ничего не делать», чтобы соблюсти синтаксис языка. Однако не всегда pass служит заглушкой – есть и более интересные применения.
Публикация представляет собой сокращенный перевод статьи Моше Задка The pass Statement: How to Do Nothing in Python.
В Python ключевое слово pass
– самостоятельная инструкция, которая буквально ничего не делает. Она даже отбрасывается на этапе компиляции байт-кода. В чем же толк от такого оператора-бездельника?
Иногда pass
можно встретить в финальном коде на продакшене, но чаще инструкцию используют в процессе разработки. Заметим, что в некоторых случаях сделать что-то – лучше, чем ничего, и pass
является не лучшим решением.
В этом туториале мы изучим:
- что собой представляет
pass
и чем полезна эта инструкция; - как использовать
pass
в продакшене; - как применять
pass
в разработке; - какие есть альтернативы этой инструкции и когда их следует использовать.
Python и синтаксис pass
Синтаксис Python предполагает, что в некоторых случаях после двоеточия новые блоки кода идут с отступом. Например, после объявления цикла for
или условия if
:
Тело условия или цикла не может быть пустым:
Чтобы структура кода осталась корректной, нужно использовать инструкцию pass
:
В первом случае из-за невалидного синтаксиса вызывается исключение, во втором – pass
позволяет соблюсти требования Python.
Временное использование pass
Есть много ситуаций, в которых инструкция pass
может быть полезна в процессе разработки, даже если она не появится в окончательной версии кода. Подобно строительным лесам pass
может поддерживать структуру программы, прежде чем ее заменят на что-то дельное.
Будущий код
При продумывании макроструктур программы не нужно отвлекаться на низкоуровневые решения. Инструкция pass
помогает оформить ключевые конструкции, а потом вернуться к деталям.
Представьте: нужна функция, которая находит среднюю часть строки, записывает результат в файл и возвращает его:
Но вам пока не нужна вызываемая функция save_to_file()
– в первую очередь вы хотите проверить, нет ли ошибки неучтенной единицы. Однако функции save_to_file()
еще не существует – при ее вызове будет вызвано исключение.
Можно закомментировать вызов save_to_file()
, но тогда придется держать в уме: это не просто комментарий – соответствующую функцию когда-то придется реализовать. Лучше сделать заготовку сразу же:
Теперь функцию get_and_save_middle()
можно тестировать.
Другой вариант использования pass
– когда мы пишем сложную структуру управления потоком и нужен заполнитель для будущего кода. Например, для реализации fizz-buzz полезно сначала набросать структуру кода:
Такие структурные скелеты выстраивают логику и порядок ветвления. В приведенном примере первый оператор if
должен проверять делимость на 15, потому что любое число, которое делится на 15, также делится на 5 и 3. Предварительное понимание общей структуры полезно независимо от реализации конкретного вывода.
После того как вы прониклись логикой задачи, можно решить, будет ли использоваться print()
прямо в коде:
Функция напрямую печатает строки, однако из-за этого ее будет неудобно тестировать. Разумная альтернатива – написать функцию, возвращающую строковое значение:
Выявление основных условий и структуры с помощью pass
позволяет лучше понять, как впоследствии должна работать программа.
Подход полезен и при написании классов. Если вы пока не до конца понимаете предметную область, используйте pass
, чтобы сначала набросать макет и представить архитектуру.
Вообразим, что мы реализуем класс Candy
, но необходимые свойства пока неочевидны. Впоследствии понадобится тщательный анализ требований, но для начала реализации прочих составляющих программы достаточно отобразить, что класс пока не готов:
Такой код даже позволит создавать экземпляры класса.
Закомментированный код
Если у вас есть условие if… else
, бывает полезно закомментировать одну из ветвей. В следующем примере expensive_computation()
запускает длительно выполняющийся код, например, перемножение больших массивов чисел. В процессе отладки может потребоваться временно закомментировать вызов expensive_computation()
.
Например, вы хотите запустить этот код с некоторыми проблемными данными и посмотреть, почему так много значений не являются None
, проверив описание в журналах. Пропуск дорогостоящих расчетов ускорит тестирование.
Другая ситуация, в которой мы ходим закомментировать код во время устранения неполадок, – когда скрываемый код имеет нежелательный побочный эффект, например, отправку электронной почты или обновление счетчика.
Маркеры для отладчиков
Запуская код в отладчике, можно установить маркер на позиции, где отладчик остановится и позволит проверить состояние программы. Многие отладчики допускают выставить точку останова, которая срабатывает только при выполнении условия. Например, можно установить точку останова в цикле for так, чтобы отладчик срабатывал, только если переменная имеет значение None
. Так можно увидеть, почему этот случай обрабатывается неправильно. Например, в следующей строке отладчик срабатывает, если строка является палиндромом.
Хотя инструкция pass
ничего не делает, она позволяет установить здесь маркер. Теперь код можно запустить в отладчике и отлавливать строки-палиндромы.
Пустые функции и методы
Распространенная ситуация – код определяет класс, наследуемый от класса, требуется переопределение метода. Вполне вероятно, что новый метод не должен делать или ему даже нужно запретить что-то делать:
Необходимые функции и методы в этом случае по-прежнему поддерживаются и не вызывают исключений при вызове.
Пустые классы на примере исключений
Python поддерживает концепцию наследования исключений. Например, встроенное исключение LookupError
является родительским для KeyError
:
При поиске несуществующего ключа в словаре возникает исключение KeyError
. Исключение KeyError
перехватывается, хотя в инструкции except
указано LookupError
. Так происходит потому, что KeyError
является подклассом LookupError
.
Иногда возникает задача вызова исключений, наследуемых от определенного класса и соответствующих некоторому набору инструкций по обработке исключений. Сами исключения не выполняют никаких действий, а служат простейшими сигнализаторами: произошла такая-то ошибка.
Простой пример: задача проверки паролей. Прежде чем пользователь сможет изменить пароль на веб-сайте, программа на сервере тестирует пароль на соответствие правилам:
- Не менее 8 символов.
- По крайней мере один символ – цифра.
- По крайней мере один специальный символ (
?
!
. и др.
).
Каждая из соответствующих ошибок должна вызывает собственное исключение. Следующий код реализует указанные правила:
Эта функция вызовет исключение, если пароль не соответствует какому-либо из описанных правил. Более реалистичный пример отметил бы все несоблюденные правила, но такая задача выходит за рамки данного руководства.
В этом примере
перехватывает только friendly_check()
поскольку другие исключения типа InvalidPasswordError
могут представлять исключения, порождаемые в самой программе проверки ошибки. Функция печататет имя и значение исключения, соответствующее правилу. Оператор ValueError
pass
позволил без особых сложностей определить четыре класса исключений.
Маркирующие методы
Некоторые методы в классах существуют не для того, чтобы их вызывать. Иногда они просто определенным образом помечают сам класс.
Представим, что вы пишете код для анализа шаблонов использования веб-сервера. Требуется различать запросы, поступающие от пользователей, вошедших в систему, и запросы от неаутентифицированных подключений. Ситуацию можно смоделировать, имея суперкласс Origin
с двумя подклассами: LoggedIn
и NotLoggedIn
. Каждый запрос должен исходить либо из источника LoggedIn
, либо из NotLoggedIn
, но ничто не должно напрямую создавать экземпляр класса Origin
. Вот минималистичная реализация:
abc
соответствует сокращению от abstract base classes. Модуль помогает определять классы, которые не предназначены для создания экземпляров, а служат базой для других классов.Хотя реалистичный класс Origin
выглядел бы сложнее, в этом примере показана его основа. Метод Origin.description()
никогда не будет вызван – все подклассы его переопределяют.
Классы с декораторами методов abstractmethod
не могут быть созданы. Любой объект, имеющий Origin
в качестве суперкласса, будет экземпляром класса, который переопределяет description()
. Из-за этого тело в Origin.description()
не имеет значения и его можно заменить инструкцией pass
.
Есть и другие примеры использования таких маркеров вне стандартной библиотеки Python. Например, они используются в пакете zope.interface
для обозначения методов интерфейса и в automat
для входных данных конечного автомата. Во всех этих случаях классы должны иметь методы, но никогда не вызывают их.
Альтернативы pass
Инструкция pass
– не единственный способ «ничего не делать». Любое выражение в Python это валидная инструкция, как и любая константа. Фактически следующие инструкции тоже сами по себе ничего не делают:
None
True
0
"hello I do nothing"
Основная причина, почему стоит избегать использования таких инструкций вместо pass
– они не идиоматичны. Люди, читающие ваш код, не сразу поймут, зачем вы их использовали. Хотя для записи инструкции pass
требуется больше символов, чем, скажем для 0
, она явным образом показывает, что блок кода намеренно оставлен пустым.
Docstrings
Есть одно важное исключение из идиомы использования pass
в качестве инструкции бездействия. В классах, функциях и методах использование константного строкового выражения приведет к тому, что выражение будет использоваться как атрибут объекта .__ doc__
. Этот атрибут используется функцией интерпретатора help()
, различными генераторами документации и многими IDE.
Даже если строка документации не является обязательной, часто она является хорошей заменой инструкции pass
в пустом блоке, так как более полно описывает назначение блока:
Во всех приведенных случаях строка документации делает код понятнее, а работу с ним – более удобной.
Ellipsis
В pyi-файлах рекомендуется использовать в качестве выражения многоточие (...
). Эта константа определяется с помощью синглтона Ellipsis
:
Первоначально объект Ellipsis
использовался для создания многомерных срезов. Однако теперь это также рекомендуемый синтаксис для заполнения блоков в stub-файлах c расширением .pyi
:
Эта функция не только ничего не делает, но и находится в файле, который интерпретатор Python обычно не запускает.
Вызов исключения
В тех случаях, когда функции или методы пусты, потому что они никогда не выполняются, иногда лучшим телом будет вызов исключения raise NotImplementedError("this should never happen")
. Вызов исключения в этом случае даст дополнительную информацию.
Перманентное использование pass
Порой использование оператора pass
не является временным – инструкция остается в окончательной версии работающего кода. В таких случаях нет более подходящего решения для заполнения пустого блока, чем использование pass
.
Применение pass в try ... except
При использовании try ... except
для определенных исключений нет необходимости как-либо его обрабатывать – главное, чтобы программа продолжала работу. В такой ситуации pass
как нельзя кстати.
Например, вы хотите быть уверены, что файл не существует и используете os.remove()
. Однако функция не только удаляет файл, но и вызывает исключение, если файл отсутствует. Но если файла нет, в нашей задаче нет и необходимости вызывать исключение:
При вызове исключения FileNotFoundError
используем pass
, чтобы не блокировать остальной код.
Стоит заметить, что в схожих ситуациях оператор pass
часто заменяется записью в журнал. Однако этого не требуется, если ошибка ожидаема и легко интерпретируема.
Вы также можете использовать диспетчер контекста contextlib.suppress()
для подавления исключения. Если нужно обрабатывать одни исключения, игнорируя другие, то проще использовать инструкцию pass
.
Например, если вы хотите, чтобы приведенная выше функция ensure_nonexistence()
работала и с каталогами, и с файлами, можно использовать следующий подход:
Здесь происходит игнорирование исключения
и обработка исключения FileNotFoundError
.IsADirectoryError
В этом примере порядок инструкций
не имеет значения, поскольку оба исключения except
и FileNotFoundError
наследуются от IsADirectoryError
. Если бы здесь был еще более общий случай, обрабатывающий OSError
, его нужно было поставить после более специфичных вариантов.OSError
Использование pass в цепочках if … elif
При обработке длинных цепочек if… elif
в каком-то из вариантов бывает просто ничего не нужно делать, но вы не можете пропустить этот elif
, потому что без него результат станет некорректным.
Представьте, что рекрутер попросил вас написать fizz-buzz с такимиусловиями:
- Если число делится на 20, программа должна печать
"twist"
. - В остальных случаях, если число делится на 15, ничего не печатать.
- В остальных случаях, если число делится на 5, печатать
"fizz"
. - В остальных случаях, если число делится на 3, печатать
"buzz"
. - Во всех остальных случаях печатать само число.
Как и во всех вопросах по кодингу, есть много способов решить эту проблему. Один из них – использовать цикл for
с цепочкой, которая имитирует само описание:
Цепочка if
… elif
отражает логику перехода между ограничениями задачи. В этом примере, если мы полностью удалим выражение
if x % 15
, мы изменим поведение программы. То есть такой вариант использования оператора pass
позволяет не только решить задачу, но и сохранить логику кода в соответствии с логикой задачи.
Заключение
Теперь вы знаете, что собой представляет инструкция pass
. Вы готовы использовать ее для повышения скорости разработки и отладки кода на Python.
Если вам понравился материал этой статьи, обратите внимание на следующие публикации:
- Тест на знание языка Python (15 вопросов)
- Всё, что нужно знать о декораторах Python
- Как хранить объекты Python со сложной структурой
- Итерируем правильно: 20 приемов использования в Python модуля itertools
- Введение в объектно-ориентированное программирование (ООП) на Python