Python Tkinter GUIプログラミング Text tag

 見に来てくださって、ありがとうございます。石川さんです。

 気が付けば、1ヶ月以上も更新していませんでした。採用活動とか、夏休みとか、もろもろあって、忙しかった、かも、と、言い訳してみましたが、よくよく考えてみると、Amazonプライムで海外ドラマの「Elementary」をずっと見ていたのが原因です。すみません。言い訳はよくないですね。

 さて、気を取り直して。ここのところ取り組んでいたtkinterの中にちょくちょく登場してくるタグ(tag)の考え方でちょっと気になったことを調べるためにプログラムを書いてみました。今回はTextで使われるタグについて、です。タグは通常、Textの文字列に色を付けたりフォントを変えたりするのにつかいます。気になりポイントは、同じ文字列にタグをセットしたとき、どちらが有効になるのか、ということです。

 急いでいる方のために結論を言いますと、タグは設定した順に積まれていき、最後の設定が有効になります。前に設定したものを優先するためには、積んだ順を入れ替えれば大丈夫です。

できあがりイメージ

タグの優先順位を変更するプログラム

 プログラムを実行すると、上部のテキストボックスに、赤、緑、青の文字列が初期値として表示された画面が表示されます。「Set Color」ボタンを押すことで、真ん中のラジオボタンで選択した色を、選択した文字列の文字色(foreground)か背景色(background)に、タグと色を付けます。

 下部の一覧は、タグ一覧です。行を選択して、「↑」、「↓」でタグを上下に移動、「DEL」ボタンでタグを削除できます。このボタンでタグを上下することで優先順位が変わった時の挙動が確認できます。行の内訳は、左側がタグ名で、括弧の中が[開始 – 終了]を表します。中の数字は、「行.列」を表していて、行は1オリジン、列は0オリジンの数値がセットされています。

 タグ一覧の中にある「sel」は、特殊なタグで、自動的に作成されます。現在選択されている範囲を表すタグです。

ソースコード

import tkinter as tk
from tkinter import colorchooser


class Application(tk.Tk):
    ''' Textタグの優先順位を確認します。
    タグをセットするためのTextの作成。
    色をセットするためのフレームの作成。
    タグの内容を表示するためのリストボックスを作成。
    リストボックスの名前を移動したり削除したりするためのフレームを作成。
    '''
    def __init__(self):
        super().__init__()
        self.title("Tag Priority")

        self.text = tk.Text(self)
        self.text.pack()
        self.text.bind("<KeyRelease>", self.refresh_list)
        self.text.bind("<ButtonRelease>", self.refresh_list)

        frame = tk.Frame(self)
        self.color = tk.StringVar(value="red")
        radio_red = tk.Radiobutton(frame, text="Red",
                                   variable=self.color, value="red")
        radio_red.pack(side=tk.LEFT)
        radio_green = tk.Radiobutton(frame, text="Green",
                                     variable=self.color, value="green")
        radio_green.pack(side=tk.LEFT)
        radio_blue = tk.Radiobutton(frame, text="Blue",
                                    variable=self.color, value="blue")
        radio_blue.pack(side=tk.LEFT)
        radio_color = tk.Radiobutton(frame, text="Color",
                                     variable=self.color, value="color",
                                     command=self.set_palet_color)
        radio_color.pack(side=tk.LEFT)

        self.color_palette = tk.Canvas(frame, width=20, height=20,
                                       background="white")
        self.color_palette.pack(side=tk.LEFT)
        self.color_palette.bind("<Button-1>", self.set_palet_color, add=True)

        self.option = tk.StringVar(self)
        self.option.set("foreground")
        combo = tk.OptionMenu(frame, self.option,
                              *{"foreground", "background"})
        combo.pack(side=tk.LEFT)

        button = tk.Button(frame, text="Set Color",
                           command=self.set_color)
        button.pack(side=tk.RIGHT)
        frame.pack()

        self.list = tk.Listbox(self)
        self.list.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        updown_frame = tk.Frame(self)
        up_button = tk.Button(updown_frame, text="↑", command=self.tag_up)
        up_button.pack()
        del_button = tk.Button(updown_frame, text="DEL", command=self.tag_del)
        del_button.pack()
        down_button = tk.Button(updown_frame, text="↓", command=self.tag_down)
        down_button.pack()
        updown_frame.pack(side=tk.LEFT)

        # テキストの初期値
        self.text.insert(1.0, "*****Red******\n*****Green****\n*****Blue*****")
        self.text.tag_add("red", 1.0, "1.end")
        self.text.tag_config("red", foreground="red")
        self.text.tag_add("green", 2.0, "2.end")
        self.text.tag_config("green", foreground="green")
        self.text.tag_add("blue", 3.0, "3.end")
        self.text.tag_config("blue", foreground="blue")

        self.refresh_list()

    def set_color(self):
        ''' 選択されたTextの文字列に色を付けます。 '''
        tag_range = self.text.tag_ranges('sel')
        if not tag_range:
            return
        option = self.option.get()
        color = self.color.get()
        if color == "color":
            color = self.color_palette.cget("background")
        tagname = "t_"+option+"_"+color+"_"+str(len(self.text.tag_names()))
        self.text.tag_add(tagname, *tag_range)
        self.text.tag_config(tagname, {option: color})
        self.refresh_list()
        for tag_name in reversed(self.text.tag_names()):
            print(tag_name, self.text.tag_ranges(tag_name))

    def refresh_list(self, event=None):
        ''' リストを更新します。 '''
        # pylint: disable=unused-argument
        self.list.delete(0, tk.END)
        for i, tag in enumerate(reversed(self.text.tag_names())):
            tag_range = self.text.tag_ranges(tag)
            if tag_range:
                self.list.insert(i, tag + f"[{tag_range[0]} - {tag_range[1]}]")
            else:
                self.list.insert(i, tag + "[未選択]")

    def tag_up(self):
        ''' タグを一行上に移動します。'''
        self.tag_move(-1)

    def tag_down(self):
        ''' タグを一行下に移動します。'''
        self.tag_move(1)

    def tag_move(self, offset):
        ''' タグを移動します。'''
        active_position = self.list.index(tk.ACTIVE)
        active_value = self.list.get(tk.ACTIVE)
        self.list.delete(active_position)
        self.list.insert(active_position + offset, active_value)
        self.list.activate(active_position + offset)
        self.list.see(active_position + offset)

        active_tag = active_value.split("[")[0]
        above_or_below_tag = self.list.get(active_position).split("[")[0]
        if offset == 1:
            self.text.tag_lower(active_tag, above_or_below_tag)
        else:
            self.text.tag_raise(active_tag, above_or_below_tag)
        print('*'*20)
        for tag_name in reversed(self.text.tag_names()):
            print(tag_name, self.text.tag_ranges(tag_name))
        print('*'*20)

    def set_palet_color(self, event=None):
        ''' パレットの色をセットするためのダイアログを呼び出し、取得した色をセットします。 '''
        # pylint: disable=unused-argument
        initial_color = self.color_palette.cget("background")
        color = colorchooser.askcolor(color=initial_color)
        if color:
            print(color)
            self.color_palette.configure(background=color[1])
        self.color.set("color")

    def tag_del(self):
        ''' リストで選択されたタグを削除します。'''
        active_position = self.list.index(tk.ACTIVE)
        active_value = self.list.get(tk.ACTIVE)
        self.list.delete(active_position)

        active_tag = active_value.split("[")[0]
        self.text.tag_delete(active_tag)


def main():
    ''' メインプログラム(呼び出し用) '''
    main_application = Application()
    main_application.mainloop()


if __name__ == "__main__":
    main()

 今回は、ソースコードにpylintで静的コード分析を実施してみました。意外といろいろとうるさくて、クラスドキュメンテーション文字列がない、メソッドドキュメンテーション文字列がない、変数名、メソッド名のルールがおかしい、使用していない引数がある、カンマの後ろに空白がない、コロンの後ろに空白がない、空行が足りない、等々、いろいろとご指摘いただきました。

 リファクタリングの提案もしてくれていて、クラスのアトリビュートが多すぎるので、見直した方がよいですよ、と、言われていました。

ソースコードの説明

 12行目、__init__(self)でこのアプリケーションの全体を定義しています。16~19行目でTextを作成、定義しています。イベントの<KeyRelease><ButtonRelease>が発生したときに、一覧を更新するように定義しています。何らかのキー入力が終了したときか、マウスボタンクリックが終わった時に、refresh_list()を呼び出します。

 21~51行目、ラジオボタン、カラーパレットのキャンバス、オプションメニュー、「Set color」ボタンを作っています。特に34行目のラジオボタン「Color」が押されたときはカラーパレットを表示するように「command=self.set_palet_color」を定義しています。ここの部分は、もし40行目のbindと同じようにマウスクリックに定義してしまうと、ラジオボタンが選択できなくなってしまいますので、注意が必要です。

 53、54行目、リストボックスを作成、56~63行目で、「↑」「DEL」「↓」のボタンを作成しています。

 65~72行目でTextの初期値をタグ付きの色をセットして、74行目でリストボックスのリフレッシュしています。

 76~90行は、set_color()メソッドで、選択されたテキストの範囲にタグを定義しつつ色をセットしています。現在選択されている範囲を取得するのに、「tag_range = self.text.tag_ranges('sel')」を使用できます。’sel’は「tk.SEL」として定義されていましたので、こちらを利用した方がよかったかもしれません。tag_ranges()で範囲が取得できないとタグをセットするときにエラーになりますので、チェックしています。
 89行目、self.text.tag_names()でタグ名の一覧が取得できますが、作成した順番に格納されていくので、スタックされているイメージに合いません。なので、組み込み関数のreversed()を使って逆順リストを出力するようにしました。

 103~129行目まで、タグが移動できるように定義しています。ポイントは123行目と125行目のself.text.tag_lower(active_tag, above_or_below_tag)self.text.tag_raise(active_tag, above_or_below_tag)です。これらのメソッドは、二つ目の引数がないと一番上か一番下までタグが移動してしまうので、設定しました。上下ボタンでひとつずつ移動してほしかったので。二つ目の引数で指定したタグの下、または上に移動します。

 131~139行目はカラーを選択してもらうためのカラーパレットを表示する機能です。ラジオボタンの「color」を選択するか、色をクリックすると以下のようなウィンドウで色を選択できるようにしています。単にcolor = colorchooser.askcolor(color=initial_color)と、呼び出すだけで使えます。あ、colorchooserは、先頭の方でfrom tkinter import colorchooserとインポートしてあります。

色の設定ダイアログ

 141~148行目まで、タグを削除しています。リストからは、self.list.delete(active_position)を使って削除して、タグの一覧からはself.text.tag_delete(active_tag)でタグを取り除きます。

 151~154行目は、main()関数を定義しました。これまでは、main_application = Application()の行を、if __name__ == "__main__":の下へ記述していましたが、それだとpylintに叱られてしまいまして。なんと、コンスタント値の変数名は、UPPER_CASEの形式で宣言することが望ましいとのことです。Pythonにはコンスタント値という考え方がないので、モジュールレベルの変数はすべてコンスタント値でしょ、というふうに考えられているようです。main()関数の中に記述することでモジュールレベルの変数になることを避ける、というのが一般的な解決方法のようです。

まとめ

 今回はTextで利用されているタグを調べました。タグを使うとテキストの文字色や、背景色、フォントの設定などを変更することができます。複数のタグを使用した場合の優先順位は最後に定義されたタグの方が優先されます。あとからtag_raise()メソッド、tag_lower()メソッドを使って優先順位を変更することも可能です。

Python プログラミング matplotlib subplotの出力順を変更したい

 見に来てくださって、ありがとうございます。石川さんです。

 さて、今回は、matplotlibのsubplotとタイトルにありますが、メインはsubplotではありませんね。subplotの出力順を変更したい、というだけの、標準的なプログラミングのお話です。

やりたいこと

 通常、subplotを使うと、複数のグラフを並べて出力することができます。こんな具合です。こちらのサイトを参考に出力してみました。

subplotサンプル

 ソースコードは以下の通りです。

import numpy as np
import matplotlib.pyplot as plt

x = np.random.rand(10)
y = np.random.rand(10)
z = np.sqrt(x**2 + y**2)
verts = np.array([[-1, -1], [1, -1], [1, 1], [-1, -1]])
markers = (">", (5, 0), verts, (5, 1), '+', (5, 2))

for i, m in enumerate(markers, 1):
    plt.subplot(3, 2, i)
    plt.scatter(x, y, s=80, c=z, marker=m)

plt.show()

 11行目の、plt.subplot(3, 2, i)でサブプロットを作成しています。最初の二つのパラメータは行列を表しています。今回は、3行2列を表します。iはインデックス値で、1から順番に以下のとおり、左から右、上から下へ、3行2列を以下のような順番で出力していきます。

subplotデフォルトの出力順

 これを以下のように出力したい、という要望です。

subplot出力順をこのように変更したい

 subplotのオプションか何かで簡単に変更できないかと思って調べてみましたが、そのようなオプションは見つけられませんでした。かわりにsubplotsを使って行列の配列を取得する方法があったので、これならば、今回のような工夫は必要なさそうです。ま、でも、今回は頭の体操ですね。もっと簡単な方法があれば、どなたか教えてください。

 1~6のループを割り算したり、余りを求めたり、numpy.transpose()が使えるかも、と、検討してみたけど、これは行列が同じ数のときしか使えないとか、いろいろと実験してみました。その結果、今回は、インデックスを[1, 3, 5, 2, 4, 6]というふうに出力すれば望みどおりに出力できることがわかりました。ここまでくればもうできたも同然ですね。コーディングしてみました。

import numpy as np
import matplotlib.pyplot as plt

x = np.random.rand(10)
y = np.random.rand(10)
z = np.sqrt(x**2 + y**2)
verts = np.array([[-1, -1], [1, -1], [1, 1], [-1, -1]])
markers = (">", (5, 0), verts, (5, 1), '+', (5, 2))


def index_generator(row, col):
    for c in range(col):
        for i in range(c + 1, row * col + 1, col):
            yield i


index = index_generator(3, 2)

for i, m in zip(index, markers):
    plt.subplot(3, 2, i)
    plt.scatter(x, y, s=80, c=z, marker=m)

plt.show()

 ポイントは11~14行目、知ったかぶりをして、ジェネレータを作ってみました。パラメータは行、列です。関数内では、列数分繰り返しますが、1から始めて列数ごとに数字を取り出していきます。これにより、3行2列を指定した場合、列の1回目のループで、1、3、5、を戻し、列の2回目のループで、2、4、6を取り出しています。

 14行目の「yield i」を使うことで関数がジェネレータになります。ジェネレータは、次の要素が取り出されるまで、値を生成しないのでメモリが節約できる、という特徴があります。

 17行目でindexを定義してインデックスの生成にはこちらを利用します。ということで、19行目のforループもenumerateをやめて、zipに変更しました。

まとめ

 割り算や余りを使って計算でできるのでは、と、簡単そうに思いましたが、実は、ちょっとひねりが必要だったという、よい問題でした。

Python tkinter GUIプログラミング Canvas move4

 今日も来てくださってありがとうございます。石川さんです。

 しつこくCanvasの移動についてやってみました。前回移動するためのクラスを作ったときに、移動のメソッドをクラス側に作ったけど、呼び出しはメインクラスの方から呼び出していて、それがちょっと気に入らなかったのですが、解決できたので、紹介します。

できあがりイメージ

 できあがりイメージは前回と同じです。機能的にはほとんど同じですが、イベントの処理がクラスに分離できたところが異なります。ま、見た目はまったく同じなのですけど。

ソースコード

import tkinter as tk


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Canvas move with class")
        self.geometry("1200x800")

        self.canvas = tk.Canvas(self, background="white")

        self.canvas.grid(row=0, column=0, sticky=tk.NSEW)
        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=1)

        self.canvas.bind("<Button-1>", self.button1)

        self.canvas.movers = [Movable(self.canvas, 160, 160, "AliceBlue"),
                              Movable(self.canvas, 200, 200, "LightYellow")]

    def button1(self, event):
        item = self.canvas.find_overlapping(event.x, event.y, event.x, event.y)
        if item:
            return
        m = Movable(self.canvas, event.x, event.y, "Red")
        self.canvas.movers.append(m)

class Movable():
    BOXID = 0
    def __init__(self, canvas, x, y, color):
        self.canvas = c = canvas
        global BOXID
        BOXID += 1
        self.tag = t = "MOVABLE-" + str(BOXID)
        self.t_id = c.create_text(x, y, text=color, tag=t)
        self.r_id = c.create_rectangle(c.bbox(self.t_id), fill=color, tag=t)
        c.tag_lower(self.r_id, self.t_id)
        self.start = None
        c.tag_bind(t, "<Button-1>", self.button1)
        c.tag_bind(t, "<Motion>", self.move)
        c.tag_bind(t, "<ButtonRelease>", self.button_release)
        c.tag_bind(t, "<Button-3>", self.remove)

     def move(self, event):
        if self.start is None:
            return
        x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
        dx, dy = x - self.start[0], y - self.start[1]
        self.canvas.move(self.tag, dx, dy)
        self.start = x, y

    def button1(self, event):
        x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
        self.start = x, y

    def button_release(self, event):
        self.start = None

    def remove(self, event):
        c = self.canvas
        c.tag_unbind(self.tag, "")
        c.delete(self.tag)
        c.movers.remove(self)


if __name__ == "__main__":
    app = App()
    app.mainloop()

説明

 最初の違いは、イベントをバインドしている16行目です。クリックしたときのイベントですが、これは、Canvasをクリックしたイベントを処理するためのものです。クリックしたところに箱がなければ、新しく箱を作ります。箱を作るための処理は、21~26行目です。以前は、この位置に、move()とbutton_release()がありましたが、今回はありません。これらは全部、クラスの方で処理するようになりました。

 29行目に追加した「BOXID」ですが、Canvas上の項目をまとめるためのタグに連番をつけるためだけのものです。Movableクラスのインスタンスが作られるごとにひとつずつ増えていきます。32行目、globalキーワードを指定することで、BOXIDが外部で宣言されたグローバル変数だということを宣言しています。33行目で1を足して連番が増えるようにして、34行目でtagを「MOVABLE-連番」となるように作成しています。35行目、36行目のテキストと四角を作成しているところでtagを指定しています。

 39~42行目でtag_bind()を使って、このクラスのオブジェクトで発生したイベントとメソッドをバインドしています。ここでのポイントはtag_bind()を使ってタグごとにイベントを割り当てているところです。前回は、キャンバスごとに割り当てていたので、オブジェクトに割り当てるたびに前回のイベント割り当てが上書きされてしまい、うまく動作しなかったのでした。調べた結果、キャンバスごとでも「add=”+”」オプションをつけてバインドすることでイベント割り当てが上書きされないということが分かったのですが、tag_bind()だとタグごとの割り当てになるので、こちらの方がしっくりきますね。

 バインドされたメソッドの方ですが、イベントはそれぞれのタグごとに発生するので、クラス内でこのオブジェクトが選択されたかどうか、という判定は不要になりました。

 あと、今回は右クリックで箱を消去するようにしてみました。

まとめ

 いかがでしょう、前回よりスッキリしたと思いませんか?
いや~、また自画自賛かなぁ。(笑)

Pythonプログラミング リストの逆順

 見に来て下さって、ありがとうございます。石川さんです。

 今日は、ちょっと簡単に、リストを逆順にする方法です。

やり方

In [1]: l = list(range(10))

In [2]: l
Out[2]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

In [3]: l[-1::-1]
Out[3]: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

In [4]: l[::-1]
Out[4]: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

In [5]: list(reversed(l))
Out[5]: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

In [6]: l.reverse()

In [7]: l
Out[7]: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

解説

 1行目でリストを作成しています。3行目、4行目で内容を確認しています。0~9のリストが出力されています。6行目、7行目で逆順のリストを生成しています。9行目、10行目も、12行目、13行目も同様に逆順のリストを生成しています。

 6行目、7行目の記述ですが、[start:stop:step]は、スライスといいます。start、stopを省略すると最初と最後を指定したことになります。例のようにstartに-1、stepに-1を指定することで逆順に取り出すことができます。9行目、10行目はstartを省略しています。

15行目のreverse()は、12行目のreversed()とは異なり、リスト(l)自身の内容が入れ替わります。スライスやreversed()は、逆順のリストのコピーを戻しています。

まとめ

 ポイントはリスト自身が逆順になってしまうのか、逆順のリストのコピーを戻すのか、という点ですね。スライスのやり方については、記憶してしまいましょう。

Python プログラミング 1から10までの合計を出す

 見に来てくださって、ありがとうございます。石川さんです。

 先日、とある勉強会で、Pythonを使って1から10までの合計を出す、ということをやっていたのですが、自分でも解答を作ってみました。宿題は10個考えてくる、ということだったので、ぼくも10個、考えてみました。

 あとで、実行時間を検証するために関数にしています。

解答と結果1

In [1]: def a1(N=10):
   ...:     return 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10
   ...: print("解答1:", a1())
解答1: 55

解説1

 10個作らなきゃ、ということで真っ先に思いついたのがこれです。単純に足し算で足した結果を戻しています。1から10までの合計を算出する、という点においては合格ですが、関数としては、汎用性がなくてイマイチですね。合計が50までとか、10000まで、と変化した場合には使えません。そういう意味で、模範解答には遠いですね。

解答と結果2

In [2]: def a2(N=10):
   ...:     s = 0
   ...:     for i in range(1, N+1):
   ...:         s += i
   ...:     return s
   ...: print("解答2:", a2())
解答2: 55

解説2

 次に思いついたのが、これですね。たぶん、いちばんオーソドックスなやつでは、と、思います。forループを使って繰り返し実行しますが、等差数列が必要なときは、組み込み関数のrange()を使用します。パラメータを二つ指定していますが、ひとつめの1は、初期値で1から始める、という意味です。ふたつめのN+1は、N+1のひとつ前の数字まで繰り返す、という意味です。これで、1から始めて11のひとつまえの10まで繰り返すことができます。

 forループの構文の最後に指定するものは、イテラブルオブジェクトといって、反復可能なオブジェクトを指定します。具体的には、リストや文字列などはイテラブルオブジェクトで、繰り返し扱うための要素を複数持っているオブジェクトのことです。forループで指定されると、その要素をひとつずつ処理することができます。

 ここで使用されている組み込み関数のrange()は、パラメータで指定された数のシーケンスで、イテラブルオブジェクトになります。もっと簡単に言うと、次の整数の値を順番に返してくれるオブジェクト、ということです。一般に、forループにおいて特定の回数の繰り返し処理に使われます。文章よりも実際の戻り値を確認してもらった方が早いと思いますので、まずは実行結果をご覧ください。

In [1]: range(3).__iter__()
Out[1]: <range_iterator at 0x26faa394f50>

In [2]: i = range(3).__iter__()

In [3]: i.__next__()
Out[3]: 0

In [4]: i.__next__()
Out[4]: 1

In [5]: i.__next__()
Out[5]: 2

In [6]: i.__next__()
Traceback (most recent call last):

  File "<ipython-input-6-aa5506447029>", line 1, in <module>
    i.__next__()

StopIteration


In [7]: 

 まずは、forループの最後の要素はイテラブルオブジェクトですので、__iter__()が呼び出されて、イテレータが取得されます。Out[1]から「range(3).__iter__()」の戻り値がrange_iteratorオブジェクトということがわかります。イテレータは__next__()を呼び出すことで次の要素を戻すという約束のあるオブジェクトです。ここで__next__()を呼び出すと、順番に、0、1、2を戻していますね。In [6]の、最後3を戻すかというところで、StopIterationの例外を発生します。forループはイテレータがStopIterationが発生するまで繰り返される、というお約束になっています。このようにして、要素の数分の繰り返し処理が実行されています。

 ちなみに、__iter__()や__next__()などの特殊メソッドの部分は以下のように書き換えることもできます。

In [7]: i = iter(range(3))

In [8]: next(i)
Out[8]: 0

In [9]: next(i)
Out[9]: 1

In [10]: next(i)
Out[10]: 2

In [11]: next(i)
Traceback (most recent call last):

  File "<ipython-input-11-a883b34d6d8a>", line 1, in <module>
    next(i)

StopIteration

In [12]: 

 この特殊メソッドについて理解が進むと、Pythonでのプログラミングがより効率的にできるようになって、もっと楽しくなってくるのでは、と思います。

解答と結果3

def a3(N=10):
    s = 0
    for i in range(N):
        s += (i + 1)
    return s
print("解答3:", a3())
解答3: 55

解説3

 解答2の類似で、0から始めて9までの10回繰り返す、というループで合計値を変えるパターンですね。そんなに違いはありませんがすべての繰り返しで足し算をしている分、遅いかもしれません。

解答と結果4

In [4]: def a4(N=10):
   ...:     s = i = 0 
   ...:     while i < N:
   ...:         i += 1
   ...:         s += i
   ...:     return s
   ...: print("解答4:", a4())
解答4: 55

解説4

 Pythonで使える繰り返しもうひとつの繰り返し構文にwhileがあります。二行目で変数、sとiにそれぞれ0を代入して初期化しています。whileループの右側に条件が書いてありますが、この条件がTrue、真のあいだ、このループは繰り返されます。条件を書き間違えると無限にループしますので、注意が必要です。
 4行目の「i += 1」は、iに1を加えた値をまたiに代入する、という意味で、iが1から始まってひとつずつ増えていくことになります。5行目の「s += i」は、sにiを加えた値を代入していきますので、この行が合計部分の処理をしていると言っていいでしょう。

解答と結果5

In [5]: def a5(N=10):
   ...:     s = 0
   ...:     while N > 0:
   ...:         s += N
   ...:         N -= 1
   ...:     return s
   ...: print("解答5:", a5())
解答5: 55

解説5

 解説4は1から10までという順番で加えていきましたが、今回は、10から1までという順番で加算するところが違いますね。5行目の「N -= 1」で1ずつ減算しているところがポイントでしょうか。

解答と結果6

In [6]: def a6(N=10):
   ...:     s = 0
   ...:     for i in range(N,0,-1):
   ...:         s += i
   ...:     return s
   ...: print("解答6:", a6())
解答6: 55

解説6

 解説5をやって、range()の場合の逆順の加算を忘れていたことに気づきました。range()の第三引数に-1を指定することで、1ずつ減算していくことが可能です。まあ、a2、a3と、たいして変わりませんね。range()の使い方が異なるだけです。

解答と結果7

In [7]: def a7(N=10):
   ...:     if N == 1:
   ...:         return 1
   ...:     return N + a7(N - 1)
   ...: print("解答7:", a7())
解答7: 55

解説7

 これは、再帰呼び出しを利用した解答です。再帰呼び出しとは、関数の中で、自分自身の関数を呼び出すことをいいます。これにより繰り返し処理が可能になるプログラミング上のテクニックです。ポイントは4行目で、Nと自分自身を「N – 1」のパラメータで呼び出したときの戻り値を加算しているところです。呼び出された関数は、「N – 1」と自分自身を「(N – 1) – 1」で呼び出した値を戻して、と、次々と呼び出され、2行目の条件、Nが1になるまで再帰的に呼び出しが繰り返され、Nから1までの合計が求まります。

 このテクニックの若干の問題は、関数呼び出しを何度も繰り返すところです。関数が呼び出されると呼び出した関数はスタックというところへ積み上げられます。呼び出された関数の処理が終了するまではそこで待機、というイメージですね。これのどこに問題があるのか、というと、このスタックの上限が整数値と比べると割と小さめに設定されている点です。

 1000回では問題なく実行できましたが、10000回を指定するとエラーになってしまいました。

  File "<ipython-input-4-124a61628543>", line 4, in a7
    return N + a7(N - 1)

  File "<ipython-input-4-124a61628543>", line 4, in a7
    return N + a7(N - 1)

  File "<ipython-input-4-124a61628543>", line 2, in a7
    if N == 1:

RecursionError: maximum recursion depth exceeded in comparison

 RecursionErrorということで、再帰エラーですね。調べてみると、こちらで現在値を確認できるようです。

In [8]: import sys

In [9]: sys.getrecursionlimit()
Out[9]: 3000

 現在の値は3000でしたね。sys.setrecursionlimit()を使うことでセットもできるようですが、長くなりますので、今回はそこまで検証しませんね。

 ちなみに、return文のところへif文を記述すると、関数内は以下の一行で記述できました。ちょっぴりだけスマートな感じがしますね。

    return N + a7(N - 1) if N > 1 else 1

解答と結果8

def a8(N=10):
    return N * (N + 1) // 2
print("解答8:", a8())
解答8: 55

解説8

 これは、等差数列の合計を求める公式です。いつも公式が覚えられないので、以下の画像のような長方形の面積を考えてから、それの半分、というふうにして公式を導き出しています。下の場合、(1+10)×10が長方形の面積になって、その半分が求める合計ですね。これからN×(N+1)÷2の公式を導き出しています。

等差数列の合計を求める

 ぼくの予想では、この式を使って答えを出力するのが一番効率がよいのでは、というふうに思いました。他の処理の計算量はO(N)(Nに比例する)なのですが、この式の計算量はNの大きさによらないので、O(1)(定数)となります。

解答と結果9

In [9]: def a9(N=10):
   ...:     return sum(range(N+1))
   ...: print("解答9:", a9())
解答9: 55

解説9

 実は、宿題になる前に思いついたのがこのやり方です。Pythonだと、今回やりたいことは1行で完成します。はい、アルゴリズムの勉強にはなりませんね。

解答と結果10

In [10]: import numpy as np
    ...: def a10(N=10):
    ...:     return np.sum(np.arange(N+1))
    ...: print("解答10:", a10())
解答10: 55

解説10

 こちらもひとつ前のa9()と同じですが、numpyのライブラリを利用しています。大量データを扱うライブラリが豊富なnumpyですが、こちらの処理速度はいかがでしょうか。

まとめ

 さて、最後になりますが、ここで紹介したロジックは、どれを使うのが正解なのでしょうか。どれを使っても結果はでるし、今回のロジックはどれも時間がかかるものではありませんので、いずれも正解です。結論から言うと、思いついたものですばやく実装する、もしくは、後で見たときにわかりやすい実装にする、というので問題ないでしょうね。あ、a1は、ダメですよ。

 でも、プロとしてどのように実装するのが正解なのか、という指針を持っておきたい、という声が聞こえてきそうですね。指針のひとつとして、処理速度を計測しておきたいと思います。IPythonには%timeitというマジックコマンドがあって、いい感じに実行速度を測ってくれます。

In [11]: for f in [a1, a2, a3, a4, a5, a6, a7, a8, a9, a10]:
    ...:     print(f.__name__, "=", f(), ":", end="")
    ...:     %timeit f()
a1 = 55 :67.2 ns ± 3.61 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
a2 = 55 :753 ns ± 51.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
a3 = 55 :842 ns ± 56.8 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
a4 = 55 :799 ns ± 145 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
a5 = 55 :801 ns ± 109 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
a6 = 55 :699 ns ± 43 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
a7 = 55 :1.23 µs ± 33.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
a8 = 55 :131 ns ± 4.65 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
a9 = 55 :509 ns ± 19.1 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
a10 = 55 :3.48 µs ± 34.1 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

 a1は参考値ですが、いちばん速いですね。1から10までの計算ではそんなに差が出ませんが、再帰処理のa7とnumpyの処理がちょっと遅いようです。ちょっと1から1000までの計算で測定しなおしてみます。

In [12]: for f in [a1, a2, a3, a4, a5, a6, a7, a8, a9, a10]:
    ...:     print(f.__name__, "=", f(1000), ":", end="")
    ...:     %timeit f(1000)
a1 = 55 :78.2 ns ± 4.14 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
a2 = 500500 :51 µs ± 3 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
a3 = 500500 :78.4 µs ± 4.88 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
a4 = 500500 :85.7 µs ± 6.29 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
a5 = 500500 :104 µs ± 13 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
a6 = 500500 :51.7 µs ± 3.34 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
a7 = 500500 :177 µs ± 26.8 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
a8 = 500500 :188 ns ± 15.5 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
a9 = 500500 :16.4 µs ± 1.3 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)
a10 = 500500 :4.76 µs ± 162 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

 やはり最初の見立て通り、a8の公式を使った処理が最速で、Nの大きさによらないパフォーマンスですね。そして、a7の再帰処理、遅いですね。そしてnumpyを使ったa10は、10回と1000回でそんなに差が出ないという面白い結果になりました。大量データを扱うための工夫がありそうですね。あと、a2とa3は予想通り、a3の方が若干遅めですね。

 ごらんの通り、遅いと言ってもすべて1ミリ秒以内で完了しているので、今回のケースでは、ほとんど誤差、と考えても差し支えないでしょうね。
(1秒=1,000ミリ秒=1,000,000マイクロ秒=1,000,000,000ナノ秒)

 1から10までの合計を出すだけでも、いろいろなやり方があって、面白いですね!

Python tkinter GUI プログラミング Canvas move3

 今日も見に来てくださって、ありがとうございます。石川さんです。

 少しあいて、一週間ぶりの更新ですね。お仕事で、ちょっと忙しくしていたのもあるのですけど、採用活動のページも作ったりしていたので、まあ、仕方ありませんね。ということで今日は、またしてもtkinterのCanvasについて書いていきたいと思います。Canvas上に描かれた要素の移動については、以前にも書いているのですが、今回は、移動するものをクラスとして扱ったらどうなるのか、ということでまとめてみました。

できあがりイメージ

キャンバス上の項目を移動できます

 このプログラムは実行すると、最初にふたつ箱が表示されます。何もないところをクリックすると赤い箱が追加されます。追加された箱はマウスでドラッグすることができます。これまでは、箱の移動についてはメインのクラスで実行していたのですが、今回別のクラスに動作を委譲している、というところが大きく異なる点です。

ソースコード

import tkinter as tk


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Canvas move with class")
        self.geometry("1200x800")

        self.canvas = tk.Canvas(self, background="white")

        self.canvas.grid(row=0, column=0, sticky=tk.NSEW)
        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=1)

        self.canvas.bind("<Motion>", self.move)
        self.canvas.bind("<ButtonPress>", self.button_press)
        self.canvas.bind("<ButtonRelease>", self.button_release)

        self.movers = [Movable(self.canvas, 160, 160, "AliceBlue"),
                       Movable(self.canvas, 200, 200, "LightYellow")]

    def move(self, event):
        for m in self.movers:
            m.move(event)

    def button_press(self, event):
        event.consume = False
        for m in self.movers:
            m.button_press(event)
            if event.consume:
                return
        m = Movable(self.canvas, event.x, event.y, "Red")
        self.movers.append(m)
        m.button_press(event)

    def button_release(self, event):
        for m in self.movers:
            m.button_release(event)


class Movable():
    def __init__(self, canvas, x, y, color):
        self.canvas = c = canvas
        self.textid = c.create_text(x, y, text=color)
        self.rectid = c.create_rectangle(c.bbox(self.textid), fill=color)
        c.tag_lower(self.rectid, self.textid)
        self.start = None

    def move(self, event):
        if self.start is None:
            return
        x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
        dx, dy = x - self.start[0], y - self.start[1]
        self.canvas.move(self.textid, dx, dy)
        self.canvas.move(self.rectid, dx, dy)
        self.start = x, y

    def button_press(self, event):
        x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
        b = self.canvas.bbox(self.rectid)
        if b[0] <= x <= b[2] and b[1] <= y <= b[3]:
            self.start = x, y
            event.consume = True

    def button_release(self, event):
        self.start = None


if __name__ == "__main__":
    app = App()
    app.mainloop()

説明

 初期化処理(__init__)の、6~8行目は、Tkの初期化処理、タイトルとサイズの設定をしています。10行目で背景が白色のCanvasを作成して、12~14行目でgridを使用して配置しています。そして、16~18行目でCanvasに対するイベントをメソッドに割り当てています。20行目は、初期値として、Movableクラスを二つ追加しています。

 イベントに割り当てたメソッドはそれぞれ追加したMovableクラスの同名のメソッドを実行するように記述してあります。button_pressだけちょっと変更していて、どのMovableクラスのインスタンスも対応する処理を実行しなかったとき、要するに、クリックしたのが箱じゃなかったとき、赤い箱を追加するようにしました。

 Movableクラスの初期化処理では、渡されたキャンバスのx,yに色の名前のテキストをcreate_text()メソッドを使って描画します。その後、そのテキストを囲む箱をcreate_rectangle()メソッドで描画します。このままだと、テキストの上に箱が描画されて字が見えないので、tag_lower()メソッドを呼び出すことで上下を入れ替えてテキストが表示されるようにしています。あと、移動開始用のポジションを保持するための変数を初期化しています。

 50~57行目もMovableクラスのmove()メソッドは、self.startの位置からeventが発生させた位置へ移動させます。self.startがセットされていない場合は特に動作しません。このmove()メソッドが動作するために、59~64行目のbutton_press()メソッドでは、自分自身が選択された場合に限り、self.startで開始位置をセットします。箱がクリックされたかどうかを判定するために、event.consumeを利用するようにしています。

 66~67行目はbutton_release()では、マウスボタンが離されたときは箱が動かないように、self.startにNoneをセットしています。

 もっといいやり方があるかも知れませんが、これで責任範囲を何とか分離することができました。他に、クリックされたら選択されたような枠の印をつけるとか、その枠を使って大きさを拡大縮小するとか、ダブルクリックしたときにメニューを出すとか、必要に応じてMovableクラスへ実装することができるようになりました。

まとめ

 キャンバス上の項目をひとつのクラスの中でまとめて一度に扱うことができるようになりました。いずれは、MVCパターンを検討してみたいと思います。

Python プログラミング Enum

 今日も見に来てくださって、ありがとうございます。石川さんです。

 今回は、Pythonプログラムでコンスタント値を扱うのはどうするのかなぁ、と調べていて発見した、Enumを紹介したいと思います。

ソースコード

import enum


class PaperSize(tuple, enum.Enum):
    # Paper size
    A0 = (841, 1189)
    A1 = (594, 841)
    A2 = (420, 594)
    A3 = (297, 420)
    A4 = (210, 297)
    A5 = (148, 210)

    def __str__(self):
        return self.name

 このコードを例えばconstants.pyのようなファイルに保存することで、モジュールが作成できます。利用するためには、

from constants import *

 のようにすればよいでしょう。
※追記:この、インポートで*を使うのはよく見かけますが、バッドプラクティスだそうです。名前空間を汚染するから、というのがその理由です。なるほど。きちんと名前を指定しましょう。

 さて、標準モジュールにあったenum.Enumですが、ちゃんとドキュメントがありました。読んでもイマイチわからないので(笑)、とりあえず使い方ですね。

In [69]: for paper in PaperSize:
    ...:     print(paper)
    ...:     
A0
A1
A2
A3
A4
A5

 このように、イテレータとして使うことができます。

In [70]: PaperSize.A0
Out[70]: <PaperSize.A0: (841, 1189)>

In [71]: PaperSize.A0[0], PaperSize.A0[1]
Out[71]: (841, 1189)

 また、A0、A1といった、それぞれの要素を直接参照することができます。

In [72]: for paper in PaperSize:
    ...:     print(paper.name, paper.value)
    ...:     
A0 (841, 1189)
A1 (594, 841)
A2 (420, 594)
A3 (297, 420)
A4 (210, 297)
A5 (148, 210)

 このように「name」「value」といった属性が利用できます。

まとめ

 このような、複数の値を保持する必要がある場合は、enumモジュールのEnumを利用できるかも知れませんね。

Pythonプログラミング Spyder設定について

 今日も見に来てくださって、ありがとうございます。石川さんです。

 今日は、Python Anacondaで提供されているSpyderというIDEの設定についてです。ちょうど先ほどまで勉強会をしていたのですけど、動きが遅い、というクレームがあったのですよねぇ。確かに遅い、と、思いましたが、カーソルの表示はちゃんとしていたので、わざと遅くしているんじゃないかなぁ、と、いうことで調べてみました。

何が遅いのかというと

 動画をご覧ください。メソッドや変数名などの名称をクリックすると、同じ名称のものをハイライトしてくれるのですが、ハイライトされるまでの時間がかかりすぎです。うん、確かに遅いよね。

ハイライトされるまでの時間

 クリックされてカーソルが表示されるのは一瞬ですね。ただ、そのあとの薄い黄色に反転するまでに、1.5秒ほどでしょうか、かかっていますね。

設定

 設定を調べたら、ありましたよ。

ツール(T)-設定(F)

 「ツール(T)」の「設定(F)」を選択します。

「エディタ」の「表示」プロパティ

 開いた「設定」ウィンドウの「エディタ」を選択すると、ありました。「表示」の「変更後に構文を強調するまでの時間」が「1500ms」に設定されていました。はい、1.5秒ほどですね。

 この値を300msほどに設定しなおすと、サクサク表示されました。

まとめ

 変更後に構文を強調するまでの時間まで設定可能とは、Spyderもなかなかやりますねぇ。

Python プログラミング __name__ってなに?

 今日も見に来てくださって、ありがとうございます。石川さんです。

 お友だちから、__name__のことがよく分からない、と、質問をいただいたので、実際にどうなっているのか、実行結果を確認しつつ、説明したいと思います。

はじめに

 まず初めに、dir()という組み込み関数がありまして、コマンドインタプリターで実行すると、現在のローカルスコープにある名前のリストを返してくれます。

In [1]: print(dir())
['In', 'Out', '_', '__', '___', '__builtin__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', '_dh', '_i', '_i1', '_ih', '_ii', '_iii', '_oh', 'exit', 'get_ipython', 'quit']

 出力結果の1行目の右端にありますね。こんな感じで、「__name__」という名前がPythonインタープリタの中に事前定義されています。さて、こちらがなにか、ちょっと調べてみます。組み込み関数のtype()を使うとオブジェクトの型を返してくれます。まずはtype()で型を調べます。

In [2]: type(__name__)
Out[2]: str

 なるほど、ここで、「__name__」が文字列の変数であることがわかりました。中身を見てみましょう。

In [3]: print(__name__)
__main__

 変数「__name__」の中身は「”__main__”」ということがわかりました。

ファイルをつくって確認します

 コマンドインタープリタで確認してみると、セットされた値は「__main__」でした。この値は、「__main__」以外に設定されることはあるのでしょうか?利用されている箇所で知っているのはファイルの中でした。と、いうことで、以下のように「mynameis.py」ファイルを作ってみました。

"""" __name__ にどんな値がセットされているか、確認します。 """

print("My name is " + __name__)

 単純に、値をprint()するだけの記述を含むファイルです。
さて、実行してみます。

In [4]: %run mynameis.py
My name is __main__

 フツーに実行すると、やはり「__main__」がセットされています。ちなみに%runはIPythonのマジックコマンドで、指定されたファイルを直接実行します。コマンドプロンプトだと、「python mynameis.py」と実行するのと同じ意味になります。
さて、次にこの「mnameis.py」ファイルがモジュールだということにして、importしてみます。

In [5]: import mynameis
My name is mynameis

 importすると、mynameis.pyの内容が読み込まれ、すべて上から順番に実行されます。ただし、「__name__」には、このようにモジュール名がセットされることになっています。モジュールをつくるときは、この違いを利用して、

if __name__ == "__main__":

というふうにモジュール内にif文を記載することで、importから実行されたときと、%runなどで直接実行されたときのふるまいを変更することができます。

まとめ

 モジュールを作るとき、importで呼び出して利用するだけでなく、直接実行することができるようにするために、「__name__」を利用することができます。

Python tkinter GUIプログラミング ログインダイアログ

 今日も見に来てくださって、ありがとうございます。石川さんです。

 今回は、ログインダイアログを作ってみました。ログインダイアログは別ウィンドウなので、値の引き渡し方法をどうするのか、というのがポイントになります。

出来上がりイメージ

ログインダイアログ

ソースコード

import tkinter as tk
from tkinter.simpledialog import Dialog

class LoginDialog(Dialog):
    def __init__(self, root):
        super().__init__(root, title="Login")
        
    def body(self, master):
        self.username = tk.StringVar()
        self.password = tk.StringVar()
        userLabel = tk.Label(master, text="User:")
        userLabel.grid(row=0, column=0)
        username = tk.Entry(master, textvariable=self.username)
        username.grid(row=0, column=1)
        passwdLabel = tk.Label(master, text="Pass:")
        passwdLabel.grid(row=1, column=0)
        password = tk.Entry(master, show="*", textvariable=self.password)
        password.grid(row=1, column=1)
        self.information = tk.Label(master)
        self.information.grid(row=2,columnspan=2)
        
    def validate(self):
        self.information.configure(text="")
        if not self.username.get():
            self.information.configure(text="Input username")
            return False
        if not self.password.get():
            self.information.configure(text="Input password")
            return False
        return True

    def apply(self):
        self.result = self.username.get(), self.password.get()

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Login dialogue")
        self.geometry("400x100")
        
        show = tk.Button(self, text="login", command=self.show)
        close = tk.Button(self, text="close", command=self.destroy)
        show.grid(row=0, column=0, sticky=tk.EW, ipadx=20, padx=40, pady=20)
        close.grid(row=0, column=1, sticky=tk.EW, ipadx=20, padx=40, pady=20)
    
    def show(self):
        ret = LoginDialog(self)
        print(ret.result)

if __name__ == "__main__":
    application = Application()
    application.mainloop()

説明

 ログインダイアログは、tkinter.simpledialogのDialogを継承して作成しました。4~33行目です。ログインダイアログの中身は、body()メソッドをオーバーライドして実装します。ユーザー名とパスワードとメッセージ出力用のラベルを追加しました。もし、OKとCancel以外のボタンを使いたいときは、buttonbox()メソッドをオーバーライドしますが、今回はこれで充分でしょう。

 OKボタンを押したときに、値をチェックするには、validate()メソッドをオーバーライドします。今回は、22~30行目に記載があります。ユーザー名かパスワードが入力されていないとき、Falseを戻します。成功時はTrueを戻すようにしました。30行目のTrueを戻すことを忘れると、PythonはデフォルトでNoneを戻す約束ですので、OK押してもその後続処理のapply()は実行されませんので、注意が必要です。

 OKボタンを押したときの処理は、apply()メソッドをオーバーライドします。今回は、33行目で、ダイアログボックスに入力した値をself.resultへセットしています。self.resultは、初期化時にNoneをセットされていますので、Cancel押されたかどうか、この値を判定すればわかりますね。

 呼び出し方法は、47行目のようにします。戻り値の判定は48行目のret.resultをチェックすれば完璧です。Noneのときは、キャンセルされています。

まとめ

 ログインダイアログを作成することを通して、簡単なダイアログを作成するための基本を学びました。これで、いろいろなダイアログ、作成できそうですね。