Python印刷機能のガイド

Python印刷機能のガイド

私を含むほとんどのPythonユーザーと同じように、print()について学ぶことからPythonの旅を始めたと思います。 それはあなたがあなた自身のhello worldワンライナーを書くのを助けました。 これを使用して、フォーマットされたメッセージを画面に表示し、おそらくいくつかのバグを見つけることができます。 しかし、Pythonのprint()関数について知っておくべきことはこれだけだと思う​​なら、多くのことを見逃していることになります。

読み続けて、この一見退屈で評価されていない小さな機能を最大限に活用してください。 このチュートリアルでは、Pythonprint()を効果的に使用する方法を学びます。 ただし、セクションを進めながら、深く掘り下げる準備をしてください。 print()がどれだけ提供されているかに驚かれるかもしれません!

このチュートリアルを終了すると、次の方法がわかります。

  • Pythonのprint()でよくある間違いを避けてください

  • 改行、文字エンコード、およびバッファリングに対処する

  • テキストをファイルに書き込む

  • ユニットテストでprint()をモックします

  • ターミナルで高度なユーザーインターフェイスを構築する

あなたが完全な初心者であれば、このチュートリアルの最初の部分を読むことから最も恩恵を受けるでしょう。これは、Pythonでの印刷の本質を説明しています。 それ以外の場合は、その部分をスキップして、必要に応じて自由にジャンプしてください。

Note:print()はPython 3への主要な追加であり、Python 2で使用可能な古いprintステートメントに取って代わりました。

すぐにわかるように、それには多くの正当な理由がありました。 このチュートリアルはPython 3に焦点を当てていますが、参考のためにPythonでの古い印刷方法を示しています。

Free Bonus:Click here to get our free Python Cheat Sheetは、データ型、辞書、リスト、Python関数の操作など、Python3の基本を示しています。

簡単に印刷する

Pythonでの印刷の実際の例をいくつか見てみましょう。 このセクションの終わりまでに、print()を呼び出すためのすべての可能な方法を理解できます。 または、プログラマーの用語では、function signatureに精通していると言えます。

印刷を呼び出す

Pythonprint()を使用する最も簡単な例では、数回のキーストロークが必要です。

>>>

>>> print()

引数は渡しませんが、最後に空の括弧を付ける必要があります。これにより、Pythonは名前で参照するのではなく、実際にexecute the functionを指定します。

これにより、目に見えない改行文字が生成され、画面に空白行が表示されます。 このようにprint()を複数回呼び出して、垂直方向のスペースを追加できます。 ワードプロセッサのキーボードで[.kbd .key-enter]#Enter#を押しているのと同じです。

ご覧のとおり、引数なしでprint()を呼び出すと、blank lineになります。これは、改行文字のみで構成される行です。 これをempty lineと混同しないでください。これには、改行も含めて、文字がまったく含まれていません。

Pythonのstringリテラルを使用して、次の2つを視覚化できます。

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

最初の文字は1文字の長さですが、2番目の文字にはコンテンツがありません。

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でメッセージをフォーマットする方法はたくさんあります。 Python 3.6で導入されたf-stringsを確認することを強くお勧めします。これは、それらすべての中で最も簡潔な構文を提供するためです。

>>>

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

さらに、f-stringsは、連結されたオペランドをキャストすることを忘れるというよくある間違いを防ぐことができます。 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()が引数なしで呼び出されて空白行が生成され、次に単一の引数で呼び出されて固定メッセージまたはフォーマットされたメッセージが表示されます。

ただし、この関数は、0、1、または複数の引数を含む、任意の数のpositional argumentsを受け入れることができることがわかりました。 これは、いくつかの要素を結合したい一般的なメッセージフォーマットの場合に非常に便利です。

この例を見てみましょう。

>>>

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

print()は、渡された4つの引数すべてを連結し、それらの間に1つのスペースを挿入して、'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()は4つの名前付きまたは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:リストまたはタプルの要素の結合には注意してください。

手動で行うと、要素の少なくとも1つが文字列でない場合、既知の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()を呼び出すことと実質的に同じです。

もう1つの興味深い例は、データをcomma-separated values(CSV)形式にエクスポートすることです。

>>>

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

これは、コンマを正しくエスケープするなどのエッジケースを正しく処理しませんが、単純なユースケースでは処理する必要があります。 上記の行は、ターミナルウィンドウに表示されます。 ファイルに保存するには、出力をリダイレクトする必要があります。 このセクションの後半では、print()を使用してPythonから直接ファイルにテキストを書き込む方法を説明します。

最後に、sepパラメータは1文字だけに制限されていません。 任意の長さの文字列で要素を結合できます。

>>>

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

次のサブセクションでは、print()関数の残りのキーワード引数について説明します。

改行の防止

print()への後続の呼び出しが同じ行で続行されるように、メッセージを末尾の改行で終了したくない場合があります。 古典的な例には、長時間実行される操作の進行状況の更新や、ユーザーに入力を求めるプロンプトが含​​まれます。 後者の場合、ユーザーは同じ行に回答を入力する必要があります。

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

多くのプログラミング言語は、標準ライブラリを介してprint()と同様の関数を公開していますが、改行を追加するかどうかを決定できます。 たとえば、JavaとC#には、2つの異なる関数がありますが、他の言語では、文字列リテラルの最後に を明示的に追加する必要があります。

そのような言語の構文の例をいくつか示します。

言語

Perl

print "hello world\n"

C

printf("hello world\n");

C++

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

対照的に、Pythonのprint()関数は、ほとんどの場合、それが必要なため、要求せずに常に を追加します。 これを無効にするには、さらに別のキーワード引数endを利用して、行の終わりを指定します。

セマンティクスに関しては、endパラメータは前に見たsepパラメータとほぼ同じです。

  • 文字列またはNoneである必要があります。

  • 任意に長くすることができます。

  • デフォルト値は' 'です。

  • Noneに等しい場合、デフォルト値と同じ効果があります。

  • 空の文字列('')と等しい場合、改行は抑制されます。

これで、引数なしでprint()を呼び出したときに、内部で何が起こっているかがわかりました。 関数に位置引数を指定しないので、結合するものは何もないので、デフォルトのセパレータはまったく使用されません。 ただし、デフォルト値のendが引き続き適用され、空白行が表示されます。

Note:オペレーティングシステムで意味のあるものではなく、endパラメータのデフォルト値が固定されているのはなぜか疑問に思われるかもしれません。

print()は変換を自動的に処理するため、印刷時に異なるオペレーティングシステム間での改行表現について心配する必要はありません。 文字列リテラルでは常に のエスケープシーケンスを使用することを忘れないでください。

これは現在、Pythonで改行文字を印刷する最も移植性の高い方法です。

>>>

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

たとえば、LinuxマシンでWindows固有の改行文字を強制的に印刷しようとすると、出力が壊れてしまいます。

>>>

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


line3

反対に、open()を使用して読み取るためにファイルを開く場合、改行表現についても気にする必要はありません。 この関数は、検出したシステム固有の改行をユニバーサル' 'に変換します。 同時に、本当に必要な場合は、入力と出力の両方で改行を処理する方法を制御できます。

改行を無効にするには、endキーワード引数を使用して空の文字列を指定する必要があります。

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

これらは2つの別々のprint()呼び出しであり、長時間離れて実行される可能性がありますが、最終的には1行しか表示されません。 まず、次のようになります。

Checking file integrity...

ただし、print()を2回呼び出した後、次のように同じ行が画面に表示されます。

Checking file integrity...ok

sepと同様に、endを使用して、カスタム区切り文字を使用して個々の部分をテキストの大きなブロブに結合できます。 ただし、複数の引数を結合する代わりに、各関数呼び出しからのテキストを同じ行に追加します。

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

これらの3つの命令は、1行のテキストを出力します。

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

2つのキーワード引数を混在させることができます。

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

テキストが1行だけでなく、すべてのアイテムがコンマで区切られています。

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

テキストの各行の後に2つの改行があります。 この記事で前述したように、行を印刷する前に、そのうちの1つを削除する必要があります。

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に委任するだけの便利なインターフェイスを提供します。 ストリームは、ディスク上の任意のファイル、ネットワークソケット、またはおそらくメモリ内バッファです。

これに加えて、オペレーティングシステムによって提供される3つの標準ストリームがあります。

  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()はそのfile引数を介してsys.stdoutにバインドされますが、これは変更できます。 そのキーワード引数を使用して、書き込みモードまたは追加モードで開いていたファイルを示し、メッセージがそのファイルに直接送られるようにします。

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

これにより、ASCIIの65および10の10進値に対応する大文字のAと改行文字が出力されます。 ただし、バイトリテラルの16進表記を使用してエンコードされます。

print()character encodingを制御できないことに注意してください。 受信したUnicode文字列をバイトに正しくエンコードするのはストリームの責任です。 ほとんどの場合、デフォルトのUTF-8が必要なため、自分でエンコードを設定することはありません。 本当に必要な場合は、おそらくレガシーシステムの場合、open()encoding引数を使用できます。

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()にキーワード引数が1つだけ残ります。これについては、次のサブセクションで説明します。 おそらく、それらの中で最も使用頻度が低いでしょう。 それにもかかわらず、絶対に必要な場合があります。

印刷呼び出しのバッファリング

前のサブセクションでは、print()sys.stdoutなどのファイルのようなオブジェクトに印刷を委任することを学びました。 ただし、一部のストリームは、パフォーマンスを向上させるために特定のI / O操作をバッファリングします。 例を見てみましょう。

カウントダウンタイマーを作成していて、毎秒同じ行に残り時間を追加していると想像してください。

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変数がゼロより大きい限り、コードは末尾の改行なしでテキストを追加し続け、その後1秒間スリープ状態になります。 最後に、カウントダウンが終了すると、Go!が出力され、行が終了します。

予想外に、プログラムは毎秒カウントダウンする代わりに、3秒間無駄にアイドル状態になり、その後突然行全体を一度に印刷します。

Terminal with buffered output

これは、この場合、オペレーティングシステムが標準出力への後続の書き込みをバッファリングするためです。 バッファリングに関して3種類のストリームがあることを知っておく必要があります。

  1. バッファなし

  2. ラインバッファリング

  3. ブロックバッファ

Unbufferedは自明です。つまり、バッファリングは行われず、すべての書き込みが即座に有効になります。 line-bufferedストリームは、I / O呼び出しを開始する前に、バッファのどこかに改行が表示されるまで待機しますが、block-bufferedストリームは、内容に関係なく、バッファが特定のサイズまでいっぱいになるのを許可します。 標準出力は、どちらのイベントが最初に来るかに応じて、line-bufferedblock-bufferedの両方です。

バッファリングは、高価なI / O呼び出しの数を減らすのに役立ちます。 たとえば、待ち時間の長いネットワークを介してメッセージを送信することを検討してください。 リモートサーバーに接続してSSHプロトコルを介してコマンドを実行すると、各キーストロークが実際に個々のデータパケットを生成する場合があります。これは、ペイロードよりも桁違いに大きくなります。 なんてオーバーヘッド! 少なくともいくつかの文字が入力されるまで待ってから、それらを一緒に送信することは理にかなっています。 そこでバッファリングが開始されます。

一方、カウントダウンの例で見たように、バッファリングは望ましくない効果をもたらすことがあります。 これを修正するには、flushフラグを使用して、バッファ内の改行文字を待たずにストリームを強制的にフラッシュするようにprint()に指示するだけです。

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クラスは、カスタマイズ可能な2つの属性を持つ特殊な種類のnamedtupleになりました。

Note: Python 3では、passステートメントをellipsis...)リテラルに置き換えて、プレースホルダーを示すことができます。

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

これにより、インデントされたコードブロックが欠落しているためにインタープリターがIndentationErrorを上げるのを防ぎます。

これは、単純なnamedtupleよりも優れています。これは、無料で正しく印刷できるだけでなく、カスタムメソッドやプロパティをクラスに追加できるためです。 ただし、1つの問題を解決しながら別の問題を導入します。 名前付きタプルを含むタプルは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の構文は、Python3.6で定義されています。

前のサブセクションから、print()が組み込みのstr()関数を暗黙的に呼び出して、その位置引数を文字列に変換することをすでに知っています。 実際、通常のPersonクラスのインスタンスに対してstr()を手動で呼び出すと、それを出力するのと同じ結果が得られます。

>>>

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

次に、str()は、通常実装するクラス本体内の2つのmagic methodsの1つを探します。 見つからない場合は、見苦しいデフォルト表示に戻ります。 これらの魔法のメソッドは、検索の順番です:

  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を理解する

この時点でhowprint()を使用することはよく知っていますが、whatがわかっていると、さらに効果的かつ意識的に使用できるようになります。 このセクションを読むと、長年にわたってPythonでの印刷がどのように改善されたかを理解できます。

印刷はPython 3の関数です

print()はPython3の関数であることがわかりました。 具体的には、組み込み関数であるため、どこからでもインポートする必要はありません。

>>>

>>> 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プログラミング言語と同じように、2つの個別の印刷機能ができました。 後でmocking sectionでもカスタムprint()関数を定義します。 また、関数でない場合、そもそもprint()を上書きできないことに注意してください。

一方、print()は、暗黙のNone以外の意味のある値を返さないため、数学的な意味での関数ではありません。

>>>

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

そのような関数は、実際には、何らかの副作用を達成するために呼び出すプロシージャまたはサブルーチンであり、最終的にはグローバルな状態の変化です。 print()の場合、その副作用は、標準出力にメッセージを表示するか、ファイルに書き込むことです。

print()は関数であるため、既知の属性を持つ明確に定義された署名があります。 特定のタスクを実行するための奇妙な構文を覚えていなくても、選択したエディターを使用してそのdocumentationをすばやく見つけることができます。

その上、関数はextendに簡単です。 関数に新しい機能を追加することは、別のキーワード引数を追加するのと同じくらい簡単ですが、その新しい機能をサポートするように言語を変更することははるかに面倒です。 たとえば、ストリームのリダイレクトやバッファのフラッシュを考えてください。

print()が関数であることのもう1つの利点は、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:dependencyは、別のコードビットに必要なコードです。

Dependency injectionは、コード設計で使用される手法であり、テスト可能、再利用可能、および拡張用にオープンになっています。 抽象インターフェースを介して間接的に依存関係を参照し、それらをpullの方法ではなくpushの方法で提供することにより、これを実現できます。

インターネット上で循環している依存性注入の面白い説明があります。

5歳児の依存注射

自分で冷蔵庫から物を取り出すと、問題が発生する可能性があります。 ドアを開けたままにしたり、ママやパパが欲しくないものを手に入れたりするかもしれません。 私たちが持っていないものや期限切れのものを探しているかもしれません。

あなたがしなければならないことは、「昼食時に何か飲む必要がある」というニーズを述べることです。そして、私たちはあなたが座って食事をするときに何かがあることを確認します。

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 statementsconditional expressionsの両方があります。 後者は、変数に割り当てられるか、関数に渡される単一の値に評価されます。 上記の例では、Noneと評価される値ではなく副作用に関心があるため、単に無視します。

ご覧のとおり、関数を使用すると、他の言語と一貫性のあるエレガントで拡張可能なソリューションが可能になります。 次のサブセクションでは、関数としてprint()を使用しないと、多くの頭痛の種が発生することを発見します。

印刷はPython 2のステートメントでした

statementは、実行時に副作用を引き起こす可能性があるが、値に評価されない命令です。 つまり、ステートメントを出力したり、次のような変数に割り当てることはできません。

result = print 'hello world'

これはPython 2の構文エラーです。

Pythonのステートメントのいくつかの例を次に示します。

  • 割り当て: =

  • 条件付き: if

  • ループ: while

  • assertionassert

Note: Python 3.8は、物議を醸しているwalrus operator:=)をもたらします。これはassignment expressionです。 これにより、式を評価し、別の式内であっても同時に結果を変数に割り当てることができます!

高価な関数を1回呼び出して、さらに計算するために結果を再利用するこの例を見てください。

# 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の立場から辞任しました。

ステートメントは通常、言語で固定された意味を持つifforprintなどの予約済みキーワードで構成されます。 変数や他のシンボルに名前を付けることはできません。 そのため、Python 2ではprintステートメントを再定義またはモックすることはできません。 あなたが得るものにこだわっています。

さらに、ステートメントはラムダ式で受け入れられないため、匿名関数から印刷することはできません。

>>>

>>> 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に戻って、Python2の構文の詳細な説明を探すことができます。

ただし、これらの問題のいくつかは、はるかに簡単なアプローチで軽減できます。 Python 3への移行を容易にするために、print()関数がバックポートされたことがわかりました。 特別な__future__モジュールからインポートできます。このモジュールは、以降のPythonバージョンでリリースされた言語機能の選択を公開します。

Note:将来の関数だけでなく、withステートメントなどの組み込み言語構造もインポートできます。

使用可能な機能を正確に調べるには、モジュールを調べます。

>>>

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

dir(__future__)を呼び出すこともできますが、それはモジュールの多くの興味のない内部の詳細を示します。

Python 2でprint()関数を有効にするには、ソースコードの先頭に次のインポートステートメントを追加する必要があります。

from __future__ import print_function

今後、printステートメントは使用できなくなりますが、print()関数を自由に使用できます。 flushキーワード引数がないため、Python 3の関数と同じ関数ではないことに注意してください。ただし、残りの引数は同じです。

それ以外は、文字エンコーディングを適切に管理することからあなたをspareしみません。

Python 2でprint()関数を呼び出す例を次に示します。

>>>

>>> 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モジュールが付属しています。これは、1行に収まらない大きなデータ構造をきれいに印刷するのに役立ちます。 より人間に優しい方法で印刷するため、JupyterLab and IPythonを含む多くの一般的なREPLツールは、通常のprint()関数の代わりにデフォルトで使用します。

Note: IPythonでプリティプリントを切り替えるには、次のコマンドを発行します。

>>>

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

これは、IPythonのMagicの例です。 パーセント記号(%)で始まる組み込みコマンドはたくさんありますが、PyPIで詳細を確認したり、独自のコマンドを作成したりすることもできます。

元のprint()関数にアクセスできないことを気にしない場合は、インポートの名前変更を使用して、コード内でpprint()に置き換えることができます。

>>>

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

個人的には、両方の関数をすぐに使えるようにしたいので、ppのようなものを短いエイリアスとして使用したいと思います。

from pprint import pprint as pp

一見したところ、2つの機能にほとんど違いはありません。場合によっては、ほとんど何もありません。

>>>

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

これは、pprint()が型キャストの通常のstr()の代わりにrepr()を呼び出すため、必要に応じてその出力を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()は、自己参照オブジェクトの一意のIDを含めることで、より明確になります。

>>>

>>> 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エスケープシーケンスを使用した色の追加

パーソナルコンピューターがより高度になると、グラフィックスが向上し、より多くの色を表示できるようになりました。 ただし、さまざまなベンダーが、それを制御するためのAPI設計について独自の考えを持っていました。 数十年前、米国規格協会の人々がANSI escape codesを定義することによってそれを統合することを決定したとき、それは変わりました。

今日の端末エミュレータのほとんどは、ある程度この標準をサポートしています。 最近まで、Windowsオペレーティングシステムは注目に値する例外でした。 したがって、最高の移植性が必要な場合は、Pythonのcoloramaライブラリを使用してください。 ANSIコードをWindowsの適切な対応するコードに変換し、他のオペレーティングシステムではそのままにします。

たとえば、色に関連するANSIエスケープシーケンスのサブセットを端末が理解しているかどうかを確認するには、次のコマンドを使用してみてください。

$ tput colors

Linuxのデフォルトのターミナルでは、256の異なる色を表示できますが、xtermでは8色しか表示されません。 色がサポートされていない場合、コマンドは負の数を返します。

ANSIエスケープシーケンスは、端末のマークアップ言語のようなものです。 HTMLでは、<b><i>などのタグを使用して、ドキュメント内の要素の外観を変更します。 これらのタグはコンテンツと混在していますが、それら自体は表示されません。 同様に、エスケープコードが認識される限り、エスケープコードは端末に表示されません。 それ以外の場合は、ウェブサイトのソースを表示しているかのように、リテラル形式で表示されます。

その名前が示すように、シーケンスは印刷不可能な[.kbd .key-escape]#Esc#文字で始まる必要があります。ASCII値は27で、16進数の0x1bまたは8進数の033として示されることもあります。 Pythonの数値リテラルを使用して、実際に同じ数値であることをすばやく確認できます。

>>>

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

さらに、シェルの\eエスケープシーケンスを使用して取得できます。

$ echo -e "\e"

最も一般的なANSIエスケープシーケンスの形式は次のとおりです。

素子 説明

[.kbd .key-escape]#Esc

印刷不可能なエスケープ文字

\033

[

角括弧を開く

[

数値コード

;で区切られた1つ以上の数値

0

文字コード

大文字または小文字

m

numeric codeは、セミコロンで区切った1つ以上の数字にすることができますが、character codeは1文字だけです。 それらの特定の意味は、ANSI標準によって定義されています。 たとえば、すべてのフォーマットをリセットするには、次のコマンドのいずれかを入力します。これらのコマンドは、コード0と文字mを使用します。

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

スペクトルのもう一方の端には、複合コード値があります。 端末が24ビット深度をサポートしている場合、RGBチャンネルで前景と背景を設定するには、複数の数字を指定できます。

$ 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

ただし、前述のcoloramaライブラリや、コンソールでユーザーインターフェイスを構築するためのツールなど、ANSIエスケープコードには高レベルの抽象化があります。

コンソールユーザーインターフェイスの構築

ANSIエスケープコードで遊ぶのは間違いなく楽しいですが、現実の世界では、ユーザーインターフェイスをまとめるためのより抽象的な構成要素が必要です。 端末に対してこのような高レベルの制御を提供するライブラリはいくつかありますが、cursesが最も一般的な選択肢のようです。

Note: Windowsでcursesライブラリを使用するには、サードパーティのパッケージをインストールする必要があります。

C:\> pip install windows-curses

これは、cursesがWindows用のPythonディストリビューションの標準ライブラリで利用できないためです。

主に、テキストの塊ではなく、独立したグラフィカルウィジェットの観点から考えることができます。 それに、空のキャンバスを描くようなものだから、内なるアーティストを表現するのに多くの自由を得ることができます。 ライブラリは、異なる端末を処理する複雑さを隠します。 それ以外に、キーボードゲームのサポートが優れており、ビデオゲームの作成に役立つ場合があります。

レトロなヘビゲームを作ってみませんか? Pythonスネークシミュレーターを作成しましょう。

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)

ベクトルとして定義できる4つの方向のいずれかでヘビを移動します。 最終的に、方向は矢印キーストロークに応じて変化するため、ライブラリのキーコードに接続できます。

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でタプルを追加すると、対応するベクトルコンポーネントの代数和ではなく、より大きなタプルが作成されます。 これを修正する1つの方法は、組み込みの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

ほぼ完了しましたが、最後の1つだけが残っています。 このコードをループすると、ヘビは移動する代わりに成長しているように見えます。 これは、各反復の前に画面を明示的に消去する必要があるためです。

最後に、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モジュールが開く可能性のほんの一部にすぎません。 このような、またはよりビジネス指向のアプリケーションのようなゲーム開発に使用できます。

クールなアニメーションで生活する

アニメーションは、ユーザーインターフェイスをより魅力的にするだけでなく、全体的なユーザーエクスペリエンスも向上させます。 たとえば、ユーザーに早期のフィードバックを提供すると、ユーザーはプログラムがまだ機能しているかどうか、またはプログラムを強制終了するときかどうかを知ることができます。

ターミナルでテキストをアニメーション化するには、カーソルを自由に動かせる必要があります。 これは、前述のツールの1つ、つまりANSIエスケープコードまたはcursesライブラリを使用して実行できます。 ただし、さらに簡単な方法をお見せしたいと思います。

アニメーションを1行のテキストに制限できる場合は、2つの特別なエスケープ文字シーケンスに関心があるかもしれません。

  • キャリッジリターン: \r

  • バックスペース: \b

最初のカーソルはカーソルを行の先頭に移動し、2番目のカーソルはカーソルを1文字だけ左に移動します。 両方とも、既に書かれているテキストを上書きすることなく、非破壊的に機能します。

いくつかの例を見てみましょう。

終了までの残り時間を正確に知らなくても、進行中の作業を示すために、ある種の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ライブラリと、他のいくつかの同様のツールがあり、はるかに包括的な方法で進捗状況を表示できます。

印刷で音を出す

PCスピーカーを搭載したコンピューターを覚えるのに十分な年齢の場合は、ハードウェアの問題を示すためによく使用される独特のbeepサウンドも覚えておく必要があります。 彼らはそれ以上かろうじて音を立てることはできませんでしたが、それでもビデオゲームは非常に良く見えました。

今日でもこの小さなスピーカーを利用できますが、ラップトップに付属していない可能性があります。 このような場合、シェルでterminal bellエミュレーションを有効にして、代わりにシステム警告音を鳴らすことができます。

先に進み、このコマンドを入力して、端末がサウンドを再生できるかどうかを確認します。

$ echo -e "\a"

これは通常テキストを出力しますが、-eフラグはバックスラッシュエスケープの解釈を有効にします。 ご覧のとおり、特別なbell characterを出力する、「アラート」を表す専用のエスケープシーケンスがあります。 一部の端末では、表示されるたびに音が鳴ります。

同様に、この文字をPythonで印刷できます。 おそらくループで何らかのメロディーを形成します。 音符は1つだけですが、連続するインスタンス間で一時停止の長さを変更できます。 それはモールス信号の再生に最適なおもちゃのようです!

ルールは次のとおりです。

  • 文字は、一連のdot(・)およびdash( - )記号でエンコードされます。

  • dotは1単位時間です。

  • dashは3単位の時間です。

  • 文字内の個々のsymbolsは、1単位時間間隔で配置されます。

  • 2つの隣接するlettersのシンボルは、3単位の時間間隔で配置されます。

  • 2つの隣接するwordsのシンボルは、7単位の時間間隔で配置されます。

これらのルールによれば、次の方法で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では、わずか10行のコードで実装できます。

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 Printのモック

現在、高品質の基準を満たすコードを出荷することが期待されています。 あなたがプロになることを熱望するならば、あなたはあなたのコードをhow to test学ぶ必要があります。

ソフトウェアテストは、明らかな間違いについて警告するコンパイラを持たないPythonなどの動的型付け言語では特に重要です。 欠陥は、本番環境に到達し、コードのブランチが最終的に実行されるその日まで、長い間休止状態になります。

確かに、linterstype checkers、および静的コード分析用の他のツールがあります。 しかし、あなたのプログラムがビジネスレベルで行うべきことを実行しているかどうかはわかりません。

では、print()をテストする必要がありますか? No. 結局のところ、これは組み込みの機能であり、包括的なテストスイートを既に通過している必要があります。 ただし、テストしたいのは、コードが期待されるパラメーターを使用して適切なタイミングでprint()を呼び出しているかどうかです。 これはbehaviorとして知られています。

mockingの実際のオブジェクトまたは関数によって動作をテストできます。 この場合、print()をモックして、その呼び出しを記録および検証する必要があります。

Note:次の用語を聞いたことがあるかもしれません:dummyfakestubspy、またはmockは同じ意味で使用されます。 区別する人もいれば、区別しない人もいます。

Martin Fowlerは、それらの違いをshort glossaryで説明し、まとめてtest doublesと呼びます。

Pythonでのモックは2つあります。 まず、依存性注入を使用することにより、静的に型付けされた言語の従来のパスを使用できます。 これにより、テスト中のコードを変更することが必要になる場合があります。コードが外部ライブラリで定義されている場合、常に変更できるとは限りません

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パラメータを外部に公開します。

ただし、オブジェクトをモックするよりPython的な方法では、monkey patchingと呼ばれる手法を使用する組み込みのmockモジュールを利用します。 この軽rog的な名前は、「ダーティハック」であることから生じます。 依存性注入ほどエレガントではありませんが、間違いなく迅速で便利です。

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()を呼び出していると思われますが、実際には、完全に制御しているモックを呼び出しています。

ソフトウェアをテストする理由はたくさんあります。 それらの1つはバグを探しています。 テストを作成するとき、たとえば、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()呼び出しを注意深く削除することを忘れないでください。

また、コードを変更する必要がありますが、常に変更できるとは限りません。 リモートWebサーバーで実行されているアプリケーションをデバッグしている場合や、問題をpost-mortemの方法で診断したい場合があります。 単に標準出力にアクセスできない場合があります。

それこそがloggingが光るところです。

ロギング

eコマースウェブサイトを運営しているふりをしてみましょう。 ある日、怒った顧客が、失敗した取引について不平を言って電話をかけ、お金を失ったと言います。 彼はいくつかのアイテムを購入しようとしたと主張しますが、結局、その注文を完了するのを妨げるいくつかの不可解なエラーがありました。 しかし、彼が銀行口座を確認したとき、お金はなくなっていました。

誠実に謝罪し、払い戻しを行いますが、これが今後再び発生することを望みません。 どのようにデバッグしますか? 出来事の痕跡があれば、理想的にはイベントの時系列のリストとそのコンテキストの形で。

印刷デバッグを実行していることに気づいたときはいつでも、永続的なログメッセージに変換することを検討してください。 これは、アクセスできない環境で問題が発生した後に分析する必要がある場合に、このような状況で役立つ場合があります。

ログの集約と検索のための洗練されたツールがありますが、最も基本的なレベルでは、ログをテキストファイルと考えることができます。 各行は、システム内のイベントに関する詳細情報を伝えます。 通常、個人を特定する情報は含まれませんが、場合によっては法律で義務付けられることがあります。

一般的なログレコードの内訳は次のとおりです。

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

ご覧のとおり、構造化された形式です。 説明的なメッセージの他に、イベントのコンテキストを提供するカスタマイズ可能なフィールドがいくつかあります。 ここには、正確な日時、ログレベル、ロガー名、およびスレッド名があります。

ログレベルを使用すると、メッセージをすばやくフィルタリングしてノイズを減らすことができます。 エラーを探している場合、たとえば、すべての警告やデバッグメッセージを表示する必要はありません。 設定を介して特定のログレベルでメッセージを無効にしたり有効にしたりするのは、コードに触れることなく簡単です。

ロギングを使用すると、デバッグメッセージを標準出力から分離できます。 すべてのログメッセージはデフォルトで標準エラーストリームに送られますが、これはさまざまな色で便利に表示できます。 ただし、個々のモジュールであっても、ログメッセージを個別のファイルにリダイレクトできます!

よくあることですが、ロギングの設定が間違っていると、サーバーのディスクのスペースが不足する可能性があります。 これを防ぐために、log rotationを設定すると、ログファイルが1週間などの指定された期間、または特定のサイズに達したときに保持されます。 それにもかかわらず、古いログをアーカイブすることは常に良い習慣です。 一部の規制では、顧客データを5年間保持することを強制しています!

loggingモジュールは標準ライブラリにバンドルされているため、他のプログラミング言語と比較して、logging in Pythonは単純です。 わずか2行のコードでインポートして設定するだけです。

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()関数からロギングに切り替える最後の理由はスレッドセーフです。 次のセクションでは、前者は複数の実行スレッドでうまく機能しないことがわかります。

デバッグ

真実は、トレースもロギングも実際のデバッグとはみなされないということです。 実際のデバッグを行うには、以下を実行できるデバッガーツールが必要です。

  • 対話形式でコードをステップ実行します。

  • 条件付きブレークポイントを含むブレークポイントを設定します。

  • メモリ内の変数を内省します。

  • 実行時にカスタム式を評価します。

ターミナルで実行される大まかなデバッガーは、当然のことながら「Pythonデバッガー」のpdbという名前で、標準ライブラリの一部として配布されています。 これにより、常に利用可能になるため、リモートデバッグを実行するための唯一の選択肢になる可能性があります。 おそらく、それがそれに慣れる正当な理由でしょう。

ただし、グラフィカルインターフェイスが付属していないため、using pdbは少し注意が必要な場合があります。 コードを編集できない場合は、モジュールとして実行し、スクリプトの場所を渡す必要があります。

$ python -m pdb my_script.py

それ以外の場合は、コードにブレークポイントを直接設定できます。これにより、スクリプトの実行が一時停止され、デバッガーにドロップされます。 これを行う古い方法では、2つの手順が必要でした。

>>>

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

これは対話型のプロンプトを表示しますが、最初は恐ろしく見えるかもしれません。 ただし、この時点でネイティブPythonを入力して、ローカル変数の状態を調べたり変更したりできます。 それとは別に、コードのステップ実行に使用したいデバッガ固有のコマンドはほんの一握りしかありません。

Note:デバッガーを起動するための2つの命令を1行に配置するのが通例です。 これにはセミコロンを使用する必要がありますが、これは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のようなものです。バグの測定と観察を同時に行うことはできません。

これらの方法は相互に排他的ではありません。 彼らはお互いを補完します。

スレッドセーフ印刷

以前、スレッドセーフの問題について簡単に触れ、print()関数よりもloggingを推奨しました。 これをまだ読んでいる場合は、the concept of threadsに慣れている必要があります。

スレッドセーフとは、複数の実行スレッド間でコードを安全に共有できることを意味します。 スレッドセーフを確保するための最も簡単な戦略は、immutableオブジェクトのみを共有することです。 スレッドがオブジェクトの状態を変更できない場合、その一貫性を損なうリスクはありません。

別のメソッドはlocal memoryを利用します。これにより、各スレッドは同じオブジェクトの独自のコピーを受け取ります。 これにより、他のスレッドは、現在のスレッドで行われた変更を見ることができません。

しかし、それで問題は解決しませんか? 多くの場合、共有リソースを変更できるようにすることで、スレッドが協力することを望みます。 このようなリソースへの同時アクセスを同期する最も一般的な方法は、lockingによるものです。 これにより、一度に1つまたは場合によってはいくつかのスレッドへの排他的な書き込みアクセスが許可されます。

ただし、ロックはコストがかかり、同時スループットが低下するため、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? 前のセクションですでに知っているように、それをearlier笑することで:

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()メソッドを変数に格納する必要があります。これは、後で委任します。 次に、偽の実装を提供します。これには、実行に最大1秒かかります。 各スレッドは、名前と文字(A、B、およびC)を使用してprint()呼び出しを数回行います。

以前にモックセクションを読んだ場合、印刷がそのように誤動作する理由がすでにわかっているかもしれません。 それでも、明確にするために、slow_write()関数に入力された値をキャプチャできます。 毎回わずかに異なるシーケンスが表示されます。

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

sys.stdout.write()自体はアトミック操作ですが、print()関数を1回呼び出すと、複数の書き込みが発生する可能性があります。 たとえば、改行は残りのテキストとは別に書き込まれ、それらの書き込み間でコンテキストの切り替えが行われます。

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()リクエストごとに2回の書き込みを行っていますが、ストリームとの対話は1つのスレッドのみに許可されており、残りは待機する必要があります。

[
    # 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()関数を常に使用したくないもう1つの理由です。

Python Print Counterparts

これで、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を通してそれらのいくつかについて知るようになりました。 どうぞ:

それでも、Pythonスクリプトに強力な行編集機能を無料で追加するrlwrapというコマンドラインツールについて言及する価値があります。 動作するために何もする必要はありません!

数字を追加するための手順を含む3つの手順を理解するコマンドラインインターフェイスを作成したと仮定します。

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

それは素晴らしいことではないですか?

結論

これで、Pythonのprint()関数に関する一連の知識と、周辺の多くのトピックについての知識が身に付きました。 あなたはそれが何であり、どのように機能するのか、そのすべての重要な要素を深く理解しています。 数多くの例から、Python 2からの進化についての洞察が得られました。

それとは別に、次の方法を学びました。

  • Pythonのprint()でよくある間違いを避けてください

  • 改行、文字エンコーディング、バッファリングに対処する

  • テキストをファイルに書き込む

  • 単体テストでprint()関数をモックします

  • ターミナルで高度なユーザーインターフェイスを構築する

これがすべてわかったので、ユーザーと通信したり、一般的なファイル形式でデータを生成したりする対話型プログラムを作成できます。 コード内の問題を迅速に診断し、それらから保護することができます。 最後になりましたが、古典的なスネークゲームを実装する方法を知っています。

まだ情報が不足している、質問がある、または単に考えを共有したい場合は、下のコメントセクションでお気軽にお問い合わせください。