第4章 Python との比較・入口
4.1 この章で学ぶこと
この章で学ぶ内容は以下のとおりです。
学ぶこと |
ポイント |
|---|---|
Python との 4 つの根本的な違い |
型・ownership・raises・interop の設計思想の差 |
同じコードを Python と Mojo で並べる |
行単位で「何が違うか」を確認する |
|
シグネチャにエラーのコストを明示する理由 |
Python 経験者がはまる落とし穴 |
思い込みを先に解消しておく |
4.2 Python との 4 つの違い(俯瞰)
Python と Mojo の根本的な違いを 4 つの観点から俯瞰します。
観点 |
Python |
Mojo |
|---|---|---|
型 |
実行時の動的型が基本。型ヒントは補助的(無視できる) |
型はコンパイル時に確定。 |
値の扱い |
すべて参照。変数はオブジェクトを指すラベル |
value semantics が基本。所有者(owner)が明示的に決まる |
エラー |
どの関数でも例外を投げられる。シグネチャに現れない |
|
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
行単位の対比をまとめます。
行 |
Python との違い |
|---|---|
|
|
|
|
|
|
|
|
図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
ポイント |
詳細 |
|---|---|
定義側に |
|
呼び出し側も |
|
|
|
「エラーのコスト」がシグネチャに現れる |
失敗し得ることをシグネチャから把握しやすい(どんな条件で失敗するかまでは本文や docstring で補う) |
コラム: raises、Go、Rust との比較
エラーをシグネチャで明示するアプローチは Mojo 独自ではありません。
Go:
func f() (T, error)で戻り値にエラーを含めるRust:
fn f() -> Result<T, E>で型システムでエラーを表現Mojo:
def f() raisesで例外をシグネチャに明記
Python の暗黙的な例外と違い、これらは「呼び出し元がエラーを処理しなければならない」ことをコンパイル時に強制します。
Mojo は Python に似た raise/try 構文を採用しながら、raises 修飾子でコンパイル時の安全性を確保しています。
4.5 Python 経験者がはまる落とし穴
Python の経験があると Mojo でつまずきやすいパターンを整理します。
はまりやすいパターン |
Mojo での正しい理解 |
|---|---|
|
|
関数の引数に型を書かなくてよいと思う |
型アノテーションを省略するとコンパイラが型を決定できない場面でエラーになる。引数には型アノテーションを明示するのが推奨 |
|
|
所有権を移動するつもりが値のコピーになる |
|
|
引数に渡せるのは |
Python の |
|
4.6 型がわかるとコンパイラが何をするか
List[Float64] という型情報があると、コンパイラは次のことを実行前に確定できます。
図4-2: List[Float64] の要素レイアウト
各要素は 8 バイト固定で連続配置される。ストライド(index × 8)はコンパイル時に確定するため、アクセスオフセットの計算が実行時ではなくコンパイル時に行われる。
Python では temps[index] にアクセスするたびに「何型の要素か」を動的に確認します。
Mojo では要素のオフセット(index * 8)が静的に計算できるため、ロード命令 1 つに直結します。
これが第1章と第2章で見た「スタックサイズの事前確定」や「定数畳み込み」と同じ原理です。 型情報はコンパイラへのヒントであり、それが実行時コストの削減につながります。