Pythonのコーディング力を引き上げるために、Effective Python Second Edition(英語版)を用いて学習しています。
(日本語版は訳が機械的でわかりづらいというレビューが多かったので、英語版を利用することにしました。)
今回は、第一章のPythonic Thinking (Item1~Item10まで)について、学習の備忘も兼ねて要点をメモしていきます。
個人的に解釈した内容を記述しているため、本の内容と厳密には一致しない表現が含まれる可能性があります。
Item1: Know which version of Python You're Using
pythonのバージョンを確認する方法は2種類ある。
--version
フラグによる確認
$ python --version
Python 2.7.16
$ python3 --version
Python 3.8.2
sys
モジュールによる確認
>>> import sys
>>> print(sys.version_info)
sys.version_info(major=3, minor=8, micro=2, releaselevel='final', serial=0)
>>> print(sys.version)
3.8.2 (default, Jun 8 2021, 11:59:35)
[Clang 12.0.5 (clang-1205.0.22.11)]
python2系は2020年1月1日にEOLを迎え、公式なメンテナンスが終了する。
Item2: Follow the PEP 8 Style Guide
PEP8のコード規約に従った一貫性のあるコードは、コードを扱いやすくしたり、可読性を向上させる。
他のプログラマーと協働しやすくなるだけでなく、自分自身が後でコードに変更を加えたり、エラーを回避するためにも役立つ。
空白
エディターのフォーマッターで自動整形できるので割愛
ネーミング
既知の内容だったため割愛
式と文
- 式全体を否定する
if not a is b
の代わりに、インライン否定if a is not b
を使う - 空のコンテナや文字列の判定を
if len(somelist) == 0
のように長さで判定せず、if not somelist
のように判定する - 空でないコンテナや文字列の判定も同様
- if文、forやwhileループ、exceptの複合文を一行で書かず、複数行に分ける
- if文の式が1行で収まらない場合、式全体を括弧で括り、改行とインデントを追加する
if (this_is_one_thing
and that_is_another_thing):
do_something()
インポート
- 現モジュールのパスからの相対名でインポート(例:
import foo
)するのではなく、常に絶対名(例:from bar import foo
)でインポートする - 相対インポートをしなければならない場合にも、明示的な構文を利用する(例:
from . import foo
) - 標準ライブラリモジュール、サードパーティモジュール、独自モジュールの順番でセクションを分けて、各セクション内ではアルファベット順で記述する
Item3: Know the Differences Between bytes and str
- bytesは8ビット値の列が含まれて、strはUnicode文字列を含み、それぞれのインスタンスは>, ==, +, %などの演算子で一緒に使えない
- 操作しているインプットが期待するシーケンスであること(8ビット値なのか、UTF8にエンコードされた文字列なのか、Unicodeなのか等)を保証するために、ヘルパー関数を利用する
- バイナリデータを読み書きする際には常にバイナリモード(読み
rb
、書きwb
)を使ってファイルを開く - Unicodeデータを読み書きする際にはシステムのデフォルトエンコーディングに注意し、意図しないエンコーディングを避けるために明示的に
encoding
パラメータを渡すことも検討する
Item4: Prefer Interpolated F-Strings Over C-style Format Strings and str.format
python3.6で追加された、interpolated format strings(f-strings)は、表現のしやすさ、簡潔さ、明快さの組み合わせによって、組み込みの選択肢の中でベストと言える。
F-strings, C-Style, str.formatの記述量の比較
それぞれを記述量の短い順に並べると以下のようになり、f_stringが最短となる。
key = 'my_var'
value = 1.234
f_string = f'{key:<10} = {value:.2f}'
c_tuple = '%-10s = %.2f' % (key, value)
str_args = '{:<10} = {:.2f}'.format(key, value)
str_kw = '{key:<10} = {value:.2f}'.format(key=key, value=value)
c_dict = '%(key)-10s = %(value).2f' % {'key': key, 'value': value}
assert f_string == c_dict == c_tuple == str_args == str_kw
表現のしやすさの比較
F-Stringsはプレースホルダー内に、Pythonの式を自由に入れることができる。
それにより、他の方法では複数行にまたがった記述になるところが、F-Stringsでは一行で収まる。
pantry = [
('avocados', 1.25),
('bananas', 2.5),
('cherries', 15),
]
for i, (item, count) in enumerate(pantry):
c_style = '#%d: %-10s = %d' % (
i + 1,
item.title(),
round(count)
)
format_style = '#{}: {:<10s} = {}'.format(
i + 1,
item.title(),
round(count)
)
f_string = f'#{i+1}: {item.title():<10s} = {round(count)}'
assert c_style == format_style == f_string
Item5: Write Helper Functions Instead of Complex Expressions
複雑な式、特に同じロジックを繰り返す必要がある場合には、ヘルパー関数に式を移すことを検討する。
可読性によって得られるものは、常に簡潔さによって得られるものよりも重要である。
例えば、以下のような要件の処理を行いたい場合、
- 値にリスト型のデータを持つ、辞書型のデータ(例:
my_values = {'red': ['5'], 'green': [''], 'blue': ['0']}
)からリストの一つ目の値を取得したい - 値がなかったり、空白の場合には0を返したい
- 常に整数であることを保証したい
1行で書く場合、以下のように記述することができる。
red = int(my_values.get('red', [''])[0] or 0)
または、複雑さを軽減するために、2行で次のように書くこともできる。
red_str = my_values.get('red', [''])
red = int(red_str[0]) if red_str[0] else 0
まだ読みにくいため、if/else条件式を利用して書き換えるとより明快になる。
red = my_values.get('red', [''])
if red[0]:
red = int(red[0])
else:
red = 0
そして、このようなロジックを繰り返し使う必要がある場合には、ヘルパー関数の利用を検討する。
def get_first_int(values, key, default=0):
found = values.get(key, [''])
if found[0]:
found = int(found[0])
else:
found = default
return found
get_first_int(my_values, 'red')
呼び出し時のコードは、1行や2行で書いたコードと比べてずっと明確になる。
Item6: Prefer Multiple Assignment Unpacking Over Indexing
pythonでは、リストやシーケンス、iterables内の複数レベルの任意のiterablesにおいて、複数の値を一行で代入すること(unpacking)ができる。
favorite_snacks = {
'salty': ('pretzels', 100),
'sweet': ('cookies', 180),
'veggie': ('carrots', 20),
}
((type1, (name1, cals1)),
(type2, (name2, cals2)),
(type3, (name3, cals3))) = favorite_snacks.items()
通常、一時的な変数を用意して順番に代入していく必要のあるindexの入れ替えもunpackingを利用すると一行で行える。
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', 'carrot', 'arugula', 'bacon']
bubble_sort(names)
print(names) # ['arugula', 'bacon', 'carrot', 'pretzels']
この方法は内包表記や、generatorの値を変数に代入する際にも利用できる。
Item7: Prefer enumerate Over range
組み込み関数enumerateは与えられたイテレーターから、インデックスと値のペアを生成する。
第二引数に最初のインデックスを指定することもできる。(デフォルトは0
)
flavor_list = ['vanilla', 'chocolate', 'pecan', 'strawberry']
for i, flavor in enumerate(flavor_list, 1):
print(f'{i}: {flavor}')
これにより、組み込み関数rangeにイテレーターの長さを渡してループを回すよりもずっと簡潔に書くことできる。
Item8: Use zip to Process Iterators in Parallel
組み込み関数zipを利用することで、同時に複数のイテレーターをループすることができる。
また、組み込み関数zipはtupleを生成するlazy generatorを生成するため、長いインプットにも対応できる。
names = ['Cecillia', 'Lise', 'Marie']
counts = [len(n) for n in names]
longest_name = None
max_count = 0
for name, count in zip(names, counts):
if count > max_count:
longest_name = name
max_count = count
print(longest_name, max_count) # Cecillia 8
ただし、zip関数の仕様上、ループするイテレーターの長さが異なる場合、短い方のイテレーターの長さ分でtupleの生成が終了し、長い方の残りのtupleは切り捨てられる。
長さの違うイテレーターで切り捨てせずにzip関数を利用したい場合は、組み込みモジュールのitertoolsのzip_longest
関数を利用する。
Item9: Avoid else Blocks After for and while Loops
if/elseやtry/except/elseの感覚で考えると、ループが完了しなければこれを実行しなさい
だと思うが、実際には逆である。
実際には、forやwhileループの後のelseは、ループ中にbreak文が実行されなかった場合
にのみ実行される。
また、空のリストやwhile False
でループを回しても、elseブロックは実行される。
for x in []:
print('Never runs')
else:
print('For Else block!') # For Else block!
while False:
print('Never runs')
else:
print('For Else block!') # For Else block!
振る舞いが直感的でなく誤解を生みやすいので、利用を避けるべきである。
elseを利用せずに、同様のことを実行したい場合、helper関数を利用する。
Item10: Prevent Repetition with Assignment Expressions
python3.8で導入された:=
(walrus operator)を使えば、式の途中結果を名前にひもづける(≒代入する)ことができる。
:=
がセイウチのように見えるためこのような名前が付けられており、a := b
を(a walrus b)と読む。
以下のような、条件式に利用した値を条件式内でも利用したい場合などに、繰り返しを減らし可読性を向上させることができる。
def make_lemonade(count):
...
def out_of_stock():
...
fresh_fruit = {
'apple': 10,
'banana': 8,
'lemon': 5,
}
if count := fresh_fruit.get('lemon', 0):
make_lemonade(count)
else:
out_of_stock()
walrus operatorは代入をしてから、評価を行うという順序で処理される。
そのため、例えば if (count := fresh_fruit.get('apple', 0)) >= 4:
のような式の場合には、左側の式を括弧で囲む必要がある。
また、pythonでは、switch/case文が利用できないため、以下のようなif/else文をを利用した深いネストを記述することがよくある。
count = fresh_fruit.get('banana', 0)
if count >= 2:
pieces = slice_bananas(count)
to_enjoy = make_smoothies(pieces)
else:
count = fresh_fruit.get('lemon', 0)
if count:
to_enjoy = make_lemonade(count)
else:
to_enjoy = 'Nothing'
このような場合にもwarlus operatorを利用することでネストを浅くすることができる。
if (count := fresh_fruit.get('banana', 0)) >= 2:
pieces = slice_bananas(count)
to_enjoy = make_smoothies(pieces)
elif (count := fresh_fruit.get('apple', 0)) >= 4:
to_enjoy = make_cider(count)
elif count := fresh_fruit.get('lemon', 0):
to_enjoy = make_lemonade(count)
else:
to_enjoy = 'Nothing'
また、同様にpythonではdo/while文が利用できないため、くどい記述を余儀なくされることがあるが、このような場合にも上手にwarlus operatorを活用することで可読性を向上させることができる。