スクリプトのフォルダからインストール可能なライブラリへ
スクリプトのフォルダに置かれたコードは、他の誰かがそれを必要とするか、別の環境で必要になる日まで、1台のマシンで1人のために動くだけです。パッケージングは、コードを名前、バージョン、宣言された依存関係を持つライブラリにし、誰でも pip install でインストールできるようにします。チームがファイルをコピーせずに評価ライブラリを共有でき、リサーチ用ノートブックと本番ジョブがまったく同じコードを実行でき、バグ修正が新しいバージョン番号で全員に届きます。
# Versions are numbers, not text: compare them as tuples
def parse_version(text):
parts = [int(part) for part in text.split(".")]
return tuple(parts + [0] * (3 - len(parts)))
print("1.10" > "1.9") # False as text, "1" < "9" decides it: wrong
print(parse_version("1.10") > parse_version("1.9")) # True as numbers (1, 10, 0) is newer than (1, 9, 0)
print(parse_version("2")) # (2, 0, 0)構成、pyproject.toml、ビルド
一般的な構成では、コードを src/yourlib/(__init__.py 付き)、テストを tests/、そしてルートに pyproject.toml を置きます。この1つのファイルが、ビルドツール([build-system] テーブル)を指定し、プロジェクトの name、version、説明、対応する Python のバージョン、dependencies を記述します。開発中は pip install -e . でフォルダを編集可能モードでインストールすると、変更がすぐ反映されます。python -m build は dist/ に wheel(.whl、すぐインストール可能)と sdist(ソースアーカイブ)を作ります。バージョンは1か所だけに置き、必要なら yourlib.__version__ として公開します。
# A minimal pyproject.toml, generated and then read back with tomllib to prove it is valid
import tomllib
def pyproject_toml(name, version, dependencies):
lines = ["[build-system]", 'requires = ["setuptools>=68"]', 'build-backend = "setuptools.build_meta"', "",
"[project]", f'name = "{name}"', f'version = "{version}"']
if dependencies:
lines += ["dependencies = ["] + [f' "{d}",' for d in dependencies] + ["]"]
else:
lines.append("dependencies = []")
return "\n".join(lines) + "\n"
text = pyproject_toml("qcpricing", "0.3.0", ["numpy>=1.26", "pandas>=2"])
data = tomllib.loads(text)
print(data["project"]["name"], data["project"]["version"]) # qcpricing 0.3.0
print(data["project"]["dependencies"]) # ['numpy>=1.26', 'pandas>=2']バージョンとバージョン指定子
ほとんどのライブラリはセマンティックバージョニング MAJOR.MINOR.PATCH に従います。バグ修正はパッチを上げ(1.4.2 → 1.4.3)、後方互換の機能追加はマイナーを上げてパッチを0に戻し(1.5.0)、互換性を壊す変更はメジャーを上げます(2.0.0)。依存関係は指定子で宣言します。numpy>=1.26,<3 は 1.26 以上 3 未満の NumPy を受け入れます。誠実に許される範囲でゆるくしましょう。下限はテストした最も古いバージョン、上限は自分を壊すと分かっているメジャーバージョンに対してだけ付けます。バージョンの厳密な固定(==)は、他の人がインストールするライブラリではなく、アプリケーションのロックファイルに置くものです。
# Bumping a version and checking a requirement, with versions compared as tuples
import operator
def parse_version(text):
parts = [int(part) for part in text.split(".")]
return tuple(parts + [0] * (3 - len(parts)))
def bump_version(version, part):
major, minor, patch = parse_version(version)
return {"major": f"{major + 1}.0.0", "minor": f"{major}.{minor + 1}.0", "patch": f"{major}.{minor}.{patch + 1}"}[part]
OPS = {">=": operator.ge, "<": operator.lt, "==": operator.eq}
def satisfies(version, specifier):
for clause in specifier.split(","):
symbol = next(op for op in (">=", "==", "<") if clause.startswith(op))
if not OPS[symbol](parse_version(version), parse_version(clause[len(symbol):])):
return False
return True
print(bump_version("1.4.2", "minor")) # 1.5.0
print(satisfies("1.26.4", ">=1.26,<3"), satisfies("3.0.0", ">=1.26,<3")) # True False# One place for the version: read it from the source instead of repeating it in several files
import re
init_py = """__version__ = "0.3.0"
from .pricing import price
"""
match = re.search(r'^__version__ = "([^"]+)"', init_py, re.MULTILINE)
version = match.group(1)
print(version) # 0.3.0
print(tuple(int(part) for part in version.split("."))) # (0, 3, 0)- パッケージングはコードに名前、バージョン、宣言された依存関係を与え、
pip installで入れられるようにする。開発はpip install -e .、ビルドはpython -m build pyproject.tomlはビルドシステムと[project]のメタデータを持つ。バージョンは文字列でなく数値のタプルで比べる- セマンティックバージョニング(
MAJOR.MINOR.PATCH)とゆるい指定子(numpy>=1.26,<3)を使う。公開したバージョンは変えず、秘密情報はパッケージに入れない
次はクオンツコードの CI/CD です。変更のたびにチェックを自動実行します。
ラボ:採点される内容
parse_versionでバージョンを数値として比べる25点bump_versionで次のバージョンを出す25点satisfiesで依存条件を確かめる25点pyproject_tomlでパッケージを記述する25点