ネストしたコンテキストマネジャーを簡潔な記述、分かりやすくなったエラーメッセージ、EncodingWarningクラスを取り上げる。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
2021年10月4日にPython 3.10がリリースされた。主要な新機能や変更点をかいつまんでまとめると以下のようになる(詳しくは「What's New In Python 3.10」を参照されたい)。
前回はこれらのうちの構造的パターンマッチについて見た。今回は、その他の項目の中から幾つかを紹介する。
Python 3.8まではコンテキストマネジャーをネストさせる(例えば、複数のファイルを同時に開くような)ときには、次のどちらかのような書き方をする必要があった。
# with文をネストさせる
with open('foo.txt') as foo:
with open('bar.txt') as bar:
print(foo.read())
print(bar.read())
# with文のコンテキスト式をカンマ区切りで並べる
with open('foo.txt') as foo, open('bar.txt') as bar:
print(foo.read())
print(bar.read())
「with」に続けて記述するコンテキスト式は上の2つ目の例のように、カンマで区切って複数並べられるが、それらを複数行に分割して書くことはサポートされていなかった。そのため、コンテキスト式がもっと多くなれば(あるいは上の例ならファイル名がもっと長ければ)、1行が非常に長くなってしまう(PEP 8では1行の文字数は79文字までとなっている)。
上記の2つ目のwith文でコンテキスト式を複数行に分割して記述したければ、明示的な行継続を使って次のようにも書ける。
with open('foo.txt') as foo,\
open('bar.txt') as bar:
print(foo.read())
print(bar.read())
こうすれば行を分割して継続できるが、バックスラッシュがあるのが煩わしいという方は多いはずだ。ここでPythonではかっこ「()」などで囲まれている部分(関数の引数など)は複数行に分割して記述できるので(非明示的な行継続)、with文でコンテキスト式をかっこで囲めばよさそうだが、このような書き方は、Python 3.8までの古いパーサーの制限でできなかった。
with (open('foo.txt') as foo,
open('bar.txt') as bar):
print(foo.read())
print(bar.read())
Python 3.9では新しいパーサーが採用された。そのメリットの1つが上記のような記述が可能になったことだ。
Python 3.9では古いパーサーも残されていたが、「What's New In Python 3.9」に「In Python 3.10, the old parser will be deleted and so will all functionality that depends on it」(Python 3.10では古いパーサーは取り除かれ、古いパーサーに依存する機能も全て取り除かれることになる)とある通り、Python 3.10では以前のパーサーが削除されている。
古いパーサーが取り除かれたからか、Pythonのドキュメントではwith文の記述も変わっている(Python 3.9のもの、Python 3.10のもの)。Python 3.10のwith文の説明では最後に「Support for using grouping parentheses to break the statement in multiple lines」(グループ化のかっこを使って、with文を複数行に分割することをサポート)ともあることから、上で見たようなかっこを使ってコンテキスト式を複数行に分割して記述することが、Python 3.10から公式にサポートされたと考えられる。
Python 3.10ではそれまでのバージョンのPythonよりもエラーメッセージが分かりやすいものになっている。
例えば、if文で節の最後にコロン「:」を付け忘れたとしよう。
num = 100
if num == 100
print(f'{num} is 100')
このコードをREPL環境で実行すると、Python 3.9では次のような結果になる。
エラーが2つ発生しているが、ここでは範囲選択をして反転表示されている部分に注目されたい。「SyntaxError: invalid syntax」というのだから、構文が間違っていることは分かる。が、具体的にどこが間違っているかまでは分からない。一方、Python 3.10のREPL環境で実行した結果は次のようになる。
こちらは「SyntaxError: expected ':'」とコロンがないことを教えてくれる。
もう1つ、[0, 1, 2]と['foo', 'bar', 'baz']という2つのリストがあり、0と'foo'、1と'bar'、2と'baz'のように、同じインデックス位置にあるものを要素とするタプルを作り、それをリストに格納したいとしよう。
以下はこれを行おうとして書いたバグ入りのコードだ。
int_list = [0, 1, 2]
str_list = ['foo', 'bar', 'baz']
result = [item0, item1 for item0, item1 in zip(int_list, str_list)]
先ほどと同じく、Python 3.9のREPL環境で実行した結果から示そう。
「SyntaxError: invalid syntax」では意味がよく分からない。一方、Python 3.10の場合は次のようになる。
こちらは「SyntaxError: did you forget parentheses around the comprehension target?」(内包表記のターゲットをかっこで囲むのを忘れていませんか?)とあるので、どこでやらかしたかが分かる。つまり、先のコードは以下のようにするのが正解だ。
int_list = [0, 1, 2]
str_list = ['foo', 'bar', 'baz']
result = [(item0, item1) for item0, item1 in zip(int_list, str_list)]
print(result) # [(0, 'foo'), (1, 'bar'), (2, 'baz')]
タプルを囲むかっこは省略できるので、いつものように省略していたのだが、内包表記のターゲットとして記述するときには省略はできなかったということだ。
ここでもう1つ注意したい点がある。上の画像(Python 3.10のREPL環境での実行画面)を見ると、エラーが発生した箇所を示す「^」が「item0, item1」全体を示すようになっている点だ。以前なら「^」が1つだけ表示されていたのが、Python 3.10からはエラーの対象となる部分全体を示してくれるようになっている。
この他にもエラーメッセージに対してなされた改善点は多数ある。詳細は「Better error messages」を参照してほしい。
Pythonでは、デフォルトのエンコーディングは基本的にプラットフォームとロケールに依存している。LinuxやmacOSであれば、多くの場合はUTF-8が使われるだろうし、日本語のWindows環境ではシフトJISがデフォルトのエンコーディングとなっている。
そのため、open関数でファイルをオープンする場合などに、encoding引数にエンコーディングを指定しないと、プラットフォーム間で問題が生じることがある。
with open('test.txt', 'w') as f:
f.write('がんばれ日本代表!')
このコードをmacOSで実行して作成できたテキストファイルをWindowsで読み込もうとして、やはりエンコーディングを指定せずに次のようなコードを書いたとする。
with open('test.txt') as f:
print(f.read())
実行すると、エンコーディングが異なることから次のように例外が発生する(Windowsで書き込み、macOSで読み込みでも同じ問題が発生するはずだ)。
このように、open関数(またはioモジュールのTextIOWrapperクラスなど)でencoding引数を省略することが問題の種となることがある。そこで、encoding引数の指定がないままテキストファイルを読み書きしようとしたときには、そのことを知らせるEncodingWarning警告クラスがPython 3.10では導入された。
ただし、これはpythonコマンドの起動時に「-X warn_default_encoding」オプションを指定するか、「PYTHONWARNDEFAULTENCODING =1」「set PYTHONWARNDEFAULTENCODING=1」などとして環境変数PYTHONWARNDEFAULTENCODINGの値を1にセットしたときにだけ有効となる。
例えば、Windowsのコマンドプロンプトで上記の書き込みを行うコードを「-X warn_default_encoding」オプションを指定して実行すると、次のようになる。
EncodingWarningが有効な環境で、この警告が出ないようにするには、open関数でencoding引数を指定することだ。上のコードなら、例えば次のようになるだろう。
# 書き込み
with open('test.txt', 'w', encoding='utf-8') as f:
f.write('がんばれ日本代表!')
# 読み込み
with open('test.txt', encoding='utf-8') as f:
print(f.read())
Python 3.10では、PCのロケールをそのまま使用するのであれば「encoding='locale'」と指定できるようにもなっている。この場合もEncodingWarningは発生しない。
# 書き込み
with open('test.txt', 'w', encoding='locale') as f:
f.write('がんばれ日本代表!')
# 読み込み
with open('test.txt', encoding='locale') as f:
print(f.read())
これは結局のところ、デフォルトのエンコーディングを使うわけだが、プログラマーがそのことを明示的に指定している点が異なる。何となくデフォルトのエンコーディングを使うのではなく、明確な意図があってデフォルトのエンコーディングを使うのだと、Python(やそのコードを読むプログラマー)に伝えているということだ。
なお、ioモジュールのドキュメントにある「Text Encoding」では「while there is no concrete plan as of yet, Python may change the default text file encoding to UTF-8 in the future」(はっきりとしたプランはまだないが、Pythonは将来、テキストファイルのデフォルトエンコーディングをUTF-8に変更するかもしれない)とある。こうしたことからも、(全てのOSプラットフォームでUTF-8がデフォルトのエンコーディングではないので)テキストファイルを扱う際にはエンコーディングを明示することが推奨される。
次回は型ヒントに関連するPython 3.10の新機能について見ていく予定だ。
Copyright© Digital Advantage Corp. All Rights Reserved.