sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

Python - PEP20 The Zen of Python

PEP20 について学習しました。

The Zen of Python は、下記のように pythonプログラマーに向けた心構え・イディオムをまとめたようなもの。Zen はそのまま "禅" みたい。

ただ著者がこのタイトルを直接つけたという感じではなさそうで Strunk & White "Elements of Style" みたいな姿勢くらい?

The Zen Of Python: A Most In Depth Article

この PEP を執筆したのは「Tim Peters」さん。古参の優れた python 貢献者でした。近年は(トラブルの多い)python の理事会から BAN を受けたりもしていて、python コミュニティの複雑な一面を感じたりもしました。

Abstract(概要)

Long time Pythoneer Tim Peters succinctly channels the BDFL’s guiding principles for Python’s design into 20 aphorisms, only 19 of which have been written down.

長年の Pythoneer である Tim Peters は、Python の設計に関する BDFL の指針を 20 の格言に簡潔にまとめていますが、そのうち 19 のみが文書化されています。(のこりの1つは、ジョーク。最初から存在していない)

NOTE: Mysteriously, only 19 of the guidelines are written down. Guido van Rosum reportedly said that the missing 20th aphorism is “some bizarre Tim Peters in-joke.” - The Invent with Python Blog

BDFL = 「Benevolent Dictator For Life」のこと。つまりは開発のリーダーのこと。

本文

1. Beautiful is better than ugly.

美しいほうが、醜いよりも良い。

意味はそのまま。コードは美しいほうがいい。それはそう。

もう少し補足すると、すべてのスクリプトコードが美しい必要はないし、美しさは主観的なものだ。

とはいえ、Python はコードを美しくする作業もそれほど複雑なことではないはず。

2. Explicit is better than implicit.

暗示よりも明示のほうが良い。

例えば、どのモジュールから、どういったものが提供されているのか、明示(わかるように)したほうがいい、ということだと思う。

NG コード:

from os import *

OK コード:

import os
print(os.getcwd())

引数の意味や、処理を明らかにすることで、コードの意図を明示的にしましょう、ということを推奨しています。

wildcard のインポートのようなものは、便利かもしれませんが、namespace が汚染されるので、個人的にも嫌いな書き方です。

3. Simple is better than complex.

シンプルなものは、複雑なものよりも良い。

コードのリファクタリングについての教本のほとんどで書いてあることだと思います。割愛。

4. Complex is better than complicated.

複雑なものは、complicated よりも良い。

ここでは、complicated をどのように訳し解釈するのかがポイントですね。調べると、日本では complicated を「込み入った」と解釈している翻訳が多いように思いました。

「込み入った」とは、色々な解釈があると思うけど、私の理解は、処理が整理されずに詰め込まれていて、コードのリファクタリングが必要な状態を指すと思います。コードの詳細を知らないと動作を予想できない状態。雑で怠惰なコード。

「複雑なもの」とは、単純ではないものの、コードのリファクタリングが必要な状態ではない。コードの詳細を知らなくても動作を予想できる状態。(構造と論理が難しいので単純化できないだけの)賢いコード。

結論だけを見ると、それはそうだろうという指摘だと思う。

5. Flat is better than nested.

フラット(平坦)なものは、ネスト(階層)のものよりも良い。

ここでの指摘は何を指すのか、微妙なところです。次は、名前空間は浅く整理された内容が良いことについて指摘されています。これは、python の特徴のように思います。(C# とはすこし用途・文化が違う)

PEP 423 に「Avoid deep nesting」という内容があります。ここでは、2階層までという紹介です。

Yes: “pyranha”
Yes: “pythonsport.climbing”
Yes: “pythonsport.forestmap”
No: “pythonsport.maps.forest”

一方で、次はコード自体のネストについてコメントされていて、ガード節の "early returns" について言及されています。

def process(x):
    # ガード節
    if not valid(x):
        return
    # ここから本命の処理:ネストせずフラットに記述
    do_something(x)

いずれにしても、ネストを嫌うのはここまでの1~4の理由があるように思うこと。プログラム実行の形式の都合から、コードは単純であることの価値が C# と比べても高いように考えています。

6. Sparse is better than dense.

まばら(隙間)は、密集しているよりも良い。

可読性に関する指摘です。

NG コード:

print('\n'.join("%i bytes = %i bits which has %i possible values." %
                (j, j*8, 256**j-1) for j in (1 << i for i in range(8))))

OK コード:

for i in range(8):
    j = 1 << i
    bits = j * 8
    values = 256**j - 1
    print(f"{j} bytes = {bits} bits which has {values} possible values.")

各行がなにをしているのかわかるようにしたほうが良い。 つまり、コードをメンテナンスしやすい形で記録しておくほうが良い、という話。

「dense」は効率的なシーンもあるけど「理解の疲労」が大きい。オープンソースのようになると、この「疲労」の存在は嫌だろう。

個人的に「dense」なコードを美しいコードだと解釈する人が一定数いると思う。つまり、それは。

7. Readability counts.

可読性こそ大切である。

ここでの count は「大切である、重きをなす」という意味。可読性については、リーダブルコードなどたくさんの参考があるので、割愛。

count を "善である"、と解釈しているものもあった。python の設計に関する指針なので、C 言語と python のコードを比較して、後者のほうが可読性が高くて "善" だ、といってもいいのかもしれないけど、コードを書くうえでの指針だと解釈したほうが得られるものがあると思う。

8. Special cases aren't special enough to break the rules.

特別扱いは、ルールを破るほどの特別な理由にならない。

常にスタイルや可読性、整合性を重視し、「特定のケースだから例外的に・・・」とルールを無視しないようにしましょう、という指針。

def process(values):
    result = []
    for v in values:
        if v == "":  # 特別ケース
            continue  # 空文字だけ例外扱い
        try:
            result.append(int(v))
        except ValueError:
            pass  # 他の文字のエラーはパス
    return result

"" の処理をしたいときは、process の中に含めないはず。

def process(values):
    result = []
    for v in values:
        try:
            num = int(v)
        except ValueError:
            continue
        result.append(num)
    return result

values = ["123", "", "456", ""]
filtered = [v for v in values if v != ""]  # 空文字を除く

# ["123", "456"]
process(filtered)

どうしても process の中で処理したいなら、それは例外的な扱いになるので次の「9」を参考にすることになる。

9. Although practicality beats purity.

とはいえ、実用性は純粋さに勝る。

8と矛盾する内容になっています。、主なルールをそのまま守りつつ、その上で明確に例外を設けるのが望ましいです。

つまり、「8」の例でいえば:

def process(values):
    result = []
    for v in values:
        if v == "":
            # 明示的にログを残す
            print("Skipping empty string", file=sys.stderr)
            continue
        try:
            num = int(v)
        except ValueError:
            print(f"Warning: cannot convert {v!r}", file=sys.stderr)
            continue
        result.append(num)
    return result

「8」と「9」はもちろんセットで理解し、言及する必要があります。

一般的にも、一貫性のないコードは困る。だけど、Java を例に挙げて説明すると、どんな小さなコードでもオブジェクト指向パラダイムに当てはめて設計をすると、多くのボイラープレートコードが生まれてしまう。

2つの境界をどのように歩くのかは「経験」という回答になっている。

Walking the line between these two aphorisms becomes easier with experience.

これは、説明になっているのか疑問だ。

10. Errors should never pass silently.

エラーは黙って見逃すべきではない。

NG コード:

def read_file(path):
    try:
        with open(path) as f:
            return f.read()
    except:
        pass  # エラーを無視
    return None

すべての例外を pass してしまうようなこと。問題が発生していても、見逃すことになるので、あとで原因を調査する手がかりがなくなる。(Silent Failures)

OK コード:

def read_file(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError:
        logging.error(f"File not found: {path}")
    except PermissionError:
        logging.error(f"No permission to read file: {path}")
    except Exception as e:
        logging.error(f"Unexpected error reading {path}: {e}")
    return None

主に発生する恐れのある例外は個別に処理、その他の例外についてもログを記録することで追跡可能にする。(ほとんど「11」の内容になる) 。

11. Unless explicitly silenced.

ただし、「あえて」エラーを無視すると明示されている場合は別とする。

これも、「10」とペアになっている。デフォルトでは、エラーを見逃さずに処理するべきなのは当たり前だけど、エラーを無視する場合は、それをコード上で、はっきりとその意図を示して書けばいい。

例えば suppress を使うことで、特定の例外を「この行では無視する」と明確に示すことができる。仕様の範囲が明確だ。

from contextlib import suppress
import os

# 明示的に FileNotFoundError を無視するケース
with suppress(FileNotFoundError):
    os.remove('temp.txt')  # ファイルがなければ無視

print("Continue execution")
def read_optional_config(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError:
        logging.debug(f"No config file found at {path}, using defaults")
        return None

これは FileNotFoundError だけ catch しています。それ以外の例外は処理されていないので pass されません。その他の例外は、普通にプログラムをクラッシュさせる。

つまり、正常な動作、仕様内の例外、それ以外は「想定外の致命的なエラー」としてはっきりさせて、運用するということだと思う。

12. In the face of ambiguity, refuse the temptation to guess.

曖昧な状態 (ambiguous) に直面したら、推測する誘惑を拒むこと。

言い直すと、直感や憶測で処理を進めてはいけない、だと思う。曖昧だとエラーになる=曖昧さがなければエラーは起きにくい。

NG ケース:

def get_positive(value):
    if value:
        return abs(value)
    # 0 や None、空文字でもここまで来てしまう
    return None  # ここは曖昧:何が返されるべきか判断できていない

OK ケース:

def get_positive(value):
    if value is None:
        raise ValueError("value must not be None")
    if not isinstance(value, (int, float)):
        raise TypeError(f"expected int or float, got {type(value).__name__}")
    if value <= 0:
        raise ValueError("value must be positive")
    return value

None など各ケースを明示的に検査して、曖昧な状況を拒否している。「仕様どおりに動く」コードを実現している。

ここで言いたいことは、例のケースのことを具体的に修正指示したいのではなくて、コードの意図が一件してわかるように全体を設計しましょう、ということ。

if not a and b よりも if (not a) and b が意図を明確化するなら後者のほうが良いということでもある。曖昧はよくない。意図を明確化しろ。

13. There should be one-- and preferably only one --obvious way to do it.

あるべきは「ひとつ」--できれば唯一の明白なやり方だ。

例えば、リストの長さを得るやり方は、通常こうする:

length = len(my_list)

やろうと思えば for や index のループを繰り返して要素数を数えるメソッドを作ることもできる。しかし、そうするべきではない。できるだけわかりやすい「ひとつ」の書き方が python には備わっているべきだ、だという考え。

転じると、python ユーザーは、もっともわかりやすい書き方を利用することでコードは統一されるし、読みやすくなるよ、ということ。

dict をマージするときはどうするのか? 以下はよくない:

d = dict(x=1, y=2)
d2 = dict(y=3, z=4)
merged = d.copy()
merged.update(d2)

現在は以下の書き方がサポートされている:

merged = d | d2

なので、python の簡単な書き方のテクニックを習熟しよう。次の「14」で示すとおり。

14. Although that way may not be obvious at first unless you're Dutch.

ただし、オランダ人 (Guido van Rossum) のような人でない限り、最初そのやり方は、明白でないかもしれない。

これは「13」とペアでセットになっています。

python には簡単な書き方が用意されてるけど、それが "明白なもの" だと理解するまでには、学習・経験が必要だよね、ということ。

ここで、オランダ人は最初からわかるように書いてあるのは、このオランダ人は (Guido van Rossum) を指しており、(Python の生みの親だからわかるという)ジョーク。

15. Now is better than never.

やらないよりは、今やるべき。

ここまでに色々な格言(指針)があったけど、コーダーが完璧な状態になるのを待って手をいつまでも付けないよりは、まずは作ってみるべきだ、という考え。

良い例だな、と思ったのは、コードを初期の段階から最適化することが難しいケースがあって、ソートの実装が必要だったとしても、最初は単純なもので仮に実装していても良いと思う。

テスト段階はバブルソートで、後から効率のよいソートに置き換えるという方針は「16」にも該当しないし悪くない。

16. Although never is often better than right now.

しかし「今」するより、やらない方がましなこともある。

これも「15」とセットなのは明白です。実装のために急いで作るより、やらない方がマシな場合もある、という注意喚起がついています。

焦りすぎて誤った方向に進まないように慎重さも求めています。

単に動くだけのものを作るのではなくて、型チェックや例外処理を加えて信頼性を向上させるとか、品質を上げる方向に改善していく。

"fail fast" ではなく "build fast" と "think fast" を両立させる姿勢のこと。

17. If the implementation is hard to explain, it's a bad idea.

もしも、実装の内容を説明することが難しいのなら、それは悪いアイデア(の実装)です。

これは、具体的な指針なので、それほど詳しい説明はいらないと思う。

コーダーにとって理解しづらい、説明しづらいコードはトリッキーなので避けようという考えだと思います。レビューをする上でも「7」の可読性が低いコードは「17」にも該当してしまう。

なにをしているのか説明に手間のかかる(かもしれない)コード:

def flatten(lst):
    return sum((x if isinstance(x, list) else [x] for x in lst), [])

説明のしやすい「実装」コード:

def flatten(lst):
    result = []
    for x in lst:
        if isinstance(x, list):
            result.extend(x)
        else:
            result.append(x)
    return result

ここまでの内容からもわかるとおり、コードの意図が明白であること、コードが読みやすいこと、コードがメンテナンスしやすいことが大切。

あたりまえだけど、本当にパフォーマンスが必要なら C や C++ を採用したほうが最終的な実行速度を追求できるはずだ。その点で、python はタイムパフォーマンスがいいと思う。開発やデバッグにかかる時間の面で、優れるケースが多いと思う。言語としての強みを意識すると「可読性」、「メンテナンス性」、「明白さ」といったところを整理したコードの価値が高いという考えになるのだと(私は)考えています。

18. If the implementation is easy to explain, it may be a good idea.

もしも、実装を説明しやすければ、それは良いアイデアかも。

これは「人が読んで納得できる実装であること」の重要性の目安になると思います。ただ、「たぶん」くらいの感じなので、必ずしも正解としておらず判断基準として有益だろうという立ち位置だと思います。

これも言語の文化的に、まずは、理解しやすいコーディングを選んだほうがいいんじゃないかな、くらいの姿勢かと。

「17」の指摘と同じように見えるのでそれだけで十分なようにも思ったのですが、丁寧に考えるなら、「17」は止めた方がよいことを断言する形で否定し「18」は断言しない形でゆるい。

具体的な例にすると、「説明しづらい」には該当せず、「説明しやすい」にも該当しない≒「説明は可能なライン」のコードなどがあると思う。これを全部除外してはいけないよね、ということかも。

19. Namespaces are one honking great idea -- let's do more of those!

名前空間(namespace)はすごく素晴らしいアイデアだ。もっと活用しよう!

名前空間を使えってこと。名前の衝突を避けるために、コードを(シンプルな名前空間を使って)構造的にコードを書け。

「2」でも説明したけど mymodule.func() のような書き方なら、名前でどこで定義されたものかが一目でわかる。

特に規模が大きくなるほど、明示的な名前空間の設計でコードを組織化して、保守が楽になる。

ただし、名前空間の深さは2つまでを python は推奨してる。PEP 423

参考