pythonicスタイルは、コンパイラによって統制されているわけでも強制されているわけでもありません。
pythonプログラマは、明示すること、複雑さよりは単純さを選ぶこと、可読性を最大化することを好む。
全ての人がPythonでもっとも普通のことを最良に、Pythonicに行う方法を知っておくことが重要です。そのパターンは、読者が各全てのプログラムに影響します。
使用するPythonのバージョンを知っておく
今回はPython3(Python3.7及び、Python3.8)を使用し、Python2は扱いません。
多くのコンピュータには、標準CPythonランタイムのsうくせいのバージョンがプリインストールされています。しかし、コマンドラインでpythonと入力した時のデフォルトは決まっていません。
Pythonのバージョンを正確に知るには'--version'フラグを使用する。
$ python --version
Python 2.7.10
$ python3 --version
Python 3.8.0
Python3 は、Pythonのコアデベロッパーとコミュニティでしっかり保守されており、常に改善されています。Pythonのよく使われるOSSライブラリのほとんどがPython3準拠となっています。すべてのPythonプロジェクトでPython3 を使用することを強く薦めます。
(Python2は、2020年4月で使用期限を迎えました。)
PEP 8スタイルガイドに従う
PEP8として知られるPython拡張提案はPythonのコードをどのようにフォーマットするかのスタイルガイドです。Pythonのコードは、正しい構文である限りは、好きなように書いていいのです。しかし、、一貫したスタイルに従うと、コードがより扱いやすく、より読みやすくなります。より大きなコミュニティで他のPythonプログラマと共通のスタイルを分かち合うことで、プロジェクトでの協働作業が捗ります。
PEP 8には、明確なPythonコードをどのように書けば良いのか詳細な説明が豊富にあります。
空白
Pythonでは、空白が構文上位身を持ちます。Pythonプログラマは、コードが明白であるために。空白の効果とその影響に特に気をつけます。
- インデントには、タブではなく空白を使う。
- 構文上意味を持つレベルのインデントには、4つの空白を使う。
- 各行は、長さ79文字かそれ以下とする
- 長い式を続けるために次の行を使うときは、通常のインデントから4個の追加空白を使ってインデントする。
- ファイルでは、関数とクラスは、空白2行で分ける
- クラスでは、メソッドは、空白行で分ける
- 辞書では、キーとコロンの間には空白をおかず、同じ行に値を各倍には値の前に空白を1つおく
- 変数代入前後には、空白を1つ、必ず1つだけを置く。
- 型ヒント(型アノテーション)では、変数名の直後にコロンを置き、型情報の前に空白を1つ置く。
名前付け
PEP8は、言語の異なる箇所ごとに他と異なるスタイルを推奨しています。これは、コードを読む時に、名前がどの種類に対応しているか区別しやすくします。
- 関数、変数、属性は、lowercase_underscoreのように小文字で_を挟む
- プロテクテッド(保護)属性は、_leading_underscoreのようにアンダースコアを先頭につける。
- プライベート属性は、__double_underscoreのようにアンダースコアを2つ先頭につける。
- クラスと例外は、CapitalizedWordのように先頭を大文字にする。
- モジュールでの定数は、All_CAPSのように全て大文字でアンダースコアを挟む
- クラスメソッドは、第一パラメータの名前にclsを使う。
式と文
- 式の否定(if not a is b)ではなく、内側の項の否定(if a is not b)を使う。
- コンテナやシーケンスの長さ(if len(somelist) == 0)を使って、空値([],''など)かどうかのチェックはしない。if not somelistを使って、空値が暗黙にFalseと評価されることを使う。
- if文、forループ,whileのループ、except複合文を1行で描かない、明確になるように複数行にする。
- \で行分けするよりは、括弧を使って複数の式を囲む方がいい。
Cスタイルフォーマット文字列とstr.formatは使わずf文字列で埋め込む
文字列はPythonコードベースのあらゆるところで使われます。Pythonには、言語と標準ライブラリに組み込まれた4種類の異なるフォーマット方式があります。1種類を除いては、深刻な欠点があるのでそれを理解し避ける必要があります。
####フォーマット演算子%
Pythonでもっと重よく使われるフォーマット方式です。
a = 0b10111011
b = 0xc5f
print('Binary is %d, hex is %d' % (a, b))
>>>
Binary is 187, hex is 3167
フォーマット演算子では、%dのようなフォーマット指定をプレースホルダにして、フォーマットの右側の値で置き換えます。フォーマット指定の構文は、Pythonや他の言語が継承しているC言語のprintf関数に由来します。
このフォーマットには4つの問題点があります。
- 第1の問題点
フォーマット式の右側のタプルにあるデータ値の型や順序が変わると型変換エラーが怒ることです。
key = 'my_var'
value = 1.234
formatted = '%-10s = %.2f' % (key, value)
print(formatted)
>>>
my_var = 1.23
これは問題ないですが、keyとvalueを入れ替えると実行時例外が発生します。
recordered_string = '%-10s = %.2f' % (value, key)
>>>
Traceback ...
TypeError: ...
右側のパラメータの順序はそのままにして、フォーマット文字列の順序を入れ替えても同じエラーが生じます。
この問題を避けるには%演算子の両側がきちんと揃っているか常にチェックする必要がある。これは変更があるたびにいちいちチェックしないといけないので、エラーが生じやすいです。
- 第2の問題点
文字列にフォーマットする前に値を少し修正しないといけない場合に、読んで理解するのが難しい。
pantry = [
('avocados', 1.25),
('bananas', 2.5),
('cherries', 15),]
for i, (item, count) in enumerate(pandry):
print('#%d: %-10s = $.2f % (i, item, count)')
>>>
#0: avocados = 1.25
#1: bananas = 2.5
#2: cherries = 15
出力メッセージがもう少し役立つ値にしようと少し変更を加えると、長くなり複数行にまたがってしまい読みやすさが損なわれる。
print('#%d: %-10s = $.2f % (i + 1, item.title(), round(count))')
- 第3の問題点
フォーマット文字列で同じ値を複数回使う場合には、右側のタプルで繰り返し書かないといけない。
template = '%s lobes food. See %s cook.'
name = 'max'
formatted = template % (name, name)
print(formatted)
>>>
Max loves food. See Max cook.
フォーマットする値にちょっとした修正を行う場合に、面倒でエラーになりやすい。
- 第4の問題点
フォーマット式で辞書を使うと、冗長でうるさい感じになる。
soup = 'lentil'
formatted = 'Today\'s soup is %{soup}s.' % {'soup': soup}
print(formatted)
>>>
Today's soup is lentil.
文字が重複するだけでなく、この重複により辞書を使うフォーマット式が長くなってしまう。
formatとstr.format
- format
a = 1234.5678
formatted = format(a, ',.2f')
print(formatted)
>>>
1,234.57
- str.format
key = 'may_var'
value = 1.234
formatted = '{} = {}'.format(key, value)
print(formatted)
>>>
my_var = 1.234
formatメソッドに渡される引数の位置インデックスを指定し、プレースホルダを使用することにより、第1の問題点と第3の問題点が解消します。
# 第1の問題点
formatted = '{1} = {0}.format(key, value)'
print(formatted)
>>>
1.234 = my_var
# 第3の問題点
formatted = '{0} loves food. See {0} cook.'.format(name)
print(formatted)
>>>
Max loves food. See Max cook.
第2の問題点は解消せず、読みやすさにはあまり変わりはない。
第4の問題点も繰り返しキーの冗長性は解消できません。
# 第2の問題点
formatted = '#{}: {:<10s} = {}'.format(i+1, item.title, round(count))
# 第4の問題点
_format= ('Today\'s soup is {soup}, '
'buy one get two {oyster} oysters, '
'and our special entree is {special}.')
formatted = _format.format(
soup='lentil',
oyster='kumamoto',
special='schnitzel',)
フォーマット済み文字列
Python3.6は、フォーマット済み文字列、略してf文字列を導入して上の問題を解消します。フォーマット文字列の先頭にfを付けますが、バイト文字列にb、生文字列にrをつけるのと同じです。
key = 'my_var'
value = 1.234
formatted = f'{key} = {value}'
print(formatted)
>>>
my_var = 1.234
f文字列のフォーマットでは上記の記述よりも文字数が少なくなります。
第2の問題点も解消できます。
formatted = f'#{i+1}: {item.title():<10s} = {round(count)}'
ヘルパー関数
Pythonの簡潔な構文を使えば、多数のロジックを実装した1行の式に簡単に書くことができます。
例えばURLのクエリ文字列を複合したいとする。クエリ文字列引数は、次のように整数値で表されるとします。
my_values = parse_qs('red=5&blue=0&green=',
keep_blank_values=True)
print(repr(my_values))
>>>
{'red': ['5], 'green': [''], 'blue': ['0']}
これをgetメソッドを使い、状況ごとに異なる値を返します。
print('Red: ', my_values.get('red'))
print('Green: ', my_values.get('red'))
print('Opacity: ', my_values.get('red'))
>>>
Red: ['5']
Green: ['']
Opacity: None
引数がないか空白の場合はデフォルト地を0にする方が良さそうです。これは、if分野ヘルパー関数を使うほどのロジックではないので、論理式で行うように選択します。
red = my_values.get('red', [''])[0] or 0
green = my_values.get('green', [''])[0] or 0
opacity = my_values.get('opacity', [''])[0] or 0
print(f'Red: {red!r}')
print(f'Green: {green!r}')
print(f'Opacity: {opacity!r}')
>>>
Red: '5'
Green: 0
Opacity: 0
- Redの場合は、値は、1要素、文字列'5'のリストで、文字列はTrueと評価されるのでredにはor式の最初の部分が代入
- Greenの場合は、1要素、空文字列のリストで、Falseと評価されるので、0が代入
- Opacityの場合は、1要素、から文字のリストで、Falseと評価されるので、0が代入
上記を組み込み関数intでラップして文字列を整数としてパースします。
red = int(my_values.get('red', [''])[0] or 0)
こうすると非常に読みにくいものになります。これを理解するのに分解して理解する必要が出てきて、時間がかかります。
そこで、Pythonでは三項式でより明確にすることが可能です。
red_str = my_values.get('red', [''])
red = int(red_str[0]) if red_str[0] else 0
これは複数行にまたがる完全なif/elseぶんほどには明確ではありません。また、このロジックを何回か使う必要があるなら、ヘルパー関数を書くことが良いでしょう。
def get_first_int(values, key, default=0):
found = values.get(key, [''])
if found[0]:
return int(found[0])
else:
return default
green = get_first_int(my_values, 'green')
これでより明確に書くことができます。
式が複雑になってきたら、より小さな部分に分けてロジックをヘルパー関数にうつすことを考える時期です。読みやすさで得られる利益は常に、簡潔さがもたらす便益を上回ります。
複数代入アンパック
item = ('Peanut butter), 'Jelly')
# インデックスでアクセス
first = item
second = item[1]
print(first, 'and', second)
# アンパック
first, second = item
print(first, 'and', second)
>>>
Peanut butter and Jelly
Peanut butter and Jelly
アンパックは、インデックスでタプルにアクセスする場合よりも見た目がスッキリして、行数が少なくなります。
ここで、照準のバブルソートアルゴリズムにおいて、listないの値をスワップしてみます。
def bubble_sort(a):
for _ in range(len(a)):
for i in range(1, len(a)):
if a[i] < a[i-1]:
a[i-1], a[i] = a[i], a[i-1]
names = ['pretzels', 'carrots', 'arugula', 'bacon']
bubble_sort(names)
print(names)
>>>
['arugula', 'bacon', 'carrots', 'pretzels']
このスワップ動作では、まず、代入演算子の右側のa[i], a[i-1]が評価され、値が一時的な無名タプルに格納されます。次に左側のアンパックパターンを使って、そのタプル値を取り出してそれぞれ変数a[i-1], a[i]に代入します。最後に一時的な無名tupleが破壊されます。
rangeではなくenumerateを使う
組み込み関数rangeは、整数集合上でループする繰り返し処理に役立ちます。
for i in range(10):
print(i)
>>>
0123456789
リストの処理で、リスト中の要素のインデックスが必要なこともよくあります。
flavor_list = ['vanilla', 'chocolate', 'pecan', 'strawberry']
for i in range(len(flavor_list)):
flavor = flavor_list[i]
print(f'{i+1}: {flavor}')
>>>
1: vanilla
2: chocolate
3: pecan
4: strawberry
これはリストの長さが必要で、配列のようにインデックスを使う必要があります。
Pythonには、このような状況に対する組み込み関数enumerateがあります。enumerateは、遅延評価ジェネレータでイテレータをラップします。enumerateはループのインデックスとイテレータの次の値をyieldします。
it = enumerate(flavor_list)
print(next(it))
print(next(it))
# アンパックでの処理
for i, flavor in enumerate(flavor_list):
print(f'{i+1}: {flavor}')
>>>
(0, 'vanilla')
(1, 'chocolate')
0, vanilla
1, chocolate
2: pecan
3: strawberry
イテレータを並列処理するにはzipを使う
文字のリストとそれの長さのリストの両方のリストを並列で処理する場合は以下の処理をおこなう。
names = ['cecilia', 'Lisa', 'Marie']
counts = [len(n) for n in names]
longest_name = None
max_count = 0
for i in enumerate(names):
count = counts[i]
if count > max_count:
longest_name = name
max_count = count
これらはnamesとcountsのインデックスでコードが読みにくくなっています。このようなコードをもっと明確にするために、Pythonには組み込み関数zipがあります。zipは2つ以上のイテレータを遅延評価ジェネレータでラップします。
for name, count in zip(names, counts):
if count > max_count:
longest_name = name
max_count = count
zipはラップしているイテレータで要素を1つずつ処理します。したがって、無限に長い入力でもメモリを使いすぎてクラッシュする危険性はない。
zipの振る舞いとして、注意する点があります。以下のようにnamesに要素を追加して、countsの更新を忘れたとします。これは予期せぬ結果となります。
names.append('Bob')
for name, count in zip(names, counts):
print(name)
>>>
Cecilia
Lise
Marie
追加した'Bob'がありません。これはzip処理の対象となるリストの笠が同じでないために起きています。
そのため、リストの長さが同じかどうか確信が保ていない場合は、組み込みモジュールitertoolのzip_longest関数を使用することを検討しましょう。
import itertools
for name, count in itertools.zip_longest(names, counts):
print(f'{name}: {count}')
>>>
Cecilia: 7
Lise: 4
Marie: 5
Bob: 3
代入式で繰り返しを防ぐ
代入式は、walrus演算子で示されるPython3.8の新構文で、重複に関するPython言語の問題を歌唱します。
- 通常の代入文: a = b
- 代入式 : a := b
代入式は、if文の条件し昨日ようなこれまで代入ぶんが許されなかった箇所で変数に代入できるので便利です。
ある処理を行いたいときに、個数がゼロかどうかif文でチェックします。
fresh_fruit = {
'apple': 10,
'banana': 8,
'lemon': 5,}
count = fresh_fruit.get('lemon', 0)
if count:
処理A
else:
処理B
この単純なコードの問題点は、読みにくいことです。countをif文の前に定義することで、elseブロックを含めてそれ以降で全コードで変数countを使用するように見えますが、実際はそんなことありません。このように、値のチェックするコードパターンは、Pythonで非常によくあります。これをwalrus演算子を使用して描き直していきます。
if count := fresh_fruit.get('lemon', 0):
処理A
else:
処理B
これで、countがif文の第1ブロックにしか関係しないことが明らかなのではるかに読みやすくなっています。代入式は、まず変数countに値を代入し、次にその値をif文のコンテキストで評価して、フロー制御をどうするか決定します。
また、ある数以上の場合'処理A'を実行したい場合は、以下のようにする。
if (count := fresh_fruit.get('lemon', 0)) >= 3:
処理A
else:
処理B
そのほかにもPythonにはswitch/case構文がありませんが、それもこれを使って以下のように書くことができます。
if (count := fresh_fruit.get('banana', 0)) >= 2:
処理A
elif (count := fresh_fruit.get('apple', 0)) >= 4:
処理B
elif (count := fresh_fruit.get('lemon', 0)) >= 3:
処理C
else:
処理D