Ваше руководство по функции печати Python

Ваше руководство по функции печати Python

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

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

К концу этого руководства вы будете знать, как:

  • Избегайте распространенных ошибок с Pythonprint()

  • Работа с символами новой строки, кодировками символов и буферизацией

  • Написать текст в файлы

  • Мокprint() в модульных тестах

  • Создавайте расширенные пользовательские интерфейсы в терминале

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

Note:print() был важным дополнением к Python 3, в котором он заменил старый операторprint, доступный в Python 2.

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

Free Bonus:Click here to get our free Python Cheat Sheet, который показывает вам основы Python 3, такие как работа с типами данных, словарями, списками и функциями Python.

Печать в двух словах

Давайте рассмотрим несколько реальных примеров печати на Python. К концу этого раздела вы будете знать все возможные способы вызоваprint(). Или, выражаясь на языке программиста, вы бы сказали, что знакомы сfunction signature.

Печать вызовов

Самый простой пример использования Pythonprint() требует всего нескольких нажатий клавиш:

>>>

>>> print()

Вы не передаете никаких аргументов, но вам все равно нужно поставить в конце пустые круглые скобки, которые сообщают Python на самом делеexecute the function, а не просто ссылаются на него по имени.

Это приведет к появлению невидимого символа новой строки, что, в свою очередь, приведет к появлению пустой строки на вашем экране. Вы можете вызыватьprint() несколько раз таким образом, чтобы добавить вертикальное пространство. Это как если бы вы нажимали[.kbd .key-enter]#Enter # на клавиатуре в текстовом редакторе.

Как вы только что видели, вызовprint() без аргументов приводит к появлениюblank line, которое представляет собой строку, состоящую исключительно из символа новой строки. Не путайте это сempty line, которое вообще не содержит никаких символов, даже символа новой строки!

Вы можете использовать литералы Pythonstring для визуализации этих двух:

'\n'  # Blank line
''    # Empty line

Первый из них имеет длину одного символа, а второй не имеет содержимого.

Note: Чтобы удалить символ новой строки из строки в Python, используйте его метод.rstrip(), например:

>>>

>>> 'A line of text.\n'.rstrip()
'A line of text.'

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

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

Во-первых, вы можете передать строковый литерал непосредственно вprint():

>>>

>>> print('Please wait while the program is loading...')

Это распечатает сообщение дословно на экране.

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

>>>

>>> message = 'Please wait while the program is loading...'
>>> print(message)

Наконец, вы можете передать выражение, напримерstring concatenation, которое будет оцениваться перед печатью результата:

>>>

>>> import os
>>> print('Hello, ' + os.getlogin() + '! How are you?')
Hello, jdoe! How are you?

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

>>>

>>> import os
>>> print(f'Hello, {os.getlogin()}! How are you?')

Кроме того, f-строки не позволят вам совершить типичную ошибку, которая забывает вводить конкатенированные операнды. Python является строго типизированным языком, что означает, что он не позволит вам сделать это:

>>>

>>> 'My age is ' + 42
Traceback (most recent call last):
  File "", line 1, in 
    'My age is ' + 42
TypeError: can only concatenate str (not "int") to str

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

>>>

>>> 'My age is ' + str(42)
'My age is 42'

Если вы сами неhandle such errors, интерпретатор Python сообщит вам о проблеме, показываяtraceback.

Note:str() - это глобальная встроенная функция, которая преобразует объект в его строковое представление.

Вы можете позвонить прямо на любой объект, например, на номер:

>>>

>>> str(3.14)
'3.14'

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

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

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

>>>

>>> print(42)                            # 
42
>>> print(3.14)                          # 
3.14
>>> print(1 + 2j)                        # 
(1+2j)
>>> print(True)                          # 
True
>>> print([1, 2, 3])                     # 
[1, 2, 3]
>>> print((1, 2, 3))                     # 
(1, 2, 3)
>>> print({'red', 'green', 'blue'})      # 
{'red', 'green', 'blue'}
>>> print({'name': 'Alice', 'age': 42})  # 
{'name': 'Alice', 'age': 42}
>>> print('hello')                       # 
hello

Однако обратите внимание на константуNone. Несмотря на то, что он используется для обозначения отсутствия значения, он будет отображаться как'None', а не как пустая строка:

>>>

>>> print(None)
None

Какprint() знает, как работать со всеми этими разными типами? Ну, короткий ответ, что это не так. Он неявно вызываетstr() за кулисами для преобразования любого объекта в строку. После этого он обрабатывает строки единообразно.

Далее в этом руководстве вы узнаете, как использовать этот механизм для печати пользовательских типов данных, таких как ваши классы.

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

Разделение нескольких аргументов

Вы видели, какprint() вызывается без каких-либо аргументов для создания пустой строки, а затем вызывается с одним аргументом для отображения фиксированного или отформатированного сообщения.

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

Давайте посмотрим на этот пример:

>>>

>>> import os
>>> print('My name is', os.getlogin(), 'and I am', 42)
My name is jdoe and I am 42

print() объединил все четыре переданных ему аргумента и вставил между ними один пробел, так что вы не получите сжатое сообщение вроде'My name isjdoeand I am42'.

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

>>>

>>> print('My age is: ' + 42)
Traceback (most recent call last):
  File "", line 1, in 
    print('My age is: ' + 42)
TypeError: can only concatenate str (not "int") to str

Помимо приема переменного числа позиционных аргументов,print() определяет четыре именованных илиkeyword arguments, которые являются необязательными, поскольку все они имеют значения по умолчанию. Вы можете просмотреть их краткую документацию, вызвавhelp(print) из интерактивного интерпретатора.

Давайте пока сосредоточимся наsep. Это означаетseparator и по умолчанию назначается один пробел (' '). Он определяет значение для объединения элементов.

Это должна быть либо строка, либо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() объединял свои аргументы отдельными строками. В этом случае просто передайте экранированный символ новой строки, описанный ранее:

>>>

>>> print('hello', 'world', sep='\n')
hello
world

Более полезный пример параметраsep - это напечатать что-то вроде путей к файлам:

>>>

>>> print('home', 'user', 'documents', sep='/')
home/user/documents

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

>>>

>>> print('/home', 'user', 'documents', sep='/')
/home/user/documents
>>> print('', 'home', 'user', 'documents', sep='/')
/home/user/documents

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

Note: Будьте осторожны при объединении элементов списка или кортежа.

Выполнение этого вручную приведет к хорошо известномуTypeError, если хотя бы один из элементов не является строкой:

>>>

>>> print(' '.join(['jdoe is', 42, 'years old']))
Traceback (most recent call last):
  File "", line 1, in 
    print(','.join(['jdoe is', 42, 'years old']))
TypeError: sequence item 1: expected str instance, int found

Безопаснее просто распаковать последовательность с помощью оператора звезды (*) и позволитьprint() обрабатывать приведение типа:

>>>

>>> print(*['jdoe is', 42, 'years old'])
jdoe is 42 years old

Распаковка фактически аналогична вызовуprint() с отдельными элементами списка.

Еще одним интересным примером может быть экспорт данных в форматcomma-separated values (CSV):

>>>

>>> print(1, 'Python Tricks', 'Dan Bader', sep=',')
1,Python Tricks,Dan Bader

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

Наконец, параметрsep не ограничен только одним символом. Вы можете объединять элементы со строками любой длины:

>>>

>>> print('node', 'child', 'child', sep=' -> ')
node -> child -> child

В следующих подразделах вы изучите оставшиеся ключевые аргументы функцииprint().

Предотвращение разрывов строк

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

Are you sure you want to do this? [y/n] y

Многие языки программирования предоставляют функции, подобныеprint(), через свои стандартные библиотеки, но они позволяют вам решить, добавлять ли новую строку или нет. Например, в Java и C # у вас есть две разные функции, в то время как другие языки требуют, чтобы вы явно добавляли в конец строкового литерала.

Вот несколько примеров синтаксиса в таких языках:

язык пример

Perl

print "hello world\n"

C

printf("hello world\n");

C++

std::cout << "hello world" << std::endl;

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

С точки зрения семантики параметрend почти идентичен параметруsep, который вы видели ранее:

  • Это должна быть строка илиNone.

  • Это может быть сколь угодно долго.

  • По умолчанию он имеет значение' '.

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

  • Если равно пустой строке (''), новая строка будет подавлена.

Теперь вы понимаете, что происходит под капотом, когда вы вызываетеprint() без аргументов. Поскольку вы не предоставляете никакие позиционные аргументы для функции, присоединять нечего, и поэтому разделитель по умолчанию вообще не используется. Однако значение по умолчаниюend по-прежнему применяется, и отображается пустая строка.

Note: Вам может быть интересно, почему параметрend имеет фиксированное значение по умолчанию, а не то, что имеет смысл в вашей операционной системе.

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

В настоящее время это наиболее переносимый способ печати символа новой строки в Python:

>>>

>>> print('line1\nline2\nline3')
line1
line2
line3

Например, если бы вы попытались принудительно напечатать характерный для Windows символ новой строки на компьютере с Linux, вы бы получили неработающий вывод:

>>>

>>> print('line1\r\nline2\r\nline3')


line3

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

Чтобы отключить новую строку, вы должны указать пустую строку через аргумент ключевого словаend:

print('Checking file integrity...', end='')
# (...)
print('ok')

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

Checking file integrity...

Однако после второго вызоваprint() на экране появится такая же строка, как:

Checking file integrity...ok

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

print('The first sentence', end='. ')
print('The second sentence', end='. ')
print('The last sentence.')

Эти три инструкции выведут одну строку текста:

The first sentence. The second sentence. The last sentence.

Вы можете смешать два ключевых аргумента:

print('Mercury', 'Venus', 'Earth', sep=', ', end=', ')
print('Mars', 'Jupiter', 'Saturn', sep=', ', end=', ')
print('Uranus', 'Neptune', 'Pluto', sep=', ')

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

Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune, Pluto

Ничто не мешает вам использовать символ новой строки с дополнительными отступами:

print('Printing in a Nutshell', end='\n * ')
print('Calling Print', end='\n * ')
print('Separating Multiple Arguments', end='\n * ')
print('Preventing Line Breaks')

Было бы распечатать следующий фрагмент текста:

Printing in a Nutshell
 * Calling Print
 * Separating Multiple Arguments
 * Preventing Line Breaks

Как видите, аргумент ключевого словаend принимает произвольные строки.

Note: Цикл по строкам в текстовом файле сохраняет их собственные символы новой строки, что в сочетании с поведением по умолчанию функцииprint() приведет к избыточному символу новой строки:

>>>

>>> with open('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

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

print(line.rstrip())

В качестве альтернативы вы можете сохранить новую строку в содержимом, но подавить автоматически добавленнуюprint(). Для этого вы должны использовать аргумент ключевого словаend:

>>>

>>> with open('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

Завершая строку пустой строкой, вы фактически отключаете одну из новых строк.

Вы знакомитесь с печатью на Python, но впереди еще много полезной информации. В следующем подразделе вы узнаете, как перехватить и перенаправить вывод функцииprint().

Печать в файл

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

print() - это абстракция над этими уровнями, предоставляющая удобный интерфейс, который просто делегирует фактическую печать потоку илиfile-like object. Потоком может быть любой файл на вашем диске, сетевой сокет или, возможно, буфер в памяти.

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

  1. stdin: стандартный ввод

  2. stdout: стандартный вывод

  3. stderr: стандартная ошибка

В Python вы можете получить доступ ко всем стандартным потокам через встроенный модульsys:

>>>

>>> import sys
>>> sys.stdin
<_io.TextIOWrapper name='' mode='r' encoding='UTF-8'>
>>> sys.stdin.fileno()
0
>>> sys.stdout
<_io.TextIOWrapper name='' mode='w' encoding='UTF-8'>
>>> sys.stdout.fileno()
1
>>> sys.stderr
<_io.TextIOWrapper name='' mode='w' encoding='UTF-8'>
>>> sys.stderr.fileno()
2

Как видите, эти предопределенные значения напоминают файловые объекты с атрибутамиmode иencoding, а также методы.read() и.write() среди многих других.

По умолчаниюprint() привязан кsys.stdout через свой аргументfile, но вы можете это изменить. Используйте этот аргумент ключевого слова, чтобы указать файл, который был открыт в режиме записи или добавления, чтобы сообщения направлялись прямо к нему:

with open('file.txt', mode='w') as file_object:
    print('hello world', file=file_object)

Это сделает ваш код невосприимчивым к перенаправлению потока на уровне операционной системы, что может быть желательным или нежелательным.

Для получения дополнительной информации оworking with files in Python, вы можете проверитьReading and Writing Files in Python (Guide).

Note: Не пытайтесь использоватьprint() для записи двоичных данных, поскольку он подходит только для текста.

Просто вызовите.write() двоичного файла напрямую:

with open('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 "", line 1, in 
TypeError: write() argument must be str, not bytes

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

>>>

>>> import sys
>>> num_bytes_written = sys.stdout.buffer.write(b'\x41\x0a')
A

Это напечатает заглавную буквуA и символ новой строки, которые соответствуют десятичным значениям 65 и 10 в ASCII. Однако они кодируются с использованием шестнадцатеричной записи в байтовом литерале.

Обратите внимание, чтоprint() не контролируетcharacter encoding. Ответственность за правильное кодирование полученных строк Юникода в байты лежит на потоке. В большинстве случаев вы не будете устанавливать кодировку самостоятельно, потому что вы хотите использовать UTF-8 по умолчанию. Если вам действительно нужно, например, для устаревших систем, вы можете использовать аргументencoding дляopen():

with open('file.txt', mode='w', encoding='iso-8859-1') as file_object:
    print('über naïve café', file=file_object)

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

>>>

>>> import io
>>> fake_file = io.StringIO()
>>> print('hello world', file=fake_file)
>>> fake_file.getvalue()
'hello world\n'

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

Распечатка вызовов

В предыдущем подразделе вы узнали, чтоprint() делегирует печать файловому объекту, напримерsys.stdout. Однако некоторые потоки буферизуют определенные операции ввода-вывода для повышения производительности, что может помешать. Давайте посмотрим на пример.

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

3...2...1...Go!

Ваша первая попытка может выглядеть примерно так:

import time

num_seconds = 3
for countdown in reversed(range(num_seconds + 1)):
    if countdown > 0:
        print(countdown, end='...')
        time.sleep(1)
    else:
        print('Go!')

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

Неожиданно, вместо обратного отсчета каждую секунду, программа бездействует в течение трех секунд, а затем внезапно печатает всю строку сразу:

Terminal with buffered output

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

  1. Unbuffered

  2. Line-буферном

  3. Блок-буферном

Unbuffered не требует пояснений, то есть буферизация не выполняется, и все записи имеют немедленный эффект. Потокline-buffered ожидает перед запуском любых вызовов ввода-вывода, пока где-то в буфере не появится разрыв строки, тогда как потокblock-buffered просто позволяет буферу заполниться до определенного размера независимо от его содержимого. Стандартный вывод - этоline-buffered иblock-buffered, в зависимости от того, какое событие наступит первым.

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

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

print(countdown, end='...', flush=True)

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

Поздравляем! На этом этапе вы видели примеры вызоваprint(), охватывающие все его параметры. Вы знаете их цель и когда их использовать. Однако понимание подписи - это только начало. В следующих разделах вы увидите, почему.

Печать пользовательских типов данных

До сих пор вы имели дело только со встроенными типами данных, такими как строки и числа, но вам часто приходилось печатать свои собственные абстрактные типы данных. Давайте посмотрим на различные способы их определения.

Для простых объектов без какой-либо логики, предназначенных для передачи данных, вы обычно используетеnamedtuple, доступный в стандартной библиотеке. Именованные кортежи имеют аккуратное текстовое представление из коробки:

>>>

>>> from collections import namedtuple
>>> Person = namedtuple('Person', 'name age')
>>> jdoe = Person('John Doe', 42)
>>> print(jdoe)
Person(name='John Doe', age=42)

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

class Person:
    def __init__(self, name, age):
        self.name, self.age = name, age

Если вы сейчас создадите экземпляр классаPerson и попытаетесь его распечатать, вы получите этот странный результат, который сильно отличается от эквивалентногоnamedtuple:

>>>

>>> jdoe = Person('John Doe', 42)
>>> print(jdoe)
<__main__.Person object at 0x7fcac3fed1d0>

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

from collections import namedtuple

class Person(namedtuple('Person', 'name age')):
    pass

Ваш классPerson только что стал специализированным типомnamedtuple с двумя атрибутами, которые вы можете настроить.

Note: В Python 3 операторpass можно заменить литераломellipsis (...) для обозначения заполнителя:

def delta(a, b, c):
    ...

Это предотвращает повышение интерпретаторомIndentationError из-за отсутствия блока кода с отступом.

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

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

Note: Вслед за другими языками и фреймворками Python 3.7 представилdata classes, которые можно рассматривать как изменяемые кортежи. Таким образом, вы получаете лучшее из обоих миров:

>>>

>>> from dataclasses import dataclass
>>> @dataclass
... class Person:
...     name: str
...     age: int
...
...     def celebrate_birthday(self):
...         self.age += 1
...
>>> jdoe = Person('John Doe', 42)
>>> jdoe.celebrate_birthday()
>>> print(jdoe)
Person(name='John Doe', age=43)

Синтаксис дляvariable annotations, который требуется для указания полей класса с соответствующими типами, был определен в Python 3.6.

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

>>>

>>> jdoe = Person('John Doe', 42)
>>> str(jdoe)
'<__main__.Person object at 0x7fcac3fed1d0>'

str(), в свою очередь, ищет один из двухmagic methods в теле класса, который вы обычно реализуете. Если он не находит, то возвращается к уродливому представлению по умолчанию. Эти магические методы в порядке поиска:

  1. def __str__(self)

  2. def __repr__(self)

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

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

>>>

>>> repr(jdoe)
"Person(name='John Doe', age=42)"
>>> type(eval(repr(jdoe)))

Обратите внимание на использование другой встроенной функции,repr(), которая всегда пытается вызвать.__repr__() в объекте, но возвращается к представлению по умолчанию, если не находит этот метод.

Note: Несмотря на то, чтоprint() сам используетstr() для приведения типов, некоторые составные типы данных делегируют этот вызовrepr() своим членам. Это случается со списками и кортежами, например.

Рассмотрим этот класс с обоими магическими методами, которые возвращают альтернативные строковые представления одного и того же объекта:

class User:
    def __init__(self, login, password):
        self.login = login
        self.password = password

    def __str__(self):
        return self.login

    def __repr__(self):
        return f"User('{self.login}', '{self.password}')"

Если вы напечатаете единственный объект классаUser, то вы не увидите пароль, потому чтоprint(user) вызоветstr(user), который в конечном итоге вызоветuser.__str__():

>>>

>>> user = User('jdoe', 's3cret')
>>> print(user)
jdoe

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

>>>

>>> print([user])
[User('jdoe', 's3cret')]

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

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

Понимание Python Print

Вы знаете, чтоhow достаточно хорошо используетprint() на данном этапе, но зная, чтоwhat это так, вы сможете использовать его еще более эффективно и осознанно. Прочитав этот раздел, вы поймете, как с годами улучшилась печать в Python.

Печать - это функция в Python 3

Вы видели, чтоprint() - это функция в Python 3. В частности, это встроенная функция, которая означает, что вам не нужно импортировать ее из любого места:

>>>

>>> print

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

>>>

>>> import builtins
>>> builtins.print

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

>>>

>>> import builtins
>>> println = builtins.print
>>> def print(*args, **kwargs):
...     builtins.print(*args, **kwargs, end='')
...
>>> println('hello')
hello
>>> print('hello\n')
hello

Теперь у вас есть две отдельные функции печати, как в языке программирования Java. Вы также определите пользовательские функцииprint() вmocking section позже. Также обратите внимание, что вы не смогли бы перезаписатьprint(), если бы это не была функция.

С другой стороны,print() не является функцией в математическом смысле, потому что она не возвращает никакого значимого значения, кроме неявногоNone:

>>>

>>> value = print('hello world')
hello world
>>> print(value)
None

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

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

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

Еще одно преимущество функцииprint() - этоcomposability. Функции в Python называютсяfirst-class objects илиfirst-class citizens, что является причудливым способом обозначить, что они являются значениями, такими же, как строки или числа. Таким образом, вы можете назначить функцию переменной, передать ее другой функции или даже вернуть одну из другой. print() в этом отношении не отличается. Например, вы можете воспользоваться этим для внедрения зависимости:

def download(url, log=print):
    log(f'Downloading {url}')
    # ...

def custom_print(*args):
    pass  # Do not print anything

download('/js/app.js', log=custom_print)

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

Note: Adependency - это любой фрагмент кода, требуемый другим битом кода.

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

Есть забавное объяснение инъекций зависимости, циркулирующих в Интернете:

Инъекция зависимости для пятилетних

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

То, что вы должны делать, это заявить о необходимости: «Мне нужно что-нибудь выпить с обедом», и тогда мы позаботимся о том, чтобы у вас было что-нибудь, когда вы сядете поесть.

-John Munsch, 28 October 2009. (Source)

Композиция позволяет объединить несколько функций в одну новую. Давайте посмотрим на это в действии, указав настраиваемую функцию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('Something went wrong')
[ERROR] Something went wrong

Эта пользовательская функция используетpartial functions для достижения желаемого эффекта. Это продвинутая концепция, заимствованная из парадигмыfunctional programming, поэтому вам не нужно сейчас слишком углубляться в эту тему. Однако, если вам интересна эта тема, я рекомендую взглянуть на модульfunctools.

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

>>>

>>> download('/js/app.js', lambda msg: print('[INFO]', msg))
[INFO] Downloading /js/app.js

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

Note:. В Python нельзя помещать такие операторы, как присваивания, условные операторы, циклы и т. д., вanonymous lambda function. Это должно быть одно выражение!

Другим видом выражения является троичное условное выражение:

>>>

>>> user = 'jdoe'
>>> print('Hi!') if user is None else print(f'Hi, {user}.')
Hi, jdoe.

В Python есть какconditional statements, так иconditional expressions. Последнее оценивается как одно значение, которое может быть присвоено переменной или передано функции. В приведенном выше примере вас интересует побочный эффект, а не значение, которое оценивается какNone, поэтому вы просто игнорируете его.

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

Печать была заявлением в Python 2

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

result = print 'hello world'

Это синтаксическая ошибка в Python 2.

Вот еще несколько примеров утверждений в Python:

  • назначение: =

  • условно: if

  • цикл: while

  • assertion:assert

Note: Python 3.8 приносит спорнуюwalrus operator (:=), которая являетсяassignment expression. С его помощью вы можете вычислить выражение и назначить результат переменной одновременно, даже внутри другого выражения!

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

# Python 3.8+
values = [y := f(x), y**2, y**3]

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

y = f(x)
values = [y, y**2, y**3]

Спор за этот новый синтаксис вызвал много споров. Обилие негативных комментариев и жарких споров в конечном итоге привело к тому, что Гвидо ван Россум ушел с должностиBenevolent Dictator For Life или BDFL.

Утверждения обычно состоят из зарезервированных ключевых слов, таких какif,for илиprint, которые имеют фиксированное значение в языке. Вы не можете использовать их для именования ваших переменных или других символов. Вот почему переопределение или имитация оператораprint в Python 2 невозможны. Вы застряли с тем, что вы получаете.

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

>>>

>>> lambda: print 'hello world'
  File "", line 1
    lambda: print 'hello world'
                ^
SyntaxError: invalid syntax

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

>>>

>>> print 'Please wait...'
Please wait...
>>> print('Please wait...')
Please wait...

В другое время они меняют способ печати сообщения:

>>>

>>> print 'My name is', 'John'
My name is John
>>> print('My name is', 'John')
('My name is', 'John')

Конкатенация строк может привести к увеличениюTypeError из-за несовместимых типов, с которыми вам придется работать вручную, например:

>>>

>>> values = ['jdoe', 'is', 42, 'years old']
>>> print ' '.join(map(str, values))
jdoe is 42 years old

Сравните это с аналогичным кодом в Python 3, который использует распаковку последовательности:

>>>

>>> values = ['jdoe', 'is', 42, 'years old']
>>> print(*values)  # Python 3
jdoe is 42 years old

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

>>>

>>> help(print)
  File "", line 1
    help(print)
             ^
SyntaxError: invalid syntax

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

Список проблем можно продолжать и продолжать. Если вам интересно, вы можете вернуться кprevious section и поискать более подробные объяснения синтаксиса в Python 2.

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

Note: Вы можете импортировать будущие функции, а также встроенные языковые конструкции, такие как операторwith.

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

>>>

>>> import __future__
>>> __future__.all_feature_names
['nested_scopes',
 'generators',
 'division',
 'absolute_import',
 'with_statement',
 'print_function',
 'unicode_literals']

Вы также можете вызватьdir(__future__), но это покажет много неинтересных внутренних деталей модуля.

Чтобы включить функциюprint() в Python 2, вам необходимо добавить этот оператор импорта в начало исходного кода:

from __future__ import print_function

С этого момента операторprint больше не доступен, но в вашем распоряжении есть функцияprint(). Обратите внимание, что это не та же функция, что и в Python 3, потому что в ней отсутствует аргумент ключевого словаflush, но остальные аргументы такие же.

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

Вот пример вызова функцииprint() в Python 2:

>>>

>>> from __future__ import print_function
>>> import sys
>>> print('I am a function in Python', sys.version_info.major)
I am a function in Python 2

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

Печать со стилем

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

Симпатичная печать вложенных структур данных

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

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

Python поставляется с модулемpprint в своей стандартной библиотеке, который поможет вам красиво печатать большие структуры данных, которые не помещаются в одну строку. Поскольку он печатает более удобным для человека способом, многие популярные инструментыREPL, включаяJupyterLab and IPython, используют его по умолчанию вместо обычной функцииprint().

Note: Чтобы переключить красивую печать в IPython, введите следующую команду:

>>>

In [1]: %pprint
Pretty printing has been turned OFF
In [2]: %pprint
Pretty printing has been turned ON

Это примерMagic в IPython. Существует множество встроенных команд, которые начинаются со знака процента (%), но вы можете найти больше оPyPI или даже создать свои собственные.

Если вас не волнует, что у вас нет доступа к исходной функцииprint(), вы можете заменить ее наpprint() в своем коде, используя переименование импорта:

>>>

>>> from pprint import pprint as print
>>> print

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

from pprint import pprint as pp

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

>>>

>>> print(42)
42
>>> pp(42)
42
>>> print('hello')
hello
>>> pp('hello')
'hello'  # Did you spot the difference?

Это потому, чтоpprint() вызываетrepr() вместо обычногоstr() для приведения типов, так что вы можете оценить его вывод как код Python, если хотите. Различия становятся очевидными, когда вы начинаете кормить его более сложными структурами данных:

>>>

>>> data = {'powers': [x**10 for x in range(10)]}
>>> pp(data)
{'powers': [0,
            1,
            1024,
            59049,
            1048576,
            9765625,
            60466176,
            282475249,
            1073741824,
            3486784401]}

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

>>>

>>> cities = {'USA': {'Texas': {'Dallas': ['Irving']}}}
>>> pp(cities, depth=3)
{'USA': {'Texas': {'Dallas': [...]}}}

Обычныйprint() также использует эллипсы, но для отображения рекурсивных структур данных, которые образуют цикл, чтобы избежать ошибки переполнения стека:

>>>

>>> items = [1, 2, 3]
>>> items.append(items)
>>> print(items)
[1, 2, 3, [...]]

Однакоpprint() более четко описывает это, включая уникальную идентификацию объекта, ссылающегося на себя:

>>>

>>> pp(items)
[1, 2, 3, ]
>>> id(items)
140635757287688

Последний элемент в списке - это тот же объект, что и весь список.

Note: Рекурсивные или очень большие наборы данных можно также обрабатывать с помощью модуляreprlib:

>>>

>>> import reprlib
>>> reprlib.repr([x**10 for x in range(10)])
'[0, 1, 1024, 59049, 1048576, 9765625, ...]'

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

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

Словари часто представляютJSON data, что широко используется в Интернете. Чтобы правильно преобразовать словарь в допустимую строку в формате JSON, вы можете воспользоваться модулемjson. У этого также есть симпатичные возможности печати:

>>>

>>> import json
>>> data = {'username': 'jdoe', 'password': 's3cret'}
>>> ugly = json.dumps(data)
>>> pretty = json.dumps(data, indent=4, sort_keys=True)
>>> print(ugly)
{"username": "jdoe", "password": "s3cret"}
>>> print(pretty)
{
    "password": "s3cret",
    "username": "jdoe"
}

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

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

Добавление цветов с помощью ANSI Escape-последовательности

По мере того как персональные компьютеры становились все более изощренными, они имели улучшенную графику и могли отображать больше цветов. Однако у разных поставщиков было свое представление о разработке API для управления им. Ситуация изменилась несколько десятилетий назад, когда люди из Американского национального института стандартов решили унифицировать это, определивANSI escape codes.

Большинство современных эмуляторов терминала поддерживают этот стандарт в некоторой степени. До недавнего времени операционная система Windows была заметным исключением. Поэтому, если вам нужна лучшая переносимость, используйте библиотекуcolorama в Python. Он переводит коды ANSI в соответствующие им аналоги в Windows, сохраняя их нетронутыми в других операционных системах.

Чтобы проверить, понимает ли ваш терминал подмножество escape-последовательностей ANSI, например, связанных с цветами, вы можете попробовать использовать следующую команду:

$ tput colors

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

Экранирующие последовательности ANSI подобны языку разметки для терминала. В HTML вы работаете с тегами, такими как<b> или<i>, чтобы изменить внешний вид элементов в документе. Эти теги смешиваются с вашим контентом, но сами по себе они не видны. Точно так же коды выхода не будут отображаться в терминале, пока он их распознает. В противном случае они будут отображаться в буквальном виде, как если бы вы просматривали источник веб-сайта.

Как следует из названия, последовательность должна начинаться с непечатаемого символа[.kbd .key-escape]#Esc #, значение ASCII которого равно 27, иногда обозначается как0x1b в шестнадцатеричном виде или033 в восьмеричном. Вы можете использовать числовые литералы Python, чтобы быстро убедиться, что это действительно одно и то же число:

>>>

>>> 27 == 0x1b == 0o33
True

Кроме того, вы можете получить его с помощью escape-последовательности\e в оболочке:

$ echo -e "\e"

Наиболее распространенные escape-последовательности ANSI имеют следующую форму:

Элемент Описание пример

[.kbd .key-escape]#Esc #

непечатаемый escape-символ

\033

[

открывающая квадратная скобка

[

числовой код

одно или несколько чисел, разделенных;

0

код символа

прописные или строчные буквы

m

numeric code может быть одним или несколькими числами, разделенными точкой с запятой, аcharacter code - всего одной буквой. Их конкретное значение определяется стандартом ANSI. Например, чтобы сбросить все форматирование, вы должны ввести одну из следующих команд, в которых используется нулевой код и букваm:

$ echo -e "\e[0m"
$ echo -e "\x1b[0m"
$ echo -e "\033[0m"

На другом конце спектра у вас есть составные значения кода. Чтобы установить передний и задний план с каналами RGB, учитывая, что ваш терминал поддерживает 24-битную глубину, вы можете указать несколько номеров:

$ echo -e "\e[38;2;0;0;0m\e[48;2;255;255;255mBlack on white\e[0m"

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

В Python вы, вероятно, написали бы вспомогательную функцию, позволяющую упаковывать произвольные коды в последовательность:

>>>

>>> def esc(code):
...     return f'\033[{code}m'
...
>>> print(esc('31;1;4') + 'really' + esc(0) + ' important')

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

Text formatted with ANSI escape codes

Однако существуют абстракции более высокого уровня над escape-кодами ANSI, такие как упомянутая библиотекаcolorama, а также инструменты для создания пользовательских интерфейсов в консоли.

Интерфейс пользователя Building Console

Хотя игра с управляющими кодами ANSI, несомненно, доставляет массу удовольствия, в реальном мире вам бы хотелось иметь более абстрактные строительные блоки для создания пользовательского интерфейса. Есть несколько библиотек, которые обеспечивают такой высокий уровень контроля над терминалом, ноcurses кажется наиболее популярным выбором.

Note: Чтобы использовать библиотекуcurses в Windows, вам необходимо установить сторонний пакет:

C:\> pip install windows-curses

Это потому, чтоcurses недоступен в стандартной библиотеке дистрибутива Python для Windows.

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

Как насчет создания ретро игры змеи? Давайте создадим симулятор Python Snake:

The retro snake game built with curses library

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

import curses

def main(screen):
    pass

if __name__ == '__main__':
    curses.wrapper(main)

Обратите внимание: функция должна принимать ссылку на объект экрана, также известный какstdscr, который вы будете использовать позже для дополнительной настройки.

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

import time, curses

def main(screen):
    time.sleep(1)

if __name__ == '__main__':
    curses.wrapper(main)

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

import time, curses

def main(screen):
    curses.curs_set(0)  # Hide the cursor
    time.sleep(1)

if __name__ == '__main__':
    curses.wrapper(main)

Давайте определим змею как список точек в координатах экрана:

snake = [(0, i) for i in reversed(range(20))]

Голова змеи всегда является первым элементом в списке, а хвост - последним. Начальная форма змеи - горизонтальная, начиная с верхнего левого угла экрана и направляясь вправо. В то время как его координата y остается в нуле, его координата x уменьшается от головы до хвоста.

Чтобы нарисовать змею, вы начнете с головы, а затем продолжите с остальными сегментами. Каждый сегмент содержит координаты(y, x), поэтому вы можете их распаковать:

# Draw the snake
screen.addstr(*snake[0], '@')
for segment in snake[1:]:
    screen.addstr(*segment, '*')

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

import time, curses

def main(screen):
    curses.curs_set(0)  # Hide the cursor

    snake = [(0, i) for i in reversed(range(20))]

    # Draw the snake
    screen.addstr(*snake[0], '@')
    for segment in snake[1:]:
        screen.addstr(*segment, '*')

    screen.refresh()
    time.sleep(1)

if __name__ == '__main__':
    curses.wrapper(main)

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

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]

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

# Move the snake
snake.pop()
snake.insert(0, tuple(map(sum, zip(snake[0], direction))))

Чтобы получить новые координаты головы, вам нужно добавить к ней вектор направления. Однако добавление кортежей в Python приводит к большему кортежу вместо алгебраической суммы соответствующих компонент вектора. Один из способов исправить это - использовать встроенные функцииzip(),sum() иmap().

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

# Change direction on arrow keystroke
direction = directions.get(screen.getch(), direction)

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

def main(screen):
    curses.curs_set(0)    # Hide the cursor
    screen.nodelay(True)  # Don't block I/O calls

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

Наконец, это все, что вам нужно для игры в змею в Python:

import time, curses

def main(screen):
    curses.curs_set(0)    # Hide the cursor
    screen.nodelay(True)  # Don't block I/O calls

    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 in reversed(range(20))]

    while True:
        screen.erase()

        # Draw the snake
        screen.addstr(*snake[0], '@')
        for segment in snake[1:]:
            screen.addstr(*segment, '*')

        # Move the snake
        snake.pop()
        snake.insert(0, tuple(map(sum, zip(snake[0], direction))))

        # Change direction on arrow keystroke
        direction = directions.get(screen.getch(), direction)

        screen.refresh()
        time.sleep(0.1)

if __name__ == '__main__':
    curses.wrapper(main)

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

Жить с крутой анимацией

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

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

Если анимацию можно ограничить одной строкой текста, вас могут заинтересовать две специальные последовательности escape-символов:

  • Возврат каретки: \r

  • Backspace: \b

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

Давайте посмотрим на несколько примеров.

Вы часто хотите отображать какие-тоspinning wheel, чтобы указать на незавершенную работу, не зная точно, сколько времени осталось до завершения:

Indefinite animation in the terminal

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

from itertools import cycle
from time import sleep

for frame in cycle(r'-\|/-\|/'):
    print('\r', frame, sep='', end='', flush=True)
    sleep(0.2)

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

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

Progress bar animation in the terminal

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

from time import sleep

def progress(percent=0, width=30):
    left = width * percent // 100
    right = width - left
    print('\r[', '#' * left, ' ' * right, ']',
          f' {percent:.0f}%',
          sep='', end='', flush=True)

for i in range(101):
    progress(i)
    sleep(0.1)

Как и прежде, каждый запрос на обновление перерисовывает всю строку.

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

Звуки с принтом

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

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

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

$ echo -e "\a"

Обычно это печатает текст, но флаг-e позволяет интерпретировать экранирование обратной косой черты. Как видите, есть специальная escape-последовательность, что означает «предупреждение», которая выводит специальныйbell character. Некоторые терминалы издают звук всякий раз, когда видят его.

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

Правила следующие:

  • Буквы кодируются последовательностью символовdot (·) иdash (-).

  • Adot - одна единица времени.

  • Adash - это три единицы времени.

  • Отдельныеsymbols в письме отстоят друг от друга на единицу времени.

  • Символы двух соседнихletters разнесены на три единицы времени.

  • Символы двух соседнихwords разнесены на семь единиц времени.

Согласно этим правилам, вы можете «печатать» сигнал SOS бесконечно следующим образом:

while True:
    dot()
    symbol_space()
    dot()
    symbol_space()
    dot()
    letter_space()
    dash()
    symbol_space()
    dash()
    symbol_space()
    dash()
    letter_space()
    dot()
    symbol_space()
    dot()
    symbol_space()
    dot()
    word_space()

В Python вы можете реализовать его всего за десять строк кода:

from time import sleep

speed = 0.1

def signal(duration, symbol):
    sleep(duration)
    print(symbol, end='', flush=True)

dot = lambda: signal(speed, '·\a')
dash = lambda: signal(3*speed, '−\a')
symbol_space = lambda: signal(speed, '')
letter_space = lambda: signal(3*speed, '')
word_space = lambda: signal(7*speed, ' ')

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

Насмешливая печать Python в модульных тестах

В настоящее время ожидается, что вы отправляете код, отвечающий высоким стандартам качества. Если вы стремитесь стать профессионалом, вы должны изучитьhow to test свой код.

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

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

Итак, стоит ли вам тестироватьprint()? No. В конце концов, это встроенная функция, которая, должно быть, уже прошла полный набор тестов. Однако вы хотите проверить, вызывает ли ваш кодprint() в нужное время с ожидаемыми параметрами. Это известно какbehavior.

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

Note: Возможно, вы слышали термины:dummy,fake,stub,spy илиmock взаимозаменяемы. Некоторые люди делают различие между ними, а другие нет.

Мартин Фаулер объясняет их различия вshort glossary и в совокупности называет ихtest doubles.

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

def download(url, log=print):
    log(f'Downloading {url}')
    # ...

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

>>>

>>> def mock_print(message):
...     mock_print.last_message = message
...
>>> download('resource', mock_print)
>>> assert 'Downloading resource' == mock_print.last_message

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

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

>>>

>>> def download(url, stream=None):
...     print(f'Downloading {url}', file=stream)
...     # ...
...
>>> import io
>>> memory_buffer = io.StringIO()
>>> download('app.js', memory_buffer)
>>> download('style.css', memory_buffer)
>>> memory_buffer.getvalue()
'Downloading app.js\nDownloading style.css\n'

На этот раз функция явно вызываетprint(), но предоставляет свой параметрfile внешнему миру.

Однако более питонический способ имитации объектов использует преимущества встроенного модуляmock, который использует технику под названиемmonkey patching. Это уничижительное имя происходит от того, что он является «грязным хакером», с которым вы можете легко застрелиться в ногу. Это менее элегантно, чем внедрение зависимостей, но определенно быстро и удобно.

Note: Модульmock был поглощен стандартной библиотекой Python 3, но до этого это был сторонний пакет. Вы должны были установить его отдельно:

$ pip2 install mock

Кроме того, вы назвали егоmock, тогда как в Python 3 он является частью модуля модульного тестирования, поэтому вы должны импортировать его изunittest.mock.

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

Чтобы высмеятьprint() в тестовом примере, вы обычно используете@patchdecorator и указываете цель для исправления, ссылаясь на нее с полным именем, которое включает имя модуля :

from unittest.mock import patch

@patch('builtins.print')
def test_print(mock_print):
    print('not a real print')
    mock_print.assert_called_with('not a real print')

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

Вы заметили что-то особенное в этом фрагменте кода?

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

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

Вот что это значит:

from unittest.mock import patch

def greet(name):
    print(f'Hello, {name}!')

@patch('builtins.print')
def test_greet(mock_print):
    greet('John')
    mock_print.assert_called_with('Hello, John!')

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

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

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

Отладка печати

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

Note: Отладка - это процесс поиска первопричинbugs или дефектов в программном обеспечении после их обнаружения, а также принятие мер по их устранению.

Терминbug содержитamusing story о происхождении своего имени.

трассировка

Также известная какprint debugging илиcaveman debugging, это самая простая форма отладки. Хотя он немного старомодный, он все же мощный и имеет свое применение.

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

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

>>>

>>> def average(numbers):
...     print('debug1:', numbers)
...     if len(numbers) > 0:
...         print('debug2:', sum(numbers))
...         return sum(numbers) / len(numbers)
...
>>> 0.1 == average(3*[0.1])
debug1: [0.1, 0.1, 0.1]
debug2: 0.30000000000000004
False

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

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

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

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

Именно здесь сияетlogging.

логирование

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

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

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

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

Вот разбивка типичной записи журнала:

[2019-06-14 15:18:34,517][DEBUG][root][MainThread] Customer(id=123) logged out

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

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

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

Как правило, неправильно настроенное ведение журнала может привести к нехватке места на диске сервера. Чтобы предотвратить это, вы можете настроитьlog rotation, который будет хранить файлы журнала в течение определенного периода времени, например, одной недели или после того, как они достигнут определенного размера. Тем не менее, всегда полезно архивировать старые журналы. Некоторые нормативные акты предписывают хранить данные клиентов в течение пяти лет!

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

import logging
logging.basicConfig(level=logging.DEBUG)

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

logging.debug('hello')  # Module-level function

logger = logging.getLogger(__name__)
logger.debug('hello')   # Logger's method

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

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

Тем не менее, вы можете заставить их работать вместе, вызвавlogging.captureWarnings(True).

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

отладка

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

  • Шаг через код в интерактивном режиме.

  • Установите контрольные точки, включая условные контрольные точки.

  • Пересмотреть переменные в памяти.

  • Оцените пользовательские выражения во время выполнения.

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

Однако у него нет графического интерфейса, поэтомуusing pdb может быть немного сложным. Если вы не можете редактировать код, вы должны запустить его как модуль и передать местоположение вашего скрипта:

$ python -m pdb my_script.py

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

>>>

>>> import pdb
>>> pdb.set_trace()
--Return--
> (1)()->None
(Pdb)

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

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

import pdb; pdb.set_trace()

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

Начиная с Python 3.7, вы также можете вызвать встроенную функциюbreakpoint(), которая делает то же самое, но более компактным способом и с некоторыми дополнительнымиbells and whistles:

def average(numbers):
    if len(numbers) > 0:
        breakpoint()  # Python 3.7+
        return sum(numbers) / len(numbers)

Вы, вероятно, по большей части будете использовать визуальный отладчик, интегрированный с редактором кода. PyCharm имеет отличный отладчик, который может похвастаться высокой производительностью, но вы найдетеplenty of alternative IDEs с отладчиками, как платными, так и бесплатными.

Отладка не является пресловутой серебряной пулей. Иногда регистрация или отслеживание будет лучшим решением. Например, трудно воспроизводимые дефекты, такие какrace conditions, часто возникают в результате временной связи. Когда вы останавливаетесь на точке останова, эта небольшая пауза в выполнении программы может скрыть проблему. Это похоже наHeisenberg principle: нельзя одновременно измерять и наблюдать за ошибкой.

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

Потокобезопасная печать

Раньше я вкратце касался вопроса безопасности потоков, рекомендуяlogging вместо функцииprint(). Если вы все еще читаете это, значит, вас устраиваетthe concept of threads.

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

Другой метод используетlocal memory, что позволяет каждому потоку получать свою собственную копию того же объекта. Таким образом, другие потоки не могут видеть изменения, внесенные в текущий поток.

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

Однако блокировка стоит дорого и снижает одновременную пропускную способность, поэтому были изобретены другие средства управления доступом, такие какatomic variables или алгоритмcompare-and-swap.

Печать не является поточно-ориентированной в Python. Функцияprint() содержит ссылку на стандартный вывод, который является общей глобальной переменной. Теоретически из-за отсутствия блокировки во время вызоваsys.stdout.write() может произойти переключение контекста, переплетая фрагменты текста из нескольких вызововprint().

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

На практике, однако, этого не происходит. Как бы вы ни старались, запись в стандартный вывод кажется атомарной. Единственная проблема, с которой вы можете иногда столкнуться - это перепутанные разрывы строк:

[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() перейти в спящий режим на случайное время. How? Подшучивая над этим, о чем вы уже знаете из предыдущего раздела:

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

def slow_write(text):
    sleep(random())
    write(text)

def task():
    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 _ in range(3):
        Thread(target=task).start()

Во-первых, вам нужно сохранить исходный метод.write() в переменной, которую вы делегируете позже. Затем вы предоставляете ложную реализацию, выполнение которой займет до одной секунды. Каждый поток выполнит несколько вызововprint() со своим именем и буквой: A, B и C.

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

[
    '[Thread-3 A]',
    '[Thread-2 A]',
    '[Thread-1 A]',
    '\n',
    '\n',
    '[Thread-3 B]',
    (...)
]

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

Note: Атомарный характер стандартного вывода в Python является побочным продуктомGlobal Interpreter Lock, который применяет блокировку инструкций байт-кода. Имейте в виду, однако, что многие версии интерпретаторов не имеют GIL, где многопоточная печать требует явной блокировки.

Вы можете сделать символ новой строки неотъемлемой частью сообщения, обработав его вручную:

print(f'[{thread_name} {letter}]\n', end='')

Это исправит вывод:

[Thread-2 A]
[Thread-1 A]
[Thread-3 A]
[Thread-1 B]
[Thread-3 B]
[Thread-2 B]
[Thread-1 C]
[Thread-2 C]
[Thread-3 C]

Обратите внимание, однако, что функцияprint() по-прежнему выполняет отдельный вызов для пустого суффикса, что переводится в бесполезную инструкциюsys.stdout.write(''):

[
    '[Thread-2 A]\n',
    '[Thread-1 A]\n',
    '[Thread-3 A]\n',
    '',
    '',
    '',
    '[Thread-1 B]\n',
    (...)
]

По-настоящему потокобезопасная версия функцииprint() может выглядеть так:

import threading

lock = threading.Lock()

def thread_safe_print(*args, **kwargs):
    with lock:
        print(*args, **kwargs)

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

from thread_safe_print import thread_safe_print

def task():
    thread_name = current_thread().name
    for letter in 'ABC':
        thread_safe_print(f'[{thread_name} {letter}]')

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

[
    # Lock acquired by Thread-3
    '[Thread-3 A]',
    '\n',
    # Lock released by Thread-3
    # Lock acquired by Thread-1
    '[Thread-1 B]',
    '\n',
    # Lock released by Thread-1
    (...)
]

Я добавил комментарии, чтобы указать, как блокировка ограничивает доступ к общему ресурсу.

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

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

>>>

>>> import logging
>>> logging.basicConfig(format='%(threadName)s %(message)s')
>>> logging.error('hello')
MainThread hello

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

Python Print аналоги

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

Встроенный

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

>>>

>>> name = input('Enter your name: ')
Enter your name: jdoe
>>> print(name)
jdoe

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

try:
    age = int(input('How old are you? '))
except ValueError:
    pass

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

>>>

>>> x = input()
hello world
>>> print(x)
hello world

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

Note: Для чтения из стандартного ввода в Python 2 вы должны вместо этого вызватьraw_input(), который является еще одной встроенной функцией. К сожалению, существует функцияinput() с ошибочным названием, которая делает несколько иное.

Фактически, он также принимает входные данные из стандартного потока, но затем пытается оценить его, как если бы это был код Python. Поскольку это потенциальныйsecurity vulnerability, эта функция была полностью удалена из Python 3, аraw_input() был переименован вinput().

Вот быстрое сравнение доступных функций и того, что они делают:

Python 2 Python 3

raw_input()

input()

input()

eval(input())

Как вы можете сказать, все еще возможно моделировать старое поведение в Python 3.

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

>>>

>>> from getpass import getpass
>>> password = getpass()
Password:
>>> print(password)
s3cret

В модулеgetpass есть еще одна функция для получения имени пользователя из переменной окружения:

>>>

>>> from getpass import getuser
>>> getuser()
'jdoe'

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

Третья сторона

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

  • Расширенное форматирование и стилизация

  • Автоматический анализ, проверка и очистка пользовательских данных

  • Декларативный стиль определения макетов

  • Интерактивное автозаполнение

  • Поддержка мыши

  • Предопределенные виджеты, такие как контрольные списки или меню

  • История поиска набранных команд

  • Подсветка синтаксиса

Демонстрация таких инструментов выходит за рамки этой статьи, но вы можете попробовать их. Я лично узнал о некоторых из них черезPython Bytes Podcast. Вот они:

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

Предположим, вы написали интерфейс командной строки, который понимает три инструкции, в том числе одну для добавления чисел:

print('Type "help", "exit", "add a [b [c ...]]"')
while True:
    command, *arguments = input('~ ').split(' ')
    if len(command) > 0:
        if command.lower() == 'exit':
            break
        elif command.lower() == 'help':
            print('This is help.')
        elif command.lower() == 'add':
            print(sum(map(int, arguments)))
        else:
            print('Unknown command')

На первый взгляд, когда вы запускаете его, это выглядит как типичное приглашение:

$ python calculator.py
Type "help", "exit", "add a [b [c ...]]"
~ add 1 2 3 4
10
~ aad 2 3
Unknown command
~ exit
$

Но как только вы совершите ошибку и захотите ее исправить, вы увидите, что ни одна из функциональных клавиш не работает должным образом. Например, нажатие стрелки[.kbd .key-arrow-left]#Left # приводит к следующему результату вместо перемещения курсора назад:

$ python calculator.py
Type "help", "exit", "add a [b [c ...]]"
~ aad^[[D

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

$ rlwrap python calculator.py
Type "help", "exit", "add a [b [c ...]]"
(reverse-i-search)`a': add 1 2 3 4

Разве это не здорово?

Заключение

Теперь вы вооружены знаниями о функцииprint() в Python, а также о многих других темах. У вас есть глубокое понимание того, что это такое и как оно работает, включая все его ключевые элементы. Многочисленные примеры дали вам представление о его эволюции от Python 2.

Кроме того, вы научились:

  • Избегайте распространенных ошибок сprint() в Python

  • Работа с символами новой строки, кодировки символов и буферизации

  • Написать текст в файлы

  • Имитация функцииprint() в модульных тестах

  • Создавайте расширенные пользовательские интерфейсы в терминале

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

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