Если вы относитесь к большинству пользователей Python, то скорее всего начали путешествие по Python со знакомства с функцией print(). Вы можете использовать её для отображения форматированных сообщений на экране, и возможно найти какие-то ошибки в своей программе. Но если вы думаете, что это все, что нужно знать про Python-функцию print(), то многое упускаете (это перевод статьи [1])!
После изучения этого руководства вы узнаете, как:
• Избежать распространенных ошибок с использованием print(). • Как работать с символами новой строки, кодировками и буферизацией. • Записывать текст в файлы. • Подменять print() в unit-тестах. • Реализовать продвинутый интерфейс пользователя в терминале.
Если вы действительно полный новичок, то скорее всего будет полезным прочитать первую часть этого руководства, где раскрываются основы печати в Python. Иначе пропускайте введение, и пользуйтесь этой статьей как справочником по различным вопросам.
Примечание: функция print() получила значительное добавление в Python 3, что заменило старый оператор print, доступный в Python 2. Для этого было немало веских причин. Хотя это руководство фокусируется на Python 3, для справки показан также и старый способ печати Python 2 (см. врезки "Синтаксис в Python 2").
[Основы печати в Python]
В этой главе мы рассмотрим реальные примеры печати в Python. Здесь вы познакомитесь с различными способами использования функции print().
Вызов print(). Самый простой пример использования print() переведет позицию печати консоли на новую строку:
print()
Здесь не вы не передаете никакие аргументы, однако все равно необходимо после имени функции указать пару круглых скобок, чтобы Python выполнил реальный вызов функции вместо ссылки на функцию по имени.
Этот пример сгенерирует невидимый символ новой строки (newline), в результате на вашем экране будет выведена пустая строка. Вы можете вызвать print() несколько раз, чтобы добавить в вывод пустые строки. Это выглядит наподобие нажатия на Enter в редакторе текста.
Символ новой строки (newline) это специальный управляющий символ, используемый для обозначения конца строки (end of line, EOL). Обычно у него нет специального представления на экране, однако некоторые текстовые редакторы могут определенной графикой показывать не печатаемые символы.
Слово "символ" для newline в данном случае является неправильным, потому что newline часть имеет длину больше одного символа. Например, в операционной системе Windows, как и в протоколе HTTP, newline представлен парой символов. Иногда следует учитывать эти отличия, чтобы разрабатывать реально портируемый код программы.
Чтобы узнать, что именно используется для newline в вашей операционной системе, воспользуйтесь модулем os, встроенным в Python. Это покажет вам, что на Windows и DOS сущность newline это последовательность из символов \r и \n:
>>> import os >>> os.linesep '\r\n'
На Unix, Linux, FreeBSD и последних версиях macOS это один символ \n:
>>> import os >>> os.linesep '\n'
Однако классическая Mac OS X, следуя своей философии "think different", выдаст другое представление:
>>> import os >>> os.linesep '\r'
Обратите внимание, как эти символы появляются в строковых литералах. Это так называемое экранирование: используется специальный символ обратного слеша (backslash, \), чтобы обозначить начало управляющей последовательности символов (escape character sequence). Это позволяет выразить управляющие символы, которые иначе были бы невидимы на экране.
На большинстве языков программирования имеется предопределенный набор специальных символов, таких как:
\\: backslash \b: backspace \t: tab \r: carriage return (CR, возврат каретки) \n: newline, также известный как line feed (LF, перевод строки)
Последние два специальных символа \r и \n напоминают о работе механических печатных машинок, где требовались специальные команды для того, чтобы начать печатать с новой строки или вставить новую строку. Символ \r обозначает команду, переводящую каретку машинки обратно на начало строки, а символ \n обозначает прокрутку листа вверх на интервал одной строки.
Сравнивая соответствующие коды символов ASCII, вы увидите, что размещение обратной косой черты перед символом полностью меняет его значение. Однако не все символы допускают это - только специальные.
Для сравнения кодов символов ASCII можно использовать встроенную функцию ord():
>>> ord('r') 114 >>> ord('\r') 13
Имейте в виду, чтобы сформировать корректную esc-последовательность, не должно быть пробела между символом backslash и буквой.
Как уже было сказано, вызов print() без аргументов приведет к созданию и выводу строки, которая состоит только из символа newline. Не путайте это с пустой строкой, которая не содержит никаких символов, даже newline. Эти две разные строки можно показать следующим образом:
'\n' # пропущенная строка '' # пустая строка
У первой строки длина равна 1 символу, в то время как вторая ничего не содержит, в ней 0 символов.
Замечание: чтобы удалить символ новой строки, используйте её метод .rstrip(), вот так:
>>> 'Строка текста.\n'.rstrip() 'Строка текста.'
Метод rstrip() вырежет все пробелы с правой стороны строки. Под пробелами здесь подразумеваются такие невидимые символы, как ' ', '\r', '\n', '\t'.
В более общем случае конечному пользователю вы вероятно хотели бы передать отдельное сообщение. Это можно сделать несколькими способами.
Во-первых, вы можете непосредственно передать в функцию print() строковый литерал:
>>> print('Пожалуйста, подождите, идет загрузка...')
Это дословно выведет текстовое сообщение на экран.
Строковые литералы в Python могут быть закодированы с помощью одиночных (') или двойных кавычекs ("). В соответствии с руководством по стилю Python [2], вы просто должны выбрать один из этих способов, и сохранять этот выбор для целостности вашего кода. Нет никакой разницы между одинарными и двойными кавычками, за исключением случая, когда нужно использовать кавычки внутри строки.
Например, вы не можете использовать двойные кавычки для литерала, когда в его тело также включены двойные кавычки, потому что это неоднозначно для интерпретатора Python:
"Моя любимая книга это "Python Tricks""# неправильно!
Все, что вам нужно для литерала, когда внутри него есть двойные кавычки, так это заключить его в одинарные кавычки:
'Моя любимая книга это "Python Tricks"'
Тот же самый трюк сработает и наоборот:
"Моя любимая книга это 'Python Tricks'"
Альтернативно вы можете использовать упомянутый ранее символ экранирования, чтобы Python обрабатывал эти внутренние двойные кавычки как часть строкового литерала:
"Моя любимая книга это \"Python Tricks\""
Экранирование с помощью обратного слеша хорошая вещь, но иногда это может мешать. В частности, когда вы хотите, чтобы строка литерально содержала символы backslash. Один из классических примеров: файловый путь операционной системы Windows:
Существует и несколько других префиксов строковых литералов, но мы пока не будем в это углубляться.
И наконец, вы можете определить многострочные строковые литералы, заключив их между ''' или """, что часто используется в качестве строк документации (комментариев кода).
Вот пример:
"""
Привет!
Это пример многострочной
строки в Python.
"""
Чтобы избавиться от начального символа newline, просто поместите текст сразу после открывающей """:
"""Привет!
Это пример многострочной
строки в Python.
"""
Также вы можете использовать backslash, чтобы избавиться от newline:
"""\
Привет!
Это пример многострочной
строки в Python.
"""
Для удаления отступа из пробелов из многострочной строки вы можете воспользоваться функционалом встроенного модуля textwrap:
>>> import textwrap >>> paragraph = '''
Привет!
Это пример многострочной
строки в Python.
''' >>> print(paragraph)
Привет!
Это пример многострочной
строки в Python.
>>> print(textwrap.dedent(paragraph).strip())
Привет!
Это пример многострочной
строки в Python.
Есть также несколько других полезных функций в TextWrap для выравнивания текста, которые вы найдете в продвинутом редакторе текста.
Во-вторых, для вывода сообщения вы можете воспользоваться отдельной переменной, которой дадите осмысленное имя, что поспособствует читаемости кода и его повторному использованию:
>>> message = 'Пожалуйста, подождите, идет загрузка...' >>> print(message)
И наконец, вы можете передать в функцию print() выражение, наподобие string concatenation (т. е. склейка строк), которое будет вычислено перед печатью результата:
>>> import os >>> print('Привет, ' + os.getlogin() + '! Как дела?')
Привет, jdoe! Как дела?
На самом деле существует еще с десяток способов формирования сообщений на Python. Я рекомендую вам присмотреться к f-строкам, которые появились в Python 3.6, потому что они предлагают самый краткий синтаксис из всех:
>>> import os >>> print(f'Привет, {os.getlogin()}! Как дела')
Дополнительно f-строки избавят вас от общей ошибки, когда вы забываете сделать приведение типа перед склейкой частей строки. Python это язык со строгой типизацией, и он не разрешит что-то подобное:
>>> 'Мне ' + 42 + 'года'
Traceback (most recent call last):
File "< input>", line 1, in < module>
msg = 'Мне ' + 42 + 'года'
~~~~~~~^~~~
TypeError: can only concatenate str (not"int") to str
Ошибка произошла из-за того, что нельзя склеивать строки с числами, это не имеет смысла. Вам нужно сначала явно преобразовать число в строку, чтобы её можно было склеить с другой строкой:
>>> 'Мне ' + str(42) + 'года' 'Мне 42 года'
Если вы не обработаете подобные ошибки самостоятельно, интерпретатор Python сообщит вам о проблеме, показав обратную связь.
Замечание: str() это глобальная встроенная функция, которая преобразует любой объект в строковое представление. Вы можете вызывать эту функцию на любом объекте, например, на объекте числа float:
>>> str(3.14) '3.14'
Встроенные типы данных имеют предопределенное представление строк из коробки, но позже в этой статье вы узнаете, как предоставить его для ваших пользовательских классов.
Как и с любой функцией, не имеет значение, что вы в неё передаете - литерал, переменную или выражение. Однако в отличие от многих функций, print() примет любое значение, независимо от его типа.
Пока что мы рассматривали только строки, но как насчет других типов данных? Попробуем литералы разных встроенных типов и посмотрим, что получится:
>>> print(42) # < class 'int'> 42 >>> print(3.14) # < class 'float'> 3.14 >>> print(1 + 2j) # < class 'complex'>
(1+2j) >>> print(True) # < class 'bool'> True >>> print([1, 2, 3]) # < class 'list'>
[1, 2, 3] >>> print((1, 2, 3)) # < class 'tuple'>
(1, 2, 3) >>> print({'red', 'green', 'blue'}) # < class 'set'>
{'red', 'green', 'blue'} >>> print({'name': 'Alice', 'age': 42}) # < class 'dict'>
{'name': 'Alice', 'age': 42} >>> print('hello') # < class 'str'>
hello
Особенный случай с константой None. Несмотря на то, что это используется для указания отсутствия значения, она будет показана как 'None', а не как пустая строка:
>>> print(None) None
Каким образом функция print() знает, как работать со всеми этими разными типами? На самом деле никак не знает. Просто она неявно вызывает str() на каждом типе, чтобы преобразовать любой объект в строку. После этого полученные строки обрабатываются обычным образом.
Позже в этом руководстве вы узнаете, как использовать этот механизм для печати пользовательских типов данных, таких как ваши собственные классы.
Это потому что тогда print была оператором, а не функцией, как мы увидим в следующей секции. Однако следует заметить, что далеко не всегда круглые скобки в Python обязательны. Не будет ошибкой даже их излишнее количество, потому что в крайнем случае они будут просто отброшены. Означает ли это, что оператор print следует использовать так же, как если бы это была функция? Абсолютно нет!
Например, необязательно закрывать в круглые скобки одиночное выражение или литерал. Обе следующие инструкции дают в Python 2 одинаковый результат:
Круглые скобки фактически являются частью выражения, а не оператора print. Если ваше выражение содержит только один элемент, то не имеет значение, заключаете ли вы его в скобки.
Другими словами, заключение в скобки несколько элементов сформируют кортеж (tuple):
>>> # Python 2 >>> print'Меня зовут', 'John'
Меня зовут John >>> print('Меня зовут', 'John')
('Меня зовут', 'John')
Это известный источник путаницы. Фактически вы также получите кортеж, добавив последнюю запятую к единственному элементу, заключенному в скобки:
>>> # Python 2 >>> print('Пожалуйста, подождите...')
Пожалуйста, подождите... >>> print(Пожалуйста, подождите...',) # обратите внимание на запятую
('Пожалуйста, подождите...',)
Суть в том, что на нижней строке в Python 2 не стоит использовать скобки вместе с print. Хотя, если быть полностью точным, вы можете обойти это с помощью импорта __future__, о чем мы поговорим в соответствующей секции.
ОК, теперь вы можете вызывать print() с одним аргументом или без аргументов. Вы знаете, как печатать на экране фиксированные или форматированные сообщения. В следующей главе мы рассмотрим подробнее форматирование сообщений.
[Разделение нескольких аргументов]
Вы уже видели, как print () вызывался без каких-либо аргументов для создания пустой строки, а затем вызывался с одним аргументом для отображения фиксированного или форматированного сообщения.
Однако получается, что эта функция может принимать любое количество позиционных аргументов, включая ноль, один или более аргументов. Это очень удобно в общем случае форматирования сообщений, где вы хотели бы объединить несколько элементов вместе.
Аргументы могут быть переданы в функцию различными способами. Один из способов - явное именование аргументов при вызове функции, примерно так:
>>> defdiv(a, b): ... return a / b
... >>> div(a=3, b=4) 0.75
Поскольку у аргументов могут быть уникальные, идентифицирующие их имена, их порядок следования в вызове не имеет значения. Если их переставить наоборот, то это даст такой же результат:
>>> div(b=4, a=3) 0.75
Но если передавать аргументы без указания их имени, то они идентифицируются по их позиции. Вот почему позиционные аргументы должны строго следовать порядку, заданному сигнатурой функции:
Функция print() позволяет передавать в себя произвольное количество позиционных аргументов благодаря параметру *args.
Давайте посмотрим на этот пример:
>>> import os >>> print('Меня зовут', os.getlogin(), 'и мне', 42, 'года.')
Меня зовут jdoe и мне 42 года.
Функция print() склеила все 5 переданных аргументов друг с другом, и вставила по одному пробелу между ними.
Обратите внимание, что print() также позаботилась о правильном преобразовании типа переданных аргументов благодаря неявно вызванной функции str() на каждом аргументе перед их склеиванием друг с другом. Если вы вспомните пример из предыдущей секции, наивное склеивание легко может привести к ошибке несовместимости типов:
>>> 'Мне ' + 42 + 'года'
Traceback (most recent call last):
File "< input>", line 1, in < module>
msg = 'Мне ' + 42 + 'года'
~~~~~~~^~~~
TypeError: can only concatenate str (not"int") to str
Помимо принятия переменного количества позиционных аргументов, функция print() определяет 4 именованных (или ключевых) аргумента [3], которые являются необязательными, поскольку у них у всех заданы значения по умолчанию. Вы можете просмотреть их краткую документацию, если вызовите help(print) из интерактивной командной строки интерпретатора Python.
>>> help(print) Подсказка по встроенной функции print в модуле builtins:
print(*args, sep=' ', end='\n', file=None, flush=False) Печатает значения в указанный поток, или по умолчанию в sys.stdout.
sep строка, вставляемая между значениями, по умолчанию пробел.
end строка, добавляемая после последнего значения, по умолчанию newline.
file объект наподобие файла (поток, stream); по умолчанию текущий sys.stdout.
flush нужно ли принудительно послать данные в поток.
Давайте сейчас обратим внимание на именованный параметр sep. Имя этого параметра - сокращение от separator (т. е. разделитель). Это строка, которую print вставляет между аргументами, и по умолчанию ей назначен пробел (' ').
Для sep можно указать любую нужную строку, в том числе и None, однако указание None даст такой же эффект, что и пробел по умолчанию:
>>> print('hello', 'world', sep=None)
hello world >>> print('hello', 'world', sep=' ')
hello world >>> print('hello', 'world')
hello world
Если вы хотите подавить вставку разделителя, то должны вместо этого передать пустую строку (''):
>>> print('hello', 'world', sep='')
helloworld
Если вы хотите, чтобы print() печатала свои аргументы каждый на отдельной строке, то просто передайте для sep строку с символом newline:
>>> print('hello', 'world', sep='\n')
hello
world
Еще один полезный пример - передача sep строки со слешем, что позволит формировать строки типа путей к файлу:
В частности, вы можете либо вставить символ слеша (/) в первый позиционный аргумент, либо добавить первый аргумент в виде пустой строки, чтобы вставился слеш спереди.
Замечание: будьте осторожны, когда склеиваете элементы списка или кортежа. Выполнение этого действия вручную приведет к известной ошибке TypeError, если хотя бы один из элементов списка или кортежа не является строкой:
>>> print(' '.join(['jdoe', 42, 'года']))
Traceback (most recent call last):
File "< input>", line 1, in < module>
print(','.join(['jdoe', 42, 'года']))
TypeError: sequence item 1: expected str instance, int found
Безопаснее просто предварительно распаковать последовательность с помощью оператора звездочки (*), что позволит функции print() применить преобразование типа элементов последовательности:
>>> print(*['jdoe', 42, 'года'])
jdoe 42 года
Распаковка фактически делает то же самое, что и вызов print() с перечислением отдельных элементов из списка.
Еще один полезный пример - экспорт данных в формат CSV (comma-separated values):
Это не будет обрабатывать частные случаи, такие как корректное экранирование запятых, но для простых случаев будет работать. Показанная в примере выше строка появится в окне терминала. Чтобы сохранить её в файл, необходимо перенаправить вывод. Далее будет показано, как использовать print() для записи текстовых файлов напрямую из Python.
И наконец, параметр sep не ограничивается только одним символом. Вы можете склеивать элементы, вставляя между ними строку произвольной длины:
Кроме того, в Python 2 нет способа изменить разделитель по умолчанию между элементами. Один из способов решить проблему - использовать интерполяцию строки (форматированный вывод наподобие printf языка C), примерно так:
>>> # Python 2 >>> import os >>> print'Меня зовут %s и мне %d года.' % (os.getlogin(), 42)
Меня зовут jdoe и мне 42 года.
Это был способ форматирования строк по умолчанию, пока метод .format () не был обратно перенесен из Python 3.
В последующих секциях мы рассмотрим остальные именованные аргументы функции print().
[Отмена печати символов конца строки]
Иногда вам нужно, чтобы сообщение не заканчивалось завершающим newline, чтобы следующий вызов print() продолжил вывод на той же строке. Классические примеры включают индикацию прогресса какой-то долгой операции, или запрос к пользователю на ожидании его ввода. В последнем случае вы захотите, чтобы ответ пользователя был на той же строке:
Вы уверены, что хотите продолжить? [y/n] y
Многие языки программирования предоставляют функции, подобные print(), через свои стандартные библиотеки, но они позволяют вам самим решать, добавлять ли newline, или нет. Например, на Java и C# у вас есть для этого две разные функции, в то время как на других языках программирования требуется явно указать \n в конце строкового литерала.
Вот примеры такого синтаксиса на некоторых языках:
Язык
Пример
Perl
print"hello world\n"
C
printf("hello world\n");
C++
std::cout << "hello world" << std::endl;
В отличие от этого Python-функция print() всегда добавит \n в конец строки без всякого спроса, потому что это чаще всего именно то, что требуется. Чтобы запретить это поведение, вы можете использовать другой специальный именованный аргумент end, который задает, как должна заканчиваться строка.
С точки зрения семантики параметр end практически идентичен параметру sep, который вы видели ранее:
• Он должен быть строкой или None. • Он может быть произвольной длины. • Его значение по умолчанию '\n'. • Если он равен None, то это дает такой же эффект, как значение по умолчанию. • Если в него передать пустую строку (''), то это подавляет добавление newline.
Теперь давайте разберемся, что происходит, когда мы вызываем print() без аргументов. Поскольку никакие позиционные аргументы не были предоставлены, то ничего склеивать друг с другом не требуется, и разделитель по умолчанию вообще не используется. Однако значение по умолчанию для end все еще применимо, и в результате в вывод вставляется пустая строка, т. е. позиция ввода опускается на одну строку вниз и в начало строки.
Замечание: вам может быть интересно, почему параметру end назначено фиксированное значение по умолчанию '\n', а не то, что используется в вашей операционной системе. В общем, вам не нужно беспокоиться о вариантах представления newline между различными операционными системами при печати, потому что print() будет обрабатывать преобразование автоматически. Просто всегда не забывайте использовать escape-последовательность \n в строковых литералах.
В настоящее время это наиболее переносимый способ печати newline-символа в Python:
Например, если бы вы попытались принудительно напечатать специфичный для Windows символ newline (т. е. \r\n) на машине Linux, то получите поломанный вывод:
С другой стороны, когда открываете файл для чтения с помощью open(), вам также не нужно заботиться о представлении newline. Функция будет транслировать любую зависящую от системы newline, которая встретится, в универсальный '\n'. В то же время у вас есть контроль над тем, как newline должны обрабатываться на вводе и выводе, если вам это действительно нужно.
Чтобы запретить newline в конце строки, вы должны явно указать пустую строку в параметре end:
Эти два вызова должны вызываться с большим интервалом по времени друг относительно друга, потому что необходима проверка файла. По этой причине должно на экране сначала появиться сообщение:
Проверка целостности файла...
После второго вызова print() на той же самой строке должно отображаться:
Проверка целостности файла...ok
Как было с параметром sep, вы можете использовать параметр end для соединения друг с другом отдельных частей текста в большой текст с настроенным разделителем. Однако вместо того, чтобы соединять части текста с помощью нескольких аргументов в одном вызове print(), вы можете добиться того же самого результата, используя несколько вызовов print() и передачу в end текста разделителя:
Ничто не помешает вам использовать символ новой строки с дополнительным заполнением вокруг него:
print('Основы печати в Python', end='\n * ') print('Вызов print()', end='\n * ') print('Разделение нескольких аргументов', end='\n * ') print('Отмена печати символов конца строки')
Это напечатает следующую порцию текста:
Основы печати в Python * Вызов print() * Разделение нескольких аргументов * Отмена печати символов конца строки
Как видите, именованный аргумент end принимает произвольные строки.
Замечание: проход в цикле по строка в текстовом файле сохраняет их собственные символы новой строки, что в сочетании с поведением функции print() по умолчанию приведет к избыточному символу новой строки:
>>> withopen('file.txt') as file_object: ... for line in file_object: ... print(line)
...
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
Получилось два newline после каждой строки текста. Вы захотите вырезать один из них перед печатью строки, это делается следующим образом:
print(line.rstrip())
Альтернативно вы можете сохранить newline в содержимом файла, подавив автоматическое добавление newline при вызове print(). Для этого используйте именованный аргумент end:
>>> withopen('file.txt') as file_object: ... for line in file_object: ... print(line, end='')
...
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
Завершение вывода пустой строкой эффективно исключит из вывода один из символов newline.
Чтобы предотвратить завершение строки в Python 2, требуется добавить в выражение завершающую запятую:
print'hello world',
Однако это не идеальное решение, потому что добавит нежелательный пробел. Вы можете проверить это следующим куском кода:
print'BEFORE' print'hello', print'AFTER'
Обратите внимание на вывод: вставлен пробел между словом hello и AFTER:
BEFORE hello AFTER
Чтобы получить желаемый результат, необходимо воспользоваться трюком, который будет описан далее. Этот трюк заключается в импорте функции print() из будущего релиза Python (функция из модуля __future__), или можно вернуться к помощи модуля sys:
Хотя модуль sys дает вам полный контроль над тем, что печатается в stdout, код становится несколько более загроможденным.
[Печать в файл]
Хотите верьте, хотите нет, но функция print() на самом деле ничего не знает о том, как превратить текст в появление сообщения на экране консоли, и по правде говоря, не нуждается в этом. Это работа для низкоуровневых подпрограмм, которые получают на входе байты и знают, как их продвигать дальше.
Функция print() служит абстракцией над этими слоями, предоставляющая удобный интерфейс, который просто делегирует фактическую печать потоковому или файловому объекту. Потоком может быть любой файл на диске, сетевой сокет или, возможно, буфер в памяти.
В дополнение к этому существует три стандартных потока, предоставляемые операционной системой:
Стандартный вывод это то, что вы видите в терминале, когда запускаете различные утилиты командной строки. К этому относятся и ваши собственные скрипты Python:
$ cat hello.py
print('Это должно появиться в выводе stdout')
$ python hello.py
Это должно появиться в выводе stdout
Если специально не указано что-то иное, функция print() по умолчанию пишет в stdout. Однако вы можете указать вашей операционной системе временно переключиться из stdout на поток файла (file stream), в результате любой вывод попадет в файл вместо экрана:
$ python hello.py > file.txt
$ cat file.txt
Это должно появиться в выводе stdout
Это называют перенаправлением потока (stream redirection).
Поток стандартного вывода ошибок stderr подобен stdout тем, что он также показывает сообщения на экране. Тем не менее, это все-таки отдельный поток, предназначением которого является регистрация сообщений об ошибках для диагностики. С помощью перенаправления одного или обоих этих потоков вы можете обеспечить более ясное взаимодействие с используемой программой.
Замечание: чтобы перенаправить stderr, вам нужно знать про дескрипторы файла, известные также как file handle.
Это произвольные числа, хотя и постоянные, связанные со стандартными потоками. Вот таблица дескрипторов файла для семейства POSIX-совместимых операционных систем:
Поток
Дескриптор файла
stdin
0
stdout
1
stderr
2
Зная эти дескрипторы, вы можете перенаправить сразу один или несколько потоков:
Команда
Описание
./program > out.txt
Перенаправляет stdout.
./program 2> err.txt
Перенаправляет stderr.
./program > out.txt 2> err.txt
Перенаправляет stdout и stderr в отдельные файлы.
./program &> out_err.txt
Перенаправляет stdout и stderr в один и тот же файл.
Обратите внимание, что > то же самое, что и 1>.
Некоторые программы используют разные цвета текста для сообщений, печатаемых в stdout и stderr.
В то время как stdout и stderr это потоки только для записи (write-only), поток стандартного ввода stdin это поток только для чтения (read-only). Вы можете думать stdin как о вашей клавиатуре, однако как и с другими двумя, вы можете переключить ввод stdin на файл, чтобы читать из него данные.
В Python вы можете обращаться ко всем стандартным потокам через встроенный модуль sys:
Как вы можете видеть, этит предопределенные значения похожи на файловые объекты с атрибутами mode и encoding, а также методами .read() и .write() среди многих других.
По умолчанию print() связана с sys.stdout через её именованный аргумент file, но вы можете это поменять. Используйте ключевой аргумент file, чтобы указать файл, который был открыт в режиме добавления, так чтобы сообщения попадали прямиком туда:
withopen('file.txt', mode='w') as file_object:
print('hello world', file=file_object)
Это сделает ваш код имунным к перенаправлению потока на уровне операционной системы, что может быть или не быть желательным.
Для дополнительной информации по работе с файлами в Python см. руководство [4].
Замечание: не пытайтесь использовать функцию print() для записи двоичных файлов, поскольку она хорошо подходит только для текста. Просто вызывайте на двоичных файлах непосредственно метод .write() directly:
withopen('file.dat', 'wb') as file_object:
file_object.write(bytes(4))
file_object.write(b'\xff')
Если вы захотите записывать сырые байты в стандартный вывод, то это окончится неудачей, потому что sys.stdout это символьный поток:
>>> import sys>>> sys.stdout.write(bytes(4))
Traceback (most recent call last):
File "< stdin>", line 1, in < module>
TypeError: write() argument must be str, notbytes
Вместо этого вам следует копнуть глубже, чтобы получить дескриптор нижележащего байтового потока:
>>> import sys >>> num_bytes_written = sys.stdout.buffer.write(b'\x41\x0a')
A
Это напечатает букву A в верхнем регистре и символ newline, что соответстует ASCII-кодам 65 и 10. Однако здесь они кодируютс с помощью шестнадцатеричной нотации в байтах литерала.
Обратите внимание, что print() никак не управляет кодировкой символов. Поток отвечает за корректное кодирование полученных строк Unicode в байты. В большинстве случаев вы не устанавливаете кодировку самостоятельно, потому что умолчание UTF-8 это то, что вам нужно. Если же вам реально нужна смена кодировки, возможно для устаревших систем, то вы можете использовать аргумент encoding в функции open():
withopen('file.txt', mode='w', encoding='iso-8859-1') as file_object:
print('über naïve café', file=file_object)
Вместо реального файла, который присутствует где-то в вашей операционной системе, вы можете предоставить поддельный файл, который находится в памяти вашего компьютера. Мы позже будем использовать эту mock-технику для для подмены print() в unit-тестах:
Если вы дошли до этого места, то останется рассмотреть только еще один именованный аргумент (keyword argument) функции print(), который мы рассмотрим в следующей секции. Вероятно он используется меньше всего по сравнению с остальными. Тем не менее бывают случаи, когда это абсолютно необходимо.
Специальный синтаксис Python 2 для замены умолчания sys.stdout на пользовательский файл в операторе print:
withopen('file.txt', mode='w') as file_object:
print >> file_object, 'hello world'
Поскольку в Python 2 строки и байты представлены одним и тем же типом str, оператор print может обрабатывать двоичные данные:
withopen('file.dat', mode='wb') as file_object:
print >> file_object, '\x41\x0a'
Хотя здесь есть проблема с кодировкой символов. Функция open() в Python 2 лишена параметра encoding, что часто приводит к ужасной ошибке UnicodeEncodeError:
>>> withopen('file.txt', mode='w') as file_object: ... unicode_text = u'\xfcber na\xefve caf\xe9' ... print >> file_object, unicode_text
...
Traceback (most recent call last):
File "< stdin>", line 3, in < module>
UnicodeEncodeError: 'ascii' codec can't encode character u'\xfc'...
Обратите внимание, что non-Latin символы должны быть экранированы как в Unicode-литералах, так и в строковых литералах, чтобы избежать ошибки синтаксиса. Для нешего примера это должно выглядеть так:
Альтернативно вы можете указать кодировку исходного кода в соответствии с рекомендацией PEP 263 в начале файла, но это не будет лучшей практикой из-за проблем переносимости:
Однако более удобная опция - использовать встроенный модуль codecs:
import codecs
with codecs.open('file.txt', 'w', encoding='utf-8') as file_object:
unicode_text = u'\xfcber na\xefve caf\xe9'print >> file_object, unicode_text
Это позаботится о подходящих преобразованиях, когда вы читаете или записываете файлы.
[Буферизация вызовов print()]
В предыдущей секции мы научились, как делегировать печать print() в объект наподобие файла, такой как sys.stdout. Однако некоторые потоки буферизируют операции ввода вывода (I/O) для повышения производительности, что может мешать. Рассмотрим это на примере.
Представим себе, что мы пишем таймер обратного отсчета, который должен печатать оставшееся время каждую секунду на одной и той же строке:
3...2...1...Go!
Ваша первая попытка может выглядеть примерно так:
import time
num_seconds = 3 for countdown inreversed(range(num_seconds + 1)):
if countdown > 0:
print(countdown, end='...')
time.sleep(1)
else:
print('Go!')
Пока переменная countdown больше нуля, код продолжает добавлять текст без завершающего newline, с засыпанием на одну секунду. В конце обратного отсчета печатается Go! и строка завершается.
Однако неожиданно вместо того, чтобы выводить печать каждую секунду, программа ждет три секунды без какого-либо вывода, и потом вдруг печатает всю строку полностью:
Это потому что операционная система для этого случая буферизирует последовательные записи в stdout. Необходимо знать, что существует три вида потоков в контексте буферизации:
Поток unbuffered - буферизации нет, и все записи немедленно дают эффект. Поток line-buffered ждет, пока вызовы I/O достигнут завершения строки, который появится где-то в буфере, в то время как block-buffered просто позволяет заполнять буфер до определенного размера, независимо от его содержания. Стандартный вывод использует одновременно line-buffered и block-buffered, в зависимости от того, какое событие наступит раньше.
Буферизация помогает уменьшить чрезмерное количество вызовов I/O. Вы можете представить это как отправку сообщений через сеть с большими задержками. Когда вы соединяетесь с сетевым сервером по протоколу SSH для выполнения на нем, каждое ваше нажатие на клавишу может привести к генерации отдельного пакета MTU, размер которого намного превышает размер его полезной нагрузки. Получается бесполезная нагрузка на медленный канал связи! Имеет смысл подождать ввода еще нескольких символов, чтобы отправить их все вместе в одном пакете. Вот где вступает в дело буферизация.
С другой стороны, буферизация иногда может приводить к нежелательным эффектам, когда программа в реальном времени ведет себя неожиданным образом, как это происходит в нашем примере. Чтобы исправить это, вы можете просто указать print() принудительно сбрасывать на выход буфер потока (flush) без ожидания появления символа newline в буфере. Это делается указанием параметра флага flush:
Нет простого способа применить flush для потока в Python 2, потому что сам оператор print сам по себе этого не позволяет. Нужно получить его дескриптор нижнего уровня, который является стандартным выводом, и вызвать его напрямую:
Альтернативно вы можете запретить буферизацию стандартных потоков путем передачи флага -u интерпретатору Python, или путем установки переменной окружения PYTHONUNBUFFERED:
Обратите внимание, что print() была обратно портирована в Python 2, и она становится доступной через импорт модуля __future__. Но к сожалению, она не предоставляется с параметром flush:
>>> from __future__ import print_function >>> help(print)
Help on built-in function printin module __builtin__:
Здесь вы видите строку документации (docstring) функции print(). Вы можете аналогично просматривать строки docstring различных объектов Python, используя встроенную функцию help().
Отлично! Вы познакомились с несколькими примерами вызова функции print(), где применялись все её параметры. Теперь вы знаете, для чего они нужны, и можете начать их использовать. Однако понимание сигнатуры функции print() это только начало. В последующих секциях вы поймете почему.
[Печать пользовательских типов данных]
Пока мы имели дело только со встроенными типами данных, такими как строки и числа, но вам часто понадобится печатать свои собственные абстрактные типы данных. Давайте рассмотрим несколько разных способов их определения.
Для простых объектов, не содержащих какой-либо логики, назначение которых просто переносить данные, обычно вы можете применить namedtuple (именованный кортеж), доступный в стандартной библиотеке. Именованные кортежи из коробки предоставляют аккуратное текстовое представление типа:
Это работает хорошо, пока хватает места для хранения данных, но для того, чтобы добавить функционал поведений в тип Person, вам в итоге потребуется определить класс. Взгляните на этот пример:
classPerson:
def__init__(self, name, age):
self.name, self.age = name, age
Если вы сейчас создадите экземпляр класса Person и попытаетесь его распечатать, вы получите этот причудливый вывод, который сильно отличается от эквивалентного namedtuple:
Это представление по умолчанию для объектов, где присутствует их адрес в памяти, имя типа объекта и модуль, в котором объект определен. Вы это немного исправите, но просто для записи, как быстрый обходной путь, объединив namedtuple и пользовательский класс через наследование:
При этом ваш класс Person просто становится специализированной разновидностью namedtuple с двумя атрибутами, которые вы можете настраивать.
Замечание: в Python 3 оператор pass может быть заменен литералом многоточия (...), чтобы указать местозаполнитель:
defdelta(a, b, c):
...
Это предотвращает генерацию ошибки IndentationError интерпретатора из-за отсутствие отступа для блока кода.
Это лучше, чем чистый namedtuple, потому что вы не просто получаете бесплатно печать, но также можете добавить свои собственные методы и свойства в класс. Однако такое решение одной проблемы создает другую проблему. Помните, что кортежи, включая именованные кортежи, являются в Python не изменяемыми, поэтому они не могут изменять свои значения после создания.
Это подходит, когда разработка немутируемых типов данных является желательной, но во многих случаях вы захотите допускать изменения, так что снова придется вернуться к обычным классам.
Замечание: следуя примеру других языков и рабочих сред, Python 3.7 ввел классы данных (dataclass из модуля dataclasses), о которых вы можете думать как об изменяемых кортежах (mutable tuples). Таким образом, вы получаете лучшее из обоих миров:
Синтаксис аннотаций переменной, которые требует указания полей класса с их соответствующими типами, был определен в Python 3.6.
Из предыдущих секций вы уже знаете, что функция print() неявно вызывает встроенную функцию str() чтобы преобразовать позиционные аргументы в строки. Действительно, вызов str() вручную против обычного класса Person дает тот же результат, что и его печать:
Функция str(), в свою очередь, ищет один из двух магических методов в теле класса, который вы обычно реализуете. Если какой-то из них не найден, то происходит откат к грубой реализации по умолчанию. Вот эти magic-методы, в порядке поиска:
def__str__(self) def__repr__(self)
Для первого из них рекомендуется возваращать короткий, удобочитаемый текст, который включает информацию из большинства соответствующих атрибутов. В конце концов, вы не захотите выдавать конфиденциальные данные, такие как пароли, при печати объектов.
Однако другой из них должен предоставлять полную информацию об объекте, чтобы позволить восстановить его состояние из строки. В идеальном варианте он должен возвращать рабочий код Python, так чтобы его напрямую передать в eval():
Обратите внимание на использование другой встроенной функции repr(), которая всегда пытается вызвать .__repr__() в объекте, но возвращается к реализации по умолчанию, если не находит этот метод.
Замечание: несмотря на то, что print() сама использует str() для приведения типов (type casting), некоторые составные типы данных делегируют вызов функции repr() своим членам. Например, это происходит для списков (lists) и кортежей (tuples).
Рассмотрим этот класс с обоими magic-методами, которые возвратят альтернативные строковые представления для одного и того же объекта:
Если вы печатаете один объект класса User, то вы не увидите password, потому что print(user) вызовет str(user), которая в конечном итоге вызовет user.__str__():
>>> user = User('jdoe', 's3cret') >>> print(user)
jdoe
Однако если поместить ту же пользовательскую переменную внутрь списка, обернув её в квадратные скобки, то пароль станет хорошо виден:
>>> print([user])
[User('jdoe', 's3cret')]
Это потому, что последовательности, такие как списки и кортежи, реализуют их метод .__str__() так, что все их элементы сначала преобразуются с помощью repr().
Python дает вам большую свободу, когда дело доходит до определения собственных типов данных, если ни один из встроенных типов не соответствует вашим потребностям. Некоторые из них, такие как именованные кортежи (named tuples) и классы данных (data classes), предлагают строковые представления, которые выглядят хорошо, не требуя никакой работы с вашей стороны. Тем не менее, для большей гибкости вам придется определить класс и переопределить его magic-методы, описанные выше.
Семантика .__str__() и .__repr__() не поменялась со времен Python 2, но вы должны помнить, что строки были тогда не более чем прославленными массивами байт. Чтобы преобразовать ваши объекты в правильный Unicode, который был отдельным типом данных, вы должны предоставить только другой magic-метод: .__unicode__().
Как вы можете видеть, эта реализация делегирует некую работу, чтобы избежать дублирования путем вызова встроенной функции unicode() на самой себе.
Оба метода .__str__() и .__repr__() должны возвратить строки, чтобы кодировать символы Unicode в определенные байтовые представления, называемые наборами символов. UTF-8 является наиболее распространенной и безопасной кодировкой, в то время как unicode_escape является специальной константой для выражения странных символов, таких как é, как escape-последовательности на чистом коде ASCII, как например \xe9.
Оператор print ищет magic-метод .__str__() в классе, поэтому выбранная кодировка должна соответствовать кодировке, используемой терминалом. Например, кодировка по умолчанию для DOS и Windows это CP 852, а не UTF-8, поэтому запуск может привести к UnicodeEncodeError или даже искаженному выводу:
>>> user = User(u'\u043d\u0438\u043a\u0438\u0442\u0430', u's3cret') >>> print user
đŻđŞđ║đŞĐéđ░
Однако если вы запустите тот же самый код на системе с кодировкой UTF-8, то вы получите правильное написание русского имени:
>>> user = User(u'\u043d\u0438\u043a\u0438\u0442\u0430', u's3cret') >>> print user
никита
Рекомендуется преобразовывать строки в Unicode как можно раньше, например при чтении данных из файла, и использовать его последовательно везде в вашем коде. При этом кодировать Unicode обратно в выбранный набор символов следует непосредственно перед представлением его пользователю.
Кажется, что у вас больше контроля над строковым представлением объектов в Python 2, потому что больше нет никакого magic-метода .__unicode__() в Python 3. Вы можете спросить себя, возможно ли на Python 3 преобразовать объект в его представление строки байт, а не в строку Unicode. Это возможно с помощью специального метода .__bytes__(), который делает именно это:
Использование встроенной функции bytes() на экземпляре делегирует вызов на нем метода __bytes__(), определенного в соответствующем классе.
[Понимание работы Python-функции print()]
Сейчас вы довольно хорошо знаете, как использовать функцию print(), но обладая знанием, что она такое, вы сможете использовать её еще более эффективно и осознанно. После прочтения этой секции вы поймете, как с годами улучшалась печать в Python.
Оператор print в Python 3 стал функцией. Вы видели, что print() это функция Python 3. Если быть точнее, это встроенная функция, т. е. её не нужно ниоткуда импортировать:
>>> print
< built-in function print>
Она всегда доступна в глобальном пространстве имен, так что её можно вызывать напрямую, но вы можете также обращаться к ней через модуль стандартной библиотеки:
>>> import builtins >>> builtins.print
< built-in function print>
Таким способом можно избежать конфликта имен с пользовательскими функциями. Допустим, что вы хотели бы переопределить print(), чтобы он не добавлял в конце newline. Одновременно вы хотите переименовать оригинальную функцию в что-то наподобие println():
Теперь у вас две отдельные функции печати, точно как на языке программирования Java. Позже вы также определите свои функции print() в секции экспериментов. Также обратите внимание, что не сможете перезаписать print() в первом месте, если это не функция.
С другой стороны, print() не является функцией в математическом смысле, потому что она не возвращает ничего значимого, кроме неявного None:
>>> value = print('hello world')
hello world >>> print(value) None
Такие функции фактически являются процедурами или подпрограммами, которые вы вызываете для достижения какого-то дополнительного эффекта, полностью меняющего глобальное состояние. В случае функции print(), этот эффект заключается в отображении сообщения на стандартном выводе или на записи в файл.
Поскольку print() это функция, то у неё хорошо определенная сигнатура с известными атрибутами. Вы можете быстро найти документацию по ней с использованием выбранного вами редактора, без необходимости запоминать какой-нибудь странный синтаксис при выполнении определенной задачи.
Помимо всего прочего, функции проще расширить. Добавление новой фичи в так же просто, как добавление еще одного именованного аргумента (keyword argument), в то время как изменение языка для поддержки новой фичи гораздо более сложно и громоздко. Например, подумайте о перенаправлении потока или сбросе (flush) буфера.
Еще одним преимуществом print() как функции является возможность компоновки. Функции в Python это так называемые объекты первого класса (first-class objects) или граждане первого сорта (first-class citizens), что является причудливым способом показать, что они значения, такие же как строки или числа. Таким образом, вы можете назначить функцию переменной, передать её другой функции, или даже возвратить одну функцию из другой. Функция print() в этом отношении ничем не отличается. Например, вы можете воспользоваться её преимуществом для внедрения зависимости (dependency injection):
defcustom_print(*args):
pass# ничего не печатается
download('/js/app.js', log=custom_print)
Здесь параметр log позволяет вам внедрить функцию обратного вызова (callback function), которая по умолчанию print(), но может быть любым другим вызываемым объектом. В этом примере печать полностью запрещена заменой print() на пустую функцию, которая ничего не делает.
Замечание: зависимость (dependency) это любая часть кода, которая необходима для другой части кода.
Инжектирование зависимости это техника, используемая для того, чтобы код было легче тестировать, применять повторно, и чтобы он был открыт для расширения. Вы можете достичь этого, косвенно ссылаясь на зависимости через абстрактные интерфейсы, и предоставляя их в режиме проталкивания (push) вместо режима выталкивания (pull).
Существует одно веселое объяснение для инжектирования зависимости, гуляющее по Интернету ("Dependency injection для пятилетних детей", источник John Munsch, 28 октября 2009):
"Когда вы что-то берете для себя из холодильника, то это может создавать проблемы. Вы можете оставить дверцу холодильника открытой, вы можете взять оттуда что-то, что мама или папа запрещают. Возможно, что даже вы там ищете что-то, чего там нет, или срок годности которого истек.
Все, что вы должны делать - просто выразить какую-то свою потребность, типа "мне надо чего-нибудь выпить во время обеда", а затем уже мы позаботимся о том, чтобы у вас что-то было, когда вы сядете есть."
Композиция позволяет вам каким-то способом комбинировать несколько функций в новую такого же рода. Рассмотрим это в действии, указав пользовательскую функцию error(), которая печатает в стандартный поток ошибок и снабжает все сообщения префиксом заданного уровня лога:
>>> from functools import partial >>> import sys >>> redirect = lambda function, stream: partial(function, file=stream) >>> prefix = lambda function, prefix: partial(function, prefix) >>> error = prefix(redirect(print, sys.stderr), '[ERROR]') >>> error('Что-то пошло не так')
[ERROR] Что-то пошло не так
Эта пользовательская функция использует функции partial для достижения желаемого эффекта. Эта продвинутая концепция была позаимствована из парадигмы функционального программирования, так что пока вам не нужно углубляться в эту тему. Однако, если вам это интересно, то рекомендую ознакомиться с модулем functools [5].
В отличие от операторов, функции это значения. Это означает, что вы можете смешивать их с выражениями, в частности, с лямбда-выражениями. Вместо определения полномасштабной функции для замены print(), вы можете создать анонимное лямбда-выражение, вызывающее её:
Однако, поскольку лямбда-выражение определено на месте, нет никакого способа ссылаться на него в другом месте кода.
Замечание: в Python вы не можете поместить операторы, такие как присваивания, операторы проверки условия, циклы, и т. д., в анонимную лямбда-функцию. Это должно быть единое выражение!
Другой вид выражения - троичное (ternary) условное выражение:
>>> user = 'jdoe' >>> print('Hi!') if user isNoneelseprint(f'Hi, {user}.')
Hi, jdoe.
В Python есть как операторы ветвления по условию (conditional statements), так и условные выражения (conditional expressions). Последние вычисляются в одно значение, которое может быть присвоено переменной или передано в функцию. В приведенном выше примере вас интересует побочный эффект, а не значение, которое равно None, поэтому вы просто игнорируете его.
Как вы можете видеть, функции позволяют создать элегантное и расширяемое решение, которое согласуется с остальным языком. В следующей подсекции вы узнаете, как отсутствие печати как функции print() вызывало много головной боли.
В Python 2 print был оператором. Оператор это инструкция, которая может вызывать побочный эффект при выполнении, но никогда не вычисляется как значение. Другими словами, вы не сможете напечатать оператор, или назначить его переменной, наподобие следующего:
result = print'hello world'
На Python 2 это вызовет ошибку синтаксиса.
Вот еще несколько примеров операторов Python:
присваивание: = ветвление по условию: if цикл: while утверждение: assert
Замечание: Python 3.8 привнес спорный оператор моржа (:=), который является выражением присваивания. С его помощью вы можете вычислить выражение и одновременно присвоить его результат переменной, даже внутри другого выражения!
Давайте рассмотрим следующий пример, который вызывает дорогую функцию один раз, а затем повторно использует её результат для дальнейших вычислений:
# Python 3.8+
values = [y := f(x), y**2, y**3]
Это полезно для упрощения кода без потери его эффективности. Обычно производительный код был бы более подробным:
y = f(x)
values = [y, y**2, y**3]
Этот контроверсионный оператор вызвал много дискуссий. Обилие негативных комментариев и жаркие дебаты в конечном итоге привели к тому, что Guido van Rossum ушел с позиции Benevolent Dictator For Life (BDFL).
Операторы обычно состоят из зарезервированных ключевых слов, таких как if, for или print, которые имеют фиксированный смысл на языке. Вы не можете использовать их для имени ваших переменных или других символов. Вот почему переопределение, или "издевательство" над оператором print невозможно в Python 2. Вы застряли в том, что получили.
Кроме того, вы не можете печатать из анонимных функций, потому что операторы не принимаются в лямбда-выражениях:
А в других случаях они полностью меняют вид печатаемого сообщения:
>>> print'My name is', 'John'
My name is John >>> print('My name is', 'John')
('My name is', 'John')
Склеивание строк (string concatenation) может привести к ошибке TypeError из-за несовместимости типов, что необходимо обработать вручную, например:
>>> values = ['jdoe', 'is', 42, 'years old'] >>> print' '.join(map(str, values))
jdoe is42 years old
Сравните это с аналогичным кодом в Python 3, который использует распаковку последовательности:
>>> values = ['jdoe', 'is', 42, 'years old'] >>> print(*values) # Python 3
jdoe is42 years old
На Python 2 у оператора print нет именованных аргументов для таких общих задач, как сброс (flush) буфера или перенаправление потока. Вместо этого вам необходимо помнить причудливый синтаксис. Даже встроенная функция help() не поможет для оператора print:
Удаление newline в конце строки работает не совсем корректно, потому что добавляет нежелательный пробел. Вы не можете составить несколько операторов друг с другом, и кроме этого, вы должны быть очень внимательны к кодировке символов.
Этот список проблем можно продолжать и продолжать. Если вам интересно, вы можете вернуться назад и просмотреть другие врезки, описывающие отличия синтаксиса Python 2.
Однако вы можете разобраться с некоторыми из этих проблем более простым способом, чем перенос программы на Python 3. Он заключается в подключении функции print(), которая была обратно портирована для упрощения миграции на Python 3. Вы можете импортировать её из специального модуля __future__, который предоставляет выбор фич языка, появившихся в более новых версиях Python.
Замечание: вы можете импортировать как будущие функции, так и встроенные языковые конструкции, такие как оператор with.
Чтобы получить список фич, которые стали для вас доступны, вы можете проинспектировать модуль:
Вы также можете вызвать dir(__future__), но это покажет множество не очень интересных внутренних подробностей модуля.
Чтобы можно было использовать функцию print() в Python 2, вам нужно добавить следующий оператор import в начало вашего кода:
from __future__ import print_function
Начиная с этого места оператор print больше перестанет быть доступным, но в вашем распоряжении окажется функция print(). Обратите внимание, что это не та же самая функция, что в Python 3, потому что в ней нет именованного аргумента flush, но остальные аргументы такие же.
Кроме того, это не избавит вас от правильного управления кодировками символов.
Вот пример вызова функции print() в Python 2:
>>> from __future__ import print_function >>> import sys >>> print('Я функция в Python', sys.version_info.major)
Я функция в Python 2
Теперь у вас есть представление о том, как развивалась функция печати Python, и что самое главное, вы понимаете, почему эти обратно несовместимые изменения были необходимы. Такие знания наверняка помогут вам стать лучшим программистом Python.
[Продвинутые методы печати]
В этом разделе вы узнаете, как форматировать сложные структуры данных, добавлять цвета и другие украшения, создавать интерфейсы, использовать анимацию и даже воспроизводить звуки с текстом.
Pretty-printing для печати вложенных структур данных. Языки программирования позволят вам представить в структурированном виде как данные, так и исполняемый код. Однако в отличие от Python, большинство языков дают вам некоторую свободу в использовании пробелов и форматирования. Это может быть полезным, например в лаконичности, но иногда приводит к менее удобочитаемому коду.
Pretty-printing это о том, чтобы порция данных или кода выглядела красиво, и была понятнее. Это делается путем отступа определенных строк, вставки новых строк, переупорядочивания элементов, и так далее.
Python поставляется с модулем pprint (сокращение от pretty-printing) в своей стандартной библиотеке, который поможет вам качественно напечатать большие структуры данных, не помещающиеся в одну строку. Поскольку он печатает более удобным для человека способом, многие популярные инструменты REPL, включая JupyterLab и IPython, его по умолчанию используют вместо обычной функции print().
Read-Eval-Print Loop, или REPL, это рабочее окружение в компьютере, где пользователь вводит входные данные для вычисления, например выражения, и затем получает результат. Система REPL предоставляет интерактивное взаимодействие с пользователем, предоставляя ему описание своих возможностей или язык программирования. Примерами таких систем являются Node.js консоль, IPython, Bash shell, а также консоль разработчика, которую можно найти в большинстве web-браузеров.
Для иллюстрации, как работает REPL, рассмотрим следующий пример в консоли Bash shell на сервере Debian. Для взаимодействия с сервером пользователь вводит команды, инструктируя сервер выполнить необходимое действие, или возвратить определенную информацию. Например, пользователь может выполнить команду expr, которая используется для вычисления математических выражений. В этом примере команда expr вычислит выражение суммы 2 + 2:
$ expr 2 + 2
4 $ _
После выполнения этой команды и возврата результата оболочка Bash возвращается в режим чтения, замыкая тем самым цикл, и позволяя пользователю ввести новую команду.
Замечание: чтобы переключиться на pretty printing в IPython, выдайте следующую команду:
In [1]: %pprint Pretty printing has been turned OFF In [2]: %pprint Pretty printing has been turned ON
Это пример магии IPython. Есть много встроенных команд, которые начинаются со знака процента (%), но вы можете найти еще больше возможностей на PyPI, или можете даже создать свои варианты.
Если вам не надо беспокоиться о доступе к оригинальной функции print(), то вы просто можете заменить её в своем коде на pprint() с помощью импорта с переименованием:
>>> from pprint import pprint asprint >>> print
< function pprint at 0x7f7a775a3510>
Мне лично нравится иметь под рукой обе функции, что можно достигнуть переименованием pprint в сокращенное имя наподобие pp:
from pprint import pprint as pp
На первый взгляд, между print() и pprint() почти нет разницы, и в некоторых случаях различий действительно нет:
Причина в том, что pprint() вызывает repr() вместо обычной str() для приведения типов (type casting). Различия станут более очевидными после вывода более сложных структур данных:
>>> data = {'powers': [x**10for x inrange(10)]} >>> pp(data)
{'powers': [0,
1,
1024,
59049,
1048576,
9765625,
60466176,
282475249,
1073741824,
3486784401]}
Функция применила разумное форматирование вывода для улучшения читаемости, но вы можете настроить это еще больше с помощью нескольких параметров. Например, вы можете ограничить вывод глубоко вложенной иерархии, показывая многоточие ниже заданного уровня:
Обычная функция print() также использует многоточия, но для отображения рекурсивных структур данных, которые формируют цикл, чтобы избежать ошибки переполнения стека:
Этот модуль поддерживает большинство встроенных типов, и его использует отладчик Python.
Функция pprint() автоматически сортирует для вас ключи словаря перед печатью, что обеспечивает непротиворечивое сравнение. Когда вы сравниваете строки, вы часто не заботитесь о конкретном порядке сериализованных атрибутов. В любом случае, всегда лучше сравнивать фактические словари перед сериализацией.
Словари часто представлены данными JSON, которые широко используются в Интернете. Чтобы правильно сериализовать словарь в допустимую строку формата JSON, вы можете воспользоваться модулем json. Он также обладает изрядными возможностями печати:
Однако обратите внимание, что иногда вам нужно самостоятельно обрабатывать печатаемой строки, потому печать не всегда то, что вы хотите получить. Подобным образом модуль pprint имеет дополнительную функцию pformat(), которая возвратит строку в случае, когда нужно делать что-то помимо печати.
Удивительно, что сигнатура pprint() не похожа на сигнатуру функции print(). Нельзя даже передать более одного позиционного аргумента, который показывает, насколько он орентирован на печать структур данных.
Добавление цветов с помощью escape-последовательностей ANSI. По мере совершенствования персональных компьютеров они стали более сложными, получили продвинутые графические возможности и могут отображать большее количество цветов. Однако различные производители имели часто собственное представление о дизайне API и его управлении. Это поменялось несколько десятилетий назад, когда в американском институте национальных стандартов (American National Standards Institute, ANSI) придумали стандарт управления переключением цвета, определив escape-коды управления ANSI.
Большинство программ эмулятора терминала до некоторой степени поддерживают этот стандарт. До недавнего времен операционная система Windows была заметным исключением. Таким образом, если вы хотите достичь лучшей портируемости, используйте в Python библиотеку colorama. Она транслирует ANSI-коды и их соответствующие аналоги Windows, сохраняя коды неизменными в других операционных системах.
Чтобы проверить, понимает ли ваш терминал подмножество escape-последовательностей ANSI, связанные например с цветом, вы можете попробовать использовать следующую команду:
$ tput colors
Мой терминал по умолчанию на Linux говорит, что может отобразить 256 различных цветов, в то время как xterm позволяет показать только 8. Эта команда возвратит отрицательное значение, если цвета не поддерживаются.
Escape-последовательности ANSI работают наподобие языка разметки для терминала. На HTML вы работаете с тегами, такими как < b> или < i>, чтобы изменить вид элементов документа. Эти теги применяют к содержимому документа, но сами они не отображаются. Подобным образом escape-коды не отображаются в терминале, пока он их распознает. Иначе они появятся на экране в литеральной форме, как если бы вы просматривали исходный код web-страницы сайта.
Как говорит название, последовательность должна начинаться на не печатаемый символ Esc, у которого ASCII-значение кода 27. Иногда это значение предсталяется в шестнадцатеричном виде как 0x1b, или в восьмеричном виде как 033. Вы можете использовать литералы чисел Python, чтобы быстро проверить, что эти значения одинаковые:
>>> 27 == 0x1b == 0o33 True
Кроме того, его можно получить с помощью escape-последовательности \e в командной строке шелла:
$ echo -e "\e"
Наиболее распространенные escape-последовательности ANSI имеют следующий вид:
Элемент
Описание
Пример
Esc
Не печатаемый управляющий символ
\033
[
Открывающая квадратная скобка
[
числовой код
Одно число, или несколько чисел, разделенных ;
0
символьный код
Буква в верхнем или нижнем регистре
m
Числовой код может быть одним или несколькими числами, разделенными точкой с запятой, в то время как код символа - всего одна буква. Их конкретное значение определяется стандартом ANSI. Например, чтобы сбросить все форматирование, вы должны ввести одну из следующих команд, которые используют код ноль и букву m:
На другом конце спектра находятся значения составного кода. Для настройки цвета переднего плана (foreground) и фона (background) с каналами RGB, учитывая, что ваш терминал поддерживает 24-битную глубину, вы можете предоставить несколько номеров:
$ echo -e "\e[38;2;0;0;0m\e[48;2;255;255;255mBlack on white\e[0m"
Вы можете установить с помощью escape-кодов ANSI не только цвет текста. Вы можете, например, очистить и прокрутить окно терминала, изменить его фон, переместить курсор, заставить текст мигать или выделить его подчеркиванием.
В Python вы, вероятно, напишете вспомогательную функцию, позволяющую упаковывать произвольные коды в последовательность:
Этот делает слово напечатанным красным цветом (red), широким шрифтом (bold), с применением подчеркивания (underline):
Однако существуют абстракции более высокого уровня по сравнению с escape-кодами ANSI, такие как уже упомянутая библиотека colorama, а также инструменты для построения пользовательских интерфейсов в консоли.
Реализация интерфейса пользователя в консоли. Хотя игра с escape-кодами ANSI занимательная вещь, в реальном мире вы скорее всего захотите использовать более абстрактные строительные блоки, чтобы собрать из них интерфейс пользователя. Существует несколько библиотек, которые предоставляют подобный уровень высокоуровневого управления через терминал, однако библиотека curses выглядит наиболее популярным выбором.
Замечание: для использования библиотеки curses на Windows, вам нужно будет установить сторонний пакет:
C:\> pip install windows-curses
Причина в том, что curses недоступна в дистрибутиве Python стандартной библиотеки Windows.
В первую очередь это позволит при проектировании программы мыслить в терминах независимых графических графических виджектов вместо вывода больших блоков текста. Кроме того, вы получаете больше свободы как художник, потому что как будто реально распоряжаетесь рисованием на чистом холсте. Библиотека скрывает сложности обработки различий реализаций терминалов. Кроме этого, библиотека имеет большую поддержку событий клавиатуры, что может быть полезно для написания видеоигр.
Как насчет создания ретро-игры "змейка"? Давайте создадим на Python симулятор змеи:
Сначала вам нужно импортировать модуль curses. Поскольку это изменит состояние работающего терминала, важно обрабатывать ошибки и корректно восстанавливать предыдущее состояние. Вы можете сделать это вручную, но библиотека поставляется с удобной оберткой для вашей функции main:
import curses
defmain(screen):
pass
if __name__ == '__main__':
curses.wrapper(main)
Обратите внимание, что функция должна принять ссылку на объект screen, также известный как stdscr, который вы будете использовать позже для дополнительной настройки.
Если вы сейчас запустите эту программу, то не увидите никакого эффекта, потому что она немедленно завершится. Однако вы можете добавить небольшую задержку для реализации продвижения змеи:
import time, curses
defmain(screen):
time.sleep(1)
if __name__ == '__main__':
curses.wrapper(main)
В течение этого времени 1 секунды экран будет полностью темным, будет только мерцать курсор. Чтобы его спрятать, вызовите функцию конфигурации, определенную в модуле:
import time, curses
defmain(screen):
curses.curs_set(0) # это спрячет курсор
time.sleep(1)
if __name__ == '__main__':
curses.wrapper(main)
Давайте определим змейку как список точек в виде координат на экране:
snake = [(0, i) for i inreversed(range(20))]
Голова змеи всегда будет первым элементом этого списка, в то время хвостом будет последний элемент. Сначала змея будет выглядеть как горизонтальная полоска из звездочек, обращенная вправо, начинаясь от верхнего левого угла экрана. В то время как координата y сохраняется нулевой, координата x уменьшатеся от головы к хвосту.
Чтобы нарисовать змею, начните с головы, и выполните цикл по оставшимся сегментам. Каждый сегмент соответствует координатам (y, x):
# Рисование змеи:
screen.addstr(*snake[0], '@') for segment in snake[1:]:
screen.addstr(*segment, '*')
И опять-таки, если вы запустите этот код, то он ничего не отобразит, потому что после цикла рисования вы должны явно обновить экран:
import time, curses
defmain(screen):
curses.curs_set(0) # это спрячет курсор
snake = [(0, i) for i inreversed(range(20))] # Нарисует змейку
screen.addstr(*snake[0], '@')
for segment in snake[1:]:
screen.addstr(*segment, '*')
screen.refresh()
time.sleep(1)
if __name__ == '__main__':
curses.wrapper(main)
Змейка должна перемещаться в одном из 4 направлений, которые вы можете определить как векторы. Так или иначе, направление перемещения змеи будет соответстовать кажатию клавиш со стрелками, поэтому вы можете связать направление с кодами коды клавиш:
Как движется змея? Получается, что голова появится на новом месте, при этом её надо на старом месте заменить звездочкой, и все тело должно последовать за головой, т. е. звездочку в позиции хвоста нужно стереть. На каждом шаге почти все сегменты тела змеи останутся на старом месте, меняются только позиции головы и хвоста. Предположим, что змея не растет, тогда вы можете удалить элемент хвоста и вставить новую голову в начало списка:
Чтобы получить новые координаты головы, вам нужно для неё добавить вектор направления. Однако добавление кортежей в Python приводит к большему кортежу вместо алгебраической суммы соответствующих векторных компонентов. Один из способов исправить это - использовать встроенные функции zip(), sum() и map().
Направление будет меняться по нажатию клавиши, так что вам нужно будет вызвать .getch(), чтобы получить код нажатой клавиши. Однако если нажатая клавиша не соответствует клавишам со стрелками, которые мы ранее определили в словаре directions, то направление не поменяется:
# Изменение направления по клавише со стрелкой
direction = directions.get(screen.getch(), direction)
Однако по умолчанию .getch() работает как блокирующий вызов, который не даст переместиться змейке, пока не будет нажата клавиша. Таким образом, вам нужно сделать не блокирующий вызов, путем добавления немного другой конфигурации:
defmain(screen):
curses.curs_set(0) # это спрячет курсор
screen.nodelay(True) # не блокировать вызовы I/O
И это почти все, но осталось только одно последнее. Если вы теперь зацикливаете этот код, змея будет расти, а не двигаться. Это потому, что перед каждой итерацией вы должны явно стереть экран.
Наконец, вот получившийся код для игры в змею на Python:
import time, curses
defmain(screen):
curses.curs_set(0) # это спрячет курсор
screen.nodelay(True) # не блокировать вызовы I/O
directions = {
curses.KEY_UP: (-1, 0),
curses.KEY_DOWN: (1, 0),
curses.KEY_LEFT: (0, -1),
curses.KEY_RIGHT: (0, 1),
}
direction = directions[curses.KEY_RIGHT]
snake = [(0, i) for i inreversed(range(20))] whileTrue:
screen.erase() # Рисование змеи
screen.addstr(*snake[0], '@')
for segment in snake[1:]:
screen.addstr(*segment, '*') # Перемещение змеи
snake.pop()
snake.insert(0, tuple(map(sum, zip(snake[0], direction)))) # Изменение направления по клавише со стрелкой
direction = directions.get(screen.getch(), direction)
screen.refresh()
time.sleep(0.1)
if __name__ == '__main__':
curses.wrapper(main)
Этот пример только приоткрывает часть возможностей, которые предоставляет модуль curses. Вы можете использовать его для разработки игр, подобных этой, или более бизнес-ориентированных приложений.
Добавление анимаций. Есть несколько интересных способов оживить интерфейс пользователя. Этим вы предоставите обратную связь, что программа не зависла, над чем-то работает, и пока не время её останавливать.
Для анимации текста в терминале необходимо иметь возможность свободного перемещения курсора. Вы можете сделать это с помощью одного из инструментов, упомянутых ранее, то есть escape-кодов ANSI или библиотеки curses. Тем не менее есть еще более простой способ.
Если анимация может быть ограничена одной строкой текста, то вас могут заинтересовать две специальные escape-последовательности символов:
Возврат каретки (Carriage Return, CR): \r Возврат на одну позицию обратно (Backspace): \b
Код \r переместит курсор в начало текущей строки, в то время как \b переместит курсор на одну позциию влево. Оба этих кода работают не деструктивным образом, без перезаписи текста в позиции, где он уже был записан.
Давайте рассмотрим несколько примеров.
Вы иногда вероятно захотите отобразить вращающуюся палочку, чтобы показать, что идет какая-то работа, и пока неизвестно сколько времени потребуется для завершения:
Многие утилиты командной строки используют этот трюк при загрузке данных по сети. Вы можете сделать действительно простую анимацию из последовательности символов, которые будут сменять друг друга циклически:
from itertools import cycle from time import sleep
for frame in cycle(r'-\|/-\|/'):
print('\r', frame, sep='', end='', flush=True)
sleep(0.2)
Цикл берет следующий символ для печати, затем перемещает курсор в начало строки, и перезаписывает предыдущий символ в одной и той же позиции, без добавления новой строки. Лишний пробел между позиционными аргументами не нужен, поэтому параметр разделителя sep нужно установить в пустую строку ''. Также обратите внимание, что здесь используется префикс r для обозначения сырой строки литерала (raw), потому что в последовательности символов присутствует символы backslash.
Когда вы знаете, сколько осталось времени до окончания выполнения задачи, или сколько процентов работы уже сделано, то можете показать анимированную полосу прогресса:
Во-первых, нужно подсчитать, сколько символов # отображать, и сколько пробелов вставлять. Далее вы стираете строку и строите полоску прогресса с нуля:
from time import sleep
defprogress(percent=0, width=30):
left = width * percent // 100
right = width - left
print('\r[', '#' * left, ' ' * right, ']',
f' {percent:.0f}%',
sep='', end='', flush=True)
for i inrange(101):
progress(i)
sleep(0.1)
Как и раньше, каждый запрос обновления перерисует всю строку.
Замечание: существует многофункциональная библиотека progressbar2, наряду с несколькими другими аналогичными инструментами, которые могут показать прогресс гораздо более всеобъемлющим образом.
Звуки вместе с print(). Если вы достаточно стары, что помните компьютеры с PC спикером, то должны также помнить их отличительный звуковой сигнал, часто используемый для индикации аппаратных проблем. Эти PC-спикеры редко могли создавать более сложные звуки, однако для видеоигр даже это имело большое значение.
Сегодня в некоторых случаях вы все еще можете воспользоваться этим маленьким громкоговорителем, но скорее всего в вашем ноутбуке его уже нет. В таком случае вы можете включить эмуляцию звукового сигнала терминала в своем шелле (terminal bell emulation), чтобы вместо этого воспроизводился системный предупреждающий звук.
Введите эту команду, чтобы увидеть, может ли ваш терминал воспроизводить звук:
$ echo -e "\a"
Обычно это приводит к печати текста, но флаг -e позволяет интерпретировать esc-коды backslash. Как вы можете видеть, здесь присутствует специальная escape-последовательность \a, которая означает "alert", что выведет специальный символ "звонка" (bell character). Некоторые терминалы формируют звуковой сигнал, когда встречаются с таким символом.
Аналогично можно напечатать этот символ на Python. Возможно, в цикле сформировать какую-то мелодию. Хотя получится реально только одна нота, вы все-таки можете менять длительность пауз между проигрыванием звуков. Это кажется идеальным методом для воспроизведения морзянки!
Правила кода Морзе следующие:
• Буквы кодируются последовательностью символов точек (·) и тире (–). • Точка имеет длительность один квант времени. • Тире длится 3 кванта времени. • Отдельные элементы в одной букве отделены друг от друга одним квантом времени. • Символы двух соседних букв отделены друг от друга паузой длительностью 3 кванта времени. • Символы двух соседних слов отделены друг от друга паузой в 7 квантов времени.
По этим правилам вы можете "печатать" повторяющийся сигнал SOS следующим образом:
В настоящее время ожидается, что вы поставляете код, который соответствует высоким стандартам качества. Если вы стремитесь стать профессионалом, вы должны научиться тестировать свой код.
Тестирование ПО особенно важно для языков с динамической типизацией, таких как Python, у которых нет компилятора, который предупредит вас об очевидных ошибках. Дефекты могут пробраться в производственную среду и оставаться незаметными длительное время, то того дня, когда соответствующая ветвь кода будет выполнена.
Конечно, у вас есть линтеры, чекеры и другие инструменты для статического анализа кода, которые помогут вам. Но они не скажут, делает ли ваша программа то, что она должна делать на уровне бизнеса.
Итак, вы должны тестировать print()? Нет. В конце концов это встроенная функция, которая вероятно уже прошла комплексный набор тестов. Однако вы захотите проверить, вызывает ли ваш код print() в нужное время с ожидаемыми параметрами. Это называется поведением.
Вы можете протестировать поведение путем подмены (mocking) реальных объектов или функций. В этом случае вы захотите подменить функцию print() для записи и проверки её вовлечений.
Замечание: возможно вы слышали такие термины: dummy, fake, stub, spy или mock, используемые взаимозаменяемым образом. Некоторые люди делают различие между ними, а другие нет.
Martin Fowler объясняет их различия в коротком словарике [6], и называет их все двойным тестированием (test doubles).
Mocking в Python можно сделать двояко. Во-первых, вы можете выбрать традиционный путь статически типизируемых языков, используя внедрение зависимостей (dependency injection). Это может иногда потребовать от ваз поменять тестируемый код, что не всегда возможно, если этот код определен во внешней библиотеке:
Это тот же пример, что был ранее показан, когда шла речь про композицию функции. Этот метод в основном позволяет заменять print() пользовательской функцией с таким же интерфейсом. Чтобы проверить, правильно ли печатается сообщение, придется перехватить его, внедрив mocked-функцию:
Вызов этого макета приводит к сохранению последнего сообщения в атрибуте, который можно проверить позже, например, в инструкции assert.
В несколько альтернативном решении вместо замены всей функции print() пользовательской оберткой можно было бы перенаправить стандартный вывод в файловый поток символов в памяти:
На этот раз функция явно вызывает print(), однако выставляет свой параметр file во внешний мир.
Тем не менее, более питонский способ подмены объектов использует преимущества встроенного макетного модуля mock, который использует технику, называемую "обезьяньим патчем" (monkey patching). Это уничижительное название проистекает из того, что оно похоже на "грязный хак", с которым вы можете легко выстрелить себе в ногу. Это менее элегантно, чем инъекция зависимостей, но определенно быстро и удобно.
Замечание: модуль mock был встроен в стандартную библиотеку Python 3, но до этого он был сторонним пакетом. Вы должны будете установить его отдельно:
$ pip2 install mock
Кроме того, вы будете обращаться к нему по имени mock, в то время как в Python 3 он работает как часть модуля юнит-тестов, и вы должны его импортировать из unittest.mock.
Monkey patching исправляет реализацию динамически, во время выполнения кода. Такое изменение видится глобально, поэтому может иметь нежелательные последствия. Однако на практике патч влияет на код только во время выполнения теста.
Чтобы имитировать print() в тестовом кейсе, вы обычно используете декоратор @patch и указываете цель для исправления, ссылаясь на нее по полному имени, которое включает имя модуля:
from unittest.mock import patch
@patch('builtins.print') deftest_print(mock_print):
print('not a real print')
mock_print.assert_called_with('not a real print')
Это автоматически создаст для вас mock и инжектирует его в функцию теста. Однако вам нужно декларировать, что ваша тестируемая функция теперь принимает mock. Нижележащий mock-объект имеет несколько полезных методов и атрибутов для проверки поведения.
Вы заметили что-нибудь особенное в этом фрагменте кода?
Несмотря на внедрение mock в функцию, вы не вызываете ее напрямую, хотя и можете. Этот внедренный mock используется только для последующей вставки assert и, возможно, для подготовки контекста перед запуском теста.
В реальной жизни техника mock помогает изолировать тестируемый код путем устранения таких зависимостей, как соединение с базой данных. Вы редко вызываете mock-и в тесте, потому что это не имеет смысла. Скорее, это будет происходит в других частях кода, которые называют ваш mock косвенно, не зная об этом.
Здесь тестируемый код это функция, которая печатает приветствие. Несмотря на то, что это довольно простая функция, вы не можете легко ее проверить, потому что она не возвращает значение. Это побочный эффект.
Чтобы устранить этот побочный эффект, вам нужно имитировать зависимость. Наложение патча позволит вам избежать внесение изменений в оригинальную функцию, которая может оставаться независимой от print(). Она будет думать, что вызывает print(), но на самом деле вызовет mock, который вы полностью контролируете.
Здесь только 2 отличия. Первое отличие: синтаксис перенаправления потока использует шеврон (>>) вместо аргумента file. Другое отличие там, где определяется StringIO. Вы можете импортировать его из модуля StringIO, или из cStringIO для более быстрой реализации.
Исправление стандартного вывода это именно то, как это звучит, однако вам нужно знать о нескольких подводных камнях:
Прежде всего не забудьте инсталлировать модуль mock, поскольку он не был доступен в стандартной библиотеке Python 2.
Во-вторых, оператор print вызывает нижележащий метод .write() на mock-объекте вместо вызова самого объекта. Вот почему вы будете запускать assert-ы против mock_stdout.write.
И наконец, один оператор print не всегда соответствует одному вызову sys.stdout.write(). Фактически вы увидите символ newline, записываемый отдельно.
В последнем варианте вы должны импортировать функцию print() из будущего и применить на ней патч:
from __future__ import print_function from mock import patch
И снова, это делается почти идентично Python 3, но функция print() определена в модуле __builtin__ вместо модуля builtins.
Существует множество веских причин для тестирования ПО. Одна из них - поиск багов. При написании тестов вам часто захочется подменить функцию print(), например с помощью техники mock. Однако парадоксально, что та же самая функция может помочь вам найти баги, во время связанного процесса отладки, о чем вы узнаете в следующем разделе.
[Отладка с помощью print()]
В этом разделе вы познакомитесь с доступными инструментами для отладки на Python, начиная от скромной функции print(), затем познакомитесь с модулем лога, и закончите полноценным отладчиком. Прочитав его, вы сможете принять обоснованное решение о том, какой из них является наиболее подходящим в конкретной ситуации.
Замечание: отладка это процесс поиска основных причин багов или дефектов в ПО после их обнаружения, а также принятие мер по их устранению. Термин "баг" имеет забавную историю о происхождении своего названия (см. "Debugging" в Википедии).
Трассировка. Это также известно как отладка с помощью печати, или пещерная отладка, и является самой базовой формой отладки. Хотя несколько старомодная, она все еще обладает мощными возможностями и имеет право на жизнь.
Идея состоит в том, чтобы следовать по пути выполнения алгоритма программы, пока она резко не остановится или не даст неправильных результатов, чтобы идентифицировать точную инструкцию с проблемой. Это делается путем тщательного выбора мест, в которые вставляются вызовы печати сообщений, чтобы не загромождать вывод.
Давайте рассмотрим следующий пример, в котором проявляется ошибка округления:
Как вы можете видеть, функция не возвращает ожидаемое значение 0.1, но теперь вы знаете, что это потому, что sum немного ошибается. Трассировка состояния переменной на отдельных шагах алгоритма может дать вам подсказку, где находится проблема.
Для этого случая проблема заключается в том, как представлены числа с плавающей запятой в памяти компьютера. Вспомните, что числа хранятся в двоичном виде. Десятичное значение 0.1 превращается в бесконечное двоичное представление, которое округляется.
Для дополнительной информации по округлению чисел в Python см. [7].
Этот метод прост, интуитивно понятен и будет работать практически на любом языке программирования. Не говоря уже о том, что это отличное упражнение в процессе обучения.
С другой стороны, когда вы освоите более продвинутые техники, трудно вернуться назад, потому что они позволяют быстрее найти ошибки. Трассировка это трудоемкий, выполняемый вручную процесс, который может пропустить еще больше ошибок. Цикл сборки и публикации (или перепрошивки firmware) занимает время. И после завершения исправления найденных ошибок нужно дотошно удалить все вставленные трассировочные вызовы print(), не удалив случайно рабочие вызовы.
Помимо всего прочего, трассировка требует внесения изменений в код, что не всегда возможно. Могут быть случаи, когда приложение работает на удаленном web-сервере, или вы хотите найти причину проблемы уже по факту, когда она уже свершилась. Иногда у вас просто нет доступа к стандартному выводу.
Это как раз то место, где проявляет свои лучшие качества вывод в лог.
Вывод в лог. Давайте представим на минуту, что вы управляете сайтом электронной коммерции. Однажды разгневанный клиент звонит по телефону с жалобой на неудачную транзакцию и говорит, что потерял свои деньги. Он утверждает, что пытался приобрести несколько предметов, но в конце концов произошла какая-то загадочная ошибка, которая помешала ему закончить этот заказ. Тем не менее, когда он проверил свой банковский счет, деньги пропали.
Вы искренне извиняетесь и делаете возврат средств, но также не хотите, чтобы это повторилось в будущем. Как вы это отладите? Можно было бы попытаться, если бы у вас был какой-то след произошедшего, в идеале в виде хронологического списка событий с их контекстом.
Всякий раз, когда вы проводите отладку с помощью печати сообщений, подумайте о том, следует ли превратить это в постоянный вывод сообщений в лог. Это может помочь в подобных ситуациях, когда понадобится анализ проблемы после того, как она возникла в среде, куда у вас нет доступа.
Существуют сложные инструменты для накопления логов и поиска по ним, но на самом базовом уровне вы можете думать о логах как о файлах с текстом. Каждая строка файла лога содержит подробную информацию о событии в вашей системе. Обычно в логе не содержатся персональные идентифицирующие данные, хотя в некоторых случаях это предписывается законом.
Вот так может выглядеть типовая запись лога:
[2019-06-14 15:18:34,517][DEBUG][root][MainThread] Customer(id=123) logged out
Как вы видите, запись имеет структурированную форму. Кроме описания события (пользователь с идентификатором 123 разлогинился) в записи содержится несколько настраиваемых полей, предоставляющих информацию о контексте события. В них вы получаете точную дату и время (2019-06-14 15:18:34,517), уровень лога (DEBUG), от имени какой учетной записи произошло событие (root), и имя потока программы (MainThread).
Уровни лога позволят вам быстро отфильтровать сообщения лога, избавившись от лишнего шума. Например, если вы ищете ошибку, то вам не надо видеть все предупреждающие (WARNING) или отладочные (DEBUG) сообщения. Очень легко запретить или разрешить сообщения на определенных уровнях лога через конфигурацию сервиса, не затрагивая его код.
С помощью вывода в лог вы можете удерживать свои отладочные сообщения отдельно от стандартного вывода. По умолчанию все сообщения лога поступают в стандартный поток ошибок (stderr), который для удобства подсвечивается другими цветами текста. Однако вы можете перенаправить сообщения лога в отдельные файлы, даже для отдельных модулей!
Довольно часто неправильно настроенное ведение журнала может привести к нехватке места на диске сервера. Чтобы предотвратить это, вы можете настроить ротацию журналов, которая будет хранить файлы журналов в течение определенной продолжительности, например, одной недели или после того, как они достигнут определенного размера. Тем не менее, всегда полезно архивировать старые журналы. Некоторые правила предписывают хранить данные о клиентах до пяти лет!
В сравнении с другими языками программирования логирование в Python устроено проще, потому что модуль logging [8] поставляется в стандартной библиотеке. В просто импортируете и конфигурируете его двумя строчками кода:
Вы можете вызывать функции, определенные на уровне модуля, которые подключены к корневому логу (root logger), но более распространенная практика - получить выделенный лог для каждого вашего исходного файла:
logging.debug('hello') # функция на уровне модуля
logger = logging.getLogger(__name__)
logger.debug('hello') # вызов метода лога
Достоинство использования отдельных настроенных логов заключается в более тонком управлении. Обычно логи называются по имени модуля, где лог определен, через переменную __name__.
Замечание: в Python есть модуль warnings, несколько связанный с выводом в лог, который может также выводить сообщения лога в стандартный поток ошибок. Однако у него более узкий спектр применения, в основном в библиотечном коде, тогда как клиентские приложения должны использовать модуль logging.
Тем не менее, вы можете заставить их работать совместно, путем вызова logging.captureWarnings(True).
Последняя причина переключения с функции print() на вывод в лог - написание потокобезопасного кода (thread safety). В следующем разделе вы увидите, что первый print() не очень хорошо стыкуется с несколькими работающими потоками.
Отладка. Правда состоит в том, что ни трассировку, ни вывод в лог нельзя считать полноценной отладкой. Чтобы выполнить реальную отладку, вам необходима утилита отладчика (debugger), который позволит вам получить следующие возможности:
• Пошаговое выполнение по исходному коду. • Вставка точек останова (breakpoint), включая точки останова, срабатывающие по условию. • Просмотр переменных в памяти. • Вычислять пользовательские выражения во время выполнения.
Сырой отладчик, работающий в терминале, который ожидаемо получил имя pdb от "The Python Debugger", поставляется как часть стандартной библиотеки. Это делает его всегда доступным, поэтому он может быть вашим единственным выбором для выполнения удаленной отладки. Возможно, это хорошая причина, чтобы ознакомиться с ним.
Однако в нем нет графического интерфейса, так что использование pdb может быть несколько усложнено. Если вы не можете редактировать код, то вы должны запустить его как модуль, и передать в него место размещения вашего скрипта:
$ python -m pdb my_script.py
В противном случае вы можете вставить breakpoint непосредственно в код, который остановит выполнения вашего скрипта и выбросит вас в отладчик. Старый способ для этого требует двух шагов:
Это отобразит интерактивное приглашение, которое поначалу может напугать. Тем не менее в этом месте вы все еще можете ввести родной Python, чтобы проанализировать или изменить состояние локальных переменных. Кроме того, есть несколько специфичных для отладчика команд, которые вы захотите использовать для пошагового выполнения кода.
Замечание: две инструкции для инжекции отладчика принято вставлять на одну строку. Это требует использования символа точки с запятой, что иногда встречается в программах Python:
import pdb; pdb.set_trace()
Поскольку этот стиль определенно не питонский, он дополнительно напоминает о необходимости удаления после завершения отладки.
Начиная с Python 3.7 вы можете также вызвать встроенную функцию breakpoint(), которая делает то же самое, но более компактным способом и с некоторыми дополнительными наворотами:
Вы вероятно предпочтете использовать визуальный отладчик, в большинстве случаев интегрированный в редактор кода. PyCharm имеет отличный отладчик с превосходной производительностью, но вы найдете множество альтернативных IDE с отладчиками, как платных, так и бесплатных.
Отладку не стоит считать серебряной пулей, решающей все проблемы. Иногда вывод в лог или трассировка будет лучшим решением. Например редко проявляющиеся дефекты, такие как условия гонки (race conditions), часто происходят из-за случайной связи. Когда вы выполняете остановку программы в breakpoint, то эта маленькая пауза в выполнении программы маскирует проблему. Это похоже на принцип Гейзенберга: с помощью отладки вы не можете измерить и наблюдать баг одновременно.
Перечисленные методы поиска ошибок не являются взаимоисключающими. Они дополняют друг друга.
[Потокобезопасная печать]
Я ранее кратко упоминал проблему thread safety, когда рекомендовал вывод в лог через функцию print(). Если вы дочитали до этого места, то скорее всего находите удобным использование концепции потоков (threads).
Thread safety означает, что определенная часть кода может безопасно использоваться несколькими потоками так, что они не нарушают нормальную работу друг друга. Самая простая стратегия обеспечить thread-safety - шаринг только неизменяемых объектов. Если потоки не могут модифицировать состояние общего объекта, то не будет риска нарушения его целостности.
Другой метод - использование локальной памяти, когда каждый поток получает собственную копию одного и того же объекта. Таким способом другой поток не может видеть изменения, сделанные в текущем потоке.
Но это не решает проблему, не так ли? Вы часто хотите, чтобы ваши потоки сотрудничали, имея возможность мутировать общий ресурс. Наиболее частый способ синхронизации конкурентного доступа к такому ресурсу является блокировка всех конкурирующих потоков, кроме одного. Это дает эксклюзивный доступ на запись одному потоку, или иногда нескольким потокам одновременно.
Однако блокировка является дорогим методом, и она снижает параллельную пропускную способность, поэтому были изобретены другие средства управления доступом, такие как атомарные переменные или алгоритм сравнения и замены.
Печать в Python не thread-safe. Функция print() хранит ссылку на стандартный вывод, который является общей глобальной переменной. Теоретически, поскольку нет блокировки, переключение контекста может произойти во время вызова sys.stdout.write(), переплетая байты текста из нескольких вызовов print().
Замечание: переключение контекста означает, что один поток приостанавливает свое выполнение, добровольно или нет, чтобы другой поток мог взять выполнение кода на себя. Это может произойти в любой момент, даже посередине вызова функции.
Однако на практиrе этого не происходит. Как ни старайся, запись в стандартный вывод выглядит атомарной. Единственная проблема, которую вы можете иногда наблюдать связана с испорченными местами появления разрывов строки:
[Thread-3 A][Thread-2 A][Thread-1 A]
[Thread-3 B][Thread-1 B]
[Thread-1 C][Thread-3 C]
[Thread-2 B]
[Thread-2 C]
Для симуляции этого вы может увеличить вероятность переключения контекста, заставив нижележащий метод .write() уйти в сон на случайное время. Как? Применив для него mock-технику, с которой вы познакомились в предыдущей секции:
import sys
from time import sleep from random import random from threading import current_thread, Thread from unittest.mock import patch write = sys.stdout.write
defslow_write(text):
sleep(random())
write(text)
deftask():
thread_name = current_thread().name
for letter in'ABC':
print(f'[{thread_name}{letter}]')
with patch('sys.stdout') as mock_stdout:
mock_stdout.write = slow_write
for _ inrange(3):
Thread(target=task).start()
Сначала вам нужно сохранить оригинальный метод .write() в переменной, которую вы делегируете позже. Затем вы предоставите поддельную реализацию, время выполнения которой займет до одной секунды. Каждый поток будет делать несколько вызовов print(), обозначая себя по имени и букве: A, B и C.
Если вы читали ранее секцию, посвященную mock-технике, то возможно уже имеете представление, почему печать так себя ведет. Тем не менее, чтобы окончательно все прояснить, вы можете перехватить значения, передаваемые вашей функции slow_write(). Вы заметите, что получаете каждый раз немного другую последовательность:
Несмотря на то, что sys.stdout.write() сам по себе является атомарной операцией, один вызов функции print() может привести к нескольким операциям записи. Например, символы разрыва строк записываются отдельно от остального текста, и между этими записями происходит переключение контекста.
Замечание: атомарный характер стандартного вывода в Python является побочным продуктом глобальной блокировки интерпретатора (Global Interpreter Lock, GIL), которая применяет блокировку вокруг инструкций байт-кода. Однако имейте в виду, что многие разновидности интерпретатора не имеют GIL, где многопоточная печать требует явной блокировки.
Вы можете сделать символ newline интегральной частью сообщения, выполнив это вручную:
Однако обратите внимание, что функция print() продолжает делать отдельный вызов для пустого суффикса, что транслируется в бесполезную инструкцию sys.stdout.write(''):
Реально thread-safe версия функции print() может выглядеть примерно так:
import threading
lock = threading.Lock()
defthread_safe_print(*args, **kwargs):
with lock:
print(*args, **kwargs)
Вы можете поместить эту функцию в модуль, и импортировать её в другом месте:
from thread_safe_print import thread_safe_print
deftask():
thread_name = current_thread().name
for letter in'ABC':
thread_safe_print(f'[{thread_name}{letter}]')
Теперь, несмотря на выполнение двух операций записи на каждый запрос print(), только одному потоку разрешается взаимодействовать с выводом, в то время как остальные должны ждать:
Добавленные здесь комментарии показывают, как блокировка ограничивает доступ к общему ресурсу.
Замечание: даже в однопоточном коде вы можете столкнуться с подобной ситуацией. В частности, когда вы печатаете одновременно в stdout и stderr. Если вы не перенаправили их в отдельные файлы, все их сообщения будут появляться в одном общем окне терминала.
И наоборот, модуль logging является thread-safe благодаря своему дизайну, что отражается в его способности отображать имена потоков в отформатированном сообщении:
Это еще одна причина, по которой вы возможно не захотите все время использовать функцию print().
[Ввод в Python]
Сейчас вы уже многое знаете из того, что следует знать о функции print()! Тема тем не менее не будет полной, если немного не рассказать о противоположном функционале. В то время как print() это вывод, существуют функции и библиотеки для ввода.
Встроенные средства. Python поставляется со встроенной функцией для обработки ввода пользователя, которая предсказуемо называется input(). Она принимает данные из стандартного потока ввода, который обычно соответствует клавиатуре:
Эта функция всегда возвратит строку, так что вы сможете проанализировать её соответствующим образом:
try:
age = int(input('How old are you? ')) except ValueError:
pass
Параметр приглашения prompt совсем не обязателен для функции input(), и если его опустить, то никакой текст не будет выведен, но функция все еще будет работать:
>>> x = input()
hello world >>> print(x)
hello world
Тем не менее выбрасывание соответствующего запроса, описывающего необходимое действие при вызове input(), позволяет лучше информировать пользователя.
Замечание: чтобы прочитать из стандартного ввода на Python 2, вы должны вместо этого вызвать raw_input(), которая просто другая встроенная функция. К сожалению, также существует неудачно именованная функция input(), которая делает несколько другое.
Фактически она также принимает ввод из стандартного потока stdin, но затем пытается обработать его как код Python. Из-за того, что эта фича представляет потенциально дыру в безопасности, она была полностью удалена из Python 3, в то время как raw_input() была переименована в input().
Таблица для быстрого сравнения доступных встроенных функций ввода:
Python 2
Python 3
raw_input()
input()
input()
eval(input())
В Python 3 все еще можно имитировать старое поведение.
Запрашивать ввод пароля пользователя через input() это плохая идея, потому что вводимый пароль при вводе будет отображаться в консоли открытым текстом. Для такого случая вместо input() вы должны использовать функцию getpass(), которая маскирует вводимые символы. Эта функция определена в модуле с таким же именем, который также доступен в стандартной библиотеке:
Модуль getpass имеет другую функцию для получения имени пользователя из переменной окружения:
>>> from getpass import getuser >>> getuser() 'jdoe'
Встроенные функции Python для обработки стандартного ввода довольно ограниченные. В то же время существует множество сторонних пакетов, которые предлагают намного более продвинутые инструменты.
Сторонние средства. Существуют внешние пакеты Python, которые позволяют строить сложные графические интерфейсы, специально предназначенные для сбора данных от пользователя. Некоторые из их особенностей включают в себя:
• Продвинутое форматирование и поддержку стилей. • Автоматический парсинг, проверка корректности ввода, очистка данных пользователя. • Декларативный стиль определения макетов интерфейса. • Интерактивное автодополнение. • Поддержка мыши. • Предварительно определенные виджеты, такие как выпадающие списки (checklist) или меню. • История команд с возможностью прокрутки команд и поиска по командам. • Подсветка синтаксиса.
Демонстрация таких инструментов выходит за рамки этой статьи, но вы можете их попробовать самостоятельно. Автор сам узнал о них из Python Bytes Podcast:
Тем не менее стоит упомянуть утилиту командной строки rlwrap, которая бесплатно добавит мощные возможности по редактированию строки в ваши скрипты Python. Вам ничего не надо делать, чтобы это сработало!
Предположим, что вы пишете интерфейс командной строки, которые понимает три инструкции, включая одну для сложения чисел:
Но когда вы заметите ошибку ввода, и захотите её исправить, то увидите, что ни одна из функциональных клавиш не работает так, как ожидается. Если нажать на клавишу стрелки влево, то вместо возвращения курсора обратно получится следующее:
Теперь вы можете обернуть вызов того же скрипта командой rlwrap. Теперь не только будут нормально работать клавиши со стрелками, то также вы получите возможность поиска по истории введенных ранее команд, использовать автозавершение команды, и редактирование строки с шорткатами:
Теперь вы вооружены как знаниями по функции print() в Python, так и по многим связанным с ней темам. У вас есть глубокое понимание, как она работает, включая все её именованные параметры. Многочисленные примеры показали вам развитие функции печати от оператора Python 2.
Обладая этими знаниями, вы можете с успехом реализовывать интерактивные программы, которые взаимодействуют с пользователем или генерируют данные в популярных форматах файлов. Вы сможете быстро диагностировать проблемы в своем коде и обеспечить защиту от потенциальных ошибок.
[Ссылки]
1. Your Guide to the Python print() Function site:realpython.com. 2. Whitespace in Expressions and Statements site:peps.python.org. 3. Python: функции. 4. Reading and Writing Files in Python site:realpython.com. 5. functools — Tools for Manipulating Functions site:pymotw.com. 6. Test Double site:martinfowler.com. 7. How to Round Numbers in Python site:realpython.com. 8. Python: модуль вывода в лог.