🔰

pythonをrustへ変換したときの対応をまとめる

に公開

もともとpythonのアプリがあって、それをrustへ書き直す作業をしていたことがありまして、
この記事ではその際につまづいた変換部分をまとめておきます。(随時追加予定)

それぞれの言語で特徴や目的が違うので、機能が対応しているわけではないです。
(例:pythonのdictをrustのstructに対応させるなど)

変換の定義
・python:処理が最後まで動けばOK。
・rust:テストが動けばOK

注意点
pythonの動的型付けとrustの静的型付けの違いは大きく、そのまま変換してもうまく動かないことがあります。
その場合、無理にrust側で柔軟な構造を定義するよりも、python側を修正する方が結果的に可読性の高いコードになることが多かったです。
(rust側で複雑な処理を作る必要が出てきたら、技術的負債がたまっているかも)

python dict 動的にkeyが決まる

config = {
    "key1": "value1",
    "key2": "value2",
}

print(config["key1"])
let config = HashMap::from([
    ("key1", "value1"),
    ("key2", "value2"),
]);

println!("{}", config["key1"]);

python dict 静的にkeyが決まる

dictのkeyが静的に決まる場合はrustのstructで定義してしまった方がvscodeの補完機能が効くので便利

config = {
    "key1": ["value1_1","value1_2","value1_3"],
    "key2": ["value2_1","value2_2","value2_3"],
}

print(config["key1"])

struct Config<'a> {
    key1: Vec<&'a str>,
    key2: Vec<&'a str>,
}

let config = Config {
    key1: vec!["value1_1", "value1_2", "value1_3"],
    key2: vec!["value2_1", "value2_2", "value2_3"],
};

println!("{:?}", config.key1);

python structured arrayの各行を繰り返し処理

pythonのpandasやnumpyでデータハンドリングをしているものについては、rustのstructによってrecordレベルの定義をしておく

import numpy as np

data = np.array([
    (1, 'Alice', 25),
    (2, 'Bob', 30),
    (3, 'Charlie', 35)
], dtype=[('id', 'i4'), ('name', 'U10'), ('age', 'i4')])

for row in data:
    print(row['id'],row['name'],row['age'])
pub struct DataFrame {
    pub rec: Vec<DtypesPerson>,
}

pub struct DtypesPerson {
    id: i32,
    name: String,
    age: i32,
}

let data = DataFrame {
    rec: vec![
        DtypesPerson { id: 1, name: String::from("Alice"), age: 25 },
        DtypesPerson { id: 2, name: String::from("Bob"), age: 30 },
        DtypesPerson { id: 3, name: String::from("Charlie"), age: 35 },
    ],
};

for row in &data.rec {
    println!("{} {} {}", row.id, row.name, row.age);
}

python if 単一の変数

x = 10

if x > 5:
    y = 20
else:
    y = 0

print(y)
let x = 10;

let y = if x > 5 {
    20
} else {
    0
};

println!("{}", y);

python if 複数の変数

x = 10

y = 0
z = 5

if x > 5:
    y = 20
    z = 30
else:
    y = 0

print(y,z)
let x = 10;

let mut y = 0; 
let mut z = 5; 

if x > 5 {
    y = 20;
    z = 30;
} else {
    y = 0; 
}

println!("{} {}", y, z);

python def 関数

import numpy as np

data = np.array([
    (1, 'Alice', 25, ''),
    (2, 'Bob', 30, ''),
    (3, 'Charlie', 35, '')
], dtype=[('id', 'i4'), ('name', 'U10'), ('age', 'i4'), ('greeting', 'U10')])


def add_greeting_column(data, suffixe):
    data["greeting"] = [i + suffixe for i in data["name"]]
    return data

suffixe = "!!!"
data_new = add_greeting_column(data, suffixe)

for row in data_new:
    print(row)


> (1, 'Alice', 25, 'Alice!!!')
> (2, 'Bob', 30, 'Bob!!!')
> (3, 'Charlie', 35, 'Charlie!!!')

↑のpythonコードではnumpyのdata_newを新たに作成しているけど、実は引数側のdataも書き変わっている
↓のrustだと所有権がdataからdata_newへ移動しているので、dataを再利用を防止できる


pub struct DataFrame {
    pub rec: Vec<DtypesPerson>,
}

pub struct DtypesPerson {
    id: i32,
    name: String,
    age: i32,
    greeting:String,
}

fn add_greeting_column(df: DataFrame,suffix: &String) -> DataFrame {
    let mut new_data = Vec::new();

    for row in &df.rec {
        new_data.push(DtypesPerson {
            id: row.id,
            name: row.name.clone(),
            age: row.age,
            greeting: row.name.clone() + &suffix,
        });
    }
    DataFrame { rec :new_data}
}

let data = DataFrame {
    rec: vec![
        DtypesPerson { id: 1, name: String::from("Alice"), age: 25, greeting: String::from("")},
        DtypesPerson { id: 2, name: String::from("Bob"), age: 30,  greeting: String::from("")},
        DtypesPerson { id: 3, name: String::from("Charlie"), age: 35,  greeting: String::from("")},
    ],
};

let suffix = "!!!".to_string();
let data_new = add_greeting_column(data, &suffix);


for row in &data_new.rec {
    println!("{:?} {:?} {:?} {:?}", row.id, row.name, row.age, row.greeting);
};

// 1 "Alice" 25 "Alice!!!"
// 2 "Bob" 30 "Bob!!!"
// 3 "Charlie" 35 "Charlie!!!"

主となる引数が1つあるようなケースだと、rustの所有権を移動する形で使うことが多い。(上の例だとdata:DataFrame)
そのほかちょっとした設定などの補助的なものは、それほど意識して区別していない(上の例だとsuffix)

pythonのデータでrustをテストする

pythonをrustに変換できたかどうか不安になったらテストを作りましょう。
pythonの出力結果をrustのtestsディレクトリのファイルへコードを埋め込みます。

import pandas as pd
import json

# jsonデータを作成
json_data = pd.DataFrame({"a": [1, 2, 3]}).to_dict("records")
json_str = json.dumps(json_data)

# rustコードを埋め込む
rust_code = f"""
#[cfg(test)]
mod tests {{
    use serde_json;
    use serde::Deserialize;

    #[derive(Debug, Deserialize, PartialEq)]
    struct Data {{
        a: i32
    }}

    #[test]
    fn test_json_parsing() {{
        let json_str = r#"{json_str}"#;
        let parsed: Vec<Data> = serde_json::from_str(json_str).expect("Failed to parse JSON");
        assert_eq!(parsed, vec![Data {{ a: 1 }}, Data {{ a: 2 }}, Data {{ a: 3 }}]);
    }}
}}
"""

# rustコードを出力
print(rust_code)
with open("./sample/tests/json_test.rs","w") as f:
    f.write(rust_code)

ディレクトリ構成
sampleディレクトリ配下にcargo initで初期化後に serdeの依存関係を追記します。


└── sample
    ├── Cargo.lock
    ├── Cargo.toml
    ├── src            <- cargo init で作成
    ├── target         <- cargo init で作成
    └── tests
        └── json_test.rs

依存関係の追記

with open("./sample/Cargo.toml","a") as f:
    f.write('''
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
''')

補足: この章は要点を絞るために、環境構築やテストの実行順序について逆転している部分があります。もし詳しく知りたい場合は、プロンプトでAIに補足してもらうと便利です。

~この章の全文~
上記の説明について実行順番にまとめてください。
GitHubで編集を提案

Discussion