はじめに
サニパ様〜(328)、nikkieです。
『ロバストPython』を読み始めました。
将来の開発者に意図を伝えるPythonの書き方が指南された(議論のための)本です。
10章のクラス、これはいくつか議論したい事項があるのですが、まずは読んでの私の理解をアウトプットします。
目次
- はじめに
- 目次
- 前回の『ロバストPython』!
- 10章 クラス
- クラスの導入(10.1)
- 不変式!(10.2, 10.3)
- 学び:ユニットテストでコンテキストマネージャーを使って重複を減らせる
- 終わりに
前回の『ロバスト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』、議論(ガチバトル)しようぜ〜
やっぱり読書会があるとよさそうな本だと思います〜(どなたかご一緒しませんか〜?📣)
- https://github.com/pviafore/RobustPython/blob/dafb95d801dff2c8ff7856ba46d3c052d54e0033/code_examples/chapter10/person_construction.py↩
- https://learning.oreilly.com/library/view/robust-python/9781098100650/↩
-
__init__
でassert
を使っています https://github.com/pviafore/RobustPython/blob/dafb95d801dff2c8ff7856ba46d3c052d54e0033/code_examples/chapter10/pizza_maker.py#L7-L14↩ - アサーションか例外かはけっこう話したい話題だったりします。またの機会に(おそらく近日)↩
- プライベートな属性を使うかもけっこう話したい話題です。こちらもまたの機会に(おそらく近日)↩
- https://github.com/pviafore/RobustPython/blob/dafb95d801dff2c8ff7856ba46d3c052d54e0033/code_examples/chapter10/pizza_maker.py#L26-L51↩