[Python] pip installが依存関係を解決する挙動について調べてみた

pipでも思ったより依存関係解決してくれる!!
2023.06.27

pipとpipenvの違いをChatGPTに聞いてみると、こんな解答が返ってきました。

(pipは)依存関係の解決を手動で行う必要があります。
(中略)
(pipenvは)パッケージの依存関係を明示的かつ精確に管理できます。

これだけを読むとpipは依存関係の解決を自動でしてくれない?というようにも読めます。 この辺は正直きちんと理解できないなかったので、 バージョンを指定してパッケージをインストールした時にどんな動きになるのかを検証してみました。

具体的には、2つの依存関係のあるパッケージをインストールする際に、 それぞれを個別にインストールするのと、requirements.txtに書き出してインストールするのでは違いがあったため、 その内容について共有させていただきます。

なおrequirements.txtに記載してそれを一気にインストールすることと、

pip install aaa bbb ccc

のように、pip installに複数パッケージを指定することは全く同義のようです。 以降ではファイルから読むという記述に統一していますが、どちらも同じ動きとなります。

検証環境

  • python 3.11.3
  • venvで仮想環境を作っている
    • 条件を変えて実行するような際には、仮想環境を再構築してから行う

題材

以下のようなrequirements.txtをベースとします。 ただ依存関係の様子が確認したいだけなので、 特にこれらのパッケージを選んでいる理由は何もありません。

requirements.txt

mysql-connector-python
protobuf==3.1.0

検証時点でのこれらのパッケージは以下のような関係になっています。

  • mysql-connector-python
    • 8.0.33が最新
    • 8.0.30以降ではprotobuf<=3.20.1 and >=3.11.0を要求
    • 8.0.29ではprotobuf(のどのバージョンでも良い)を要求
    • 8.0.29より古いものについては今回登場していないので未調査
  • protobuf
    • 4.23.3が最新
    • 一定以上古いバージョンだとsixに依存するが、この検証には無関係なので特に言及しない

mysql-connector-pythonは名前が長いので、今後はmysqlと表記します。 (この記事で本質的にMySQLを指すものは一切登場しません)

ちょっと不思議ですが、mysqlは、最新バージョン付近では一定の範囲のprotobufのバージョンを要求し、 それより古いものでは、バージョン問わずに要求するという形になっています。

バージョン指定のおさらい

バージョン指定についておさらいしておきます。

# バージョン1.0.0固定
package-name==1.0.0
# バージョン1.0.0以降を許容
package-name>=1.0.0
# バージョン1系のみを許容
package-name>=1.0.0,<2.0.0
# 全てのバージョンを許容
package-name

どの表記も特に違和感のないものなので、特に解説の必要はないかと思います。

ただ気をつける必要があるのは、 上記の表記はバージョンを「指定」するというよりは「許容」すると理解すべきということです。 つまり、「許容」されたバージョンはインストールされ得るということです。 当たり前のことを書いていますが、バージョン指定を一切書かない場合は全てのバージョンが許容されます。

「とりあえず最新を使ってほしい」という意図でバージョン指定を一切書かなかったりしていませんか? この指定は実際には「どんなに古いバージョンでも許容する」という動作として目の前に現れることになります。 この点はよく注意した方が良さそうです。

なお、~=という表記もあり、 これは「のバージョンと互換性がある」を意味しますが、今回はこれには触れません(よく調べていません)。

ではpipの動きを見ていきます。

pip installで個別インストール

まずは2つのパッケージを独立にインストールしてみます。

mysql -> protobufの順

まずはmysqlのインストール

$ pip install mysql-connector-python
collecting mysql-connector-python
  using cached mysql_connector_python-8.0.33-cp311-cp311-macosx_12_0_arm64.whl (8.4 mb)
collecting protobuf<=3.20.3,>=3.11.0
  using cached protobuf-3.20.3-py2.py3-none-any.whl (162 kb)
installing collected packages: protobuf, mysql-connector-python
successfully installed mysql-connector-python-8.0.33 protobuf-3.20.3

最新の8.0.33が入りました。 またそれにともなって適合するprotobufの最も最新バージョンである3.20.3が入っています。

次にprotobuf==3.1.0を入れてみます。 どんな動きになるか予想してから読み進めてください。

$ pip install protobuf==3.1.0
collecting protobuf==3.1.0
  using cached protobuf-3.1.0-py2.py3-none-any.whl (339 kb)
collecting six>=1.9
  using cached six-1.16.0-py2.py3-none-any.whl (11 kb)
requirement already satisfied: setuptools in ./.venv/lib/python3.11/site-packages (from protobuf==3.1.0) (65.5.0)
installing collected packages: six, protobuf
  attempting uninstall: protobuf
    found existing installation: protobuf 3.20.3
    uninstalling protobuf-3.20.3:
      successfully uninstalled protobuf-3.20.3
error: pip's dependency resolver does not currently take into account all the packages that are installed. this behaviour is the source of the following dependency conflicts.
mysql-connector-python 8.0.33 requires protobuf<=3.20.3,>=3.11.0, but you have protobuf 3.1.0 which is incompatible.
successfully installed protobuf-3.1.0 six-1.16.0

errorと書いてありますが、さらに下を見るとsuccessfully installedとも書いてあります。 どうやらエラーになったのは依存関係の解決についてだけで、protobufのインストール自体はうまくいったようです。

入ったものの一覧を見てみると

$ pip list
package                version
---------------------- -------
mysql-connector-python 8.0.33
pip                    22.3.1
protobuf               3.1.0
setuptools             65.5.0
six                    1.16.0

となっており、mysql 8.0.33とprotobuf 3.1.0という組み合わせでインストールされた状態になります。 もちろんこの状態ではmysqlは正常に利用できません。

個別インストールでは、バージョン範囲が指定された場合は依存関係を調べてerrorとは表示はしてくれるものの、 実際は依存関係を壊してでもインストールが行われるようです。 この辺もきちんと認識しておいた方が良さそうです。

この状態からprotobufをバージョン指定なしで入れてみる

このmysqlが正常に使用できない状態で、

pip install protobuf

するとどうなるでしょうか? 上で見た通りpipは、インストールするかどうかはともかく、 すでにインストールされているパッケージとの依存関係を調べる機能を持っていることがわかっています。 なので、mysqlが使えるように適合したバージョンを入れてくれるでしょうか?

答えとしては、protobufのインストールや更新は行われません(3.1.0のまま)。 pip installは原則、指定されたバージョン範囲に当てはまるバージョンが入っている場合は何もしてくれません。 先述した通り、指定がないときは「何でもいい」と言われていると解釈しますので、 とにかくなんかしらのバージョンがすでにインストールされているなら、何もしません。 mysqlとの依存関係は依然壊れたままですが、そこを考慮してはくれません。

protobuf->mysqlの順

次に順番を逆にしてみます。 先ほどインストールしたものが影響しないように、一度venvの環境を作り直してから確認しています。

まずはprotobuf==3.1.0から

$ pip install protobuf==3.1.0
collecting protobuf==3.1.0
  using cached protobuf-3.1.0-py2.py3-none-any.whl (339 kb)
collecting six>=1.9
  using cached six-1.16.0-py2.py3-none-any.whl (11 kb)
requirement already satisfied: setuptools in ./.venv/lib/python3.11/site-packages (from protobuf==3.1.0) (65.5.0)
installing collected packages: six, protobuf
successfully installed protobuf-3.1.0 six-1.16.0

もちろん指定されたものがそのまま入ります。 次にmysqlをバージョン指定なしで入れます。

$ pip install mysql-connector-python
collecting mysql-connector-python
  using cached mysql_connector_python-8.0.33-cp311-cp311-macosx_12_0_arm64.whl (8.4 mb)
collecting protobuf<=3.20.3,>=3.11.0
  using cached protobuf-3.20.3-py2.py3-none-any.whl (162 kb)
installing collected packages: protobuf, mysql-connector-python
  attempting uninstall: protobuf
    found existing installation: protobuf 3.1.0
    uninstalling protobuf-3.1.0:
      successfully uninstalled protobuf-3.1.0
successfully installed mysql-connector-python-8.0.33 protobuf-3.20.3

最新版である8.0.33が入り、またそれに合わせてprotobufの適合範囲の最新である3.20.3が入りました。 これは内部的には、最新のmysqlのインストールの時に

pip install protobuf<=3.20.3,>=3.11.0

が動いていると考えられます。 mysqlの依存関係を解決したかったら、 親のmysqlの方をインストールするようにすれば、子も自然と適合したものが入るようです。

この辺、個人的にはちょっと盲点でした。 「protobufの新しいものを入れたら依存関係解決させられるのにな〜」と考えてprotobufの更新のことを考えてしまいがちですが、 何も考えずに使いたい目的のmysqlのインストールを実行するという方法の方が適切なようです。

なお、mysqlの方は特にバージョンを指定していませんが、 古いバージョンのmysqlを使うことでprotobuf 3.1.0のままでもいけるかもしれないという可能性は探りません。 インストールすることを示されたパッケージがどのバージョンのものもインストールされていない場合、 依存関係とは無関係に最新を入れるように動くようです。

また、ここまでの結果から、pip installでの個別インストールでは、指定する順序によって結果が変わるということもわかりました。

最初から適合するものが入っていた場合

先ほどは最新のmysqlに適合しないバージョンのprotobufが入っていた状況でしたが、 今度は適合しているが適合範囲の最新ではないprotobufが入っているとします。

$ pip install protobuf==3.11.0
collecting protobuf==3.11.0
  using cached protobuf-3.11.0-py2.py3-none-any.whl (434 kb)
collecting six>=1.9
  using cached six-1.16.0-py2.py3-none-any.whl (11 kb)
requirement already satisfied: setuptools in ./.venv/lib/python3.11/site-packages (from protobuf==3.11.0) (65.5.0)
installing collected packages: six, protobuf
successfully installed protobuf-3.11.0 six-1.16.0

$ pip install mysql-connector-python
collecting mysql-connector-python
  using cached mysql_connector_python-8.0.33-cp311-cp311-macosx_12_0_arm64.whl (8.4 mb)
requirement already satisfied: protobuf<=3.20.3,>=3.11.0 in ./.venv/lib/python3.11/site-packages (from mysql-connector-python) (3.11.0)
requirement already satisfied: six>=1.9 in ./.venv/lib/python3.11/site-packages (from protobuf<=3.20.3,>=3.11.0->mysql-connector-python) (1.16.0)
requirement already satisfied: setuptools in ./.venv/lib/python3.11/site-packages (from protobuf<=3.20.3,>=3.11.0->mysql-connector-python) (65.5.0)
installing collected packages: mysql-connector-python
successfully installed mysql-connector-python-8.0.33

mysqlのインストール時に、protobufの適合バージョンがすでに入っていることがわかったため、 protobufのインストールや更新は生じません。 すでに適合するバージョンがインストールされている場合、 わざわざより新しいものをインストールしたりはしないようです。

$ pip list
package                version
---------------------- -------
mysql-connector-python 8.0.33
pip                    22.3.1
protobuf               3.11.0
setuptools             65.5.0
six                    1.16.0

結果的に、 protobuf==3.1.0が入っていた場合はprotobuf==3.20.3となり、 protobuf==3.11.0が入っていた場合はそのままprotobuf==3.11.0状態となります。

中途半端に適合したバージョンが入っていると(適合した)最新バージョンに更新してはくれず、 そうでない時は最新にしてくれるという、ちょっとした逆転現象みたいなことが起きるのが面白いです。

適合した最新バージョンに更新をさせたい場合は、 一度アンインストールするなどが必要となるかと思います。 (これはもっとうまいやり方がきちんと用意されている気がしますが、未調査です)

requirements.txtからインストール

さて、今度はrequirements.txtからインストールを行ってみます。

pip install -r requirements.txt

でインストールしてみます。 --dry-runをつけることで実際にはインストールを行わずに、 何がインストールされるのか確認することができるので、これも活用していきます。

もちろんここでもvenvによる仮想環境を作り直した状態にしています。

基本の状態

requirements.txt

mysql-connector-python
protobuf==3.1.0

でインストールしてみます。

$ pip install -r requirements.txt --dry-run
collecting mysql-connector-python
  using cached mysql_connector_python-8.0.33-cp311-cp311-macosx_12_0_arm64.whl (8.4 mb)
collecting protobuf==3.1.0
  using cached protobuf-3.1.0-py2.py3-none-any.whl (339 kb)
collecting six>=1.9
  using cached six-1.16.0-py2.py3-none-any.whl (11 kb)
requirement already satisfied: setuptools in ./.venv/lib/python3.11/site-packages (from protobuf==3.1.0->-r requirements.txt (line 2)) (65.5.0)
collecting mysql-connector-python
  using cached mysql_connector_python-8.0.32-cp311-cp311-macosx_12_0_arm64.whl (4.8 mb)
  using cached mysql_connector_python-8.0.31-cp311-cp311-macosx_11_0_arm64.whl (4.6 mb)
  using cached mysql_connector_python-8.0.30-py2.py3-none-any.whl (351 kb)
  using cached mysql_connector_python-8.0.29-py2.py3-none-any.whl (342 kb)
would install mysql-connector-python-8.0.29 protobuf-3.1.0 six-1.16.0

色々書いてありますが、最終行を見ると

mysql-connector-python-8.0.29
protobuf-3.1.0

がインストールされることがわかります。 mysqlは8.0.29というバージョンがインストールされます。 最新ではないものが出てきました!

出力内容をもう少し見てみると、

collecting mysql-connector-python
  using cached mysql_connector_python-8.0.32-cp311-cp311-macosx_12_0_arm64.whl (4.8 mb)
  using cached mysql_connector_python-8.0.31-cp311-cp311-macosx_11_0_arm64.whl (4.6 mb)
  using cached mysql_connector_python-8.0.30-py2.py3-none-any.whl (351 kb)
  using cached mysql_connector_python-8.0.29-py2.py3-none-any.whl (342 kb)

となっており、mysqlについてはバージョンを1つずつ下げて、 指定されているprotobufのバージョンに適合するかどうかを確認していることがわかります。 そして8.0.29でめでたくprotobufの指定されたバージョンが使えることがわかったので、このバージョンがインストールされます。

このように、複数パッケージを一度にインストール指定したときは、 それぞれの項目が独立に評価されるのではなく、全体での依存関係解決が行われていることがわかりました。 なおこの挙動はファイルの中の順番を変えても一緒でした。

解決不能な指定にしてみる

次に、mysqlとprotobufの間で整合性が取れないようなバージョン指定をしてみます。

requirements.txt

mysql-connector-python>=8.0.30
protobuf==3.1.0

これでインストールしてみると、

$ pip install -r requirements.txt --dry-run
collecting mysql-connector-python>=8.0.30
  downloading mysql_connector_python-8.0.33-cp311-cp311-macosx_12_0_arm64.whl (8.4 mb)
collecting protobuf==3.1.0
  downloading protobuf-3.1.0-py2.py3-none-any.whl (339 kb)
collecting six>=1.9
  downloading six-1.16.0-py2.py3-none-any.whl (11 kb)
requirement already satisfied: setuptools in ./.venv/lib/python3.11/site-packages (from protobuf==3.1.0->-r requirements.txt (line 2)) (65.5.0)
collecting mysql-connector-python>=8.0.30
  downloading mysql_connector_python-8.0.32-cp311-cp311-macosx_12_0_arm64.whl (4.8 mb)
  downloading mysql_connector_python-8.0.31-cp311-cp311-macosx_11_0_arm64.whl (4.6 mb)
  downloading mysql_connector_python-8.0.30-py2.py3-none-any.whl (351 kb)
info: pip is looking at multiple versions of protobuf to determine which version is compatible with other requirements. this could take a while.
error: cannot install -r requirements.txt (line 1) and protobuf==3.1.0 because these package versions have conflicting dependencies.

the conflict is caused by:
    the user requested protobuf==3.1.0
    mysql-connector-python 8.0.33 depends on protobuf<=3.20.3 and >=3.11.0
    the user requested protobuf==3.1.0
    mysql-connector-python 8.0.32 depends on protobuf<=3.20.3 and >=3.11.0
    the user requested protobuf==3.1.0
    mysql-connector-python 8.0.31 depends on protobuf<=3.20.1 and >=3.11.0
    the user requested protobuf==3.1.0
    mysql-connector-python 8.0.30 depends on protobuf<=3.20.1 and >=3.11.0

to fix this you could try to:
1. loosen the range of package versions you've specified
2. remove package versions to allow pip attempt to solve the dependency conflict

error: resolutionimpossible: for help visit https://pip.pypa.io/en/latest/topics/dependency-resolution/#dealing-with-dependency-conflicts

mysqlについて、許容範囲内の古いバージョンを見て行っていますが、 protobuf==3.1.0に対応したものが見つからず、エラーが発生してインストールは行われません。 バージョン制約を緩くしろ、という解決法が示されるだけでコマンドは終了となります。 この指定は無矛盾に解決することは不可能なので、 勝手に制約を緩めることなどできないpipがエラーとなるのは当然の結果となります。

まとめ

pip installでパッケージを個別にインストールする時と、 pip install -r requirements.txtで一気にインストールするときの挙動の違いを見てみました。

私はこれらの間でこんなにも明確な違いが出ることを認識していなかったので、 今回調べてみて挙動が全然違うことに驚きました。 いかにバージョンについてあやふやな理解だったかと反省しております。。

特に重要な点として、バージョンが指定されない場合は「どんなバージョンも許容する」という動作になるということです。 とりあえず最新入れてね、という意味合いでバージョン指定を一切しないという行為は、 どんなに古くてもOKだよ!と解釈されてしまうので、ものすごい認識の相違がありますね。 pipの気持ちに寄り添って、きちんと最低バージョンは指定していこうと思います。

最後に、ここまでの挙動を軽くまとめておきます。

一度に指定した時

  • ファイル全体を見て、整合性を考える
  • バージョン指定が緩いものは、古いものを選ぶことで他と整合性が取れないか試みる
  • ファイル内での順序の概念はない

個別に指定した時

  • 指定されたバージョン範囲がすでにインストールされているか確認する
    • インストールされている時
      • 何もしない
      • (もっと新しいバージョンが利用可能でも、何もしない)
      • (それに依存する他パッケージの依存関係が壊れていても、何もしない)
    • インストールされていない時
      • 指定バージョン範囲で最新のものをインストールする
  • 依存しているパッケージがあれば、適合するバージョンがインストールされているか確認する
    • インストールされている時
      • 何もしない
      • (もっと新しいバージョンが利用可能でも、何もしない)
    • インストールされていない時
      • 指定バージョン範囲で最新のものをインストールする

pipenvとの違いはまだきちんとわかっていない

以上、pipの依存関係解決についてまとめてみました。 pipでも、十分な依存関係の解決をしてくれているようにも見えます。

pipenvではさらに何ができるのか、まだきちんと理解しきれていないのですが、 今回で言うと、protobufがさらに依存するものに関する依存関係など、 孫レベルの依存関係という概念が出てきた際にはpipenvやpoetryなどのツールがなければ 依存関係を自動では解決してくれないようです。 その辺についても今後理解できたら記事にしてみたいと思っています。

以上、誰かの参考になれば幸いです。