0
0

More than 1 year has passed since last update.

Effective Pythonまとめ 1. Pythonic Thinking (Item1~Item10)

Last updated at Posted at 2022-02-21

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を活用することで可読性を向上させることができる。


参考文献
Effective Python: 90 Specific Ways to Write Better Python (Effective Software Development Series) (English Edition)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0