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を見てください。
言語の概念:PythonからRustへ
Section titled “言語の概念:PythonからRustへ”この表は、旅行の会話帳のように使ってください。いちばん右の列は、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) |
コレクション
Section titled “コレクション”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 を使う) |
スライス |
ツール:pipからcargoへ
Section titled “ツール:pipからcargoへ”ここで少しほっとする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の選択肢は少なく、まだ若いものばかりです。
Webとネットワーク
Section titled “Webとネットワーク”| 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 |
データとデータベース
Section titled “データとデータベース”| Python | Rust | メモ |
|---|---|---|
| SQLAlchemy ORM | Diesel(クエリはコンパイル時に型チェックされる)、SeaORM(非同期、エンティティ中心) | Rustのデータベースでは、両方を動くコード付きで紹介している |
| psycopg、生のSQL | sqlx:普通のSQLを書くと、コンパイル時に実際のデータベースと照らし合わせてチェックされる | 非同期で、とても人気。ORMではない |
| sqlite3 | rusqlite |
|
| redis-py | redis |
|
| Celery | ジョブキューなら apalis、プロセス内のバックグラウンドタスクなら tokio::spawn |
データサイエンスと機械学習
Section titled “データサイエンスと機械学習”| 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 |
日常的な部品
Section titled “日常的な部品”| 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): 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 { _ => 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: メールアドレスが登録されていません不明Some("[email protected]")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: 3cat: 1end: 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個合計: 9592thread::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ライブラリは、この方法で作られています。
(有効にした仮想環境の中で)プロジェクトを用意します。
pip install maturinmaturin new --bindings pyo3 fastmathcd fastmathCargo.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])) # 30print(fastmath.word_count("the quick brown fox")) # 4fastmath.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から来た人は、みんな同じところでつまずきます。前もって知っておくだけで、ずいぶん楽になります。
代入は値をムーブする
Section titled “代入は値をムーブする”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` trait3 | let mut b = a; | - value moved here4 | 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())。この所有権のルールこそが、あなたにとっていちばん大事な新しい考え方です。そのページと借用は、ゆっくり読んでください。
そのほかの注意点
Section titled “そのほかの注意点”- 整数にはサイズがあります。 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が今までの知識といちばん違う部分に時間をかけられます。
- Rustを始めるとRustのツールと環境構築:Rustをインストールして
cargo newを実行します。 - Rustの変数、Rustのデータ型、整数オーバーフロー:型は、最初に身につける新しい習慣です。
- 関数、if…else、ループ:さっと読めます。式であることと、セミコロンがないところに注目しましょう。
- 所有権、次に借用、そしてスライス:あなたにとっていちばん大事なページです。じっくり時間をかけてください。
- Rustの文字列:
Stringと&strの違いと、文字列にインデックスでアクセスできない理由。 - 構造体と列挙型、次にmatchとパターンマッチング。
- OptionとResult、次に独自のエラー型。
- ベクタ、HashMap、HashSetとBTreeMap。
- クロージャとイテレータ:これがあなたの内包表記とジェネレータです。
- トレイト、ジェネリクス、よく使うトレイト、トレイトオブジェクト。
- モジュール、Cargoとクレート、テスト。
- ライフタイムとスマートポインタ。
- 並行処理、共有状態、async/await。
- RustでJSONを扱う。そのあとclapでコマンドラインツールかWeb APIを作ってみましょう。
どちらの問題も、Pythonのコード片をRustに移植(ポーティング)します。
1. 内包表記と辞書を移植する
Section titled “1. 内包表記と辞書を移植する”この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.0HashMap をイテレートすると参照のペアが得られます。**score や *name と書いているのはそのためで、参照をたどって値を取り出しています。ここでの sort() は省略できません。並べ替えないと、名前がどんな順番で出てくるかわからないからです。そして total as f64 が必要なのは、Rustが整数を勝手に浮動小数点数に変えることは決してないからです。
2. try/except を Result に移植する
Section titled “2. try/except を Result に移植する”この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 string1件を保存しました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のモジュールになります。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. Pythonの None の代わりに、Rustでは何を使いますか?
2. Pythonでは、b = a で同じリストに2つの名前が付きます。Rustで Vec に対して let b = a; と書くと、どうなりますか?
3. Rustで、両方の数が整数のとき、7 / 2 の値はいくつになりますか?
4. Pythonプロジェクトの遅い関数をひとつ、Rustで速くしたいと考えています。どのツールを使いますか?
スコア: 0 / 4
さらに詳しく
Section titled “さらに詳しく”- 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の機械学習・データ関連のクレートをまとめたサイトです。