第4章 Python との比較・入口

4.1 この章で学ぶこと

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

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

学ぶこと

ポイント

Python との 4 つの根本的な違い

型・ownership・raises・interop の設計思想の差

同じコードを Python と Mojo で並べる

行単位で「何が違うか」を確認する

raises が表すもの

シグネチャにエラーのコストを明示する理由

Python 経験者がはまる落とし穴

思い込みを先に解消しておく

4.2 Python との 4 つの違い(俯瞰)

Python と Mojo の根本的な違いを 4 つの観点から俯瞰します。

表4-2: Python との 4 つの違い(俯瞰)

観点

Python

Mojo

実行時の動的型が基本。型ヒントは補助的(無視できる)

型はコンパイル時に確定。List[Float64] は要素が 8 バイトと確定する

値の扱い

すべて参照。変数はオブジェクトを指すラベル

value semantics が基本。所有者(owner)が明示的に決まる

エラー

どの関数でも例外を投げられる。シグネチャに現れない

raises がある関数だけがエラーを返せる。呼び出し元も明示が必要

Python との関係

—(Python 自身)

Python を直接呼び出せるが、型境界でコストが発生する

4.3 同じ処理を Python と Mojo で並べる

calculate_average を Python と Mojo で対比します。

Python 版(型情報なし)

def calculate_average(temps):          # 型なし。何でも渡せる
    total = 0.0
    for index in range(len(temps)):
        total += temps[index]          # 要素の型は実行時に毎回判定
    return total / len(temps)          # 整数除算か浮動小数点かは実行時に決まる

Mojo 版

def calculate_average(temps: List[Float64]) -> Float64:
    var total: Float64 = 0.0
    for index in range(len(temps)):
        total += temps[index]
    return total / Float64(len(temps))


def main():
    var temps: List[Float64] = [20.5, 22.3, 19.8, 25.1]
    var avg = calculate_average(temps)
    print("Average:", avg)

リスト4-1: python_comparison_calculate_average_simple.mojo

行単位の対比をまとめます。

表4-3: 同じ処理を Python と Mojo で並べる

Python との違い

def calculate_average(temps: List[Float64]) -> Float64:

List[Float64] により要素が 64 bit 浮動小数点と確定。コンパイラは要素サイズ(8 バイト)、ストライド、ループ展開をすべて事前に決定できる

var total: Float64 = 0.0

var はスコープを持つ変数宣言。Python の = は型なし代入だが、これはコンパイル時に Float64 スロットを確保する

total += temps[index]

Float64 の加算は直接 addsd(スカラー)や vaddpd(SIMD)命令に対応。Python では毎回「total の型は何か」を判定してから演算する

return total / Float64(len(temps))

Float64(len(temps)) で整数を浮動小数点に明示変換。Python では / が自動で float 除算になるが、Mojo では型の変換は明示が基本

calculate_average 関数の Python 版と Mojo 版の比較

図4-1: calculate_average 関数の Python 版(左)と Mojo 版(右)の比較

左(Python 版)は型情報がないため実行時に型判定が発生し最適化余地が小さい。 右(Mojo 版)は List[Float64] により要素サイズがコンパイル時に確定し、加算命令(addsd/vaddpd)への直結や明示的な型変換によって最適化大となる。

4.4 raises が示すもの

Python では、どの関数でも raise できます。 呼び出し元が例外を受け取るかどうかはドキュメントか実行時の挙動を見るしかありません。

# Python: 例外の可能性がシグネチャに現れない
def calculate_average(temps):
    if len(temps) == 0:
        raise ValueError("empty")   # 例外を投げる可能性が見えない
    ...

Mojo では raises が必要です。

def calculate_average(temps: List[Float64]) raises -> Float64:
    if len(temps) == 0:
        raise Error("No temperature data")
    var total: Float64 = 0.0
    for index in range(len(temps)):
        total += temps[index]
    return total / Float64(len(temps))


def main() raises:
    var temps: List[Float64] = [20.5, 22.3, 19.8, 25.1]
    print(calculate_average(temps))

リスト4-2: python_comparison_calculate_average_raises.mojo

表4-4: raises が示すもの

ポイント

詳細

定義側に raises が必要

def calculate_average(...) raises -> Float64: と書かないとコンパイルエラー

呼び出し側も raises が必要

def main() raises: のように、エラーが伝播することを明示する

try / except で受け取ることも可能

raises を呼び出すが mainraises を書きたくない場合は try ブロックで囲む

「エラーのコスト」がシグネチャに現れる

失敗し得ることをシグネチャから把握しやすい(どんな条件で失敗するかまでは本文や docstring で補う)

コラム: raises、Go、Rust との比較

エラーをシグネチャで明示するアプローチは Mojo 独自ではありません。

  • Gofunc f() (T, error) で戻り値にエラーを含める

  • Rustfn f() -> Result<T, E> で型システムでエラーを表現

  • Mojodef f() raises で例外をシグネチャに明記

Python の暗黙的な例外と違い、これらは「呼び出し元がエラーを処理しなければならない」ことをコンパイル時に強制します。 Mojo は Python に似た raise/try 構文を採用しながら、raises 修飾子でコンパイル時の安全性を確保しています。

4.5 Python 経験者がはまる落とし穴

Python の経験があると Mojo でつまずきやすいパターンを整理します。

表4-5: Python 経験者がはまる落とし穴

はまりやすいパターン

Mojo での正しい理解

var x = 5 は Python の x = 5 と同じだと思う

var はブロックスコープを持ち、宣言の型が確定する。Python の = はスコープを新たに作らない

関数の引数に型を書かなくてよいと思う

型アノテーションを省略するとコンパイラが型を決定できない場面でエラーになる。引数には型アノテーションを明示するのが推奨

List[Float64][] はインデックスだと思う

[] は型パラメータ(compile-time の情報)。インデックスアクセスは list[i]() ではなく []

所有権を移動するつもりが値のコピーになる

^ を付けないと move にならない。def f(var x: String) の呼び出しは f(x^) と書く

print(x) が何でも通ると思う

引数に渡せるのは Writable trait を実装した型のみ

Python の import と同じだと思う

from python import Python; np = Python.import_module("numpy") という形式で Python を経由する

4.6 型がわかるとコンパイラが何をするか

List[Float64] という型情報があると、コンパイラは次のことを実行前に確定できます。

List[Float64] の要素レイアウト

図4-2: List[Float64] の要素レイアウト

各要素は 8 バイト固定で連続配置される。ストライド(index × 8)はコンパイル時に確定するため、アクセスオフセットの計算が実行時ではなくコンパイル時に行われる。

Python では temps[index] にアクセスするたびに「何型の要素か」を動的に確認します。 Mojo では要素のオフセット(index * 8)が静的に計算できるため、ロード命令 1 つに直結します。

これが第1章と第2章で見た「スタックサイズの事前確定」や「定数畳み込み」と同じ原理です。 型情報はコンパイラへのヒントであり、それが実行時コストの削減につながります。