コンテンツにスキップ

Python開発者のためのRust

Pythonを知っているなら、Rustの考え方の多くはもう知っています。Rustには、イテレータを回す for ループ、タプル、クロージャ(Pythonのラムダに近いものですが、もっと便利です)、Pythonの match とよく似た match 文があります。そして、リスト内包表記と同じ仕事をするイテレータのチェーンもあります。Rustは、Pythonの好みもいくつか取り入れています。コードの書式はひとつの決まった形にそろえる、パッケージ管理ツールは使いやすい、エラーメッセージはできるだけ親切に、という考え方です。

新しく覚えることは、ほとんどがコンパイラに関係しています。Pythonはプログラムを実行しながら型をチェックしますが、Rustは実行前にすべての型をチェックします。Pythonはガベージコレクタでメモリを片付けますが、Rustはコンパイル時にチェックされる所有権(ownership)と借用(borrowing)を使います。Pythonには None と例外がありますが、RustにはOptionとResultがあります。学習時間のほとんどは、この3つに使うことになるでしょう。

では、Python開発者はなぜRustを学ぶのでしょうか。正直に言うと、理由はこんなところです。

  • 速さ。 Rustはネイティブの機械語にコンパイルされます。純粋なPythonでは遅いCPU中心の処理(パース、数値計算、大きなループ)も、Rustなら特別なチューニングなしで、たいていずっと速くなります。
  • 本物のスレッド。 標準のCPythonでは、GILのせいで一度にPythonのコードを実行できるスレッドはひとつだけです。Rustのスレッドは、CPUのすべてのコアで同時に動きます。しかも、データ競合はプログラムの実行前にコンパイラが止めてくれます。
  • バグが早く見つかる。 属性名のタイプミス、うっかり忘れていた None、間違った型での関数呼び出し。Pythonでは、その行が実行されたときに初めてわかります。Rustでは、直すまでプログラムがコンパイルできません。
  • どちらかを選ぶ必要はない。 PyO3(英語)とmaturinを使えば、Pythonプロジェクトの遅い部分だけをRustで書き、ほかのモジュールと同じようにPythonから import できます。Polars、Ruff、uvといった人気のPythonツールも、Rustで書かれています。

RustとPython、Go、C++を全般的に比べたい場合は、Rust vs C++・Go・Pythonを見てください。

この表は、旅行の会話帳のように使ってください。いちばん右の列は、Rust側を説明しているこのサイトのページへのリンクです。

Python Rust 詳しくは
動的型付け。実行時にチェックされる 静的型付け。コンパイル時にチェックされる。ほとんどの型はRustが推論するので、思ったほど型を書かなくてよい データ型
型ヒント(def f(x: int) -> str) 関数シグネチャの型は必須で、コンパイラが必ずチェックする 関数
x = 5 let x = 5;(変更できない)。変更したいときは let mut x = 5; 変数
int(必要なだけ大きくなる) 固定サイズの整数:i32、i64、u64、usize、i128…。オーバーフローすることがある データ型、整数オーバーフロー
float f64(または f32) データ型
str String(所有していて、伸ばせる)と &str(テキストを借用したビュー) 文字列
f文字列:f"Hi {name}" format!("Hi {name}") と println!("Hi {name}") 出力
print(repr(x)) / __repr__ #[derive(Debug)] を付けて println!("{x:?}") 出力、よく使うトレイト
__str__ Display を実装する(すると .to_string() も使える) よく使うトレイト
__eq__、__hash__、__lt__ PartialEq、Eq、Hash、PartialOrd、Ord をderiveする よく使うトレイト
None、Optional[T] Option<T>:Some(value) か None のどちらか Option
x or default x.unwrap_or(default) Option
raise、try/except Result<T, E> + ? 演算子 Result、独自のエラー型
本物のバグを表す例外(assert、IndexError) panic!(スレッドを止める) panic
__init__ とメソッドを持つ class データ用の struct + メソッド用の impl ブロック。new は普通の関数 構造体
@dataclass #[derive(Debug, Clone, PartialEq)] を付けた struct 構造体
self &self(読む)、&mut self(変更する)、self(所有権を受け取る) 構造体
継承 継承はない。コンポジション(構造体の中に構造体を入れる)+ デフォルトメソッド付きのトレイトを使う トレイト、デザインパターン
ダックタイピング、Protocol、ABC trait トレイト
異なるクラスのオブジェクトを入れたリスト Vec<Box<dyn Trait>>(トレイトオブジェクト) トレイトオブジェクト
TypeVar / ジェネリックな関数 トレイト境界付きのジェネリクス:fn largest<T: Ord>(items: &[T]) ジェネリクス
Enum クラス 各バリアントが独自のデータを持てる enum 列挙型
match / case(3.10以降) match(すべてのケースを網羅したかコンパイラがチェックする) match、パターンマッチング
if my_list:(真偽値としての評価) 明示的に書く:if !my_list.is_empty()。if に書けるのは bool だけ if…else
for i in range(10) for i in 0..10 forループ
enumerate(xs)、zip(a, b) xs.iter().enumerate()、a.iter().zip(b.iter()) イテレータ
lambda x: x + 1 クロージャ:|x| x + 1(複数行にも書ける) クロージャ
リスト内包表記、map、filter イテレータアダプタ:filter、map、最後に collect() イテレータ
ジェネレータと yield Iterator を実装した独自の型、または std::iter::from_fn 独自のイテレータ
with open(...) as f: 何も書かなくてよい。ファイルやロックは、所有者がスコープを抜けると閉じられる(Drop) スコープ、ファイル入出力
デフォルト引数、*args、**kwargs ない。Option の引数、スライス(&[T])、Default を実装した構造体、またはビルダーを使う 関数、デザインパターン
デコレータ 直接対応するものはない。クロージャで包むか、属性マクロ(#[tokio::main] など)を使う クロージャ、手続き的マクロ
import とモジュール mod と use モジュール
PyPIのパッケージ crates.ioのクレート Cargoとクレート
if __name__ == "__main__": fn main() Rustを始める
threading(GILあり) std::thread:本当に並列に動くスレッド 並行処理
multiprocessing、concurrent.futures スレッド、または並列イテレータを提供する rayon クレート 並行処理、共有状態
asyncio、async def、await async fn と .await。tokioのようなランタイムと一緒に使う async/await
7 // 2、2 ** 10 7 / 2(整数どうしなら整数の割り算)、2_i64.pow(10) 演算子
int("42")、str(42) "42".parse::<i32>()(Result を返す)、42.to_string() 型変換、型変換(From・Into)

Pythonの組み込みコレクションは、どれもRustの標準ライブラリに対応するものがあります。大きな違いは2つです。Rustのコレクションには1種類の型しか入れられないこと(Vec<i32> には i32 だけ)、そして HashMap は挿入した順番を覚えていないことです。

Python Rust 詳しくは
list Vec<T> ベクタ
tuple タプル (i32, String)、またはフィールドに名前を付けた小さな struct タプル
dict HashMap<K, V>(順序は決まっていない!) HashMap
キーを並べたままにしたい dict BTreeMap<K, V> HashMap
挿入順を保ちたい dict indexmap クレート
set、frozenset HashSet<T>、BTreeSet<T> HashSetとBTreeMap
collections.Counter、defaultdict entry APIを使った HashMap:*map.entry(k).or_insert(0) += 1 HashMap
collections.deque VecDeque<T> VecDequeとBinaryHeap
heapq(小さいものから出る) BinaryHeap<T>(大きいものから出る。小さいものから出したいなら Reverse で包む) VecDequeとBinaryHeap
namedtuple struct 構造体
bytes、bytearray &[u8]、Vec<u8> スライス
スライス:xs[1:3] スライス:&xs[1..3](負のインデックスはない。xs.len() - 1 を使う) スライス

ここで少しほっとするPython開発者は多いはずです。Rustには公式のツールがひとつ、Cargoがあり、pip、venv、setuptools、pytestなどの仕事をまとめてこなします。「どのパッケージ管理ツールを使う?」という議論はありません。

Python Rust メモ
python インタプリタ rustc コンパイラ。ふつうは cargo 経由で呼ぶ まずコンパイルしてから、ネイティブのバイナリを実行する。Rustを始めるを参照
pyenv rustup(バージョンを固定するなら rust-toolchain.toml ファイル) Rustのツールと環境構築を参照
pip、poetry、uv cargo ビルド、実行、テスト、パッケージの追加、公開。Cargoとクレートを参照
pip install requests cargo add reqwest このプロジェクトの Cargo.toml にだけクレートを追加する
pyproject.toml / requirements.txt Cargo.toml
poetry.lock / uv.lock Cargo.lock 自動で作られる
venv / virtualenv 必要ない どのプロジェクトも自分専用の依存関係を持ち、自分の target/ フォルダにビルドされる
PyPI crates.io 公開されたすべてのクレートのAPIドキュメントが docs.rs にある
python main.py cargo run(全速力で動かすなら cargo run --release) デバッグビルドは実行がかなり遅い。速度はリリースモードで測る
pipx install ruff cargo install ripgrep コマンドラインツールをインストールする
PyInstaller cargo build --release ネイティブのバイナリがひとつできる。ユーザーはRustをインストールしなくてよい
black cargo fmt 全員にひとつの標準スタイル
ruff、pylint、flake8 cargo clippy 役に立つリントが何百もある公式ツール
mypy、pyright コンパイラそのもの 型チェックは省略できず、別の手順でもない
pytest、unittest 組み込みの #[test] と cargo test(より速いランナーとして cargo nextest が人気) Rustのテストを参照
pytestのフィクスチャと parametrize rstest
hypothesis proptest プロパティベーステスト。テストをもっと活用するを参照
unittest.mock mockall、またはトレイトを実装した手書きのフェイク
timeit、pytest-benchmark criterion パフォーマンスを参照
cProfile、py-spy cargo flamegraph、perf、または任意のネイティブプロファイラ
docstring + Sphinx Markdownで書く /// ドキュメントコメント + cargo doc ドキュメント内のコード例はテストとして実行される。Rustのドキュメントを参照
PythonのREPL、Jupyter Rust Playground、evcxr_repl と evcxr_jupyter Rustには組み込みのREPLがない。試すときは小さな cargo run プロジェクトを作るのがふつう
VS Code + Pylance、PyCharm VS Code + rust-analyzer、またはJetBrains RustRover rust-analyzerで補完、定義へのジャンプ、その場でのエラー表示が使える

pyproject.toml と比べられるように、小さな Cargo.toml を載せておきます。

[package]
name = "weather-cli"
version = "0.1.0"
edition = "2021"
[dependencies]
reqwest = { version = "0.13", features = ["blocking", "json"] }
serde = { version = "1", features = ["derive"] }
clap = { version = "4", features = ["derive"] }

ライブラリとフレームワーク:「代わりに何を使えばいいの?」

Section titled “ライブラリとフレームワーク:「代わりに何を使えばいいの?」”

「requests、FastAPI、pandasを使っているけど、Rust版は何?」という質問ですね。ここでは、いちばん近いものを紹介します。Pythonのエコシステムはずっと歴史が長く大きいので、分野によっては(特にデータサイエンス)、Rustの選択肢は少なく、まだ若いものばかりです。

Python Rust メモ
Flask、FastAPI axum(RustとAxumで作るWeb APIで実際に作ります)、actix-web ハンドラは普通の async fn。リクエストとレスポンスの型は自分の構造体から作られ、pydanticを使ったFastAPIに少し似ている
Django 完全に対応するものはない。Loco(loco-rs)は全部入りのフレームワーク。多くのチームはaxum + ORM + テンプレートエンジンを組み合わせる 組み込みの管理画面はない
requests、httpx reqwest(非同期。ブロッキング版のクライアントもオプションである)、ureq(シンプルでブロッキング) Rustのネットワークを参照
aiohttp reqwest + tokio
Jinja2 minijinja(Jinjaの作者本人によるもの)、tera、askama(テンプレートをコンパイル時にチェックする)
BeautifulSoup scraper
Python Rust メモ
SQLAlchemy ORM Diesel(クエリはコンパイル時に型チェックされる)、SeaORM(非同期、エンティティ中心) Rustのデータベースでは、両方を動くコード付きで紹介している
psycopg、生のSQL sqlx:普通のSQLを書くと、コンパイル時に実際のデータベースと照らし合わせてチェックされる 非同期で、とても人気。ORMではない
sqlite3 rusqlite
redis-py redis
Celery ジョブキューなら apalis、プロセス内のバックグラウンドタスクなら tokio::spawn
Python Rust メモ
pandas Polars Rustで書かれている。すでに知っているかもしれないPython版のAPIも、同じRustのライブラリの上に作られている
NumPy ndarray スライスやブロードキャストに対応したN次元配列。NumPyに比べると周辺機能はずっと少ない
scikit-learn linfa、smartcore アルゴリズムが少なく、コミュニティもずっと小さい
PyTorch candle(クレート名は candle-core)、burn モデルを動かすのには向いている。学習(トレーニング)はまだほとんどがPythonで行われている
matplotlib plotters
csv モジュール csv クレート + serde
Python Rust メモ
json serde + serde_json 定番。TOML、YAMLなどにも対応。RustでJSONを扱うを参照
pydantic パースには serde、検証ルールには validator か garde 構造体そのものがスキーマになる
argparse、click、typer clap 引数を構造体として定義する。Rustとclapで作るコマンドラインツールを参照
logging tracing、または log + env_logger ログ出力を参照
re regex 先読み・後読みや後方参照はないが、線形時間で動くことが保証されている。それらが必要なら fancy-regex。Rustの正規表現を参照
datetime chrono または jiff Rustの日付と時刻を参照
random rand Rustの乱数を参照
asyncio tokio async/awaitを参照
multiprocessing.Pool.map rayon:.iter() を .par_iter() に変えるだけ
itertools ほとんどは Iterator に組み込まれている。残りは itertools クレートが追加する
os、pathlib、subprocess std::env、std::path、std::fs、std::process ファイル入出力、環境変数とプロセスを参照

並べて比較:クラスは struct + impl になる

Section titled “並べて比較:クラスは struct + impl になる”

Pythonで書いた小さなクラスです。

class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
def __str__(self):
return f"{self.owner} has {self.balance}"
account = Account("Ada", 100)
account.deposit(50)
print(account)

Rustではこうなります。

use std::fmt;
struct Account {
owner: String,
balance: i64,
}
impl Account {
// `new` はただの慣習で、キーワードではありません。デフォルト引数はないので、
// 呼び出す側はいつも両方の値を渡します。
fn new(owner: &str, balance: i64) -> Self {
Account { owner: owner.to_string(), balance }
}
// `&mut self`:このメソッドは口座を変更します。
fn deposit(&mut self, amount: i64) {
self.balance += amount;
}
}
// これがRust版の __str__ です。
impl fmt::Display for Account {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}の残高は{}です", self.owner, self.balance)
}
}
fn main() {
let mut account = Account::new("Ada", 100);
account.deposit(50);
println!("{account}");
}

実行結果:

Adaの残高は150です
▶ 実際に試してみよう

いくつか違いがあります。フィールドは最初に型と一緒に宣言するので、Pythonの account.nickname = "A" のように、あとから新しい属性を足すことはできません。データ(struct)と振る舞い(impl)は別々のブロックに分かれています。どのメソッドも、値を読むだけ(&self)なのか変更する(&mut self)のかを宣言します。そして deposit は口座を変更するので、変数は let mut で宣言しなければなりません。Rustにはデフォルト引数がありません。残高ゼロから始まる口座がほしいなら、fn empty(owner: &str) -> Self のような関数をもうひとつ追加します。

並べて比較:ダックタイピングはトレイトになる

Section titled “並べて比較:ダックタイピングはトレイトになる”

Pythonは、オブジェクトがどのクラスかは気にしません。正しいメソッドを持っているかどうかだけを気にします。

class Duck:
def speak(self):
return "Quack"
class Robot:
def speak(self):
return "Beep boop"
def introduce(thing):
print(f"It says: {thing.speak()}")
for thing in [Duck(), Robot()]:
introduce(thing)
introduce(42) # AttributeError。ただし、この行が実行されたときに初めてわかる

Rustでは、「speak メソッドを持っている」ことをトレイトとして書き表し、それぞれの型が impl ブロックで名乗りを上げます。

trait Speak {
fn speak(&self) -> String;
}
struct Duck;
struct Robot;
impl Speak for Duck {
fn speak(&self) -> String {
"ガーガー".to_string()
}
}
impl Speak for Robot {
fn speak(&self) -> String {
"ピポパポ".to_string()
}
}
// Speak を実装した型なら何でも受け取ります。
fn introduce(thing: &dyn Speak) {
println!("「{}」と言っています", thing.speak());
}
fn main() {
let things: Vec<Box<dyn Speak>> = vec![Box::new(Duck), Box::new(Robot)];
for thing in &things {
introduce(thing.as_ref());
}
// introduce(&42); // コンパイルエラー:`{integer}` は Speak を実装していません
}

実行結果:

「ガーガー」と言っています
「ピポパポ」と言っています
▶ 実際に試してみよう

考え方はダックタイピングのままですが、コンパイラがチェックしてくれます。introduce(&42) は、本番環境での AttributeError ではなく、プログラムを実行する前のエラーになります。typing.Protocol を使ったことがあれば、トレイトはすぐになじむはずです。型が混ざったリストには Box<dyn Speak> が必要です。それぞれの要素に対してメソッドをどう呼ぶのかを、Rustが知っておく必要があるからです(トレイトオブジェクトを参照)。また、自分で定義したトレイトを、自分が書いていない型、たとえば String や i32 に実装することもできます。

並べて比較:None は Option になる

Section titled “並べて比較:None は Option になる”

Pythonでは、どんな値もこっそり None になりえます。それがわかるのは、使ったときです。

def find_email(user_id):
emails = {1: "[email protected]", 2: "[email protected]"}
return emails.get(user_id) # 見つからなければ None を返す
print(find_email(1).upper())
print(find_email(3).upper()) # AttributeError: 'NoneType' object has no attribute 'upper'

Rustでは、値がないかもしれないものは Option<T> 型になります。None の場合を処理するまでは、普通の値として使うことができません。

fn find_email(user_id: u32) -> Option<String> {
match user_id {
1 => Some("[email protected]".to_string()),
2 => Some("[email protected]".to_string()),
_ => None,
}
}
fn main() {
for id in [1, 3] {
match find_email(id) {
Some(email) => println!("{id}: {}", email.to_uppercase()),
None => println!("{id}: メールアドレスが登録されていません"),
}
}
// Pythonの `find_email(3) or "unknown"` と同じです。
let email = find_email(3).unwrap_or("不明".to_string());
println!("{email}");
// 値があれば、中身を変換します。
let upper = find_email(2).map(|e| e.to_uppercase());
println!("{upper:?}");
}

実行結果:

3: メールアドレスが登録されていません
不明
▶ 実際に試してみよう

Rustのコンパイラは、Option<String> に対して .to_uppercase() を呼ぶことを許しません。だから「'NoneType' object has no attribute」のエラーは、そもそも起きようがないのです。Rustの String は、いつでも本物の文字列です。便利なヘルパーメソッドはOptionのページで、if let Some(email) = ... という短い書き方はif let / while letで学べます。

並べて比較:例外は Result と ? になる

Section titled “並べて比較:例外は Result と ? になる”

Pythonでは、int() が例外を投げ、その例外は呼び出し元のどこかでキャッチします。

def parse_port(text):
port = int(text) # ValueError を投げるかもしれない
if not 1 <= port <= 65535:
raise ValueError(f"port {port} is out of range")
return port
for text in ["8080", "http", "70000"]:
try:
print(f"port {parse_port(text)}")
except ValueError as e:
print(f"error: {e}")

Rustでは、失敗する可能性のある関数は、そのことを戻り値の型で示します。? 演算子は「失敗していたら、今すぐそのエラーを呼び出し元に返す」という意味です。例外を上に伝えていくのと同じ役割ですが、それがコードの上で目に見えます。

fn parse_port(text: &str) -> Result<u32, String> {
let port: u32 = text
.parse()
.map_err(|e| format!("{text:?} は数値ではありません({e})"))?;
if !(1..=65535).contains(&port) {
return Err(format!("ポート {port} は範囲外です"));
}
Ok(port)
}
fn main() {
for text in ["8080", "http", "70000"] {
match parse_port(text) {
Ok(port) => println!("ポート {port}"),
Err(e) => println!("エラー: {e}"),
}
}
}

実行結果:

ポート 8080
エラー: "http" は数値ではありません(invalid digit found in string)
エラー: ポート 70000 は範囲外です
▶ 実際に試してみよう

invalid digit found in string は標準ライブラリが返す英語のメッセージで、「文字列に数字として不正な文字が含まれています」という意味です。

大きな違いはここです。Pythonではどの関数も例外を投げるかもしれず、シグネチャを見てもわかりません。Rustでは Result がシグネチャの一部なので、呼び出す側が処理を忘れることがありません。学習中は、エラーの型が String でもかまいません。実際のプロジェクトでは独自のエラー用enum(独自のエラー型)や、「どんな例外でも投げる」に近い感覚で使える anyhow クレート(anyhowとthiserror)を使います。

並べて比較:内包表記と辞書はイテレータと HashMap になる

Section titled “並べて比較:内包表記と辞書はイテレータと HashMap になる”

リスト内包表記と辞書を使った数え上げは、Pythonでは日常的なコードです。

text = "the cat sat on the mat the end"
loud = [w.upper() for w in text.split() if len(w) == 3]
print(loud)
counts = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
top = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))[:3]
for word, n in top:
print(f"{word}: {n}")

Rustでは、内包表記は collect() で終わるイテレータのチェーンに、辞書はHashMapになります。

use std::collections::HashMap;
fn main() {
let text = "the cat sat on the mat the end";
// [w.upper() for w in text.split() if len(w) == 3]
let loud: Vec<String> = text
.split_whitespace()
.filter(|w| w.len() == 3)
.map(|w| w.to_uppercase())
.collect();
println!("{loud:?}");
// counts[word] = counts.get(word, 0) + 1
let mut counts: HashMap<&str, u32> = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word).or_insert(0) += 1;
}
// 多いものから順に。同じ数ならアルファベット順。
let mut top: Vec<(&str, u32)> = counts.into_iter().collect();
top.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
for (word, n) in top.iter().take(3) {
println!("{word}: {n}");
}
}

実行結果:

["THE", "CAT", "SAT", "THE", "MAT", "THE", "END"]
the: 3
cat: 1
end: 1
▶ 実際に試してみよう

この例の文章は英語のままにしています。w.len()(バイト数)やアルファベット順の並べ替えを使っているので、日本語にすると結果が変わってしまうからです。

チェーンは内側から外側ではなく、左から右へ読みますが、考え方は同じです。ジェネレータ式と同じく遅延評価(lazy)で、collect()、sum()、count()、for ループなどが値を取り出すまで、何も起きません。collect() は何を作ればいいのかを知る必要があるので、loud には Vec<String> という型を付けています。並べ替えの手順にも注目してください。Pythonの dict は挿入した順番を覚えていますが、Rustの HashMap は覚えていません。そのため、表示する前に必ず並べ替えています。並べ替えについては並べ替えと検索のページで説明しています。

並べて比較:GILと本物のスレッド

Section titled “並べて比較:GILと本物のスレッド”

標準のCPythonでは、グローバルインタプリタロック(GIL)のせいで、一度にPythonのバイトコードを実行できるスレッドはひとつだけです。ネットワークを待つ処理ならスレッドで速くなりますが、次のようなCPU中心の処理は、スレッドを増やしても速くなりません。そのため、Python開発者は代わりに multiprocessing を使います。

from concurrent.futures import ThreadPoolExecutor
def is_prime(n):
if n < 2:
return False
d = 2
while d * d <= n:
if n % d == 0:
return False
d += 1
return True
def count_primes(lo, hi):
return sum(1 for n in range(lo, hi) if is_prime(n))
ranges = [(0, 25_000), (25_000, 50_000), (50_000, 75_000), (75_000, 100_000)]
with ThreadPoolExecutor(4) as pool:
counts = list(pool.map(lambda r: count_primes(*r), ranges))
print("total:", sum(counts))

RustにはGILがありません。thread::spawn のひとつひとつが本物のOSスレッドで、別々のCPUコアで同時に動きます。

use std::thread;
fn is_prime(n: u32) -> bool {
if n < 2 {
return false;
}
let mut d = 2;
while d * d <= n {
if n % d == 0 {
return false;
}
d += 1;
}
true
}
fn count_primes(lo: u32, hi: u32) -> usize {
(lo..hi).filter(|&n| is_prime(n)).count()
}
fn main() {
let ranges = [(0, 25_000), (25_000, 50_000), (50_000, 75_000), (75_000, 100_000)];
// 範囲ごとに専用のOSスレッドを作ります。本当に同時に動きます。
let counts: Vec<usize> = thread::scope(|s| {
let handles: Vec<_> = ranges
.iter()
.map(|&(lo, hi)| s.spawn(move || count_primes(lo, hi)))
.collect();
handles.into_iter().map(|h| h.join().unwrap()).collect()
});
for ((lo, hi), count) in ranges.iter().zip(&counts) {
println!("{lo}..{hi}: 素数は{count}個");
}
println!("合計: {}", counts.iter().sum::<usize>());
}

実行結果:

0..25000: 素数は2762個
25000..50000: 素数は2371個
50000..75000: 素数は2260個
75000..100000: 素数は2199個
合計: 9592
▶ 実際に試してみよう

thread::scope は、すべてのスレッドが終わるのを待ってから戻るので、スレッドはローカルなデータを安全に借用できます。GILがないと、2つのスレッドが同じ値を同時に変更してしまわないか心配になるかもしれません。Rustはそれをコンパイル時に防ぎます。変更される共有データは Arc<Mutex<T>> のようなもので包まなければならず、そうしないとプログラムがコンパイルできません(共有状態を参照)。実際のプロジェクトでは、rayon クレートを使うともっと短く書けます。スレッドを使う multiprocessing.Pool.map のようなものです。

use rayon::prelude::*;
let total = (0..100_000u32).into_par_iter().filter(|&n| is_prime(n)).count();

Python側も変わりつつあります。Python 3.13では、GILのない「フリースレッド」版のビルドがオプションとして追加されました。ただし、ほとんどの人がインストールしてデプロイしている標準のCPythonにはまだGILがありますし、多くのC拡張はまだフリースレッドに対応していません。

RustでPythonの拡張モジュールを書く(PyO3 + maturin)

Section titled “RustでPythonの拡張モジュールを書く(PyO3 + maturin)”

Rustを使うために、Pythonをやめる必要はありません。PyO3(英語)を使うとPythonのモジュールをRustで書くことができ、maturin(英語)がそれを pip でインストールできる普通のwheelにビルドしてくれます。Polars、pydantic-core、そのほか多くの高速なPythonライブラリは、この方法で作られています。

(有効にした仮想環境の中で)プロジェクトを用意します。

Terminal window
pip install maturin
maturin new --bindings pyo3 fastmath
cd fastmath

Cargo.toml と pyproject.toml はmaturinが書いてくれます。あとは、Rustの関数を src/lib.rs に書くだけです。

use pyo3::prelude::*;
/// Rustで書いたPythonモジュール。
#[pymodule]
mod fastmath {
use pyo3::prelude::*;
/// 数値のリストの二乗和を求めます。
#[pyfunction]
fn sum_of_squares(numbers: Vec<i64>) -> i64 {
numbers.iter().map(|n| n * n).sum()
}
/// テキストに含まれる単語を数えます。
#[pyfunction]
fn word_count(text: &str) -> usize {
text.split_whitespace().count()
}
}

maturin develop で仮想環境の中にビルドし(速さが大事なら --release を付けます)、ほかのモジュールと同じようにPythonから使います。

import fastmath
print(fastmath.sum_of_squares([1, 2, 3, 4])) # 30
print(fastmath.word_count("the quick brown fox")) # 4
fastmath.sum_of_squares(["a"]) # TypeError: 'str' object cannot be interpreted as an integer

型の変換はPyO3がやってくれます。整数が入ったPythonの list は Vec<i64> に、str は &str になり、ドキュメントコメントはPythonのdocstringになります。間違った型を渡すと、普通のPythonの TypeError になります。公開する準備ができたら、maturin build --release でwheelを作り、maturin publish でPyPIにアップロードします。コツは、まず計測して、ホットなループだけをRustに移すことです。PythonとRustの間を行き来するのには少しコストがかかるので、たくさんの仕事をする1回の呼び出しのほうが、100万回の小さな呼び出しよりも有利です。

Python開発者がつまずきやすいところ

Section titled “Python開発者がつまずきやすいところ”

Pythonから来た人は、みんな同じところでつまずきます。前もって知っておくだけで、ずいぶん楽になります。

Pythonでは、b = a と書くと、同じリストに2つの名前が付きます。Rustでは、let b = a; は Vec や String の所有権を b にムーブし、a はもう使えなくなります。

fn main() {
let a = vec![1, 2, 3];
let mut b = a;
b.push(4);
println!("{a:?}");
}
error[E0382]: borrow of moved value: `a`
--> src/main.rs:5:16
|
2 | let a = vec![1, 2, 3];
| - move occurs because `a` has type `Vec<i32>`, which does not implement the `Copy` trait
3 | let mut b = a;
| - value moved here
4 | b.push(4);
5 | println!("{a:?}");
| ^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let mut b = a.clone();
| ++++++++

このエラーは「ムーブ済みの値 a を借用しようとしている」という意味で、help の行は「性能上のコストを許容できるなら、値のクローンを検討してください」と提案しています。直すには、借用する(読むだけなら let b = &a;)か、意図して本物のコピーを作ります(a.clone())。この所有権のルールこそが、あなたにとっていちばん大事な新しい考え方です。そのページと借用は、ゆっくり読んでください。

  • 整数にはサイズがあります。 Pythonの int はオーバーフローしません。Rustの i32 の上限は2,147,483,647で、それを超えるとデバッグビルドではパニックします。大きな数には i64 や u64 を選びましょう。整数オーバーフローを見てください。
  • 割り算と剰余の動きが違います。 Rustでは 7 / 2 は 3 です。両方が整数だからです(Pythonの // にあたります)。3.5 がほしいなら 7.0 / 2.0 と書きます。また、-7 % 3 はPythonでは 2 ですが、Rustでは -1 です。Pythonと同じ答えがほしいなら (-7_i32).rem_euclid(3) を使います。
  • 数値の型を混ぜられません。 let x: f64 = 2.5 * 3; はコンパイルできません。3 が整数だからです。3.0 と書くか、as f64 で変換します(型変換を参照)。
  • 文字列にインデックスでアクセスできません。 word[0] はコンパイルエラーです。Rustの文字列はUTF-8で、1つの「文字」が数バイトになることがあるからです。word.chars().next() や word.chars().nth(i) を使いましょう。Rustの文字列を見てください。
  • String と &str。 Pythonの文字列型は str ひとつですが、Rustには2つあります。String はテキストを所有し、&str はテキストを借用します。関数はたいてい &str を受け取り、構造体はたいてい String を持ちます。
  • 変数はデフォルトで不変です。 値を変更する必要があるときは let mut と書きます。忘れていたら、コンパイラが教えてくれます。
  • 波かっこ({ })とセミコロン。 Rustにとってインデントは意味を持たず、意味を持つのは { } です。また、関数の最後の行にセミコロンを付けないと、それが戻り値になります:fn double(x: i32) -> i32 { x * 2 }。
  • if と match は、もれなく、型も正しく書く必要があります。 真偽値としての評価(truthiness)はありません。if count はコンパイルできないので、if count > 0 と書きます。match はすべてのケースを網羅しなければなりません。
  • HashMapには順序がありません。 Pythonの dict は挿入した順番を覚えていますが、Rustの HashMap は覚えていません。表示する前に並べ替えるか、BTreeMap を使うか、indexmap クレートを追加しましょう。
  • コンパイルの手順があります。 REPLはなく、多くのクレートを使うプロジェクトのクリーンビルドには時間がかかります。編集中は cargo check(コード生成を省略するので、ずっと速い)を使い、速度を測るときは cargo run --release を使いましょう。
  • 借用チェッカーに押し返されます。 たくさんのオブジェクトがひとつの共有オブジェクトを持って変更する、オブジェクトが親を指し返す、といったPythonではごく普通のパターンには、スマートポインタか別の設計が必要になります。最初の数週間はイライラするかもしれませんが、そのあとはずっと楽になります。

Python開発者のための学習ルート

Section titled “Python開発者のための学習ルート”

このサイトを順番どおりに読む必要はありません。Python開発者なら、次のルートで進むと、Rustが今までの知識といちばん違う部分に時間をかけられます。

  1. Rustを始めるとRustのツールと環境構築:Rustをインストールして cargo new を実行します。
  2. Rustの変数、Rustのデータ型、整数オーバーフロー:型は、最初に身につける新しい習慣です。
  3. 関数、if…else、ループ:さっと読めます。式であることと、セミコロンがないところに注目しましょう。
  4. 所有権、次に借用、そしてスライス:あなたにとっていちばん大事なページです。じっくり時間をかけてください。
  5. Rustの文字列:String と &str の違いと、文字列にインデックスでアクセスできない理由。
  6. 構造体と列挙型、次にmatchとパターンマッチング。
  7. OptionとResult、次に独自のエラー型。
  8. ベクタ、HashMap、HashSetとBTreeMap。
  9. クロージャとイテレータ:これがあなたの内包表記とジェネレータです。
  10. トレイト、ジェネリクス、よく使うトレイト、トレイトオブジェクト。
  11. モジュール、Cargoとクレート、テスト。
  12. ライフタイムとスマートポインタ。
  13. 並行処理、共有状態、async/await。
  14. RustでJSONを扱う。そのあとclapでコマンドラインツールかWeb APIを作ってみましょう。

どちらの問題も、Pythonのコード片をRustに移植(ポーティング)します。

このPythonのコードを移植してください。ヒント: 名前を順番どおりに表示するには、Vec<&str> に集めて .sort() を呼び、それから .join(", ") します。平均を求めるときは、割り算の前に as f64 で変換しましょう。

scores = {"Ada": 92, "Bob": 47, "Cy": 75, "Dee": 38}
passed = sorted(name for name, score in scores.items() if score >= 50)
print("passed:", ", ".join(passed))
odd_squares = [n * n for n in range(1, 10) if n % 2 == 1]
print(odd_squares)
average = sum(scores.values()) / len(scores)
print(f"average: {average:.1f}")
解答を見る
use std::collections::HashMap;
fn main() {
let scores: HashMap<&str, u32> =
HashMap::from([("Ada", 92), ("Bob", 47), ("Cy", 75), ("Dee", 38)]);
let mut passed: Vec<&str> = scores
.iter()
.filter(|(_, score)| **score >= 50)
.map(|(name, _)| *name)
.collect();
passed.sort();
println!("合格: {}", passed.join(", "));
let odd_squares: Vec<u32> = (1..10).filter(|n| n % 2 == 1).map(|n| n * n).collect();
println!("{odd_squares:?}");
let total: u32 = scores.values().sum();
let average = total as f64 / scores.len() as f64;
println!("平均: {average:.1}");
}

実行結果:

合格: Ada, Cy
[1, 9, 25, 49, 81]
平均: 63.0
▶ 実際に試してみよう

HashMap をイテレートすると参照のペアが得られます。**score や *name と書いているのはそのためで、参照をたどって値を取り出しています。ここでの sort() は省略できません。並べ替えないと、名前がどんな順番で出てくるかわからないからです。そして total as f64 が必要なのは、Rustが整数を勝手に浮動小数点数に変えることは決してないからです。

このPythonのコードを移植してください。各行は 名前: 年齢 の形式です。ヒント: line.split_once(':') は Option<(&str, &str)> を返します。.ok_or(...) で Result に変えてから、? を使いましょう。関数の戻り値は Result<(String, u32), String> にします。

def parse_line(line):
if ":" not in line:
raise ValueError("missing ':'")
name, age = line.split(":", 1)
try:
return name.strip(), int(age.strip())
except ValueError as e:
raise ValueError(f"bad age: {e}")
ages = {}
for line in ["Ada: 36", "Grace 45", "Linus: forty"]:
try:
name, age = parse_line(line)
ages[name] = age
print(f"ok: {name} is {age}")
except ValueError as e:
print(f"skipped {line!r}: {e}")
print(len(ages), "saved")
解答を見る
use std::collections::HashMap;
fn parse_line(line: &str) -> Result<(String, u32), String> {
let (name, age) = line.split_once(':').ok_or("':' がありません")?;
let age: u32 = age.trim().parse().map_err(|e| format!("年齢が不正です: {e}"))?;
Ok((name.trim().to_string(), age))
}
fn main() {
let mut ages = HashMap::new();
for line in ["Ada: 36", "Grace 45", "Linus: forty"] {
match parse_line(line) {
Ok((name, age)) => {
println!("OK: {name}は{age}歳");
ages.insert(name, age);
}
Err(e) => println!("スキップ {line:?}: {e}"),
}
}
println!("{}件を保存しました", ages.len());
}

実行結果:

OK: Adaは36歳
スキップ "Grace 45": ':' がありません
スキップ "Linus: forty": 年齢が不正です: invalid digit found in string
1件を保存しました
▶ 実際に試してみよう

2つの raise は Err の値になり、try/except は match になりました。ok_or("':' がありません") のあとの ? は、エラーが &str で関数のエラー型が String でもちゃんと動きます。? が From を使ってエラーを変換してくれるからです。Ok の中の順番にも注目してください。先に表示してから、insert で name をマップにムーブしています。この2行を入れ替えると、name はムーブ済みだとコンパイラが教えてくれます。

  • Pythonの多くはそのまま活かせます。イテレータを回すループ、タプル、クロージャ、match、そして内包表記のようなデータ処理(イテレータのチェーンとして)です。
  • 大きな違いは、実行前にチェックされる静的な型、ガベージコレクタの代わりに所有権、None の代わりに Option、例外の代わりに Result + ? を使うことです。
  • クラスは struct + impl に、ダックタイピングはトレイトに、with ブロックは自動で動く Drop になります。HashMap には順序がなく、整数にはサイズがあります。
  • RustにはGILがありません。スレッドは並列に動き、データ競合はコンパイラが防いでくれます。
  • Cargoがpip、venvなどの代わりになり、cargo fmt、cargo clippy、cargo test がblack、ruff、pytestの代わりになります。
  • 2つの言語は混ぜて使えます。PyO3とmaturinを使えば、RustのコードがPythonのモジュールになります。

理解度チェック

1. Pythonの None の代わりに、Rustでは何を使いますか?

2. Pythonでは、b = a で同じリストに2つの名前が付きます。Rustで Vec に対して let b = a; と書くと、どうなりますか?

3. Rustで、両方の数が整数のとき、7 / 2 の値はいくつになりますか?

4. Pythonプロジェクトの遅い関数をひとつ、Rustで速くしたいと考えています。どのツールを使いますか?

スコア: 0 / 4

  • The Rust Programming Language 日本語版:公式の本の日本語訳です。Python開発者にとって特に大事なのは、4章(所有権)、6章(enumと Option)、9章(エラー処理)、13章(クロージャとイテレータ)です。
  • PyO3 user guide(英語):RustをPythonから呼ぶ方法(とPythonをRustから呼ぶ方法)のすべてがわかります。
  • Maturin user guide(英語):Rustで作ったPythonパッケージのビルド、テスト、公開の方法です。
  • Rust by Example 日本語版:短くて実行できる例がたくさん載っています。コードを読んで学ぶのが好きな人に向いています。
  • Are We Learning Yet?(英語):Rustの機械学習・データ関連のクレートをまとめたサイトです。