こんにちは、ナナオです。

ここ5年ほどpythonを触ってきて、Webバックエンドを中心に、良いところも悪いところも一通り踏んできました。

そんな中、新規プロジェクトでpythonを導入する一番のメリットが何かと言われたら何でしょうか?

チームメンバーがpythonを使っているから?社内の既存プロジェクトがpythonばかりだから?

たしかにそれも理由の一つにはなるでしょう。ですが、プロジェクトのことを考えたときにメンバーが使っているからというのがメリットになるでしょうか?

新しい言語のラーニングコストと、プロジェクトそのもののコスト(運用、パフォーマンスなど)を天秤にかければ、ラーニングコストはそこまで大きなコストではないです。

pythonを採用する最も大きなメリット、それは実装速度です。

とにかくすぐ実装できる

コードの制約があまりなく、すぐ実行可能なのはpythonです。これはスクリプト言語の強みでもあります。

バックエンドは以下のように数行で実装できます。

FastAPI

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}

@app.get("/items/{item_id}")
def read_item(item_id: int, q: str | None = None):
    return {"item_id": item_id, "q": q}

pythonになじみのない方向けに説明すると、@はデコレーターといって、簡単に言えばラッパー関数を端的に表現したものになります。

https://zenn.dev/ryo_kawamata/articles/learn_decorator_in_python

ただ、Rustでも数行で書くことはできます。

GitHub - actix/actix-web: Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust.

use actix_web::{get, web, App, HttpServer, Responder};

#[get("/hello/{name}")]
async fn greet(name: web::Path<String>) -> impl Responder {
    format!("Hello {name}!")
}

#[actix_web::main] // or #[tokio::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().service(greet)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

型情報があるというのはいいことです。スレッドセーフなのも最高。Rustは最高の言語のひとつだと思います。

「じゃあRustでいいじゃん!」と思う方。まぁそれも一理あります。

しかし、「頭の中にあるものをいかに速く組み立てられるか」という点で言えばやはりpythonは強いです。

例えばここでDBを使いたいという要件が出たとしましょう。WebアプリであればDBを使うのは日常茶飯事だと思います。

セッション管理は?モデルの定義は?プロパティの制約は?…考えることは尽きません。

Rustの場合、まず型の制約があります。これを考えながら実装しなくちゃならない。

また、Rustはオブジェクト指向言語ではないので、ORMの導入も少し特殊です。

(一応SeaORMというライブラリはあるらしい)

Rust製のアプリでORMを探したら、SeaORMが良かったので少し紹介する #プログラミング - Qiita

要は、この型と所有権というRustのメリットが、「思考のブレーキになる瞬間」になってしまうということです。

その点pythonはSQLAlchemyだけでモデル定義とDBコネクションの部分が実装可能です。

ORM Quick Start — SQLAlchemy 2.0 Documentation

from typing import List
from typing import Optional
from sqlalchemy import ForeignKey
from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "user_account"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(30))
    fullname: Mapped[Optional[str]]
    addresses: Mapped[List["Address"]] = relationship(
        back_populates="user", cascade="all, delete-orphan"
    )
    def __repr__(self) -> str:
        return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"

class Address(Base):
    __tablename__ = "address"
    id: Mapped[int] = mapped_column(primary_key=True)
    email_address: Mapped[str]
    user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
    user: Mapped["User"] = relationship(back_populates="addresses")
    def __repr__(self) -> str:
        return f"Address(id={self.id!r}, email_address={self.email_address!r})"

一方でRustの場合はこうなります。

https://www.sea-ql.org/SeaORM/

use sea_orm::entity::prelude::*;

#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "user")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub name: String,
    #[sea_orm(unique)]
    pub email: String,
    #[sea_orm(has_one)]
    pub profile: HasOne<super::profile::Entity>,
    #[sea_orm(has_many)]
    pub posts: HasMany<super::post::Entity>,
}

impl ActiveModelBehavior for ActiveModel {}

Rustのほうが短いですが、型と所有権を常に意識して書かなくてはならない分、コーディングのハードルが高いんですよね。

個人的な体感としては、ここまで迷わず実装を前に進められる言語は、Python以外にあまり思い当たりません。

プロダクトの鮮度を保った状態ですぐにリリースできる言語、それがPythonの魅力だと思います。

鮮度よりも質を上げたい、という場合は、今ならRustやGoでいいと思います。

まとめ

Pythonを使っていて最高だったのは、常に最高速度で書けるということ。

「こういうコードを書きたい!」と思ってから動くコードになるまでの時間が本当に短く済むんですよね。

逆にパフォーマンスを求める場面ではPythonは全然ハマらないと思います。

その代償として、ランタイムやメモリ効率にはかなり目をつぶることになります。

簡単なWebサーバーを実装しただけでも、Goなら100MBいくかいかないかくらいですが、Pythonだと700MBくらい行きますからね。。

obadahmh/go-helloworld - Docker Image