Dasturlash

Rust va Python: PyO3 bilan birgalikda ishlash

13-sentabr, 2026, 22:280 ko'rish5 daqiqa o'qish
Rust va Python: PyO3 bilan birgalikda ishlash

Har safar Pydantic v2 bilan ma'lumotlarni tasdiqlasangiz, Rust kengaytmasi ishini bajaradi. Pydantic-core, bu dasturning asosi, PyO3 vositalari yordamida yaratilgan. Bu maqola shu turdagi ko'prikni yaratish jarayonini tushuntiradi: Rust tilida yozilgan JSON parserni Python ilovasida ishlatish.

Rust kodini Python ilovasida ishlatish

Rust kodini Python ilovasida ishlatish uchun to'rtta asosiy qadam bor:

Rust in Python: write a Rust module, annotate it with PyO3 macros, build it with maturin, import the shared library
  • Oddiy Rust modulini yozing.
  • Uni PyO3 makroslari bilan belgilang.
  • Maturin yordamida uni to'plang va o'rnating.
  • Natijani import qiling.

Rustda #[pyfunction] va #[pymodule] makroslari bu bog'lovchi vazifasini bajaradi. Bu makroslar Python dekoratorlariga o'xshaydi: ular ustiga joylashtirilgan funksiyani qayta yozadi va Python obyektlariga chaqirishni, tur konvertatsiyalarini va referentsiya sanashni boshqaradi.

Maturin Rust paketini umumiy kutubxona (.so, .dylib, .dll) sifatida to'playdi va sizning virtual muhitga joylashtiradi, shunda import qilish oddiy bo'ladi. Bu jarayonni How to run Rust in Python with PyO3 and Maturin maqolasida batafsil ko'rsatilgan.

Parserning ish jarayoni

Bu parser Rust qiymatini birinchi navbatda yaratadi. Bu qiymat JSON daraxti shaklida bo'lib, bu postning qolgan qismida misol sifatida ishlatiladi. Rust tilida JSON parser yozish kursida talabalar JSON parserni Rust tilida yozishni o'rgatiladi, keyin esa uni PyO3 yordamida Python ilovasida ishlatishni o'rgatiladi.

pub enum JsonValue {
    Null,
    Boolean(bool),
    Number(f64),
    String(String),
    Array(Vec<JsonValue>),
    Object(HashMap<String, JsonValue>),
}

Parser tomonidan yaratilgan Rust enumi JSON daraxtini to'liq tasvirlaydi:

pub enum JsonValue {
    Null,
    Boolean(bool),
    Number(f64),
    String(String),
    Array(Vec<JsonValue>),
    Object(HashMap<String, JsonValue>),
}

Bu daraxt faqat Rustda yashaydi. Python bu daraxtni hech qachon ko'rmaydi. PyO3 qatlami bu daraxt uchun ingichka moslashuvdir.

Funksiyani Python ilovasida ishlatish

Funksiyani Python ilovasida ishlatish uchun ikkita qator kerak:

#[pyfunction]
fn parse_json<'py>(py: Python<'py>, input: &str) -> PyResult<Bound<'py, PyAny>> {
    parse(input)?.into_pyobject(py)
}
#[pyfunction]
fn parse_json<'py>(py: Python<'py>, input: &str) -> PyResult<PyObject> {
    parse(input)?.into_pyobject(py)
}

Bu funksiyaning imzosi Python o'quvchisi uchun eng qiziq qismdir:

  • py: Python<'py> — bu Python interpretatori bilan bog'lanishni ifodalovchi token. Bu token PyO3 APIlariga Python obyektlariga kirishni ta'minlaydi. An'anaviy Python qurilishlarida bu kirish GIL (Global Interpreter Lock) bilan bog'liq.
  • Bound<'py, PyAny> — bu Python obyektiga bog'langan handle, Rust tomonidagi PyObject.
  • PyResult<PyObject> — bu Result<PyObject, PyErr> tipiga o'xshaydi: qiymatni qaytaradi yoki PyO3 tomonidan Python istisnosiga aylantiriladigan xatoni qaytaradi.
  • ? operatori bu xatoni tarqatadi. Agar parse amalga oshirilmasa, funksiya erta qaytariladi va Python istisno ko'radi; aks holda, JsonValue ni ochib beradi va davom etadi.

Shuning uchun parse(input)? asl ishni bajaradi, va .into_pyobject(py) so'ralgan Python obyektlarini yaratadi. Bu oxirgi chaqirishda xarajat yashirin: u daraxtning tugunlarida Python obyektlarini yaratishi kerak bo'ladi, va katta hujjatda bu ishning o'zidan ko'proq ishni talab qilishi mumkin.

Konvertatsiya xarajati

.into_pyobject butun JsonValue daraxtini kuzatadi va uni Python obyektlariga qayta quradi: har bir obyekt uchun dict, har bir massiv uchun list, har bir barg uchun float yoki str. Siz ushbu tarjimaning amalga oshirilishini IntoPyObject traitini amalga oshirib berasiz, PyO3 bu traitni Rust qiymatini Python qiymatiga aylantirish uchun chaqiradi:

impl<'py> IntoPyObject<'py> for JsonValue {
    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
        match self {
            JsonValue::Null => Ok(py.None().into_bound(py)),
            JsonValue::Number(n) => Ok(n.into_pyobject(py)?.to_owned().into_any()),
            JsonValue::Object(obj) => {
                let py_dict = PyDict::new(py);
                for (k, v) in obj {
                    py_dict.set_item(k, v.into_pyobject(py)?)?;  // recurses
                }
                Ok(py_dict.into_any())
            }
            // ...arrays, strings, booleans
        }
    }
}
impl<'py> IntoPyObject<'py> for JsonValue {
    fn into_pyobject(self, py: Python<'py>) -> Result<PyObject, PyErr> {
        match self {
            JsonValue::Null => Ok(py.None().into_bound(py)),
            JsonValue::Number(n) => Ok(n.into_pyobject(py)?.to_owned().into_any()),
            JsonValue::Object(obj) => {
                let py_dict = PyDict::new(py);
                for (k, v) in obj {
                    py_dict.set_item(k, v.into_pyobject(py)?)?; // rekursiv
                }
                Ok(py_dict.into_any())
            }
            // ...arrays, strings, booleans
        }
    }
}

100 000 qiymatli hujjat 100 000 dan ortiq Python obyektlarini yaratishni anglatadi, bu butun parsdan keyin sodir bo'ladi. Kattaroq hujjatlarda bu materializatsiya tsikli, pars emas, umuman vaqtni belgilaydi.

Xatolarni tarqatish

Qaytish qiymati yagona narsa emas, bu tarjimalash kerak bo'lgan narsadir. Pars xatosi turlangan Rust xatosi bo'lib, Python istisnosi kerak. Bir From impl, bu Rust turini boshqa turga aylantirish uchun ishlatiladigan trait, ? operatoriga ishini bajarish uchun ishlatiladi:

impl From<JsonError> for PyErr {
    fn from(err: JsonError) -> PyErr {
        match err {
            JsonError::UnterminatedString { position } => PyValueError::new_err(
                format!("Unterminated string starting at position {position}")
            ),
            // ...har bir xato variant uchun bir tarmoq, pozitsiya saqlanadi
        }
    }
}

Shu bilan noto'g'ri kiritilgan ma'lumot ValueError istisnosini keltirib chiqaradi, bu yerda pars buzilgan joyda ofsetni olib yuradi. Fayl o'qish yo'li ham shu tarzda ishlaydi: std::io::Error allaqachon mos Python istisnosiga aylantiriladi, shuning uchun yo'q fayl FileNotFoundError istisnosini keltirib chiqaradi.

impl From<JsonError> for PyErr {
    fn from(err: JsonError) -> PyErr {
        match err {
            JsonError::UnterminatedString { position } => PyValueError::new_err(
                format!("Unterminated string starting at position {position}")
            ),
            // ...one arm per error variant, position preserved
        }
    }
}

Chaqiruvchi Python semantikasini oladi, Rust qatlami o'tmaydi.

Sizning portingiz uchun nima degani

Agar siz portlayotgan Rust funksiyasi skalyar qiymat qaytarsa, uni portlaying va davom eting. Chegara odatda kichik bo'ladi va unga e'tibor bermaslik mumkin.

Agar u katta tuzilma qaytarsa, konvertatsiya sizning asl xarajat bo'ladi, va bu parser o'zini tezlashtirgandan keyin optimallashtirish uchun keyingi narsa bo'ladi. PyDict ni oldindan ajratish cheklangan darajada yordam berishi mumkin, lekin katta yutuq arxitekturaviy: agar chaqiruvchi butun daraxtni ko'rmasa, uni butunlay materializatsiya qilmang. Rust orqali kechikishli, faqat zarur bo'lgan qismini qaytarib bering.

Asl manba: belderbos.dev

Manba: Hacker News
#Rust #Python #PyO3 #JSON parser #programming
Telegram da muhokama qilish