【Python】感動までしたはずのuvを忘れてPixiに心酔した一つの理由

に公開

今やuvよりPixi

私がuvを知ったのは凡そ一年前、この記事を見たことがきっかけであったと記憶します。

https://gihyo.jp/article/2024/03/monthly-python-2403

pipとは異なる「そこに必要なものだけ用意する」というやり方が、ついにRustからPythonへと輸入されたのだと、当時感動したことをよく覚えています。以来pipではなくuvを使うようになるほど、感動したはずでした。

しかし現在の私は、uvなど全く使わなくなっていました。

あらすじ:そもそもuvは何が素晴らしいのか

https://docs.astral.sh/uv/

思うにuvには、「仮想環境」に関して、従来のやり方に比して幾つか利点があります。

ここで言う仮想環境とは、Pythonに限った意味で使われます。Windows(ホストOS)でLinux(ゲストOS)を動かすというような、OSの仮想環境等ではありません。

雑な言い方をすれば、「自分のためだけのPython」が仮想環境であると言えます。つまり、そうではないPythonもあるということです。

仮想環境とuv

前提として、インターネットからPythonの「モジュール」(有名なものとしてはnumpymatplotlibなど)をインストールするには、pipを使います。Windowsユーザーであれば、これに親しんだ方も多いでしょう。

今となっては有名な話になったものと思って居りますが、Linuxではこのpipが存在せず、またどうにかpipを導入しても、今度はエラーになって使えないといった問題が起こるようになっています。

$ pip install numpy
error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
    python3-xyz, where xyz is the package you are trying to
    install.
    
    If you wish to install a non-Debian-packaged Python package,
    create a virtual environment using python3 -m venv path/to/venv.
    Then use path/to/venv/bin/python and path/to/venv/bin/pip. Make
    sure you have python3-full installed.
    
    For more information visit http://rptl.io/venv

note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages.
hint: See PEP 668 for the detailed specification.

これは不具合でも嫌がらせでもなく、「公共物を私的に使うな」とでも言ったところです。これを回避するために使うものが、件の仮想環境です。

公式のやり方は煩雑

公式のやり方としては、次のようにして仮想環境を作るのが一般的でしょう。

pythonというコマンドは使えないことも多い
python3 -m venv

しかしvenvというものが存在しない場合もあります(存在する場合もある)。ないのですから、python3-venvをインストールする必要があります(存在する場合は不要)。

aptの場合
sudo apt install python3-venv -y

改めて、先のコマンドで仮想環境を作ります。すると、.venvという隠しフォルダーが作られます。これで漸く仮想環境が作られましたが、更に、この仮想環境を作動させなければなりません

source .venv/bin/activate

これで漸くpipが使えるようになります。

pip install numpy

但しこれは仮想環境であるために過ぎず、sourceコマンドでactivateした状態になっていなければ、pipは使えないままです。更に、pipでインストールしたnumpyも使えません。


この作業には、Pythonと直接関係ないコマンド(aptsource)が使われています。終始「何を言っているのだろう」、「何をさせられているのだろう」と感じた人も多いのではないでしょうか。私が初めてこれを見た際には、何も理解できませんでした。

呆れることに、何とかこの順序に慣れたとしても、なおも問題は残ります。仮想環境作成に掛かる時間が微妙に長く、微々たるストレスからは逃れられないようになっています。

uvはその煩雑を解消してみせた

uvの素晴らしい点の一つは、上に述べた公式の手順を殆ど回避できることでしょう。

  1. uvのインストール

pipを使わずともインストールできます。但しインストールした情報が反映されるよう、ターミナルの再読み込みは必要です。

https://docs.astral.sh/uv/getting-started/installation/

公式サイトにある通り
curl -LsSf https://astral.sh/uv/install.sh | sh
# curlが使えない場合はwget
wget -qO- https://astral.sh/uv/install.sh | sh
  1. 「プロジェクト」を作る

要するにフォルダー分けして整理整頓するというのが、プロジェクトという概念です。関係ないプログラムがデスクトップに散乱するようなことを避けるものとも言えるでしょう。このあたりから、従来のPythonと思想を異にする様子が垣間見えます。

uv_prjという名前にした場合
# uv init プロジェクト名
uv init uv_prj
  1. 「モジュール」をインストールする

先にも例にしていたnumpyをインストールする場合、このようになります。

$ uv add numpy
Using CPython 3.12.3
Creating virtual environment at: .venv
Resolved 2 packages in 712ms
Prepared 1 package in 2.18s
Installed 1 package in 515ms
 + numpy==2.3.3

「プロジェクト」という概念に戸惑うかもしれませんが、python3 -m venvからも、sourceからも解放されました。更に、仮想環境が作られるのに掛かる時間も大幅に短縮されており、ストレスに感じる点が悉く解消されたのです。

cargouv

私がuvに感動したのは、「そこに必要なものだけ用意する」というRustのやり方が、Pythonで手軽に真似できるようになった点でした。

uvRustという言語で作られています。仮想環境の作成時間が短縮されたのも、Rustの高いパフォーマンスによる恩恵でしょう。更に、Rustの思想にも影響を受けているような節が見られます。

Rustではcargoというパッケージ管理ツールが使われています。その使い方は、uvのそれとよく似ており、反対にpipとは全く異なります。次を比較すると良く分かるでしょう。

cargopackagecrate

  1. 「パッケージ」を作る
sampleという名前にした場合
cargo new sample
  1. 「クレート」をインストールする

これは「パッケージ」の中で行われます。インストールしたものはこの「パッケージ」でのみ有効であり、他に影響しません。

Rustにもnumpyがある
cargo add numpy

uvprojectmodule

  1. 「プロジェクト」を作る
sampleという名前にした場合
uv init sample
  1. 「モジュール」をインストールする

これは「プロジェクト」の中で行われます。インストールしたものはこの「プロジェクト」でのみ有効であり、他に影響しません。

uv add numpy

uvにできなかったこと

Rustによって成立し、Rustのような開発体験を齎したuvであるからこそ、どうしてもcargoと比較してしまうことがありました。そうするとやはり、「cargoならできたのに」と感じる点があったのです。

実行方法の統一

プログラムを実行するにあたって、毎回次のように入力することが徐々に煩わしく感ぜられるのでした。

uv run main.py

あるいは、srcフォルダーを作っていたとしたら。

uv run ./src/main.py

Rustであれば、cargoであれば、(基本的には)プログラムを実行する際にファイル名を指定する必要がないのです。

cargo run

後始末

更に、「パッケージ」にインストールしたものや、作成されたもの(実行ファイルなど)は、次のコマンドで一掃できます。

cargo clean

uvにも似たようなコマンドがあります。しかしこれはuvの全体的なキャッシュを削除するものであり、「プロジェクト」単位の後始末をするものではありません。また、uvによって作られた仮想環境は残留したままになっています。

uv clean
# あるいは
uv cache clean

uvPythonらしさを尊重した?

  • 実行時にファイル名を指定する必要がある
  • 仮想環境が削除されずに残留する

これらは従来のPythonに近しいと言えます。Pythonらしさを残したと好意的に受け取ることもできますが、個人的には不便なところを残されたというのが忌憚ない所感です。

本題:Pixiuvを凌駕する

https://pixi.sh/latest/

PixiもまたRustで作られたパッケージ管理ツールであり、基本的な使い方はcargouvと大差ありません。

  1. Pixiをインストールする

uv同様、curlまたはwgetでインストールし、ターミナルを読み込み直します。

curl -fsSL https://pixi.sh/install.sh | sh
# または
wget -qO- https://pixi.sh/install.sh | sh
  1. workspaceを作る

projectとも言われていますが、コマンドの説明には

Creates a new workspace

とあるため、ここではworkspaceの呼称を採ります。

sampleの名で作成
pixi init sample
  1. Pythonモジュールをインストールする

またもや、numpyをインストールしてみましょう。

pixi add numpy

uvを過去のものにした機能:Task

パッケージ管理ツールとしては珍しい機能として、「タスク」というものがあります。「タスク」と言っても非常に単純な機能で、「簡略化」というニュアンスの方が近いかもしれません。

特別仰々しく大層な機能というわけではないかと思いますが、uvに丁度足りないと感じていたところを満足させてくれました。これを使ってみるまでは「uvで充分だ」と考えていたものを、気が付けばすっかりPixiの虜でした。もうuvには戻れません。

Pythonでの便利な使い方

プログラムを実行する際には、次のようになります。

# windowsでは python
pixi run python3 src/main.py

「タスク」を使わないままでは、uvより長くなります。これを簡略化するべく、pixi.tomlというファイルに「タスク」を登録します。pixi.tomlは、pixi initを実行した時点で勝手に作られています。

pixi.toml
[workspace][tasks]
# windowsでは python
main = "python3 src/main.py"
test = "python3 test/test_main.py"

[dependencies]

自動登録

コマンドで登録することもできます。

# pixi task add タスクの名前 タスクで実行するコマンド
pixi task add main "python3 src/main.py"

python3 src/main.pyというコマンドにmainという名前を付けて簡略化できるようになりました。つまり、実行コマンドは次のようになります。

pixi run main

更に、test/test_main.pyというテスト用のプログラムの実行もpixi run testと簡略化できるようになります。「タスク」の登録こそ必要であるものの、実行ファイル名の省略は、uvにはできなかった芸当です。

Pythonだけでは然程複雑なコマンドはありませんが、次のようなこともできるようになります。

複雑な使い方の例

突然ですが、PixiDockerをインストールしてみましょう。

# ワークスペースを作る
> pixi init pixi_docker
# ワークスペースに移動する
> cd pixi_docker
# Dockerをインストールする
> pixi add docker

次を見れば、確かに異なるDockerがインストールされたことが分かります。

# 元々あったDocker
> docker --version
Docker version 24.0.6, build ed223bc

# PixiでインストールしたDocker
> pixi run docker --version
Docker version 20.10.9, build 591094d   
hello-world

先ずは本当に動くか確認したいので、hello-worldコンテナで試験しましょう。こちらの記事を参考にしました。

https://zenn.dev/sutobu000/articles/cd4400b3656a8b

  1. hello-worldimageを取り寄せる

docker pullのタスクを登録しておきます。

pixi.toml
[workspace][tasks]
pull_hello_world = "docker pull hello-world"

[dependencies]

このタスクを実行すれば、hello-worldのイメージがダウンロードされます。

> pixi run pull_hello_world
Pixi task (pull_hello_world): docker pull hello-world                                                                                 
Using default tag: latest                                                                                                             
latest: Pulling from library/hello-world
17eec7bbc9d7: Pull complete
Digest: sha256:54e66cc1dd1fcb1c3c58bd8017914dbed8701e2d8c74d9262e26bd9cc1642d31
Status: Downloaded newer image for hello-world:latest
docker.io/library/hello-world:latest

Windowsに元からあるものと混同されているようです。

> pixi run docker images
REPOSITORY        TAG       IMAGE ID       CREATED         SIZE                                                                       
hello-world       latest    1b44b5a3e06a   7 weeks ago     10.1kB
bitnami/moodle    4.5       4c9bb25125b7   10 months ago   765MB
bitnami/mariadb   11.4      1b79cab0fb6f   10 months ago   433MB
alpine            3.16.3    bfe296a52501   2 years ago     5.54MB

> docker images     
REPOSITORY        TAG       IMAGE ID       CREATED         SIZE
hello-world       latest    1b44b5a3e06a   7 weeks ago     10.1kB
bitnami/moodle    4.5       4c9bb25125b7   10 months ago   765MB
bitnami/mariadb   11.4      1b79cab0fb6f   10 months ago   433MB
alpine            3.16.3    bfe296a52501   2 years ago     5.54MB

本当に起動するか確認してみましょう。IMAGE ID1b44b5a3e06aとあるのを使っています。

> pixi run docker run 1b44b5a3e06a

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

動いたようです。

後始末

動くことが分かれば用もないため、削除します。

# コンテナを片付ける
> docker container prune      
WARNING! This will remove all stopped containers.
Are you sure you want to continue? [y/N] y
Deleted Containers:
562f0b945e28edcec3ae292403709c1a5a7ab0441de69480cb9e298eccf77d70
a1a2ccebc7279a2279367bc6677656ae50117aa4d3df857a7cd49d1b66d7cc2e
39099fc0a65c28702aaec4441571638ccb88cacb1e1b855b4585f217026bc7b0

Total reclaimed space: 102.2kB
# イメージを削除する
> docker image rm 1b44b5a3e06a
Untagged: hello-world:latest
Untagged: hello-world@sha256:54e66cc1dd1fcb1c3c58bd8017914dbed8701e2d8c74d9262e26bd9cc1642d31
Deleted: sha256:1b44b5a3e06a9aae883e7bf25e45c100be0bb81a0e01b32de604f3ac44711634
Deleted: sha256:53d204b3dc5ddbc129df4ce71996b8168711e211274c785de5e0d4eb68ec3851
> docker image ls
REPOSITORY        TAG       IMAGE ID       CREATED         SIZE
bitnami/moodle    4.5       4c9bb25125b7   10 months ago   765MB
bitnami/mariadb   11.4      1b79cab0fb6f   10 months ago   433MB
alpine            3.16.3    bfe296a52501   2 years ago     5.54MB
MQTT broker

MQTT brokerであるMosquittoを起動してみましょう。先のものと同じワークスペースを使っています。

長いのでアコーディオン

ワークスペース構成
ワークスペース構成

# MQTT用パッケージ
pixi add paho-mqtt
# デバッグ用パッケージ
pixi add icecream
pixi.toml
[workspace][tasks]
# イメージ取り寄せ
pull_eclipse_mosquitto = "docker pull eclipse-mosquitto"
# 起動前に必ず pull_eclipse_mosquitto を実行する
broker = {cmd = "docker compose -f mosquitto/docker-compose.yaml up", depends-on = ["pull_eclipse_mosquitto"]}
publisher = "python src/pub.py"
subscriber = "python src/sub.py"

[dependencies]
docker = ">=20.10.9,<21"
paho-mqtt = ">=2.1.0,<3"
icecream = ">=2.1.8,<3"

docker-compose.yaml
version: '3' # 高く設定しすぎると動かない
services:
  # mosquittoコンテナについて
  broker:
    image: eclipse-mosquitto:latest # 取り寄せたmosquittoのイメージ
    container_name: broker_container # コンテナの名前
    # ホストOS(Windows):Docker のTCP/IPポート対応定義
    # これが無いと接続できない
    ports:
      - 1883:1883
    # 設定ファイルをコンテナ側にコピー
    # これがないと接続できない
    volumes:
      - type: bind
        source: "mosquitto.conf" # 手許にあるファイル(設定内容記載済み)
        target: "/mosquitto/config/mosquitto.conf" # コンテナの中のファイル(初期状態)
mosquitto.conf
# 来るもの拒まず
allow_anonymous true
sub.py
from icecream import ic
from paho.mqtt.client import Client
from paho.mqtt.enums import CallbackAPIVersion

BROKER = '127.0.0.1'
PORT = 1883
TOPIC = 'pixi/sample'

def subscriber():
    ic('start subscriber')

    def on_connect(_client, _userdata, _flags, rc, _props):
        if rc == 0:
            ic("Connected to MQTT Broker!")
        else:
            ic(f"Failed to connect, return code {rc}\n")

    def on_message(_client, _userdata, msg):
        ic(f"Received `{msg.payload.decode()}` from `{msg.topic}` topic")

    SUB_ID = 'pixi/subscriber'
    client: Client = ic(
        Client(
            client_id = SUB_ID,
            callback_api_version = CallbackAPIVersion.VERSION2,
            clean_session = True
        )
    )
    client.on_connect = on_connect
    client.on_message = on_message
    ic(client.connect(BROKER, PORT))
    ic(client.subscribe(TOPIC))
    ic(client.loop_forever())

subscriber()

pub.py
import time

from icecream import ic
from paho.mqtt.client import Client, MQTTMessageInfo
from paho.mqtt.enums import CallbackAPIVersion

BROKER = '127.0.0.1'
PORT = 1883
TOPIC = 'pixi/sample'

def publicher():
    ic('start publisher')

    def on_connect(_client, _userdata, _flags, rc, _props):
        if rc == 0:
            ic("Connected to MQTT Broker!")
        else:
            ic(f"Failed to connect, return code {rc}\n")

    PUB_ID = 'pixi/publisher'
    client: Client = ic(
        Client(
            client_id = PUB_ID,
            callback_api_version = CallbackAPIVersion.VERSION2,
            clean_session = True
        )
    )
    client.on_connect = on_connect
    ic(client.connect(BROKER, PORT))
    ic(client.loop_start())
    count = 0
    while True:
        time.sleep(3)
        msg = ic(f"message {count}")
        result: MQTTMessageInfo = ic(client.publish(TOPIC, msg))
        status = result[0]
        if status == 0:
            ic(f"Send `{msg}`")
        else:
            ic(f"Failed to send message to {TOPIC}")
        count += 1

publicher()

Pixiの「タスク」はあくまでも「コマンドの別名」のようなものであって、OSのタスクとは異なります。つまりPixiだけでマルチタスクができるわけではありません。特に他の手段でもマルチタスクは実装していないため、それぞれ別のターミナルで一つ一つ実行します。

pixi run broker
pixi run subscriber
pixi run publisher

動作の様子はGIFにすると何も見えなかったので、動画で共有しています。

https://vimeo.com/1122558385

案外便利な特徴・機能

  1. pixi clean

cargo cleanが「パッケージ」内の後始末をしていたように、pixi cleanは「ワークスペース」内の仮想環境と、インストールしたものを削除します。

  1. 多言語・開発環境対応

condaがそうであるように、PixiPythonに限定したパッケージ管理ツールではありません。CRustJavaJuliaといった言語のほか、Node.jsにも、ROSにも対応しています。

  1. PyPI以外のチャンネルへの対応

pipuvは、PyPI (Python Package Index)からパッケージをインストールします。pip install numpyとしても、uv add numpyとしても、PyPInumpyがインストールされます。

Pixiは基本的にconda-forgeからパッケージをインストールしてきます。先ほどpixi add numpyとした時も、conda-forgenumpyがインストールされます。PyPInumpyをインストールするには、pixi add numpy --pypiと明記します。

conda-forgeの他にも様々なチャンネルを使うことができ、より幅広い開発シーンへの活用が期待できます。

https://prefix.dev/channels

少し困るところ

  1. PyPI以外への対応

PyPI以外に対応しており、かつその標準はconda-forgeであるため、慣れるまではパッケージの名前が分からず迷ってしまいました。

例えばOpenCVをインストールするとき、PyPIではopencv-pythonであるのに対し、conda-forgeではそのままopencvとなります。

Pixiでパッケージを検索する際は、prefix.devで検索することが最良の選択でしょう。

  1. インストールした言語と環境との不和

Pythonは仮想環境という概念が公式に存在するため、複数のPythonがあろうとも、柔軟に環境を切り替えることができます。しかし言語によっては、その切り替えが難しいものもあります。

Pixiでは、Rustをインストールすることもできます。

https://pixi.sh/latest/tutorials/rust/#create-a-pixi-project

pixi add rust

これで確かに、Rustによる開発が行えるようになります。しかし、VSCodeの拡張機能であるrust-analyzerが、どうもこのRustを認識しませんでした。

rust-analyzerPixiを併用するならば、PixiRustをインストールしないほうが良いのかもしれません。

本記事では、Pythonのパッケージ管理ツールとして充分画期的なuvと、それを凌駕し得るPixiについて紹介程度に述べました。一人でもPixiに関心を持つ方があればこれ幸いと思い聿を置きます。

Discussion