Python Fire コマンドラインインターフェース作成の革命

Pythonでスクリプトを書くとき、コマンドラインから引数を渡して実行したい場面はよくありますよね。標準ライブラリの `argparse` や、サードパーティの `Click` など、様々なライブラリが存在しますが、「もっと手軽に、簡単にCLIを作りたい!」と思ったことはありませんか? 🤔

そんなあなたにおすすめなのが、Googleが開発した Python Fire です!🐍🔥

Python Fireは、わずか数行のコードを追加するだけで、既存のPythonコード(関数、クラス、オブジェクト、辞書など)をコマンドラインインターフェース(CLI)に変換できる画期的なライブラリです。引数のパース処理やヘルプメッセージの生成などを自動で行ってくれるため、開発者は本来のロジック実装に集中できます。

このブログ記事では、Python Fireの基本的な使い方から、少し応用的な使い方、そしてそのメリット・デメリットまで、詳しく解説していきます。この記事を読めば、あなたもPython Fireを使いこなし、日々の開発効率を爆上げできるはずです!🚀

インストール

Python Fireのインストールは非常に簡単です。pipを使ってインストールできます。特別な依存関係もありません(PythonとpipがインストールされていればOK)。

pip install fire

condaを使用している場合は、conda-forgeチャンネルからインストールできます。

conda install fire -c conda-forge

これで準備完了です!早速使ってみましょう。

基本的な使い方

Python Fireの最大の魅力は、そのシンプルさです。既存のコードに最小限の変更を加えるだけでCLI化できます。

1. 関数をCLI化する

最も基本的な使い方は、単一の関数をCLIとして公開する方法です。

例として、挨拶をする簡単な関数 `hello` を持つ `example.py` を作成します。

# example.py
import fire

def hello(name="World"):
  """指定された名前で挨拶します。"""
  return f"Hello {name}!"

if __name__ == '__main__':
  fire.Fire(hello)

このスクリプトの最後に `fire.Fire(hello)` を追加するだけです。これで `hello` 関数がコマンドラインから呼び出せるようになります。

ターミナルから実行してみましょう。

# 引数なしで実行 (デフォルト値 "World" が使われる)
$ python example.py
Hello World!

# 引数を指定して実行
$ python example.py David
Hello David!

# --name=value 形式でも指定可能
$ python example.py --name=Google
Hello Google!

このように、関数名を指定せずに関数の引数を直接渡すことができます。Fireが自動的に引数を解釈し、適切な型の値(この場合は文字列)として関数に渡してくれます。

💡 ヒント: 関数のdocstringは、自動生成されるヘルプメッセージに使用されます。 `–help` フラグを付けて実行すると、使い方を確認できます。

$ python example.py --help
INFO: Showing help with the command 'python example.py -- --help'.

NAME
    example.py - 指定された名前で挨拶します。

SYNOPSIS
    example.py [<name>]

POSITIONAL ARGUMENTS
    name
        Type: str
        Default: 'World'

NOTES
    You can also use flags syntax for POSITIONAL ARGUMENTS

`–help` や `-h` オプションは、コマンド全体だけでなく、個別のコマンド(後述するクラスのメソッドなど)に対しても使用できます。その場合、フラグの前に `–` を置くことで、Fireのフラグであることを明示します。

# 個別コマンドのヘルプ表示 (calculator.py double のヘルプを見る例)
$ python calculator.py double -- --help

2. クラスをCLI化する

クラスを `fire.Fire()` に渡すと、そのクラスのメソッドがサブコマンドとして利用できるようになります。

簡単な計算機クラス `Calculator` を持つ `calculator.py` を作成します。

# calculator.py
import fire

class Calculator(object):
  """簡単な計算機クラス。"""
  def __init__(self, base=0):
      self._base = base
      print(f"Calculator initialized with base {self._base}")

  def double(self, number):
    """与えられた数を2倍します。"""
    return 2 * number

  def add(self, x, y):
    """2つの数を足し合わせます。"""
    return x + y + self._base

  def _private_method(self):
      # アンダースコアで始まるメソッドはデフォルトでは公開されない
      return "This is private"

if __name__ == '__main__':
  fire.Fire(Calculator)

これを実行してみましょう。クラスのメソッド名がサブコマンドとして認識されます。

# double メソッドを実行
$ python calculator.py double 10
Calculator initialized with base 0
20

# --number=value 形式でも引数を渡せる
$ python calculator.py double --number=15
Calculator initialized with base 0
30

# add メソッドを実行 (位置引数)
$ python calculator.py add 5 3
Calculator initialized with base 0
8

# add メソッドを実行 (キーワード引数)
$ python calculator.py add --x=7 --y=2
Calculator initialized with base 0
9

# コンストラクタ引数を指定してメソッドを実行
$ python calculator.py --base=10 add 5 3
Calculator initialized with base 10
18

# プロパティ (インスタンス変数) にアクセス (デフォルトでは非公開)
# fire.Fire(Calculator, command=['double', 'add']) のように明示的に指定するか、
# --verbose フラグを使うことでアクセス可能になる場合がある (要確認)
# $ python calculator.py _base # 通常はエラーになる

# プライベートメソッドは呼び出せない
# $ python calculator.py _private_method # エラーになる

クラスを `fire.Fire()` に渡すと、まずクラスがインスタンス化され、そのインスタンスのメソッドが呼び出されます。コンストラクタ (`__init__`) に引数がある場合は、コマンドラインから `–引数名=値` の形式で渡すことができます。メソッドの引数も同様に、位置引数またはキーワード引数 (`–引数名=値`) で渡せます。

⚠️ 注意: アンダースコア (`_`) で始まるメソッドやプロパティは、デフォルトではCLIとして公開されません。意図的に公開したい場合は、後述する辞書を使う方法などを検討してください。

3. モジュール全体をCLI化する

`fire.Fire()` に何も引数を渡さない場合、そのスクリプト(モジュール)内で定義されている全ての公開可能なオブジェクト(関数、クラス、変数など)が自動的にCLIとして公開されます。

# tools.py
import fire

PI = 3.14159

def greet(name="User"):
  return f"Hi, {name}!"

class Greeter:
  def formal_greet(self, title, name):
    return f"Good day, {title} {name}."

# アンダースコアで始まるものは無視される
_INTERNAL_VAR = "secret"

if __name__ == '__main__':
  fire.Fire() # 引数なしで呼び出す

実行例:

# 変数 PI の値を表示
$ python tools.py PI
3.14159

# greet 関数を実行
$ python tools.py greet Alice
Hi, Alice!

# Greeter クラスの formal_greet メソッドを実行
# クラス名 -> メソッド名 の順で指定
$ python tools.py Greeter formal_greet --title Dr. --name Bob
Good day, Dr. Bob.

# _INTERNAL_VAR は表示されない
# $ python tools.py _INTERNAL_VAR # エラー

この方法は非常に手軽ですが、意図しないオブジェクトまで公開されてしまう可能性があるため、公開する対象を明確にしたい場合は、関数やクラス、辞書を明示的に渡す方が安全です。

4. 辞書を使ってコマンドを定義する

複数の関数や異なるオブジェクトを組み合わせてCLIを構築したい場合、辞書を `fire.Fire()` に渡すのが便利です。辞書のキーがコマンド名、値が対応するPythonオブジェクト(関数、クラス、値など)になります。

# commands.py
import fire
import math

def add(x, y):
  return x + y

def subtract(x, y):
  return x - y

class AdvancedMath:
    def power(self, base, exp):
        return math.pow(base, exp)

if __name__ == '__main__':
  # コマンド名と対応するオブジェクトを辞書で定義
  commands = {
      'plus': add,
      'minus': subtract,
      'math': AdvancedMath, # クラスを渡すことも可能
      'version': '1.0.0'   # 文字列などの値も渡せる
  }
  fire.Fire(commands)

実行例:

# 'plus' コマンド (add関数) を実行
$ python commands.py plus 10 5
15

# 'minus' コマンド (subtract関数) を実行
$ python commands.py minus 10 5
5

# 'math' コマンド (AdvancedMathクラス) の 'power' メソッドを実行
$ python commands.py math power 2 8
256.0

# 'version' コマンド (文字列) を表示
$ python commands.py version
1.0.0

辞書を使うことで、コマンド名を自由に設定したり、異なる種類のオブジェクトを組み合わせて柔軟なCLIを構築できます。

引数パースの挙動

Python Fireは、コマンドラインから渡された引数を賢く解釈しようとします。

  • 数値: `10`, `-5`, `3.14` などは数値(intまたはfloat)として解釈されます。
  • 文字列: クォートなしの単語 (`hello`, `world`) や、クォートで囲まれた値 (`”Hello World!”`, `’single quote’`) は文字列として解釈されます。
  • ブール値: `–flag`, `–no-flag`, `–flag=true`, `–flag=false` などの形式でブール値を渡せます。
  • リスト/タプル: `[1,2,3]`, `(a,b,c)` のように角括弧や丸括弧で囲まれた値はリストやタプルとして解釈されます。要素間にスペースがある場合は、全体をクォートで囲む必要があります (`”[1, 2, 3]”` など)。
  • 辞書: `{‘key’:’value’, ‘num’:123}` のように波括弧で囲まれた値は辞書として解釈されます。リスト/タプルと同様に、スペースを含む場合はクォートが必要です。

例:

# parser_example.py
import fire

def process_data(data, count, active=False, options=None):
  print(f"Data: {data} (Type: {type(data)})")
  print(f"Count: {count} (Type: {type(count)})")
  print(f"Active: {active} (Type: {type(active)})")
  print(f"Options: {options} (Type: {type(options)})")

if __name__ == '__main__':
  fire.Fire(process_data)
# 文字列、数値、ブール値、リストを渡す
$ python parser_example.py my_string 10 --active '[item1, item2]'
Data: my_string (Type: <class 'str'>)
Count: 10 (Type: <class 'int'>)
Active: True (Type: <class 'bool'>)
Options: ['item1', 'item2'] (Type: <class 'list'>)

# 辞書を渡す (クォートが必要)
$ python parser_example.py data_dict 5 --options "{'mode':'fast', 'retries':3}"
Data: data_dict (Type: <class 'str'>)
Count: 5 (Type: <class 'int'>)
Active: False (Type: <class 'bool'>)
Options: {'mode': 'fast', 'retries': 3} (Type: <class 'dict'>)

# ブール値のフラグ (値を省略するとTrue)
$ python parser_example.py data 1 --active
Data: data (Type: <class 'str'>)
Count: 1 (Type: <class 'int'>)
Active: True (Type: <class 'bool'>)
Options: None (Type: <class 'NoneType'>)

# --no-フラグ名 で False を指定
$ python parser_example.py data 1 --no-active
Data: data (Type: <class 'str'>)
Count: 1 (Type: <class 'int'>)
Active: False (Type: <class 'bool'>)
Options: None (Type: <class 'NoneType'>)

FireはPythonの `ast.literal_eval` に似た方法で引数を評価しますが、より柔軟な解釈を行います。ただし、複雑なオブジェクトや意図しない型変換が発生する可能性もあるため、注意が必要です。

高度な使い方

Python Fireには、基本的な使い方以外にも便利な機能がいくつかあります。

1. 対話モード (`–interactive`)

`–interactive` フラグを付けてコマンドを実行すると、コマンド実行後にPythonの対話型シェル(REPL)が起動します。このシェル内では、コマンドの実行結果や、スクリプト内で定義された変数・関数・クラスなどが利用可能な状態で開始されるため、デバッグや動作確認に非常に便利です。 ✨

# interactive_example.py
import fire

message = "Initial message"

def update_message(new_msg):
  global message
  message = new_msg
  return f"Message updated to: {message}"

def get_message():
  return message

class Counter:
    def __init__(self, start=0):
        self.count = start
    def increment(self, amount=1):
        self.count += amount
        return self.count

if __name__ == '__main__':
  fire.Fire()

対話モードを試してみましょう。

# update_message を実行した後に対話モードに入る
$ python interactive_example.py update_message "New content" -- --interactive
INFO: Running command: python interactive_example.py update_message 'New content' -- --interactive
Message updated to: New content
INFO: Entered interactive mode. Type q or quit to exit.
Fire behavior: > indicates command results. Variables are directly available.
>>> message  # スクリプト内の変数にアクセスできる
'New content'
>>> get_message() # スクリプト内の関数も呼び出せる
'New content'
>>> result = _ # 直前のコマンドの実行結果は _ に格納される
>>> print(result)
Message updated to: New content
>>> c = Counter(10) # スクリプト内のクラスも使える
>>> c.increment(5)
15
>>> quit # シェルを終了

対話モードでは、 `_` 変数に直前のコマンドの実行結果が格納されます。また、スクリプト内のグローバル変数や関数、クラスに直接アクセスできるため、状態を確認したり、追加の操作を試したりするのに役立ちます。

2. メソッドチェーン

`fire.Fire()` に渡したオブジェクトのメソッドが別のオブジェクトを返す場合、その返されたオブジェクトのメソッドを続けて呼び出すことができます。これにより、メソッド呼び出しを連鎖させることが可能です。🔗

# chain_example.py
import fire

class TextProcessor:
    def __init__(self, text):
        self.text = text
        print(f"Initialized with: '{self.text}'")

    def upper(self):
        self.text = self.text.upper()
        print(f"After upper: '{self.text}'")
        return self # 自分自身を返す

    def add_suffix(self, suffix):
        self.text += suffix
        print(f"After add_suffix: '{self.text}'")
        return self # 自分自身を返す

    def get_length(self):
        print(f"Final text: '{self.text}'")
        return len(self.text)

if __name__ == '__main__':
  fire.Fire(TextProcessor)

実行例:

# コンストラクタ -> upper -> add_suffix -> get_length を実行
$ python chain_example.py "hello world" upper add_suffix --suffix="!!!" get_length
Initialized with: 'hello world'
After upper: 'HELLO WORLD'
After add_suffix: 'HELLO WORLD!!!'
Final text: 'HELLO WORLD!!!'
14

コンストラクタ引数 (`”hello world”`) が `TextProcessor` に渡され、インスタンスが生成されます。その後、`upper` メソッド、`add_suffix` メソッド (`–suffix` フラグで引数を指定)、`get_length` メソッドが順に呼び出されています。各メソッドが `self` (インスタンス自身) を返すことで、チェーンが可能になっています。

3. コマンドのグループ化

クラスや辞書をネストさせることで、コマンドを階層的にグループ化できます。これにより、関連するコマンドをまとめて整理し、より複雑なCLIを構築できます。📂

# group_example.py
import fire

class Git:
  def status(self):
    return "On branch main. Nothing to commit, working tree clean."
  def commit(self, message):
    return f"Committing with message: '{message}'"

class Docker:
  def build(self, tag="latest"):
    return f"Building Docker image with tag: {tag}"
  def run(self, image):
    return f"Running Docker container: {image}"

class Tools:
    def __init__(self):
        self.git = Git()
        self.docker = Docker()

    def system_info(self):
        import platform
        return platform.system()

if __name__ == '__main__':
  fire.Fire(Tools) # Toolsクラスを起点とする
  # または、辞書で定義しても良い
  # fire.Fire({'git': Git, 'docker': Docker, 'sysinfo': system_info_func})

実行例:

# git グループの status コマンドを実行
$ python group_example.py git status
On branch main. Nothing to commit, working tree clean.

# docker グループの build コマンドを実行
$ python group_example.py docker build --tag v1.0
Building Docker image with tag: v1.0

# Tools クラス直下の system_info コマンドを実行
$ python group_example.py system_info
# (実行環境によって Darwin, Linux, Windows などが出力される)
Darwin

`Tools` クラスのインスタンス変数 `git` と `docker` がそれぞれサブコマンドグループとなり、その中のメソッド (`status`, `commit`, `build`, `run`) がさらにサブコマンドとして呼び出せるようになっています。

4. その他のフラグ

Python Fireには、他にもいくつかの便利なフラグがあります。これらのフラグは、コマンドと引数の後に `–` で区切って指定します。

  • `–separator=X`: メソッドチェーンや引数名の区切り文字をデフォルトのハイフン (`-`) から指定した文字 `X` に変更します。例えば `–separator=_` とすると、`my_command` のようにアンダースコア区切りでメソッドを呼び出せるようになります。
  • `–trace`: コマンドがどのように解釈され、実行されたかの詳細なトレース情報を表示します。デバッグ時に役立ちます。
  • `–verbose` / `-v`: ヘルプメッセージなどに、アンダースコアで始まるプライベートなメンバーも含めて表示します。
  • `–completion [shell]`: 指定したシェル(bash, zshなど)用のタブ補完スクリプトを生成します。
# トレース情報を表示
$ python calculator.py add 1 2 -- --trace
INFO: Running command: python calculator.py add 1 2 -- --trace

Fire trace:
1. Initial component: <class '__main__.Calculator'>
2. Instantiated class `Calculator`
3. Accessed member `add`
4. Called function `add` with args `(1, 2)`

3

メリットとデメリット

Python Fireは非常に便利なライブラリですが、他のツールと同様にメリットとデメリットがあります。

メリット 👍

  • 圧倒的な手軽さ: 既存のPythonコードに対する変更が最小限で済みます。`import fire` と `fire.Fire(…)` の呼び出しを追加するだけで、基本的なCLIが完成します。
  • 学習コストの低さ: `argparse` や `Click` のようなデコレータや複雑な設定を覚える必要がほとんどありません。Pythonの基本的な知識があればすぐに使えます。
  • 柔軟性: 関数、クラス、メソッド、モジュール、辞書、リストなど、あらゆるPythonオブジェクトをCLI化できます。
  • 自動ヘルプ生成: docstringを記述しておけば、それが自動的にヘルプメッセージに反映されます。
  • 対話モード: `–interactive` フラグによる対話モードは、デバッグや動作確認、ちょっとした試行錯誤に非常に強力です。
  • 開発効率の向上: ライブラリやツールの開発中に、その機能をコマンドラインから素早くテストできます。

デメリット 👎

  • 暗黙的な動作: 引数の型解釈やコマンドの探索などが自動で行われるため、時に意図しない挙動をすることがあります。特に複雑な引数やコマンド構造の場合、予期せぬエラーが発生する可能性があります。
  • カスタマイズ性の限界: `argparse` や `Click` ほど詳細なカスタマイズ(引数のバリデーションルール、複雑なオプション定義、独自のエラーハンドリングなど)はできません。非常に凝ったCLIを作りたい場合には、機能不足を感じるかもしれません。
  • 型ヒントの無視: 引数の型は、関数の型ヒントではなく、コマンドラインからの入力値に基づいて自動的に解釈されます。これにより、予期しない型変換が発生したり、型ヒントによる静的解析の恩恵を受けにくかったりする場合があります。(代替として `jsonargparse` などが挙げられます)
  • 依存関係: 標準ライブラリではないため、利用環境に `fire` ライブラリをインストールする必要があります。(これは多くのサードパーティライブラリに共通しますが)

Python Fireは、特に個人の開発ツール、簡単なスクリプト、ライブラリのテスト、プロトタイピングなど、手軽さとスピードが重視される場面で非常に強力な選択肢となります。一方で、非常に堅牢で複雑なCLIアプリケーションを配布する場合には、`argparse` や `Click` の方が適している場合もあります。

ユースケース・活用例 💡

Python Fireは様々な場面で活用できます。

  • 日常的なスクリプトのCLI化: ファイル操作、データ処理、API呼び出しなど、日々の作業を自動化するちょっとしたスクリプトを簡単にコマンドラインから使えるようにします。
  • ライブラリ開発時のテスト・デバッグ: 開発中のライブラリの特定の関数やクラスメソッドを、対話モードなどを活用して素早く実行し、動作を確認します。
  • 実験管理: 機械学習の実験などで、ハイパーパラメータや設定をコマンドライン引数で渡し、異なる設定での実行を容易にします。Google Brainでも実験管理ツールに利用されている実績があります。
  • 簡単なバッチ処理: 定期的に実行するタスクなどを、引数を変えてコマンドラインから実行できるようにします。
  • 既存コードのCLI化: 他の人が書いたコードや、既存のプロジェクトの一部機能を、コードを大きく変更することなく手早くCLIツールとして利用できるようにします。
  • 教育・デモンストレーション: Pythonのコードがどのように動作するかを、コマンドラインから対話的に示すのに役立ちます。

例えば、画像処理ライブラリを使ってリサイズを行う関数を書いたとします。

# image_resizer.py
import fire
from PIL import Image # Pillowライブラリが必要 pip install Pillow
import os

def resize_image(input_path, output_path, width, height):
  """画像をリサイズして保存します。"""
  try:
    img = Image.open(input_path)
    resized_img = img.resize((width, height))
    # 出力ディレクトリが存在しない場合は作成
    os.makedirs(os.path.dirname(output_path), exist_ok=True)
    resized_img.save(output_path)
    print(f"Resized image saved to: {output_path}")
  except FileNotFoundError:
    print(f"Error: Input file not found at {input_path}")
  except