第14章 Pythonista 向けの読み替えとコード例
14.1 この章で学ぶこと
この章で学ぶ内容は以下のとおりです。
学ぶこと |
ポイント |
|---|---|
Python との違いの地図 |
文法は似ているが、値の考え方とコンパイル時の仕組みが根本的に違う |
ownership vs Python の参照モデル |
|
|
|
trait の意味 |
duck typing ではなく、能力をコンパイル時に宣言する仕組み |
アセンブリで見る comptime |
|
14.2 Python との違いの地図
Python と Mojo は文法が似ていますが、値の考え方とコンパイル時の仕組みが異なります。 コードを読むときに「Python と同じ」と思い込むと予想外の動作に遭遇します。 この節では、つまずきやすい 5 つの領域を地図として整理します。
Python の感覚 |
Mojo での読み替え |
|---|---|
変数は「オブジェクトへの名前」(参照) |
「誰がその値を所有しているか」(所有権)を意識する |
関数は何でも受け取れる(暗黙の参照渡し) |
|
例外は実行時に勝手に伝わる |
|
型ヒントは補助的( |
|
duck typing( |
|
それぞれを見る前に、大まかな感覚をつかんでください。
図14-2: Python との違いの地図
「最初の見た目は Python に近い」「でも中身はコンパイル言語寄り」という 2 点がこの章全体のテーマです。
14.3 ownership と value semantics
Python の変数は「オブジェクトへの参照」です。
a = b と書いても両方が同じオブジェクトを指すだけで、データはコピーされません。
Mojo は逆に「誰がその値を所有しているか」で動作が変わる value semantics(値意味論)を採用しています。
Python との動作比較
# Python
a = [1, 2, 3]
b = a # 同じリストオブジェクトを参照
b.append(4)
print(a) # [1, 2, 3, 4] ← a も変わる
# Mojo
var a: List[Int] = [1, 2, 3]
var b = a # a のコピーが作られる
b.append(4)
print(a) # [1, 2, 3] ← a は変わらない
print(b) # [1, 2, 3, 4]
Mojo の var b = a はデータを丸ごとコピーします。
Python の「名前を増やす」とは根本的に違います。
Python |
Mojo |
|
|---|---|---|
|
同じオブジェクトを指す参照のコピー |
値のコピー(独立した別データ) |
関数に渡す |
参照を共有(呼び出し先が変更できる) |
デフォルト |
メモリ管理 |
GC(参照カウント + サイクル検出) |
所有権の規則によってコンパイル時に決まる |
|
参照カウントを減らす |
|
ref:コピーなしの参照
List などの大きな値をコピーせずに要素を参照したい場合は ref を使います。
ref value_ref = list[0] # list[0] への読み取り借用
ref はコピーを発生させません。
ただし、list が生きている間だけ有効です(list が解放されると value_ref は使えません)。
これは Python の「どこかに参照がある限り生き続ける」とは異なり、スコープがライフタイムを決めます。
14.4 imm / mut:シグネチャが意図を語る
Python では「この引数を関数内で変更するかどうか」はドキュメントや命名規則でしか伝えられませんでした。 Mojo は引数修飾子によってシグネチャに意図を埋め込み、コンパイラが保証します。
# `mut` と `imm` 修飾子で引数の意図をシグネチャに明示する例。
# `mut x` は呼び出し元の変数を直接書き換え、`imm y` は読み取り専用。
def add(mut x: Int, imm y: Int):
x += y
def main():
var a = 1
var b = 2
add(a, b)
print(a) # 3
リスト14-1: mut_read.mojo
mut x と書くことで「x は呼び出し元の変数を直接書き換える」という意図がシグネチャに現れます。
imm y は「y は読み取るだけ」の保証です。
修飾子 |
意味 |
特徴 |
Python の対応 |
|---|---|---|---|
|
読み取り専用の借用 |
コピーせず参照を借りる。変更不可 |
慣習的に「変更しない」と示すのみ |
|
変更可能な借用 |
呼び出し元の変数を直接書き換える |
リストや辞書をそのまま渡す感覚に近い |
|
コピーまたはムーブして受け取る |
呼び出し元とは独立した所有者 |
引数をコピーしてローカル変数にする |
誤った修飾子はコンパイルエラー
def wrong(imm x: Int):
x += 1 # エラー:imm な引数は変更不可
Python では x += 1 が Int のような immutable 型に対してローカル変数の再束縛になるだけですが、Mojo では imm 修飾子付きの引数への代入はコンパイルエラーです。
「読み取り専用」という意図がコンパイラに強制されます。
14.5 mut の実務的な意味:コピーしない更新
mut は「呼び出し元の変数をそのまま変更する」という宣言です。
Python で言えばリストや辞書を関数に渡してその場で書き換える感覚に似ていますが、Mojo では Int や String のような値型でも同じ仕組みが働きます。
# `mut` で List をコピーせず直接書き換える例。
# 呼び出し元の values が mutate 内の append で更新される。
def mutate(mut l: List[Int]) raises:
l.append(5)
def main() raises:
var values: List[Int] = [1, 2, 3, 4]
mutate(values)
print(values) # [1, 2, 3, 4, 5]
リスト14-2: mut_list.mojo
mut l と書くことで「l を変更することがある」がシグネチャに現れます。
呼び出し元の values はコピーされず、直接 mutate 内から書き換えられます。
var values: List[Int] = [1, 2, 3, 4] のように型注釈が必要な点に注意してください。
[1, 2, 3, 4] のようなリストリテラルは、型注釈がないと固定長の Array と推論されます。
可変長の List として使いたい場合は List[Int] と明示します。
効果 |
説明 |
|---|---|
コピーが起きない |
|
呼び出し元に変化が反映される |
|
シグネチャで明示 |
|
mut を使うかどうかの判断
mut は「呼び出し元の値を変えてよい」という設計判断です。
変更結果を戻り値で返す設計と比べると次のようなトレードオフがあります。
# mut 方式:コピーなし。元の値を直接変更
def push(mut l: List[Int], v: Int):
l.append(v)
# 戻り値方式:コピーが発生するが元の値は不変
def with_pushed(l: List[Int], v: Int) -> List[Int]:
var result = l # コピー
result.append(v)
return result
大きなリストや頻繁に呼ばれる関数では mut 方式がコピーコストを節約します。
不変性が重要な場面では戻り値方式が安全です。
14.6 var と ^:ownership transfer
var 引数修飾子は「関数がその値の所有権を持つ」ことを宣言します。
^(transfer operator)は「この変数の所有権を今すぐ手放す」演算子です。
# `var` 引数と `^`(transfer operator)による所有権移動の例。
# message^ で所有権を take_text に渡すと、以降 message は使えない。
def take_text(var text: String):
text += "!"
print(text)
def main():
var message = "Hello"
take_text(message^)
# print(message) # ここ以降 message は使えない(コンパイルエラー)
リスト14-3: var_move.mojo
message^ を渡すと message の所有権が take_text に移り、main の message はその行以降で無効になります。
コンパイラが無効な変数の使用を検出します。
記法 |
意味 |
|---|---|
|
|
|
|
|
|
^ を使う理由:コピーを避ける
String や List は Copyable なので ^ なしでも動きますが、コピーコストが発生します。
大きなデータを関数に「譲り渡す」場合は ^ でコピーを防ぎます。
take_text(message) # String がコピーされる → message も生き続ける
take_text(message^) # コピーなし → message は無効になる
Copyable でない型(OwnedPointer など)は ^ なしでは渡せません。
コンパイラがコピー不可能な型の誤った複製を防ぎます。
コラム: Python との対比
Python では del message で参照を外せますが、実際の解放タイミングは GC が決めます。
message^ は「今すぐこの値の所有権を放棄する」という明示的な宣言です。
コンパイラが「^ の後で message を使っていない」ことを型として保証します。
Rust の move セマンティクスに相当します。
14.7 compile-time parameters:[] と ()
Mojo の関数には 2 種類の引数スロットがあります。
[] はコンパイル時に確定する パラメータ、() は実行時に渡る 引数 です。
Python の型ヒント Generic[T] や TypeVar に近い概念ですが、実際にコード生成に効く点が違います。
記法 |
タイミング |
効果 |
Python との対比 |
|---|---|---|---|
|
|
ループ展開・定数畳み込みが起きる |
|
|
|
通常の実行時ループになる |
通常の引数と同じ |
# `[]` パラメータと `comptime for` によるループ展開の例。
# count はコンパイル時に確定し、ループは展開される。
def repeat[count: Int](msg: String):
comptime for i in range(count):
print(msg)
def main():
repeat[3]("Hello")
リスト14-4: comptime_repeat.mojo
repeat[3]("Hello") を実行すると、アセンブリには callq print が 3 回連続で並びます(comptime for の展開。06 章のアセンブリで確認済み)。
ループ命令は生成されません。
; comptime for が展開された結果(ch09/comptime_for_unroll.asm より)
db: callq print ; i = 0
19d: callq print ; i = 1
299: callq print ; i = 2
; ループカウンタ・分岐命令は存在しない
なぜ [] が重要か
コンパイル時に値が確定することで:
ループ展開:
comptime forがループ命令なしに展開される定数畳み込み:
count * sizeof[Float32]()がコンパイル時に計算される型特化:型 parameter に基づいて最適なコードが生成される(14.8 節)
配列サイズの固定:
Array[Float32, count]のようにスタック上の固定長配列が作れる
Python の型ヒントは実行時に無視されますが、Mojo の [] parameter はコンパイルに直接作用します。
14.8 型 parameter と値 parameter
[] に入れられるのは「コンパイル時確定の整数」だけではありません。
型そのものも parameter にできます。
型 parameter に trait 制約を付けることで「Writable を満たす任意の型」のような汎用関数が書けます。
# 型パラメータと値パラメータを併用した汎用関数の例。
# MsgType は Writable を満たす任意の型、count はコンパイル時確定の整数。
def repeat[
MsgType: Writable,
count: Int
](msg: MsgType):
comptime for _ in range(count):
print(msg)
def main() raises:
repeat[Int, 2](42) # MsgType=Int, count=2
repeat[String, 3]("mojo") # MsgType=String, count=3
リスト14-5: comptime_generic.mojo
MsgType は Writable を満たす任意の型です。
repeat[2](42) を呼ぶと、コンパイル時に MsgType=Int / count=2 と決まり、Int に特化した関数が生成されます。
Parameter |
種類 |
意味 |
|---|---|---|
|
型 parameter |
|
|
値 parameter |
コンパイル時に確定する整数。ループ展開や固定長配列に使える |
monomorphization(単相化)
repeat[2](42) と repeat[3]("mojo") はそれぞれ別の関数としてコンパイルされます(monomorphization)。
実行時に「MsgType は何か」を判定するコードはなく、型チェックのオーバーヘッドがゼロです。
コンパイル後のシンボル(概念)
repeat__Int__2 ← repeat[2](42) 向け
repeat__String__3 ← repeat[3]("mojo") 向け
Python の TypeVar はランタイムの型チェックを省略しないため実行コストが残りますが、Mojo の型 parameter はコンパイル時に型が確定し、専用コードが生成されるため実行時コストはゼロです。
型 parameter の推論
repeat[2](42) では MsgType を明示していません。
42 が Int であることをコンパイラが推論し、MsgType=Int を自動で埋めます(型推論)。
推論できない場合は repeat[Int, 2](42) と明示します。
14.9 traits / generics:必要な能力を宣言する
Python では「この関数は == が使えるオブジェクトを受け取る」という意図を、型ヒントの Protocol や実行時の hasattr チェックで表現していました。
Mojo では trait(トレイト)を使ってコンパイル時に宣言します。
trait とは
trait は「この型が持つべき能力(メソッドやインターフェース)」の仕様書です。
関数の型 parameter に T: SomeTrait と書くことで「SomeTrait の能力を持つ型のみ受け付ける」という制約になります。
def all_equal[
T: Equatable & Copyable
](ref lhs: List[T], ref rhs: List[T]) -> Bool:
if len(lhs) != len(rhs):
return False
for left, right in zip(lhs, rhs):
if left != right:
return False
return True
print(all_equal([1, 2, 3], [1, 2, 3])) # True
print(all_equal(["hello", "world"], ["hello", "world"])) # True
T: Equatable & Copyable は「==/!= が使えて、かつコピーできる型だけを受け付ける」という制約です。
trait |
意味 |
実装が必要なもの |
|---|---|---|
|
|
|
|
|
|
|
コピーできる |
|
|
ムーブできる |
|
|
|
|
|
|
|
Python の対応と違い
Python Mojo
--- ---
Protocol / ABC trait
実行時チェック(isinstance) コンパイル時チェック
hasattr(x, "__eq__") T: Equatable(コンパイル時)
duck typing(呼べれば OK) trait 制約(能力を明示宣言)
Equatable でない型(例:カスタム struct で __eq__ を実装していない)を all_equal に渡すとコンパイルエラーになります。
「実行してみたら AttributeError」ではなく、「書いた時点でエラー」です。
独自 trait の定義
# 独自 trait を定義し、struct で実装する例。
# Point は Printable の能力(display メソッド)を実装している。
trait Printable:
def display(self) -> String: ...
@fieldwise_init
struct Point(Printable):
var x: Float64
var y: Float64
def display(self) -> String:
return "(" + String(self.x) + ", " + String(self.y) + ")"
def main():
var p = Point(1.0, 2.0)
print(p.display()) # (1.0, 2.0)
リスト14-6: trait_printable.mojo
struct Point(Printable) と書くことで「Point は Printable の能力を持つ」と宣言します。
display メソッドを実装しないとコンパイルエラーになります(Python の ABC に近いですが、実行時ではなくコンパイル時に検出されます)。
14.10 まとめ:読み替えのポイント
Python の書き方を Mojo に読み替える際の主なポイントをまとめます。
仕組み |
Python の感覚 |
Mojo での違い |
|---|---|---|
|
in/out を名前やドキュメントで示す |
シグネチャで明示。コンパイラが保証 |
|
|
所有権の移動。移動後は使用不可(コンパイル時検出) |
|
型ヒント(実行時無効) |
コンパイル時入力。コード生成・ループ展開に効く |
|
Protocol / ABC / duck typing |
コンパイル時に能力を宣言。monomorphization で特化コードを生成 |
|
例外は実行時に伝わる |
シグネチャで型として明示。書かないと呼び出し元に伝播できない |
文法は Python に近いが、値の考え方とコンパイル時の仕組みは根本的に違います。 この 1 点を押さえると以降のコードが読みやすくなります。