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パターンを検討してみたいと思います。