nikkie-ftnextの日記

イベントレポートや読書メモを発信

読書ログ | 『ロバストPython』10章「クラス」、不変式を維持せよ!と声高に叫ぶ章(議論する前の理解のまとめ)

はじめに

サニパ様〜(328)、nikkieです。

ロバストPython』を読み始めました。
将来の開発者に意図を伝えるPythonの書き方が指南された(議論のための)本です。
10章のクラス、これはいくつか議論したい事項があるのですが、まずは読んでの私の理解をアウトプットします。

目次

前回の『ロバストPython』!

1章の正論ガトリングに打たれたり、

5章でTypedDictを知って「神じゃん!」ってなったりしています。

10章 クラス

第I部 型アノテーション(2章〜7章)に続く、第II部 ユーザ定義型(8章〜14章)。
そこで紹介されるユーザ定義型は3つで

  • 列挙型(8章)
  • データクラス(9章)
  • クラス(10章)

です。

オライリーさんのページで公開されている目次から引用です。

  • 10.1 クラスの解剖
  • 10.2 不変式
  • 10.3 不変式のカプセル化と保守
  • 10.4 まとめ

10章は著者の想いがけっこう込められた章だと思います。

一般にクラスはとても早い段階で教わり、後で取り上げられることはまずない。そのためか、ほとんどの開発者がクラスの目的を考えずにクラスを使いすぎる傾向にある。(10.4)

クラスの導入(10.1)

ロバストPython』はデータクラス -> クラスの順に解説する構成なので、10.1ではデータクラスと比較してクラスが導入されます。
なので、(些末な点ですが)この本で初めて見るクラスの定義はインスタンス変数でなくクラス変数を使ったものになります1

class Person:
    name: str = ""

この構成は「面白いな〜」と思いました。
そして、データクラスの書き方と比較する形で__init__(初期化)が導入されます。

dataclassではデータクラスの中で変数を定義していたが、クラスでは__init__()の内部で変数を定義する。

不変式!(10.2, 10.3)

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

不変式の元の語はInvariants2
英英辞典を引いたところ、always the same; never changing(nikkie訳:常に同じ;決して変わらない)という意味でした。

不変式は、オブジェクト(=インスタンス)が必ず持つ性質を伝えるもので、「数学的な性質、ビジネスルール、整合性の保証、その他常に真でなければならないあらゆる条件(10.2)」です。

例にしているのは、ピザの仕様(PizzaSpecification)でした。
ピザの仕様における不変式:

  • 半径の値(整数)はこの範囲(何インチから何インチ)
  • トッピングリストのうち、先頭は必ずソース

初期化する際に不変式を維持(10.2.1)

2つの方法が紹介されます(上のコードではどちらも使っています)

  • 例外送出
    • ピザの半径が不変式を満たしていなければ__init__で例外送出
  • データ操作
    • __init__でトッピングリストを並び替えて、ソースを先頭にする

例外送出について、著者のオススメはアサーション(assert文)3
他の選択肢としては例外も挙げられています4

カプセル化(10.3)

10.2までの内容だと、初期化した後、インスタンスの属性を変更して不変式を破れてしまうということで、カプセル化が解説されます。

カプセル化とは

クラスの作者が呼び出し元に見せるプロパティを選択してデータの読み書きの方法を制限する手法

その実装として、2つ紹介されます

  • データアクセス保護(10.3.2)
    • 属性名に_をつけて、プロテクト(_spam)やプライベート(__egg)にする
  • 不変式を必ず満たすようにデータを操作するメソッド(10.3.3)
    • オブジェクトの状態を変更するメソッド(ミューテータ)と不変式

著者のアプローチとしては

  • プライベートな属性
  • しかし、Pythonは名前マングリングでプライベートな属性にもアクセス可能
    • リンターで検知
  • ミューテータ(メソッド)は、不変式を維持する

と理解しました5

学び:ユニットテストでコンテキストマネージャーを使って重複を減らせる

10.2.3〜10.2.5は不変式を開発者に伝える方法を扱っています。

  • クラスの利用者には、ドキュメント(主にdocstring)で不変式について伝える
  • 将来のメンテナには、ユニットテストで不変式について伝える
    • メンテナが不変式を変更してしまったときに気づける

ユニットテストで不変式の変更を把握しやすくするテクニックとして、コンテキストマネージャーが紹介されます6

@contextlib.contextmanager
def create_pizza_specification(dough_radius_in_inches: int, toppings):
    pizza_spec = PizzaSpecification(dough_radius_in_inches, toppings)
    yield pizza_spec
    # 不変式を満たすかの検証がまとまる
    assert 6 <= pizza_spec.dough_radius_in_inches <= 12

# pytestを使ったユニットテストコード
def test_pizza_operations():
    with create_pizza_specification(8, ["Tomato Sauce", "Peppers"]) as pizza_spec:
        assert pizza_spec.toppings == ["Tomato Sauce", "Peppers"]
        # 具体ケースの検証が終わった後、コンテキストマネージャーを抜ける際に不変式を満たすか検証される

この例を見て、「ふだん書くユニットテストに活かせそう!」と思いました。
with create_pizza_specification(...)で、ピザ仕様の引数が変わったときにも、繰り返し不変式のassertを書かずに検証できます!
クラスのインスタンスが必ず持っていてほしい性質は、コンテキストマネージャーにassertをまとめる方法があるというのが発見でした。

これまでは毎回書くと重複になるので、あるテストメソッドで1回だけ検証というアプローチを採っていました。
コンテキストマネージャーで簡単にまとめられるなら試してみたいです。

終わりに

ロバストPython』10章は監訳のHayaoさんも言及されています

個人的に、10章の「クラス」は重要だと思っている。

著者の言いたいことは、

  • みんなクラスの目的を考えずにクラス使いすぎ
  • 実装にクラスを選択するかの判断は不変式があるか

という理解です(これはnikkieが雑にまとめたもので、著者からすると本意じゃなかったらごめんなさい)。
不変式がなければ関数にする、スタティックメソッドやクラスメソッドについての見解、なども述べられています。

このあたりを学びに感じるとともに、他の本での学びとは対立するので、判断の指針を探るためにマジで議論したいと思っています(著者と話せたらいいんですが)。
今後続く1日1エントリで思考の整理をしていきます。
おーい、『ロバストPython』、議論(ガチバトル)しようぜ〜

やっぱり読書会があるとよさそうな本だと思います〜(どなたかご一緒しませんか〜?📣)