第14章 Pythonista 向けの読み替えとコード例

14.1 この章で学ぶこと

この章で学ぶ内容は以下のとおりです。

表14-1: この章で学ぶこと

学ぶこと

ポイント

Python との違いの地図

文法は似ているが、値の考え方とコンパイル時の仕組みが根本的に違う

ownership vs Python の参照モデル

immmut^ は Python にはない概念

[]() の使い分け

[] はコンパイル時情報。型ヒントとは別物

trait の意味

duck typing ではなく、能力をコンパイル時に宣言する仕組み

アセンブリで見る comptime

comptime for が展開済みになる証拠

14.2 Python との違いの地図

Python と Mojo は文法が似ていますが、値の考え方とコンパイル時の仕組みが異なります。 コードを読むときに「Python と同じ」と思い込むと予想外の動作に遭遇します。 この節では、つまずきやすい 5 つの領域を地図として整理します。

表14-2: Python との違いの地図

Python の感覚

Mojo での読み替え

変数は「オブジェクトへの名前」(参照)

「誰がその値を所有しているか」(所有権)を意識する

関数は何でも受け取れる(暗黙の参照渡し)

immmutvar でシグネチャに意図を明示する

例外は実行時に勝手に伝わる

raises でシグネチャに明示しないと伝播できない

型ヒントは補助的(x: int は実行時に無視される)

[] はコンパイル時入力。コード生成・ループ展開に直接効く

duck typing(hasattrisinstance

trait で必要な能力をコンパイル時に宣言。違反はコンパイルエラー

それぞれを見る前に、大まかな感覚をつかんでください。

../_images/python_mojo_diff_map.jpg

図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 の「名前を増やす」とは根本的に違います。

表14-3: ownership と value semantics

Python

Mojo

a = b

同じオブジェクトを指す参照のコピー

値のコピー(独立した別データ)

関数に渡す

参照を共有(呼び出し先が変更できる)

デフォルト imm(読み取り専用の借用)

メモリ管理

GC(参照カウント + サイクル検出)

所有権の規則によってコンパイル時に決まる

del x

参照カウントを減らす

^ で所有権を手放す(以降 x は使えない)

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 は読み取るだけ」の保証です。

表14-4: 引数修飾子の比較

修飾子

意味

特徴

Python の対応

imm(省略可)

読み取り専用の借用

コピーせず参照を借りる。変更不可

慣習的に「変更しない」と示すのみ

mut

変更可能な借用

呼び出し元の変数を直接書き換える

リストや辞書をそのまま渡す感覚に近い

var

コピーまたはムーブして受け取る

呼び出し元とは独立した所有者

引数をコピーしてローカル変数にする

誤った修飾子はコンパイルエラー

def wrong(imm x: Int):
    x += 1  # エラー:imm な引数は変更不可

Python では x += 1Int のような immutable 型に対してローカル変数の再束縛になるだけですが、Mojo では imm 修飾子付きの引数への代入はコンパイルエラーです。 「読み取り専用」という意図がコンパイラに強制されます。

14.5 mut の実務的な意味:コピーしない更新

mut は「呼び出し元の変数をそのまま変更する」という宣言です。 Python で言えばリストや辞書を関数に渡してその場で書き換える感覚に似ていますが、Mojo では IntString のような値型でも同じ仕組みが働きます。

# `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] と明示します。

表14-5: mut の効果

効果

説明

コピーが起きない

List 全体をコピーせず、直接元のリストを更新する

呼び出し元に変化が反映される

mutate 後に values を参照すると [1, 2, 3, 4, 5] になっている

シグネチャで明示

mut を見るだけで「破壊的操作をする関数」と判断できる

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 に移り、mainmessage はその行以降で無効になります。 コンパイラが無効な変数の使用を検出します。

表14-6: var^ の意味

記法

意味

var text: String

take_texttext の owner になる。関数を抜けると text が解放される

message^

message の所有権を take_text に移す。この行以降 message は無効

^ なし(take_text(message)

StringCopyable なのでコピーが発生。message はそのまま使える

^ を使う理由:コピーを避ける

StringListCopyable なので ^ なしでも動きますが、コピーコストが発生します。 大きなデータを関数に「譲り渡す」場合は ^ でコピーを防ぎます。

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 に近い概念ですが、実際にコード生成に効く点が違います。

表14-7: []() の違い

記法

タイミング

効果

Python との対比

repeat[3]("Hello")

3 はコンパイル時確定

ループ展開・定数畳み込みが起きる

typing.Literal[3] に近いが実際に効く

repeat(3, "Hello")

3 は実行時に渡る

通常の実行時ループになる

通常の引数と同じ

# `[]` パラメータと `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

MsgTypeWritable を満たす任意の型です。 repeat[2](42) を呼ぶと、コンパイル時に MsgType=Int / count=2 と決まり、Int に特化した関数が生成されます。

表14-8: 型 parameter と値 parameter

Parameter

種類

意味

MsgType: Writable

型 parameter

Writable を満たす任意の型。コンパイル時に IntString などに特化

count: Int

値 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 を明示していません。 42Int であることをコンパイラが推論し、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 は「==/!= が使えて、かつコピーできる型だけを受け付ける」という制約です。

表14-9: 代表的な組み込み trait

trait

意味

実装が必要なもの

Equatable

==!= が使える

__eq____ne__

Comparable

<><=>= が使える

__lt____gt__

Copyable

コピーできる

__copyinit__

Movable

ムーブできる

__moveinit__

Writable

print で出力できる

write_to

Sized

len() が使える

__len__

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) と書くことで「PointPrintable の能力を持つ」と宣言します。 display メソッドを実装しないとコンパイルエラーになります(Python の ABC に近いですが、実行時ではなくコンパイル時に検出されます)。

14.10 まとめ:読み替えのポイント

Python の書き方を Mojo に読み替える際の主なポイントをまとめます。

表14-10: まとめ:読み替えのポイント

仕組み

Python の感覚

Mojo での違い

imm / mut

in/out を名前やドキュメントで示す

シグネチャで明示。コンパイラが保証

var / ^

del・参照のコピー

所有権の移動。移動後は使用不可(コンパイル時検出)

[] parameter

型ヒント(実行時無効)

コンパイル時入力。コード生成・ループ展開に効く

trait

Protocol / ABC / duck typing

コンパイル時に能力を宣言。monomorphization で特化コードを生成

raises

例外は実行時に伝わる

シグネチャで型として明示。書かないと呼び出し元に伝播できない

文法は Python に近いが、値の考え方とコンパイル時の仕組みは根本的に違います。 この 1 点を押さえると以降のコードが読みやすくなります。