Jupyter Notebook でボタンを使う

以前、Jupyterでインタラクティブに関数を実行する方法を紹介しました。
参考: Jupyter Notebookでインタラクティブに関数を実行する

この時は、Jupyter Widgets のinteract というのを使って、スライドバー等を動かしたら自動的に値を変更してグラフを描く関数を実行する例を取り上げました。

今回は、値を変えるとかではなく、単純にNotebook上にボタンを表示して、そのボタンを押した瞬間に何か処理を実行するような方法をまとめます。

例えば、多くのテキストの値が格納されたDataFrameを対象に、ボタンをクリックするごとに次のテキストが読めるとか、何かしらの一連の処理を都度中断して経過を観察しながら徐々に実行していくとかそういった用途を想定してます。

早速説明に入りましょう。まずボタンの作成です。
前回の時は、 ipywidgets.interact 一つでいろんなUIが作れたのですが、ボタンはその中に含まれていません。ボタンを作りたい時は、ipywidgets.widgets.Buttonを使います。
さらに、ボタンを作っただけだと表示されないので、display します。もしくは、セルの最後の行で作ったボタンインスタンスを呼び出しても表示されます。(ただ、この方法は最後の1個しか出せないので、通常はdisplayしましょう。)

import ipywidgets as widgets
from IPython.display import display


button = widgets.Button(description="ボタンです")
display(button)

もしくは、下記の通り。

button = widgets.Button(description="ボタンです")
button

これで、「ボタンです」と表示されたボタンが作成されます。
description 以外の引数はドキュメントを参照してください。スタイルを警告に変えたり等の設定ができます。
参考: Widget List — Jupyter Widgets 8.0.0rc0 documentation

さて、ボタンを作成してもまだこのボタンには何の機能も実装されていません。ドキュメントにある通り、実行したい関数(仮にfooとする)を用意して、button.on_click(foo) と登録する必要があります。ボタンを押したら出力先にボタンが押された回数を表示するメッセージを出すようにしてみましょう。

button = widgets.Button(description="ボタンです")
output = widgets.Output()  # 出力先


i = 0  # ボタンが押された回数を記録しておく
def on_button_clicked(b):
    global i
    i += 1
    output.clear_output(True)  # 前のクリック時の出力を消す
    with output:
        print(f"{i}回ボタンを押しました。")


button.on_click(on_button_clicked)  # ボタンが押されたときに実行するメソッドをセット
display(button, output)

上記のコードで、ボタンが表示され、ボタンをクックするたびに
「i回ボタンを押しました。」の文字列が更新されていきます。
outputの使い方が独特ですね。 with output: するとそのスコープ配下のprint文の出力がoutputに表示されます。詳しくはOutputに関するドキュメントをご参照ください。
参考: Output widgets: leveraging Jupyter’s display system — Jupyter Widgets 8.0.0rc0 documentation

clear_output に True を渡していますが、このTrueは次の描写対象を待って消す、という処理になります。デフォルトはFalseです。
以前の記事で紹介した、セル出力のクリアと同じですね。
参考: jupyter notebookのセルの出力をコードでクリアする

ボタンの出力をoutputに出す方法は、次のように@を使ってデコレーターとして書く方法もあります。

button = widgets.Button(description="ボタンです")
output = widgets.Output()  # 出力先


i = 0  # ボタンが押された回数を記録しておく
@output.capture()
def on_button_clicked(b):
    global i
    i += 1
    output.clear_output(True)  # 前のクリック時の出力を消す
    print(f"{i}回ボタンを押しました。")


button.on_click(on_button_clicked)  # ボタンが押されたときに実行するメソッドを記録
display(button, output)

正直どちらも慣れるまではわかりにくいです。

実は昔のバージョンのjupyterはOutputを使わないとボタンの処理でprintした文を画面に出してくれなかったという話を聞いたのですが、最近のjupyterはOutputを作ってなくてもちゃんと出力してくれます。(どのバージョンからそうなったのかは調べられていないです。)
ただ、OutputはOutputとして作っておかないと、セルの出力にボタンもボタンの処理の出力も混ぜて出してしまうと、クリアしたときにボタンも消えるという不便さがあるので、Outputは必須でなくても使った方が良いです。

一度表示したボタンを消したい場合(要は、2回押すことがないとか表示するボタンの種類が次々変わるような作業をするとかといった場合)は、button.close()でボタンを消すことができます。ドキュメントのButtonのところをどう読んでもこれについての記載がなかったので僕は結構戸惑いました。buttonに実装されているメソッド/プロパティを一通り眺めて見つけたのですが、実はドキュメントでは次のページに記載されていたようです。
参考: Simple Widget Introduction — Jupyter Widgets 8.0.0rc0 documentation

以下のコードで押すと消えるボタンが

button = widgets.Button(description="押すと消えるボタン")


def on_button_clicked(b):
    b.close()


button.on_click(on_button_clicked)
display(button)

消えなくてもいいけど押せないようにしてほしい、という場合は、button.disabled の値(通常False) にTrueを代入しましょう。(ただの代入なのでコード例省略)

さて、ボタンを複数表示して押されたボタンによって処理を変えたい、という場面は多々あると思います。ボタンの数だけ on_click に設定するメソッドを定義してそれぞれ設定しても一応動くのですがコードがカッコ悪いので嫌ですね。

Buttonに何か値を持たせて、それに応じて処理を変える、というのを考えたのですが、Button()の引数に valueに相当するものがなさそうです。さっきのページに全てのウィジェットはvalueプロパティを持っている、的なことが書いてあるのに、ボタンにはvalueがないんですよね。(AttributeError: ‘Button’ object has no attribute ‘value’ が起きます。)
引用: All of the IPython widgets share a similar naming scheme. To read the value of a widget, you can query its value property.

一応、案の一つとして、button.description でボタンに表示しているテキストを取得できます。これの値によって処理を振り分けることは可能です。

ただ、実際これを使うと不便です。画面には男性/女性と表示しつつプログラム内の値としては1/2を使いたい、みたいな場面は多々あり、毎回マッピングしないといけません。

もう一つ、buttonはvalueプロパティを持っていませんでしたが、試し見てたところ後から設定することは可能でした。これを使うと一つの関数で挙動を振り分けることもできそうです。ボタンを3個作ってどれが押されたかわかるようにしてみましょう。
(ただ押したボタン名を表示するだけだったら無理やりvalue使わなくても、descriptionでいいじゃないか、って感じの例ですみません。)

# ボタンのインスタンスを作る
button_0 = widgets.Button(description="ボタン0")
button_1 = widgets.Button(description="ボタン1")
button_2 = widgets.Button(description="ボタン2")
# valueプロパティに値を設定する
button_0.value = 0
button_1.value = 1
button_2.value = 2
output = widgets.Output()  # 出力先


def on_button_clicked(b):
    output.clear_output(True)  # 前のクリック時の出力を消す
    with output:
        print(f"{b.value}番のボタンが押されました")


button_0.on_click(on_button_clicked)
button_1.on_click(on_button_clicked)
button_2.on_click(on_button_clicked)
hbox = widgets.HBox([button_0, button_1, button_2])  # HBoxを使うと横に並べることができる
display(hbox, output)

以上のコードで、ボタンが3個表示され、押したボタンによって異なる結果がアウトプット出力されます。

単純にボタンを並べると縦に並んでしまうので、HBoxというのを使って横向きに並べました。このようなレイアウト関連のツールはこちらのページにまとまっています。
参考: Layout and Styling of Jupyter widgets — Jupyter Widgets 8.0.0rc0 documentation

想定外に記事が長くなってきたので一旦今回の更新はここまでにします。
(OutputとかHBoxとかButton以外の紹介も必要でしたし。)

本当はこのButtonを使ったアノテーションのコードとか紹介したかったので次の記事でそれを書こうと思います。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です