第0章 · Pythonの仕組み
演算子とデコレータの裏側を読む¶
この教材では、基本的なPython構文は知っているものとします。 ここで準備するのは構文ではなく、ライブラリがPythonの記号へ 独自の意味を与える仕組みです。
関数、クラス、属性、条件分岐、ループ、コレクション、例外、型注釈を読めること。
特殊メソッド、デコレータ、dataclass、propertyがコードを組み替える仕組みです。
a + bから、型が実装した特殊メソッドを探せる- デコレータ適用前の関数と、適用後の公開関数を区別できる
dataclassが生成する処理と、手書きする処理を区別できる- テストコードの
selfと演算メソッドのselfを区別できる
a + b や obj.width を利用者が書きます。
特殊メソッドやpropertyへ処理が委譲されます。
委譲先へ、数の定義・検査・ログを実装します。
基本構文は前提、実行環境はブラウザ¶
各実験のコード欄は編集でき、実行すると結果が「出力」欄へ表示されます。 最初の実行だけはPythonとライブラリの読み込みに数秒かかることがあります。 「環境をリセット」は、このページで作った名前を消して最初からやり直す操作です。
以後、代入、関数定義、クラス定義、if、while、内包表記、添字、
タプルの展開、例外、型注釈そのものは説明しません。
一方、次の節で扱うPythonのデータモデルとメタプログラミングは、
このライブラリの数学的な意味を決めるため、教材の本題として説明します。
+ は型へ処理を依頼する¶
この教材のコードを追うときは、まず次の対応で実装の入口を探します。
| 利用者が書く形 | 最初に探す実装 | この教材で決める意味 |
|---|---|---|
a + b |
a.__add__(b) |
足し算 |
a * b |
a.__mul__(b) |
掛け算 |
a == b |
a.__eq__(b) |
数学的に同じ値か |
a < b |
a.__lt__(b) |
大小関係 |
str(a) |
a.__str__() |
学習者向けの表示 |
repr(a) |
a.__repr__() |
内部表現を示す表示 |
int(a) |
a.__int__() |
組み込み整数への変換 |
厳密な演算子探索には、右側の型による反射演算や継承関係も関わります。 まずはこのライブラリ内の同じ型どうしの演算を読み、必要になった整数章で 異なる型を受け入れる処理へ進みます。
デコレータ本体:
peano/utils.py
の log。
演算メソッドの例:
peano/natural_number.py
の NaturalNumber.__add__。
メタデータの検査:
tests/test_utils.py。
全章の索引は実装リファレンスにあります。
two + one と two.__add__(one) は、
上の演算子対応だけから考えると、同じ処理へ到達するでしょうか。
内部用のタプルを公開しない理由は、実行後に次節の実装で確認します。
実行すると、ここに答えが表示されます。
どちらも公開された同じ __add__ を通るため、結果は 3 と True です。
「内部用のタプルを返さない」理由は、次のデコレータにあります。
デコレータは、関数を受け取って公開関数を作る¶
引数のないデコレータなら、次の二つは同じ意味です。
このライブラリの @log(log_level=4) は、先にログレベルを受け取り、
その結果として得たデコレータでメソッドを包みます。
概念上は、次の再代入です。
元の __add__ は、計算結果と「説明文を必要なときに作る関数」のタプルを返します。
公開された __add__ は、ラッパー inner です。
次は peano/utils.py の log から、実行に関わる部分をそのまま抜粋したものです。
def outer(func: Callable[P, tuple[T, LogMessage]]) -> Callable[P, T]:
@wraps(func)
def inner(*args: P.args, **kwargs: P.kwargs) -> T:
result, message = func(*args, **kwargs)
if logger.isEnabledFor(log_level):
logger.log(log_level, message if isinstance(message, str) else message())
return result
# このあと、公開する戻り型とシグネチャも更新する。
...
return inner
... の箇所だけは、次の小節で説明するメタデータの更新を省略しています。
完全な実装は上の peano/utils.py から参照できます。
したがって利用者が two + one と書いたときに受け取るのは計算結果だけです。
説明文は戻り値には混ざらず、設定されたログへ送られます。ログが無効なら
message() を呼ばないため、表示のためだけの文字列も作りません。
自然数の章では、この変換と再帰呼び出しを組み合わせてログ順を予想します。
ラッパーが名前や型情報を壊さないための処理¶
実際の log は functools.wraps を使い、__name__ や __wrapped__ を保存します。
さらに、元の内部実装が tuple[Result, LogMessage] を返すという型注釈から
公開結果 Result を取り出し、__annotations__ と __signature__ を更新します。
これは単なる表示機能ではありません。「内部では説明文を遅延生成できるが、 公開APIは数だけ返す」 という二層の契約を、実行時のラッパーと、プログラムから関数情報を調べる イントロスペクション用メタデータの両方へ反映するメタプログラミングです。
dataclass はクラス定義からコードを生成する¶
たとえば整数クラスの先頭は次の形です。
@total_ordering
@dataclass(frozen=True, slots=True, eq=False, repr=False)
class Integer:
a: NaturalNumber
b: NaturalNumber
| 指定 | 生成または制限されるもの | このライブラリでの理由 |
|---|---|---|
| 型注釈付き属性 | __init__(a, b) |
何を保存する型かを宣言する |
frozen=True |
作成後の属性変更を拒否 | 数の内部表現を途中で変えない |
slots=True |
属性名を固定 | 宣言していない状態を持たせない |
eq=False |
dataclass版__eq__を生成しない |
属性の一致ではない数学的な等しさを書く |
repr=False |
dataclass版__repr__を生成しない |
数学的構成が見える表示を書く |
init=False |
__init__を生成しない |
多項式で正規化する独自コンストラクタを書く |
生成された __init__ は、属性を設定したあと __post_init__ を自動で呼びます。
有理数はそこで「分母が0ではない」という、作成後も常に守る条件を検査します。
このような条件を、この教材では不変条件と呼びます。
@total_ordering もコード生成です。クラスが核となる等値・順序比較を実装すると、
残りの >, <=, >= を補います。各数体系では、生成される便利さより先に、
核として手書きされた比較が数学のどの定義に対応するかを読みます。
property はメソッド呼び出しを属性アクセスに見せる¶
次は peano/polynomial.py にある、実ファイルからの短い抜粋です。
@property もデコレータです。利用者は polynomial.coefficients() ではなく
polynomial.coefficients と書きますが、取得時には上の関数が実行されます。
最終章では、端点から毎回計算する interval.width と interval.midpoint を
実コードと一緒に読みます。
テストコードの読み方を揃える¶
| テストコード | 確かめること |
|---|---|
self.assertEqual(actual, expected) |
二つが等しい |
self.assertTrue(condition) |
条件が真になる |
self.assertLess(left, right) |
左が右より小さい |
with self.assertRaises(Error): |
内側の処理が指定した例外を出す |
テストが通ることは、調べた入力について実装が期待どおりだった証拠です。 すべての数について成り立つ数学的証明とは区別します。
各章で答える六つの質問¶
- どの数学的対象と規則を使うのか
- その対象をどの属性と不変条件で保存するのか
- どの特殊メソッドや生成された処理が規則を実行するのか
- デコレータを含む実装から、どのログを予想できるか
- テストはどの性質を、どの入力で調べるのか
- 数学の定理、実装上の選択、未実装の範囲はどこか
次へ¶
これで、+ から __add__ を探し、デコレータ適用前後の戻り値を分け、
dataclass が生成した処理と手書きの処理を見分けられます。
次章では、この読み方だけを使って0から自然数を構成します。