Closed34

ロバストPythonを読む

alkshmiralkshmir

1章 ロバストPython入門

クリーンなコードとはその意図を明確かつ簡潔に表現するコードのことだ(明確さのほうが簡潔さよりも重要だ)。コードを読んでその意味を完全に理解したと思うならば、それはクリーンなコードだ。

将来の開発者に意図が伝わっていない状況でこのコードを保守するには、次のどちらかを選択することになる。

  • すべての呼び出し元のコードを読んで、この動作に依存しないことを確認してから新しいコードを実装する。このコードが外部から呼ばれる可能性があるライブラリの公開APIならば天運を祈るほかない。
  • コードを書き替えてどのような影響が生じるかを見守る。運が良ければ何も悪いことは起きないだろう。運が悪ければユースケースの修正に膨大な時間を費やし、いらだちを隠せなくなるだろう。

コードの意味が分からないと言って私に尋ねに来ることもなくなるだろう。私と話をしなくても何をしているか理解できるはずだ。これは非同期コミュニケーションである。

alkshmiralkshmir

目的に合わないコレクションを使ってはならない

  • 重複が含まれてはいけないものを格納したリスト
  • キーから値へのマッピングのために使われていない辞書
alkshmiralkshmir

静的インデックスとは、定数のリテラルを使ってコレクションの要素を参照すること

  • ユーザ定義型が必要になっている兆候

ただし、

  • シーケンスの先頭や末尾
  • 辞書をJSONやYAMLを読みだす際の中間的なデータ型として利用する場合
  • 固定長のチャンクを持つシーケンスの操作
  • パフォーマンス

によって許容される場合がある

alkshmiralkshmir

驚き最小の原則: 未来のコラボレータが動作や実装に驚くことがないようにする

複雑さには必然的な複雑性偶発的な複雑性の2種類がある。

偶発的な複雑性を取り除き、必然的な複雑性を狭い範囲に封じ込めることが必要。

alkshmiralkshmir

2章 Pythonデータ型

  • 機械的な表現
    • データ型はPython自体に振る舞いと制約を与える
  • 意味論的な表現
    • データ型は他の開発者に振る舞いと制約を与える
alkshmiralkshmir

型システム

  • 強い型付け

    • データ型がサポートする演算しか使えない(使おうとするとコンパイルエラーやランタイムエラー)
  • 弱い型付け

    • データ型がサポートしない演算を使ったら強制的に演算が意味を持つ他の型に変換される
  • 静的型付け

    • ビルド時に型情報を埋め込む
    • 実行時に型が変化しない
  • 動的型付け

    • 変数ではなく、値に型情報を埋め込む
alkshmiralkshmir

ダックタイピング

何らかのインターフェースに準拠する限り、異なる種類のオブジェクトをそのようなものとして扱える性質

  • python なら iter() メソッドが実装されていればfor文で使えるとか

ダックタイピングは諸刃の剣

  • 抽象化されたインターフェースを構築できれば、様々な特殊ケースへの対応を減らせる
  • 濫用すると開発者が信頼できる前提条件が壊れる
alkshmiralkshmir

3章 型アノテーション

私は苦い教訓を学んだ。確かに小さなプログラムでは動的型付けはすばらしい。しかし、大規模なプログラムではもっと規律の取れたアプローチが必要である。「君は何でも好きにして良いんだよ」などといわずに言語自体がその規律を与えたほうが効果的なのだ
From: https://oreil.ly/1xf01

クリーンアーキテクチャにそんな感じのこと(プログラミングパラダイムとは制約である、みたいな)書いてあった気がする

alkshmiralkshmir

4章 型制約

防御的プログラミング自体は良いことだが、…Pythonではあらゆる値がNoneになり得るため、防御的プログラミングを実践するとすべての変数参照の前に is None チェックを入れなければならないが、それはやり過ぎである

Go言語くん…

Optional

  • Optional型は値を持つか持たないかの2種類の選択肢を提供する
    • 宣言されているものがNoneである可能性を予想しなければならないという危険信号を送れる
    • 空値と値の非存在を区別できる
      • 要素のないリスト:エラーは発生していないがデータが存在しない
      • None: 処理しなければいけないエラーが発生した
    • mypy がNoneチェックをできる
      • mypy 0.961 の場合、型アノテーションのない関数には関数本体の型チェックが行われないので注意

Union

Union[int, str]int または strが使えるという意味

  • Python 3.10 以降では int | str と書ける

Literal

値の取る範囲を制限できる

Literal[1, 2, 3, 4, 5]
Literal["プレッツェル", "ホットドッグ", "べジーバーガー"]

Enumと何が違うのとおもったけどそれは8章に書いてあるらしい

Annotated

サイズや、値の範囲や、正規表現にマッチするもののみ、などの制限を与える

Annotated[int, ValueRange(3, 5)]
Annotated[str, MatchesRegex("[0-9]{4}")]

現段階では型チェックできない?らしくヒントでしかないらしい?

NewType

NewType は既存のデータ型を受け付け、その既存型のすべてのフィールドとメソッドを持つ新しいデータ型を作る。両者は交換不能である。

  • 暗黙の型変換が制限される
ReadyToServeHotDog = NewType("ReadyToServeHotDog", HotDog)

def dispense_to_customer(hot_dog: ReadyToServeHotDog):
  • ReadyToServeHotDog が求められているところで HotDog を使えない。
  • HotDog の代わりに ReadyToServeHotDog を使える

ReadyToServeHotDogを作るための方法を制限することで安全にはできるが、その方法を開発者に知らせる方法はコメント以外にない。
しかし、次のような場面で役に立つ。

  • SQL インジェクションを検知するために str と SanitizedString を区別する。
  • User と LoggedInUser を区別する
  • 有効なユーザIDをさらわす整数を追跡する
    • ちょっとこの例はよくわからなかった

クラス化して制限をもりもり書いた方が汎用性があるがNewTypeのほうが軽量である

alkshmiralkshmir

Final

値が不変であることを示す

  • 変数スコープがモジュール全体のような広い範囲に及ぶ場合に使うとよい
alkshmiralkshmir

5章 コレクション型

ジェネリクス

from typing import TypeVar

T = TypeVar("T")

def reverse(coll: list[T]) -> list[T]:
    return coll[::-1]
  • T型の要素のリストを受け取り、同じT型の要素のリストを返すことができる
    • 違う型は返せない
    • Tは具体的には何でもよい
from collections import defaultdict
from typing import Generic, TypeVar

Node = TypeVar("Node")
Edge = TypeVar("Edge")

# 有向グラフ
class Graph(Generic[Node, Edge]):
    def __init__(self):
        self.edges: dict[Node, list[Edge]] = defaultdict(list)

    def add_relation(self, node: Node, to: Edge):
        self.edges[node].append(to)

    def get_relations(self, node: Node) -> list[Edge]
        return self.edges[node]
  • Nodeに入る型は何でもよいがすべて同じであればよい
  • Edgeに入る型は何でもよいがすべて同じであればよい
    • おそらくインスタンスごとで一貫していればよい、インスタンス間での一貫性は必要ない(保証されない)

こう書けば、あらゆる要素に対するグラフを定義でき、それらに対して型チェックをかけられる

cookbooks: Graph[Cookbook, Cookbook] = Graph()
recipes: Graph[Recipe, Recipe] = Graph()
cookbook_recipes: Graph[Cookbook, Recipe] = Graph()
  • このグラフって何を表したいのか?
alkshmiralkshmir

ジェネリクスのその他の例
APIからのエラーをUnionにして返す場合

T = TypeVar("T")
APIResponse = Union[T, APIError]

def get_nutrition_info(recipe: str) -> APIResponse[NutritionInfo]:
    pass

def get_ingredients(recipe: str) -> APIResponse[llist[Ingredient]]:
    pass

def get_restaurants_serving(recipe: str) -> APIResponse[list[Restaurant]]:
    pass
alkshmiralkshmir

新しいコレクション型の定義

  • 組み込みデータ型を継承すると落とし穴あり
    • 組み込みコレクションはパフォーマンスを意識して作られており、多くのメソッドはインラインコードを使っている。
    • dict の __getitem__() のようなメソッドをオーバーライドしても、そのほかのメソッドはそのオーバライドされたメソッドを使わない。

この場合は、collections.UserDict を継承するといい感じになる

collections.abc を使うとよりローレベルでいろいろできるが嬉しさがよくわからなかった
ただし、collections.abc.Iterable は便利そうな感じがする(わざわざcollections をインポートしないといけないのか、これぐらいTypingに入ってほしいけど)

def print_items(items: collections.abc.Iterable):
    for item in items:
       print(item)
alkshmiralkshmir

6章 型チェッカのカスタマイズ

まあ困ったらあとで読めばいいか、の話かと思った

  • テイント解析する Pysa は便利そうなので使ってみたい。
alkshmiralkshmir

7章

  • monkytype: pythonファイルを実行して実際に入った型から型アノテーションを追加するツール
  • Pytype: 静的解析で型アノテーションをつけるツール
    • 型チェッカの機能も持っている
alkshmiralkshmir

8章 列挙型

  • Flag 型
from enum import auto, Flag

class Allergen(Flag):
    FISH = auto()
    SHELLFISH = auto()
    TREE_NUTS = auto()
    PENUTS = auto()

ビット演算で選択肢を組み合わせられる

allergens = Allergen.FISH | Allergen.SHELLFISH
  • 内部的には、auto()が2のべき乗を生成するので、 & や | を使えるようになる
alkshmiralkshmir

9章 データクラス

データクラスがイミュータブルであるとき、frozen=Trueを書く

@dataclass(frozen=True)
class Ingredient:
    name: str
    amount: float = 1
    units: ImperialMeasure = ImperialMeasure.CUP

データクラスの機能

  • 文字列への変換 (__str__()__repr__())
  • 等価比較
  • 比較
    • デフォルトでサポートしていないが、order=Trueで定義された順の比較を行う
    • __lt__()__le__()とかをオーバーライドすると比較動作を変えられる(order=Trueにはしない)

どこでもデータクラスを使うべきだというわけではない。...本当に適切なのはデータクラス内でのメンバーが互いに独立している場合だけだ。...開発者はいつでもデータクラスの値を書き換える可能性があり、それによって無効な状態が生み出される危険性がある。

こういう時はクラスを使おうと書いてある

alkshmiralkshmir

10章 クラス

クラスは、辞書やデータクラスが簡単には伝えられない重要な情報を伝える。それは不変式だ。

不変式とは、エンティティの生涯にわたって変化しないエンティティの性質のことである。...例えば次のようなものだ。

  • すべての従業員は一意なIDを持つ。
  • ゲームの敵は、ヒットポイント(HP)が整数の場合以外は攻撃しない。
  • 円は正の半径しか持てない。
  • ピザのチーズは必ずソースの上に置く。

不変式を維持するには、次の2つの方法がある

  • 例外を送出する
    • ファクトリメソッドを使って不変式が満たされない場合にNoneを返すこともあり
  • データを操作する
alkshmiralkshmir

次のような場合、クラスで不変式チェックをするべき

  • 型システムで検知できない形でデータに制限を加えるべきか
  • フィールド間に相互依存関係はあるか
  • データに関して提供したい保証はあるか

単一責任の原則は、関連する不変式のグループを定義し、そういったグループごとにクラスを書くとみたされる。不変式と関係のない属性やメソッドを書いていることに気づいたら、責任が多すぎる可能性がある。

不変式の伝え方

  • docstring に不変式を自然言語で書くといいよと書いてある
  • コンテキストマネージャ?を使ってユニットテストを書く
alkshmiralkshmir

カプセル化

  • パブリック
  • プロテクト: クラスと派生クラスがアクセスできる
    • アンダースコア(_)を1つ
  • プライベート: クラスのオブジェクトだけがアクセスできる
    • アンダースコア(_)を2つ

すべての属性にゲッターとセッターをつけるのはよくある間違い。クラスの中身がほとんどゲッターとセッターなら、データクラスを使うべき。

不変式を意識しないメソッドとか、クラスのメンバさえ意識しないメソッドは、おそらく関数にすべきだろう。... すでにクラスが膨れ上がっていても、クラスとは無関係なメソッドをもう1つ追加したくなるが、保守性を追求するなら、クラスとは無関係なメソッドを置くべきではない(こうすると、奇妙な依存の連鎖が発生する。ファイルが別のファイルに依存する理由を改めて考えてみると、答えはこれだったということがよくある)。

@staticmethodと@classmethodは遺構であると言ってる

alkshmiralkshmir

11章 インターフェース

コードインターフェースのパラドックス

インターフェースを正す機会は1度しかないのに、実際に使われるまでインターフェースが正しいかどうかはわからない。

スコット・メイヤーズの言葉に従う

インターフェースは、正しく使うことが簡単で間違った使い方がしにくいように作れ

alkshmiralkshmir

12章 部分型

基底クラスのオブジェクトを要求する関数に派生クラスのオブジェクトを渡せて、型チェッカも全く文句を言わないことだ。

あれ、なんか文句言ってきた記憶あるんだけど… 条件があるのかな

正方形長方形問題とLiskov置換原則の話が書いてある

この問題の解決方法は複数ある。
第一の方法はそもそもSquareはRectangleを継承できないと割り切ることだ。...
第二の方法は、フィールドをイミュータブルにするなどの方法でRectangleのメソッドに制限を加え、Squareが矛盾を起こせないようにすることだ。
第三の方法は、両者の間にクラス階層を作らず、Rectangleにis_square()メソッドを設けることだ。

  • 不変式
    • 部分型は元のデータ型の不変式をすべて守らなければならない
  • 事前条件: データを操作する前にみたされるべき条件
    • 部分型は元のデータ型の事前条件を厳しくしてはならない
  • 事後条件: データを操作した後にみたされるべき条件
    • 部分型は元のデータ型の事後条件を緩めてはならない
alkshmiralkshmir

危険信号の例

  • 引数の条件チェック
    • 基底クラスよりも厳しい引数の条件チェックを行っている可能性がある
  • 早期リターン
    • 事後条件のチェックがされていない可能性がある
  • 例外送出
    • 上位型と同じ例外か派生形しか送出してはいけない
    • 基底クラスで例外が起きる可能性を示していない場合に、派生クラスで例外を送出するのは論外
  • super()を呼ばない
    • 同じ動作をしていない可能性がある
alkshmiralkshmir

継承するときのガイドライン

基底クラスを書くとき

  • 不変式を変えない
  • 不変式でプロテクト属性に制限を加える場合には特に注意する
  • 不変式をドキュメントに書く

派生クラスを書くとき

  • 基底クラスの不変式を把握する
  • 基底クラスの機能拡張をする
    • 現在の不変式と調和しない機能が追加される場合は、基底クラスに機能を追加すべき
    • オーバーライドできるメソッドをサポートしない場合は、基底クラスに機能をサポートするかどうかを示すフラグを追加する
  • オーバーライドではsuper()を入れる
alkshmiralkshmir

コードの再利用のためだけに継承を使うのは誤り。
上位型の代わりに部分型が使えるという関係(is-a関係)をモデリングすることが主目的。

上位型を受け入れるコードで、部分型が使われることがないならば、has-a関係(コンポジション)を検討すべき。

alkshmiralkshmir

13章 プロトコル

完全に部分型にならないときどうする?

インターフェース使えばいいんじゃない?という案

単純ならばこれでもいいかも

もっと複雑になったらどうするか?

DinnerもSplittableにしたくなったらどうするのか?

ミックスインは解決案の一つである。

class BLTSandwich(Shareable, PickUppable, Substituable, Splittable):
    pass

上位クラスを汚さずに下のクラスに責任を転嫁することができる。

型チェックのために必要な変更を最小限に抑えたいところだ。また、基底クラスをインポートする際に物理的な依存関係を持ち込むことになるが、それが望ましくない場合もあるだろう。

ちょっとピンと来ていない

alkshmiralkshmir

Pythonicなやり方はプロトコルである。

最も慣れ親しんだプロトコルはイテレータプロトコルである。

from collections.abc import Iterator, MutableSequence
from random import shuffle

class ShuffleIterator:
    def __init__(self, sequence: MutableSequence):
        self.sequence = list(sequence)
        shuffle(self.sequence)

    def __iter__(self):
        return self

    def __next__(self):
        if not self.sequence:
            raise StopIteration
        return self.sequence.pop(0)

my_list = [1, 2, 3, 4]
iterator: Iterator = ShuffleIterator(my_list)

for num in iterator:
    print(num)
  • Iterator を継承しなくてもいい
    • ShuffleIterator がイテレータとして使うために必要な2つのメソッドを持っているから
alkshmiralkshmir

プロトコルとして Splittable を定義すればいい

from __future__ import annotations
from typing import Protocol

class Splittable(Protocol):
    cost: int
    name: str
    def split_in_half(self) -> tuple[Splittable, Splittable]:
        """実装不要"""
        pass

BLTSandwitch は Splittable を継承しなくていい。
Splittable に依存する関数にはこう書く

def split_dish(dish: Splittable) -> tuple[Splittable, Splittable]:
    pass

これで動的型付けの利点を生かしつつ型チェックできる

alkshmiralkshmir

でもこれってSplittableの定義を継承ツリーから外しただけで依存性は変わってないからどうなんだろう

型チェッカと人間に読ませるだけであって処理系は読まないから関係ないのかな(実際メソッドの中身は何も定義していないのだし)

alkshmiralkshmir

契約がデータ型の構造を定義するだけ(特定の属性が定義されていることを要求するだけのSplittableのように)ならば、プロトコルを使う。
上位型の契約が、守らなければならないふるまいとして特定の条件の下での操作方法などを定義するなら、is-a関係をよりよく反映する継承を使うといい。

alkshmiralkshmir

13.3.3 プロトコルを満たすモジュール

何言っているかわからなかった

alkshmiralkshmir

14章 pydantic

pydantic は dataclass に制約をつけられるライブラリ。

確かに便利なんだけど、いろんな人が使うライブラリはpydanticに依存すべきかどうか悩んでるのだけど答えはなかった。

本文で触れられていないがv1系とv2系で後方互換性が破壊されたのでまだプリマチュアなきがするんだよな。
HTTPとかで外と区切られているならいいけどimportして使うなら問題になってきそうで怖い。

このスクラップは3ヶ月前にクローズされました