Книга: Чистый Python. Тонкости программирования для профи
Назад: 1. Введение
Дальше: 3. Эффективные функции

2. Шаблоны для чистого Python

2.1. Прикрой свой з** инструкциями assert

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

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

В этом месте вы, вероятно, заинтересуетесь: «Что такое assert и в чем ее прелесть?» Позвольте дать вам несколько ответов.

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

Инструкция assert в Python — пример

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

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

def apply_discount(product, discount):

     price = int(product['цена'] * (1.0 — discount))

     assert 0 <= price <= product['цена']

     return price

Вы заметили, что здесь есть инструкция assert? Она будет гарантировать, что, независимо от обстоятельств, вычисляемые этой функцией сниженные цены не могут быть ниже 0 $ и они не могут быть выше первоначальной цены товара.

Давайте убедимся, что эта функция действительно работает как задумано, если вызвать ее, применив допустимую скидку. В этом примере товары в нашем магазине будут представлены в виде простых словарей. И скорее всего, в реальном приложении вы примените другую структуру данных, но эта безупречна для демонстрации утверждений assert. Давайте создадим пример товара — пару симпатичных туфель по цене 149,00 $:

>>> shoes = {'имя': 'Модные туфли', 'цена': 14900}

Кстати, заметили, как я избежал проблем с округлением денежной цены, использовав целое число для представления цены в центах? В целом неплохое решение… Но я отвлекся. Итак, если к этим туфлям мы применим 25 %-ную скидку, то ожидаемо придем к отпускной цене 111,75 $:

>>> apply_discount(shoes, 0.25)

11175

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

>>> apply_discount(shoes, 2.0)

Traceback (most recent call last):

   File "<input>", line 1, in <module>

     apply_discount(prod, 2.0)

   File "<input>", line 4, in apply_discount

     assert 0 <= price <= product['price']

AssertionError

Как вы видите, когда мы пытаемся применить эту недопустимую скидку, наша программа останавливается с исключением AssertionError. Это происходит потому, что 200 %-ная скидка нарушила условие утверждения assert, которое мы поместили в функцию apply_discount.

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

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

Почему просто не применить обычное исключение?

Теперь вы, вероятно, озадачитесь, почему в предыдущем примере я просто не применил инструкцию if и исключение.

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

Инструкции призваны быть внутренними самопроверками (internal self-checks) вашей программы. Они работают путем объявления неких условий, возникновение которых в вашем исходном коде невозможно. Если одно из таких условий не сохраняется, то это означает, что в программе есть ошибка.

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

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

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

Синтаксис инструкции Python assert

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

инструкция_assert ::= "assert" выражение1 ["," выражение2]

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

if __debug__:

     if not выражение1:

         raise AssertionError(выражение2)

В этом фрагменте кода есть две интересные детали.

Перед тем как данное условие инструкции assert будет проверено, проводится дополнительная проверка глобальной переменной __debug__. Это встроенный булев флажок, который при нормальных обстоятельствах имеет значение True, — и значение False, если запрашивается оптимизация. Мы поговорим об этом подробнее чуть позже в разделе, посвященном «распространенным ловушкам».

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

>>> if cond == 'x':

...     do_x()

... elif cond == 'y':

...     do_y()

... else:

...     assert False, (

...         'Это никогда не должно произойти, и тем не менее это '

...         'временами происходит. Сейчас мы пытаемся выяснить'

...         'причину. Если вы столкнетесь с этим на практике, то '

...         'просим связаться по электронной почте с dbader. Спасибо!')

Разве это не ужасно? Конечно, да. Но этот прием определенно допустим и полезен, если в одном из своих приложений вы сталкиваетесь с плавающей ошибкой Гейзенбаг.

Распространенные ловушки, связанные с использованием инструкции assert в Python

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

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

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

Предостережение № 1: не используйте инструкции assert для проверки данных

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

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

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

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

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

Поскольку вы только что узнали об assert, вам не терпится применить их в своем коде (я бы точно так поступил!), и вы пишете следующую реализацию:

def delete_product(prod_id, user):

     assert user.is_admin(), 'здесь должен быть администратор'

     assert store.has_product(prod_id), 'Неизвестный товар'

     store. get_product(prod_id).delete()

Приглядитесь поближе к функции delete_product. Итак, что же произойдет, если инструкции assert будут отключены?

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

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

2. Проверка has_product() пропускается, когда assert отключена. Это означает, что метод get_product() теперь можно вызывать с недопустимыми идентификаторами товаров, что может привести к более серьезным ошибкам, — в зависимости от того, как написана наша программа. В худшем случае она может стать началом запуска DoS-атак. Например, если приложение магазина аварийно завершается при попытке стороннего лица удалить неизвестный товар, то, скорее всего, это произошло потому, что взломщик смог завалить его недопустимыми запросами на удаление и вызвать сбой в работе сервера.

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

def delete_product(product_id, user):

     if not user.is_admin():

         raise AuthError('Для удаления необходимы права админа')

     if not store.has_product(product_id):

         raise ValueError('Идентификатор неизвестного товара')

     store.get_product(product_id).delete()

Этот обновленный пример также обладает тем преимуществом, что вместо того, чтобы вызывать неопределенные исключения AssertionError, он теперь вызывает семантически правильные исключения, а именно ValueError или AuthError (которые мы должны были определить сами).

Предостережение № 2: инструкции assert, которые никогда не дают сбоя

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

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

Например, это утверждение никогда не будет давать сбой:

assert(1 == 2, 'Это утверждение должно вызвать сбой')

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

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

assert (

     counter == 10,

     'Это должно было сосчитать все элементы'

)

На первый взгляд этот тестовый случай выглядит абсолютно приемлемым. Однако он никогда не выловит неправильный результат: это утверждение assert всегда будет давать истину, независимо от состояния переменной counter. И в чем же тут дело? А в том, что оно подтверждает истинность объекта-кортежа.

Как я уже сказал, благодаря этому довольно легко выстрелить себе в ногу (моя все еще побаливает). Хорошая контрмера, с помощью которой можно избежать неприятностей от этой синтаксической причуды, — использовать линтер (linter), инструмент статического анализа кода. Кроме того, более свежие версии Python 3 для таких сомнительных инструкций assert показывают синтаксическое предупреждение.

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

Инструкции assert — резюме

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

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

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

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

• Инструкция Python assert — это средство отладки, которое проверяет условие, выступающее в качестве внутренней самопроверки вашей программы.

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

• Инструкции assert могут быть глобально отключены в настройках интерпретатора.

2.2. Беспечное размещение запятой

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

Не поняли, о чем это я? Тогда вот вам примерчик. Предположим, что в вашем исходном коде есть вот такой список имен:

>>> names = ['Элис', 'Боб', 'Дилберт']

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

Быстрым решением будет принятие стиля оформления кода, при котором вы разворачиваете константы списка, словаря или множества на нескольких строках, как показано ниже:

>>> names = [

...     'Элис',

...     'Боб',

...     'Дилберт'

... ]

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

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

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

>>> names = [

...     'Элис',

...     'Боб',

...     'Дилберт' # <- Пропущенная запятая!

...     'Джейн'

]

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

>>> names

['Элис', 'Боб', 'ДилбертДжейн']

Как видите, Python объединил строковые литералы Дилберт и Джейн в ДилбертДжейн. Такое поведение, которое называется «конкатенацией строковых литералов», является преднамеренным и задокументированным. И оно также предоставляет фантастическую возможность выстрелить себе в ногу, внося в ваши программы трудноотлавливаемые ошибки:

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

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

my_str = ('Это супердлинная строковая константа, '

          'развернутая на несколько строк. '

          'И обратите внимание — не требуется никаких обратных косых!')

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

Добавление пропущенной запятой после Дилберт не дает объединить два строковых литерала в один:

>>> names = [

...     'Элис',

...     'Боб',

...     'Дилберт',

...     'Джейн'

]

Но теперь мы совершили полный круг и вернулись к изначальной проблеме. Мне пришлось изменить две строки кода, чтобы добавить в список новое имя. Это снова затрудняет просмотр командой Git diff того, что было изменено… Добавил ли кто-то новое имя? Изменил ли кто-то имя Дилберта?

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

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

Вот как будет выглядеть окончательный пример:

>>> names = [

...     'Элис',

...     'Боб',

...     'Дилберт',

... ]

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

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

• Продуманное форматирование и размещение запятой может упростить обслуживание ваших констант списка, словаря или множества.

• Конкатенация строковых литералов как функциональное средство Python может работать как на вас, так и против, внося в код трудноотлавливаемые ошибки.

2.3. Менеджеры контекста и инструкция with

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

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

Один из хороших способов увидеть эффективное применение данного функционального средства языка — посмотреть на примеры в стандартной библиотеке Python. Встроенная функция open() предоставляет превосходный вариант ее применения:

with open('hello.txt', 'w') as f:

     f.write('привет, мир!')

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

f = open('hello.txt', 'w')

try:

     f.write('привет, мир!')

finally:

     f.close()

Вы сразу можете сказать, что он довольно многословен. Обратите внимание: инструкция try…finally имеет важное значение. Просто написать что-то типа этого было бы недостаточно:

f = open('hello.txt', 'w')

f.write('привет, мир!')

f.close()

Если во время вызова f.write() случится исключение, то такая реализация не будет гарантировать, что файл будет закрыт, и поэтому наша программа может допустить утечку дескриптора файла. Вот почему инструкция with имеет столь важное значение. Она превращает надлежащее получение и высвобождение ресурсов в пустяковую работу.

Еще одним хорошим примером, где инструкция with эффективно используется в стандартной библиотеке Python, является класс threading.Lock:

some_lock = threading.Lock()

# Вредно:

some_lock.acquire()

try:     

    # Сделать что-то...

finally:     

    some_lock.release()

# Лучше:

with some_lock:     

    # Сделать что-то...

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

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

Поддержка инструкции with в собственных объектах

Нужно сказать, что в функции open() или классе threading.Lock нет ничего особенного или чудесного, равно как и в том, что они могут применяться вместе с инструкцией with. Ту же самую функциональность можно обеспечить в собственных классах и функциях путем реализации так называемых менеджеров контекста (context managers).

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

Давайте посмотрим, как это выглядит на практике. Вот пример простой реализации контекстного менеджера open():

class ManagedFile:

     def __init__(self, name):

         self.name = name

 

    def __enter__(self):

         self.file = open(self.name, 'w')

         return self.file

 

    def __exit__(self, exc_type, exc_val, exc_tb):

         if self.file:

             self.file.close()

Наш класс ManagedFile подчиняется протоколу менеджера контекста и теперь поддерживает инструкцию with точно так же, как и первоначальный пример с функцией open():

>>> with ManagedFile('hello.txt') as f:

...     f.write('привет, мир!')

...     f.write('а теперь, пока!')

Python вызывает __enter__, когда поток исполнения входит в контекст инструкции with и наступает момент получения ресурса. Когда поток исполнения снова покидает контекст, Python вызывает __exit__, чтобы высвободить этот ресурс.

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

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

from contextlib import contextmanager

 

@contextmanager

def managed_file(name):

     try:

         f = open(name, 'w')

         yield f

     finally:

         f.close()

 

>>> with managed_file('hello.txt') as f:

...     f.write('привет, мир!')

...     f.write('а теперь, пока!')

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

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

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

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

Написание красивых API с менеджерами контекста

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

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

with Indenter() as indent:

     indent.print('привет!')

     with indent:

         indent.print('здорово')

         with indent:

             indent.print('бонжур')

     indent.print('эй')

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

привет!

    здорово

        бонжур

эй

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

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

Если вы готовы взглянуть на мою реализацию, то ниже показано, как можно воплотить эту функциональность, использовав менеджер контекста на основе класса:

class Indenter:

     def __init__(self):

         self.level = 0

 

     def __enter__(self):

         self.level += 1

         return self

 

     def __exit__(self, exc_type, exc_val, exc_tb):

         self.level -= 1

 

     def print(self, text):

         print(' ' * self.level + text)

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

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

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

• Инструкция with упрощает обработку исключений путем инкапсуляции стандартных случаев применения инструкций try/finally в так называемые менеджеры контекста.

• Чаще всего менеджер контекста используется для управления безопасным получением и высвобождением системных ресурсов. Ресурсы выделяются при помощи инструкции with и высвобождаются автоматически, когда поток исполнения покидает контекст with.

• Эффективное применение инструкции with помогает избежать утечки ресурсов и облегчает ее восприятие.

2.4. Подчеркивания, дандеры и другое

У символов одинарного и двойного подчеркивания в Python есть особый смысл в именах переменных и методов. Отчасти этот смысл существует исключительно по договоренности и предназначен в качестве подсказки программисту — и частично он обеспечивается интерпретатором Python.

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

• Одинарный начальный символ подчеркивания: _var.

• Одинарный замыкающий символ подчркивания: var_.

• Двойной начальный символ подчеркивания: __var.

• Двойной начальный и замыкающий символ подчеркивания: __var__.

• Одинарный символ подчеркивания: _.

1. Одинарный начальный символ подчеркивания: _var

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

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

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

Взгляните на приведенный ниже пример:

class Test:

     def __init__(self):

         self.foo = 11

         self._bar = 23

Что случится, если создать экземпляр этого класса и попробовать получить доступ к атрибутам foo и _bar, определенным в его конструкторе __init__?

Давайте узнаем:

>>> t = Test() >>> t.foo

11

>>> t._bar

23

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

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

# my_module.py:

 

def external_func():

     return 23

 

def _internal_func():

     return 42

Так вот, если для импорта всех имен из модуля вы будете использовать подстановочный импорт (wildcard import) (*), то Python не будет импортировать имена с начальным символом подчеркивания (если только в модуле не определен список __all__, который отменяет такое поведение):

>>> from my_module import *

>>> external_func()

23

>>> _internal_func() NameError: "name '_internal_func' is not defined"

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

>>> import my_module

>>> my_module.external_func()

23

>>> my_module._internal_func()

42

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

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

2. Одинарный замыкающий символ подчеркивания: var_

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

>>> def make_object(name, class):

SyntaxError: "invalid syntax"

 

>>> def make_object(name, class_):

...     pass

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

3. Двойной начальный символ подчеркивания: __var

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

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

Такое переписывание также называется искажением имени (name mangling) — интерпретатор преобразует имя переменной таким образом, что становится сложнее создать конфликты, когда позже класс будет расширен.

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

class Test:

     def __init__(self):

        self.foo = 11

        self._bar = 23

        self.__baz = 23

Давайте взглянем на атрибуты объекта, использовав встроенную функцию dir():

>>> t = Test()

>>> dir(t)

['_Test__baz', '__class__', '__delattr__', '__dict__',

  '__dir__', '__doc__', '__eq__', '__format__', '__ge__',

  '__getattribute__', '__gt__', '__hash__', '__init__',

  '__le__', '__lt__', '__module__', '__ne__', '__new__',

  '__reduce__', '__reduce_ex__', '__repr__',

  '__setattr__', '__sizeof__', '__str__',

  '__subclasshook__', '__weakref__', '_bar', 'foo']

Результат показывает список с атрибутами объекта. Давайте возьмем этот список и отыщем наши первоначальные имена переменных foo, _bar, и __baz. Обещаю, вы обнаружите несколько интересных изменений.

Прежде всего, в списке атрибутов переменная self.foo появляется неизмененной как foo.

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

Однако с атрибутом self.__baz все выглядит немного по-другому. Когда вы попытаетесь отыскать в списке атрибут __baz, вы увидите, что переменной с таким именем там нет.

Так что же произошло с __baz?

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

Давайте создадим еще один класс, который расширяет класс Test и пытается переопределить его существующие атрибуты, добавленные в конструкторе:

class ExtendedTest(Test):

     def __init__(self):

         super().__init__()

         self.foo = 'переопределено'

         self._bar = 'переопределено'

         self.__baz = 'переопределено'

Итак, какими, по вашему мнению, будут значения foo, _bar и __baz в экземплярах класса ExtendedTest? Давайте посмотрим:

>>> t2 = ExtendedTest()

>>> t2.foo

'переопределено'

>>> t2._bar

'переопределено'

>>> t2.__baz

AttributeError:

"'ExtendedTest' object has no attribute '__baz'"

Постойте, почему при попытке проверить значение t2.__baz мы получаем исключение AttributeError? Искажение имени наносит очередной удар! Оказывается, что этот объект вообще не имеет атрибута __baz:

>>> dir(t2)

['_ExtendedTest__baz', '_Test__baz', '__class__',

  '__delattr__', '__dict__', '__dir__', '__doc__',

  '__eq__', '__format__', '__ge__', '__getattribute__',

  '__gt__', '__hash__', '__init__', '__le__', '__lt__',

  '__module__', '__ne__', '__new__', '__reduce__',

  '__reduce_ex__', '__repr__', '__setattr__',

  '__sizeof__', '__str__', '__subclasshook__',

  '__weakref__', '_bar', 'foo', 'get_vars']

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

>>> t2._ExtendedTest__baz

'переопределено'

>>> t2._Test__baz

42

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

class ManglingTest:

     def __init__(self):

         self.__mangled = 'Привет'

    def get_mangled(self):

         return self.__mangled

>>> ManglingTest().get_mangled()

'Привет'

>>> ManglingTest().__mangled

AttributeError:

"'ManglingTest' object has no attribute '__mangled'"

Распространяется ли искажение на имена методов? Конечно! Искажение имен затрагивает все имена, которые в контексте класса начинаются с двух символов подчеркивания (или «дандеров»):

class MangledMethod:

     def __method(self):

         return 42

 

    def call_it(self):

        return self.__method()

 

>>> MangledMethod().__method()

AttributeError:

"'MangledMethod' object has no attribute '__method'"

>>> MangledMethod().call_it()

42

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

_MangledGlobal__mangled = 23

 

class MangledGlobal:

     def test(self):

         return __mangled

 

>>> MangledGlobal().test()

23

В этом примере я назначил _MangledGlobal__mangled глобальной переменной. Затем к этой переменной я обратился в контексте класса MangledGlobal. Из-за искажения имен я смог сослаться на глобальную переменную _MangledGlobal__mangled просто как на __mangled внутри метода test() класса.

Интерпретатор Python автоматически расширил имя __mangled до _MangledGlobal__mangled, потому что оно начинается с двух символов подчеркивания. Это показывает, что искажение имен точно не связано с атрибутами класса. Оно относится к любому имени, начинающемуся с двух символов подчеркивания, которое используется в контексте класса.

Уф-ф! Многовато, надо переварить.

Буду с вами честен: я написал эти примеры и объяснения не сразу из головы. Чтобы это сделать, мне потребовалось время на исследование и редактирование. Я использую Python много лет, однако правила и вот такие особые случаи, как этот, не крутятся у меня в мозгу постоянно.

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

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

Экскурс: что такое дандеры?

Если вы слышали разговор опытных питонистов о Python или при­сутствовали при обсуждении на конференциях, то, возможно, слышали термин дандер (dunder). Вам интересно, что же это такое? Ладно, вот ответ.

В сообществе Python двойные символы подчеркивания часто называют «дандерами» (dunders — это сокращение от англ. double underscores). Причина в том, что в исходном коде Python двойные символы подчеркивания встречаются довольно часто, и, чтобы не изнурять свои жевательные мышцы, питонисты нередко сокращают термин «двойное подчеркивание», сводя его до «дандера».

Например, переменная __baz будет произноситься как «дандер baz». Аналогичным образом, метод __init__ звучит как «дандер init», хотя будет логичным предположить, что так: «дандер init дандер».

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

4. Двойной начальный и замыкающий символ подчеркивания: __var__

Пожалуй, это удивляет, но искажение имен не применяется, если имя начинается и заканчивается двойными символами подчеркивания. Интерпретатор Python не трогает переменные, окруженные префиксом и постфиксом, которые состоят из двойных символов подчеркивания:

class PrefixPostfixTest:

     def __init__(self):

         self.__bam__ = 42

 

>>> PrefixPostfixTest().__bam__

42

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

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

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

5. Одинарный символ подчеркивания: _

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

Например, в приведенном ниже цикле нам не нужен доступ к нарастающему индексу, и мы можем применить «_», чтобы показать, что этот символ подчеркивания является лишь временным значением:

>>> for _ in range(32):

...     print('Привет, Мир.')

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

В следующем ниже примере исходного кода я распаковываю кортеж в отдельные переменные, но я заинтересован только в значениях полей color и mileage. Однако для того, чтобы выражение распаковки было успешным, мне нужно назначить переменным все содержащиеся в кортеже значения. Именно тут в качестве переменной-заполнителя пригодится символ «_»:

>>> car = ('красный', 'легковой автомобиль', 12, 3812.4)

>>> color, _, _, mileage = car

 

>>> color

'красный'

>>> mileage

3812.4

>>> _

12

Помимо его применения в качестве временной переменной, символ «_» является специальной переменной в большинстве интерпретаторов Python, работающих в цикле чтение-вычисление-печать (REPL). Она представляет в них результат последнего выражения, вычисленного интерпретатором.

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

>>> 20 + 3

23

>>> _

23

>>> print(_)

23

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

>>> list()

[]

>>> _.append(1)

>>> _.append(2)

>>> _.append(3)

>>> _

[1, 2, 3]

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

• Одинарный начальный символ подчеркивания _var: согласованное правило именования, указывающее на то, что имя предназначается для внутреннего использования. Обычно не обеспечивается интерпретатором Python (за исключением подстановочного импорта) и нужно только как подсказка программистам.

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

• Двойной начальный символ подчеркивания __var: запускает механизм искажения имен при использовании в контексте класса. Обеспечивается интерпретатором Python.

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

• Одинарный символ подчеркивания _: иногда используется в качестве имени временных или незначительных переменных («неважных»). Кроме того, он представляет результат последнего выражения в сеансе интерпретатора REPL Python.

2.5. Шокирующая правда о форматировании строковых значений

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

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

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

>>> errno = 50159747054

>>> name = 'Боб'

И на основе этих переменных мы хотели бы сгенерировать выходное строковое значение с сообщением об ошибке:

'Эй, Боб! Вот ошибка 0xbadc0ffee!'

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

№ 1. «Классическое» форматирование строковых значений

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

>>> 'Привет, %s' % name

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

Здесь я использую спецификатор формата %s, чтобы сообщить Python, где подставить значение переменной name, представленной в виде строкового значения. Этот способ называется «классическим» форматированием строк.

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

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

>>> '%x' % errno

'badc0ffee'

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

>>> 'Эй, %s! Вот ошибка 0x%x!' % (name, errno)

'Эй, Боб! Вот ошибка 0xbadc0ffee!'

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

>>> 'Эй, %(name)s! Вот ошибка 0x%(errno)x!' % {

...     "name": name, "errno": errno }

'Эй, Боб! Вот ошибка 0xbadc0ffee!'

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

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

№ 2. «Современное» форматирование строковых значений

Python 3 ввел новый способ форматирования строк, который позднее был также перенесен в Python 2.7. Это «современное» форматирование строк избавляется от специального синтаксиса с использованием оператора % и делает синтаксис форматирования строк более упорядоченным. Форматирование теперь обрабатывается вызовом функции format() со строковым объектом.

Функция format() может применяться для выполнения простого позиционного форматирования, точно так же, как вы могли поступать в случае с «классическим» форматированием:

>>> 'Привет, {}'.format(name)

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

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

>>> 'Эй, {name}! Вот ошибка 0x{errno:x}!'.format(

...     name=name, errno=errno)

'Эй, Боб! Вот ошибка 0xbadc0ffee!'

Этот пример также показывает, как изменился синтаксис форматирования целочисленной переменной в виде шестнадцатеричной строки. Теперь мы должны передавать спецификацию формата (format spec) путем добавления суффикса «:x» после имени переменной.

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

В Python 3 «современному» форматированию строк отдается предпочтение по сравнению с форматированием с использованием %. Однако, начиная с Python 3.6, появился еще более оптимальный способ форматирования строковых значений. И об этом способе я вам расскажу в следующем разделе.

№ 3. Интерполяция литеральных строк (Python 3.6+)

Python 3.6 добавляет еще один способ форматирования строк, который называется форматированными строковыми литералами (Formatted String Literals). Этот новый способ форматирования строк позволяет использовать выражения Python, которые встраиваются в строковые константы. Ниже дан простой пример, который поможет вам проникнуться этим функциональным средством языка:

>>> f'Привет, {name}!'

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

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

>>> a = 5

>>> b = 10

>>> f'Пять плюс десять равняется {a + b}, а не {2 * (a + b)}.'

 

'Пять плюс десять равняется 15, а не 30.'

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

Предположим, что у нас есть следующая функция greet(), которая содержит f-строку:

>>> def greet(name, question):

...     return f"Привет, {name}! Как {question}?"

...

 

>>> greet('Боб', 'дела')

"Привет, Боб! Как дела?"

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

>>> def greet(name, question):

...     return ("Привет, " + name + "! Как " +

                 question + "?")

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

>>> import dis

>>> dis.dis(greet)

   2     0 LOAD_CONST      1 ('Привет, ')

         2 LOAD_FAST       0 (name)

         4 FORMAT_VALUE    0         

         6 LOAD_CONST      2 ("! Как ")

         8 LOAD_FAST       1 (question)

        10 FORMAT_VALUE    0

        12 LOAD_CONST      3 ('?')

        14 BUILD_STRING    5

        16 RETURN_VALUE

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

>>> f"Эй, {name}! Вот ошибка {errno:#x}!"

"Эй, Боб! Вот ошибка 0xbadc0ffee!"

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

№ 4. Шаблонные строки

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

Давайте взглянем на простой пример приветствия:

>>> from string import Template

>>> t = Template('Эй, $name!')

>>> t.substitute(name=name)

'Эй, Боб!'

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

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

>>> templ_string = 'Эй, $name! Вот ошибка $error!'

>>> Template(templ_string).substitute(

...     name=name, error=hex(errno))

'Эй, Боб! Вот ошибка 0xbadc0ffee!'

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

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

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

>>> SECRET = 'это – секрет'

>>> class Error:

...     def __init__(self):

...         pass

>>> err = Error()

>>> user_input = '{error.__init__.__globals__[SECRET]}'

 

# Ой-ей-ей...

>>> user_input.format(error=err)

'это – секрет'

Заметили, как гипотетический взломщик смог извлечь нашу секретную строку, обратившись из форматной строки к словарю __globals__? Жутко, да! Шаблонные строки закрывают это направление атаки, и это делает их более безопасным выбором, если вы обрабатываете форматные строки, генерируемые из данных, вводимых пользователем:

>>> user_input = '${error.__init__.__globals__[SECRET]}'

>>> Template(user_input).substitute(error=err)

ValueError:

"Invalid placeholder in string: line 1, col 1"

Какой метод форматирования строк мне использовать?

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

Но я этого не сделаю. Вместо этого я попытаюсь все свести к простому эмпирическому правилу, которое я применяю, когда пишу на Python.

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

Эмпирическое правило Дэна, касающееся форматирования строк Python:

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

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

• Пожалуй, это удивляет, но в Python существует более одного способа форматирования строк.

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

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

2.6. Пасхалка «Дзен Python»

Я знаю, что далее приводится привычная картина, если говорить о книгах по Python. И впрямь, нет никаких шансов пройти мимо свода правил «Дзен Python» Тима Питерса. За прошедшие годы я не раз извлекал пользу из перечитывания этих правил, и думаю, что слова Тима сделали из меня более совершенного кодера. Будем надеяться, что они смогут сделать то же самое и для вас.

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

>>> import this

Дзен Python от Тима Питерса

Красивое лучше, чем уродливое.

Явное лучше, чем неявное.

Простое лучше, чем сложное.

Сложное лучше, чем запутанное.

Плоское лучше, чем вложенное.

Разреженное лучше, чем плотное.

Читаемость имеет значение.

Особые случаи не настолько особые, чтобы нарушать правила.

При этом практичность важнее безупречности.

Ошибки никогда не должны замалчиваться.

Если не замалчиваются явно.

Встретив двусмысленность, отбрось искушение угадать.

Должен существовать один — и желательно только один — очевидный способ сделать это.

Хотя он поначалу может быть и не очевиден, если вы не голландец.

Сейчас лучше, чем никогда.

Хотя никогда зачастую лучше, чем прямо сейчас.

Если реализацию сложно объяснить — идея плоха.

Если реализацию легко объяснить — идея, возможно, хороша.

Пространства имен — отличная вещь! Давайте будем делать их больше!

См. документацию Python «Инструкция assert»:

См. Википедию: и

См. документацию Python «Константы (__debug__)»:

Нулевая операция (null-operation) — это операция, которая не возвращает данные и оставляет состояние программы без изменений. См. — Примеч. пер.

Я написал статью о том, как в своих тестах Python можно избежать поддельных утверждений. Ее можно найти тут: dbader.org/blog/catching-bogus-python-asserts

См. документацию Python «Конкатенация строковых литералов»:

См. документацию Python «Менеджеры контекста инструкции with»:

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

См. PEP8: «Руководство по стилю оформления исходного кода Python»: /

См. документацию Python «Импортирование с подстановочным знаком * из пакета»:

См. PEP 8 «Импортирование»:

См. документацию Python «Форматирование строк в стиле printf»:

См. документацию Python «str.format()»:

См. документацию Python «Синтаксис форматной строки»:

См. Python 3. Спорный вопрос в трекере ошибок № 27078:

См. документацию Python «Форматированные строковые литералы»:

Язык Python создал нидерландский программист Гвидо ван Россум.

Назад: 1. Введение
Дальше: 3. Эффективные функции