WordPressファイルアップロードサイズを拡張しました

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

 先日MX ERGOの画像をアップしようとして、あることに気づきました。そう、なぜかファイルのアップロードサイズ上限が2MBytesに変更されていたのでした。

 そういえば、先日アップロードしたドラッグアンドドロップの短めの動画も、アップロードサイズ上限に引っかかっていました。その時は、動画だからちょっとサイズが大きいのかな、ということで、外部のサービスを使って圧縮しまくって2MBytes以下にしたのでした。上限が変更されたのかも、とはまったく思いもよりませんでした。

 ところが今回は、画像です。これまで画像をアップするときには特に上限を超えたエラーなんて、出たことなかったのですよね。それで以前アップした画像のサイズを調べてみたところ、2MBytesなんて、ぶっちぎってました。同じカメラで撮ったやつです。それで、どうして急に上限を超え始めたのかなぁ、と、考えていて、ふと、思いつきました。そういえば、先日PHPのバージョンアップして、そのせいで、初期値が変わってしまったのでは。確かに、php.iniの設定の上限のせいでアップロードできません、というメッセージもでていました。

 そこで、サーバーにログインして、php.iniを探してみました。

~$ sudo find / -name php.ini -print
/etc/php/7.3/fpm/php.ini
/etc/php/7.3/cli/php.ini
/etc/php/7.3/cgi/php.ini
/etc/php/7.3/apache2/php.ini
/etc/php/7.0/fpm/php.ini
/etc/php/7.0/cli/php.ini
/etc/php/7.0/cgi/php.ini
/etc/php/7.0/apache2/php.ini
/etc/php/7.4/fpm/php.ini
/etc/php/7.4/cli/php.ini
/etc/php/7.4/cgi/php.ini
/etc/php/7.4/apache2/php.ini

 ほら、やっぱりphp.iniがバージョンごとにたくさんありますね。php.iniファイルの中身を調べて、ファイルサイズはupload_max_filesizeらしいというところまで調査。でも、ファイルがいくつかあるので、どのファイルを修正すればよいか、ちょっとわかりません。なのでもうちょっと調べてみましょう。ディレクトリを移動します。

~$ cd /etc/php

 次に、grepコマンドでそれぞれの値を調べてみました。

/etc/php$ grep upload_max_filesize */*/php.ini
7.0/apache2/php.ini:upload_max_filesize = 100M
7.0/cgi/php.ini:upload_max_filesize = 2M
7.0/cli/php.ini:upload_max_filesize = 2M
7.0/fpm/php.ini:upload_max_filesize = 2M
7.3/apache2/php.ini:upload_max_filesize = 2M
7.3/cgi/php.ini:upload_max_filesize = 2M
7.3/cli/php.ini:upload_max_filesize = 2M
7.3/fpm/php.ini:upload_max_filesize = 2M
7.4/apache2/php.ini:upload_max_filesize = 2M
7.4/cgi/php.ini:upload_max_filesize = 2M
7.4/cli/php.ini:upload_max_filesize = 2M
7.4/fpm/php.ini:upload_max_filesize = 2M

 おお、やはり7.0を利用していた時は、100Mバイトだったようです。なので、apache2ディレクトリにあるphp.iniファイルを修正してみましょう。以下のコマンドです。今回は、viエディタを使用して、8Mバイトに修正しました。さすがに100Mバイトは大きすぎですよね。

/etc/php$ sudo vi 7.4/apache2/php.ini

 これだけで完了、と言いたいところでしたが、まだ修正した値が反映されませんでした。先日バージョンアップしたときに確認していました。サーバプロセスの再起動が必要ですね。以下のコマンドを実行しました。

sudo systemctl restart apache2

 はい、ちゃんと上限が修正されました。

8MBにかわりました。

 運用していると、思ってもいないところで、いろいろありますねぇ♪

在宅勤務3

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

 4月7日から在宅勤務を始めてもう1か月以上経ちました。開始早々、少しでも仕事の効率を上げようと、アレを注文していました。

 そして、、、やっと届きましたよ~。そう、ワイヤレストラックボール、ロジクール MX ERGOです。いや~待ちましたねぇ。4月10日に注文して5月12日に到着しましたので、ほぼ1か月ですね。新型コロナウィルスの影響でずいぶんとかかりましたね。いったいいつになったら届くのかと、ずっとヤキモキしてました。途中、キャンセルして在庫を持っているところから注文しようかと何度も悩んでしまいました。でも、みんな大変だろうから、と、ぐっとこらえましたねぇ。ホント、在宅勤務、終わっちゃうよ。

MX ERGO トラックボールマウス
MX ERGO

 なんと、箱にマグネットがついています。ちょっとムダに豪華ですね。

マグネットがついています

 ここの部分が蓋になっていたのでした。そして、ジャジャーン!

MX ERGO
MX ERGO

 しばらく使ってみて、慣れてきたら使用感なぞ、ご報告しましょうかねぇ。とりあえず、今は手に入れられたことで、ホクホクしております!実は、ちょっとマニアっぽい気がしてていて、これまで購入を躊躇していたのでした。

 あ、でも、使用感とか、そんなに報告するようなことでもないかな♪

Python tkinter GUIプログラミング アウトライン2

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

 前回、アウトラインの案として、キャンバスの中に、create_window()でキャンバスを追加する、というやり方で実装してみましたが、Toplevel()で別ウィンドウとして実装した方がよいかも、ということで、前回のソースコードに追記してみました。

出来上がりイメージ

Outlineサンプル

 Toplevel()を継承したOutlineウィンドウを追加しました。

ソースコード

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

import tkinter as tk

class Outline(tk.Toplevel):
    def __init__(self, master=None, cnf={}, **kw):
        super().__init__(master, cnf, **kw)
        self.title("Outline")
        self.canvas = tk.Canvas(self, background="lightblue")
        self.canvas.pack(fill=tk.BOTH,expand=True)
        master.update()
        print(master.winfo_width(),master.winfo_height())
        w, h = master.winfo_width()//5, master.winfo_height()//5
        self.geometry(str(w)+"x"+str(h))
        self.resizable(0,0)
        self.attributes("-toolwindow",1)
        self.attributes("-topmost",1)

    def get_canvas(self):
        return self.canvas

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Canvas outline sample")
        self.outline = None
        self.outline_item = None
        
        self.canvas = tk.Canvas(self, background="white")
        self.canvas.pack(fill=tk.BOTH,expand=True)
        self.canvas.bind("<1>",self.update_outline)
        self.canvas.bind("<Configure>",self.update_outline)
        
        self.focus_force()
        
        self.outline_window = Outline(master=self)

    def update_outline(self, event):
        if self.outline == None or self.outline_item == None:
            self.outline = o = tk.Canvas(self, background="lightblue")
            c = self.canvas
            width, height = c.winfo_width(), c.winfo_height()
            w, h = width//5, height//5
            x, y = width - w//2, height - h//2
            self.outline_item = c.create_window(x, y, width=w, height=h, window=o)
            return

        if event.type == tk.EventType.Configure:
            c = self.canvas
            width, height = c.winfo_width(), c.winfo_height()
            w, h = width//5, height//5
            x, y = width - w//2, height - h//2
            oi = self.outline_item
            x0, y0 = c.coords(oi)
            dx = x - x0
            dy = y - y0
            c.move(oi,dx,dy)
            c.itemconfigure(oi,width=w,height=h)
            self.outline_window.geometry(str(w)+"x"+str(h))

        elif event.type == tk.EventType.Button:
            c = self.canvas
            x, y = event.x, event.y
            c.create_rectangle(x, y, x+100, y+100)
            o = self.outline
            o.create_rectangle(x//5,y//5,(x+100)//5,(y+100)//5)
            c = self.outline_window.get_canvas()
            c.create_rectangle(x//5,y//5,(x+100)//5,(y+100)//5)
    
if __name__ == "__main__":
    application = Application()
    application.mainloop()

詳細

 3~18行目まででOutlineウィンドウを追加しました。ポイントは9行目のmaster.update()でしょうか。これを実行することで待機中のイベントが実行されて、ウィンドウのサイズが計算されます。もし実行しない場合は、master.winfo_width()master.winfo_height()が計算されていない状態、1と1になって、とっても小さいウィンドウが表示されます。ほとんど何も見えません。

 13、14行目は、それぞれ、self.resizable(0,0)でサイズの変更をできないようにして、self.attributes("-toolwindow",1)で最小化と最大化のボタンを非表示にしています。15行目のself.attributes("-topmost",1)では、ずっと画面の一番上に表示されるように設定しています。常に前面で表示されるようになります。

 57行目で、メインウィンドウの大きさが変わった時に同じようにアウトラインも大きさが変わるように動作させています。

まとめ

 浮かんだウィンドウの方が、しっくりきますね。キャンバス上の実装はそのままでは移動できませんので、移動できるという点でも優れていると思います。本体部分がちゃんと作れれば、アウトライン部分も割と簡単に実装できそうですね。

Python tkinter GUIプログラミング アウトライン

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

 GUIのエディターを作ったときに、アウトラインを表示する方法をちょっと検討してみました。キャンバスの中にcreate_window()でキャンバスを作成して、同じ内容を描画する、というのでどうかなぁ、ということで、ちょっとお試ししてみました。

できあがりイメージ

右下にアウトライン(鳥瞰図)表示

 クリックした個所に四角形を描画します。描画と同時に右下のアウトラインにも描画します。

ソースコード

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

import tkinter as tk

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Canvas outline sample")
        self.outline = None
        self.outline_item = None
        
        self.canvas = tk.Canvas(self, background="white")
        self.canvas.pack(fill=tk.BOTH,expand=True)
        self.canvas.bind("<1>",self.update_outline)
        self.canvas.bind("<Configure>",self.update_outline)

    def update_outline(self, event):
        if self.outline == None or self.outline_item == None:
            self.outline = o = tk.Canvas(self, background="lightblue")
            c = self.canvas
            width, height = c.winfo_width(), c.winfo_height()
            w, h = width//5, height//5
            x, y = width - w//2, height - h//2
            self.outline_item = c.create_window(x, y, width=w, height=h, window=o)
            return

        if event.type == tk.EventType.Configure:
            c = self.canvas
            width, height = c.winfo_width(), c.winfo_height()
            w, h = width//5, height//5
            x, y = width - w//2, height - h//2
            oi = self.outline_item
            x0, y0 = c.coords(oi)
            dx = x - x0
            dy = y - y0
            c.move(oi,dx,dy)
            c.itemconfigure(oi,width=w,height=h)

        elif event.type == tk.EventType.Button:
            c = self.canvas
            x, y = event.x, event.y
            c.create_rectangle(x, y, x+100, y+100)
            o = self.outline
            o.create_rectangle(x//5,y//5,(x+100)//5,(y+100)//5)
    
if __name__ == "__main__":
    application = Application()
    application.mainloop()

 ウィンドウのサイズを変更したら、アウトラインのサイズも変更されます。

まとめ

 今回は、クリックしたときに同時に小さいサイズの描画をしました。実際のGUIエディターでは、アウトラインの表示、非表示の切り替えができるようにする必要がありそうですね。スクロールとか、拡大縮小も対応しなければいけないでしょうね。

Pythonプログラミング 憧れのテストファースト2

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

 前回unittestモジュールの概要をお伝えしたのですが、Mockについてお伝えしていませんでした。Mockは、モックとかスタブとかパッチとか、いわゆる、仮想のプログラムを簡単に実現するための仕組みです。

 通常は、誰かが作っているライブラリを呼び出して、その結果をもとに振る舞いが変わるようなプログラムを作っているようなときに、自分のプログラムをテストするために必要になるプログラムです。自分の作成部分のみをテストしたいわけですから、ライブラリがこのような値を返してくるはず、という想定があって、その想定をもとに処理を書いていますので、テストでは、その想定値を返してくるプログラムがあればよいわけですね。

 では、テスト駆動開発の実践をしながら、Mockを使ってみたいと思います。テスト駆動開発は、レッド、グリーン、リファクタリングが1サイクルですね。unittestだと色はつきませんけど、ちょっとやってみます。今回は現在時刻を返してくるプログラムを想定してみます。

サンプルプログラム

 いつもは出来上がったソースコードを載せるのですが、今回は、テストファーストを試みてみます、ということでまずは、想定するテストを書いてみます。

# テストコード
import unittest

class MyWatchTest(unittest.TestCase):
    def setUp(self):
        self.mywatch = MyWatch()
    def tearDown(self):
        del(self.mywatch)
    def test_time(self):
        self.assertEqual(self.mywatch.time() == "12:05:20")

 はい、実行してみます。

 おや?何も起きません。。。お、そうそう、メインプログラムを書き忘れていました。最後に追記します。

if __name__ == "__main__":
    unittest.main()

 はい、再度、実行します。

 エラーは発生したのですけど、、、前回動かしたテストもいっしょに動いています。なぜでしょうか。。。?

 dir()を実行してみると、前回作成したテスト用のクラス「MyFirstTestCase」が残っていました。

In [36]: dir()
Out[36]: 
['In',
 'MyFirstTestCase',
 'MyWatchTest',
 'Out',
 '_',
 '_10',
 '_16',
... 以下略

と、いうことで、「MyFirstTestCase」を削除して、もう一度実行してみます。

In [37]: del(MyFirstTestCase)

In [38]: runfile('C:/work/tkinter_example/mywatchtest.py', wdir='C:/work/tkinter_example')
E
======================================================================
ERROR: test_time (__main__.MyWatchTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:/work/tkinter_example/mywatchtest.py", line 11, in setUp
    self.mywatch = MyWatch()
NameError: name 'MyWatch' is not defined

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (errors=1)

In [39]: 

 はい、望み通り、エラーが発生しました。クラスが作られていない、ということですね。当然です。クラスをつくって、エラーを取り除きます。それにしても、unittestは自動的にテストを探し出して実行してくれているのですね。

MyWatch.py

# 時刻を返すプログラム
class MyWatch():
    pass

 とりあえず、クラスをつくって最初のエラーを回避。テストを実行。エラー内容が変わりません。。。作ったのにモジュールが存在していない、ということは、テストケースを実行するときに、importが必要ですね。import unittestの下に一行追加。

import MyWatch

 再度、実行。お、エラーメッセージが変わりました。「NameError: name 'MyWatch' is not defined」と主張していたところが、以下のように変化しました。エラーは回避できていませんが、メッセージが変わったので、一歩前進です。

TypeError: 'module' object is not callable

 ええと、どうしてでしょうねぇ。。。そうそう、わかりました。import の書き方が違いました。変更します。

from MyWatch import MyWatch

 はい、実行しましょう。モジュール名とクラス名が同じだと、意味がわかりずらいですね。次回からは気を付けましょう。そういえば、Pythonにはおすすめの命名規則があったような。。。調べたら、モジュール名は、snake_caseスタイルに準拠するのが望ましいそうです。ま、今日のところは、これでいきましょう。

EReloaded modules: MyWatch

======================================================================
ERROR: test_time (__main__.MyWatchTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:/work/tkinter_example/mywatchtest.py", line 16, in test_time
    self.assertEqual(self.mywatch.time() == "12:05:20")
AttributeError: 'MyWatch' object has no attribute 'time'

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (errors=1)

 ちょっと進みました。やっと、timeという属性はありません、というところまでたどり着きました。では、timeメソッドを追加してみましょう。これ、最速でテストを通すためには、以下のようにすればよいですね。

    def time(self):
        return "12:05:20"

 実行してみます。おお、AttributeErrorのところが以下のように変わりました。

TypeError: assertEqual() missing 1 required positional argument: 'second'

 お恥ずかしい、assertEqual()の使い方を間違っていました。

        self.assertEqual(self.mywatch.time(), "12:05:20", "time()が間違っています。")

 これで、どうでしょう。またテストを実行してみます。

 はい、成功しました。

.Reloaded modules: MyWatch

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

 これで、やっと1サイクルできました。ま、リファクタリング、してませんけどね。次は、ちゃんと時刻を返すようにしましょう。datetimeモジュールをインポートして、return文を以下のように修正しましょう。

        return datetime.strftime(datetime.now(),"%H:%M:%S")

 これで実行すると、以下のようにエラーが発生します。

FReloaded modules: MyWatch

======================================================================
FAIL: test_time (__main__.MyWatchTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:/work/tkinter_example/mywatchtest.py", line 16, in test_time
    self.assertEqual(self.mywatch.time(), "12:05:20", "time()が間違っています。")
AssertionError: '20:16:16' != '12:05:20'
- 20:16:16
+ 12:05:20
 : time()が間違っています。

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (failures=1)

 はい、実装できましたが、エラーになりました。テストを通すためには、datetime.now()の戻り値が「12:05:20」になる必要があります。こういうときに、Mockが役に立ちます。具体的には、以下のような感じになります。まずは、from unittest.mock import Mockと、Mockをインポートして、test_time()メソッドを以下のように修正します。

    def test_time(self):
        original_time = self.mywatch.time
        self.mywatch.time = Mock(return_value="12:05:20")
        self.assertEqual(self.mywatch.time(), "12:05:20", "time()が間違っています。")
        self.mywatch.time = original_time

 実行すると、成功です。このようにMockを使うことで、他への影響なくtime()のふるまいを変更することができました。ただ、毎回このように元のライブラリを保持しておくのはかなり悩ましい問題ですよね。より簡潔なアプローチとして、patchが用意されています。コンテキストマネージャの形式だと以下のようになります。

    def test_time(self):
        with patch('MyWatch.datetime') as faketime:
            faketime.now.return_value = datetime.datetime(2020,5,8,12,5,20)
            faketime.strftime.side_effect = datetime.datetime.strftime
            self.assertEqual(self.mywatch.time(), "12:05:20", "time()が間違っています。")

 また、デコレーター形式で記述する以下のようになります。

    @patch('MyWatch.datetime')
    def test_time(self, faketime):
        faketime.now.return_value = datetime.datetime(2020,5,8,12,5,20)
        faketime.strftime.side_effect = datetime.datetime.strftime
        self.assertEqual(self.mywatch.time(), "12:05:20", "time()が間違っています。")

 どれも大して変わりませんね。

まとめ

 unittest.Mockを使って、テスト時のふるまいに変更を加えてみました。Mockが使えるようになると、出来上がっていないライブラリをこちらの想定の下に自分のプログラムをテストすることができるようになるなど、仕事の幅が広がりそうですね。

Pythonプログラミング 憧れのテストファースト

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

 テストファーストのコンセプトを初めて聞いたとき、めっちゃステキなアイディアやん、実現できたらどれだけしあわせなひとが増えることでしょう、と、思っていたのを思い出しました。何度かチュートリアルを経験して、すっかり忘れ去っていました。

 基本的な考え方は簡単で、

  • テスト用プログラムを追加
  • すべてのテストを実行して、追加したテストのエラーが発生するのを確認する
  • テスト対象のプログラムを修正する
  • すべてのテストを実行して、追加したテストが成功することを確認する
  • リファクタリングして、重複を排除、プログラムをきれいにする

という作業を繰り返してプログラムを育てていくプログラミング方法です。

 メリットがいくつかあって、

  • 実行可能なテストが残るため、変更があった時にも再度そのテストを実行することで、これまでの動作保証ができる。
  • テストを先に考えることで、先に仕様を明確にする必要がでてくる。
  • プログラムがテストをやってくれる。
  • テストプログラムを見ることで、作成されたプログラムの利用方法がわかる。
  • プログラミングを「動作させるためのコーディング作業」と「きれいにするためのコーディング作業」に分離できる。

 テストファーストでやっているプロジェクト、これまで見たことがなかったのですよねぇ。現在は増えてきているのでしょうか。問題点は、既存プロジェクトに途中からその考え方を持ち込むことができない(やりにくい)、と、言ったところでしょうか。経験がないのは、自分でやってみるしかないでしょう、ということで、まずは導入部分をやってみたいと思います。

Pythonでのテストファースト

 Pythonの標準モジュールの中に、テスト用フレームワークのunittestがあります。標準ドキュメントにユニットテストフレームワークの説明がありますので、詳細はここで分かると思います。今回は簡単な利用方法について説明します。

unittestの使い方

 テスト用クラスの作成は、unittest.TestCaseを継承して作成します。作成したテストクラスの中にtest_で始まるメソッドを作成することでテストが追加できます。例えば、こんな感じです。

import unittest

class MyFirstTestCase(unittest.TestCase):
    def test_my_first_test(self):
        self.assertEqual(1, 1)

    def test_my_second_test(self):
        self.assertNotEqual(1, 2)

if __name__ == "__main__":
    unittest.main()

 実行するとこんな結果が出力されます。

..
----------------------------------------------------------------------
Ran 2 tests in 0.005s

OK

 出力結果ですが、最初の「..」の点ひとつがテストメソッド一件を表していて、テストが失敗したときに「E」が出力されます。失敗するテストケースを追加してみます。

    def test_my_third_test(self):
        self.assertFalse(True)

 結果はこんな感じの出力になります。

..F
======================================================================
FAIL: test_my_third_test (__main__.MyFirstTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:/work/tkinter_example/unit_test_example.py", line 18, in test_my_third_test
    self.assertFalse(True)
AssertionError: True is not false

----------------------------------------------------------------------
Ran 3 tests in 0.005s

FAILED (failures=1)

 三つ目に「E」が出力されました。

 各テストを実行する前に必ず実行したい処理がある場合は、setUp()メソッドを追加します。各テストの終了時に必ず処理を実行したい場合は、tearDown()メソッドを追加します。

    def setUp(self):
        print('setUp executed!')

    def tearDown(self):
        print('tearDown executed!')

 実行するとこんな風な出力結果になります。

..FsetUp executed!
tearDown executed!
setUp executed!
tearDown executed!
setUp executed!
tearDown executed!

======================================================================
FAIL: test_my_third_test (__main__.MyFirstTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:/work/tkinter_example/unit_test_example.py", line 24, in test_my_third_test
    self.assertFalse(True)
AssertionError: True is not false

----------------------------------------------------------------------
Ran 3 tests in 0.006s

FAILED (failures=1)

 ちょっと「..F」のすぐ後ろに「setUp」の出力結果が続いて出ているのが気になりますが、ちゃんとテストを実行する前後に実行されているようです。

 テストケースの最初に1回だけ、最後に1回だけ実行するようなメソッドは、それぞれ、setUpClass()とtearDownClass()を追加します。ただし、クラスメソッドとして追加する必要があります。

    @classmethod
    def setUpClass(cls):
        print('setUpClass executed!')

    @classmethod
    def tearDownClass(cls):
        print('tearDownClass executed!')

 実行すると以下のように出力が変わります。

..FsetUpClass executed!
setUp executed!
tearDown executed!
setUp executed!
tearDown executed!
setUp executed!
tearDown executed!
tearDownClass executed!

と、長くなってしまいましたが、おおむねこのような感じでテストケースを作成して、プログラムを作ることになります。

新型コロナウィルスの影響か!?

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

 このサイト、誰かの役に立つかどうかもわからないと思いつつ、更新してきました。いちおう、コンセプトとしては、日々の業務で発生することとか、気づいたこととか、気になるトピックを調べて共有する、という風に考えています。誰かの役に立つ記事を書けば、見に来る人が多くなるでしょう、という想定の下、どれくらいのひとが見に来てくれているのかなぁ、というのは気になるところです。なので、よくわからないなりにGoogle Analyticsでサイト訪問の情報を収集していました。

 今週は、何人くらい見に来てくれたのかなぁ、と、何気なくGoogle Analyticsを開いてみたところ、何やら新記録を達成していました。

4月の新記録

 実は、毎月新記録を出していたのですけど、4月は1000人近くということでビックリしたので記事にしてみました。数値的には、以下の感じです。

 1月は64人、2月は122人、3月は485人、4月は993人、という感じで、人数の伸びかたが指数関数的に伸びています。きっと新型コロナウィルスの影響で増えたのでしょう。

 でも、少しは、誰かの役に立っているのでしょうね。と、思いたい。(笑)

Python matplotlib 愛の方程式?

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

 明日からゴールデンウィークですね。だけどどこかへお出かけすることもなかなか難しい状況ですよね。こういう状況なので、このごろ、SNSを見る時間がちょっぴり増えてきました。先日、たぶんツイッターだと思うのですけど、エプロンに書かれた方程式の話題がおすすめで出てきました。確か、奥さまからもらったエプロンに謎の数式が描かれてあって、どうしても気になったので、描画してみたら、奥さまの方のエプロンにすでにその描画結果が描かれてあったというお話でした。ぼくは、とっさに、「これをちょっと描画してみたい」という欲求にかられまして、お試しにmatplotlibを使ってプロットしてみることにしてみました。

 書かれていた方程式、絶対に覚えておけないと思って、とっさにメモをしました。メモには手書きではなくて、Googleキープを使いました。ルートの記号などの数式が描けなかったので、プログラミング的な数式に置き換えて、テキストで保存しました。なので、式があっているかどうかちょっぴり不安なのですよねぇ。

x**2+(y-(x**2)**(1/3))**2=1

 環境依存の記号で書くと以下のようになります。他の人たちも、ちゃんと読めるかなぁ。。。

x² + (y - ∛x²)² = 1

 最近、データサイエンティストの学習で、y=の形式にすればグラフが描ける、ということを知っていたので、まずは、展開するのかなぁ、と、調査開始。この式、まあまあ話題になっているようです。いろいろと探していると、式を展開せずとも描画できそうな記事に出会いました。

スクリプト

 上記のURLを参考に作ったスクリプトは以下の通りです。シンプルでしょ?

import numpy as np
import matplotlib.pyplot as plt
fig, ax = plt.subplots()

y, x = np.ogrid[-1.5:2:1000j, -1.75:1.75:1000j]
plt.contour(
    x.ravel(), y.ravel(), x**2 + (y - (x**2)**(1/3))**2, [1])
plt.show()

実行結果

ハートです♪

まとめ

 描画、見事に成功しました。

 マニアックな愛の告白ですよねぇ、でも、嫌いじゃないです。(笑)

Python GUIプログラミング Rounded Rectangle

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

 Tkinterでいろいろとできるようになってきたのですけど、ふと、角が丸くなった四角形を描くのはどうやったらいいのでしょうか、と、気になったので調べてみました。最終的にうまくいったのでまとめておきます。

できあがりイメージ

角が丸い矩形

ソースコード

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Rounded rectangle")
        self.geometry("430x220")
        
        self.canvas = c = tk.Canvas(self)
        c.pack(fill=tk.BOTH,expand=True)
        self.rounded_rectangle(90,50,340,150,25,fill="lightblue",outline="black")
        self.canvas.bind("<ButtonPress>",self.click)
        self.canvas.bind("<ButtonRelease>",self.release)
        self.canvas.bind("<Motion>",self.move)
        self.start = None
        
    def rounded_rectangle(self, x1, y1, x2, y2, r=25, **kwargs):
        points = [
                  x1, y1,
                  x1+r, y1,
                  x2-r, y1,
                  x2, y1,
                  x2, y1+r,
                  x2, y2-r,
                  x2, y2,
                  x2-r, y2,
                  x1+r, y2,
                  x1, y2,
                  x1, y2-r,
                  x1, y1+r,
                  ]
        self.canvas.create_polygon(points,**kwargs,smooth=True,tags="r1")
    
    def click(self,event):
        self.start = (self.canvas.canvasx(event.x), self.canvas.canvasy(event.y))
            
    def release(self, event):
        self.start = None

    def move(self,event):
        if not self.start:
            return
        original_x, original_y = self.start
        x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
        self.canvas.move("r1",x - original_x, y - original_y)
        self.start = (x, y)

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

説明

 ここでは、rounded_rectangle()というメソッドを作成して描画するようにしました。何のことはない、ポイントはキャンバスのcreate_polygon()で描画するところなのですが、特にsmooth=Trueを指定しているところでしょう。ちょっと変わったところに点を打っているのですけど、smooth=Trueを指定するだけで、このような描画が可能になりました。

 その他、click()、release()、move()などを作りましたが、どうやって矩形を動かせばいいのかと、ちょっと復習してみました。描いた矩形はマウスで移動できます。

まとめ

 角が丸い矩形はCanvasで実装しておいてほしかったよねぇ。。。(笑)

Python Tkinter GUIプログラミング 数字合わせ

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

 今回紹介する数字合わせは、先日読んだ書籍に、作業記憶と注意力を鍛え、認知プロセスをスピードアップする練習として紹介されていました。練習を積むと、直観が信じられるようになるとか。作業記憶が鍛えられると一度見たものをもう一度確認しなくても済むようになるそうです。作業記憶と、注意力、鍛えたいですねぇ。

 実際は、トランプをつかってやるのですけど、せっかくtkinterでプログラムが作れるので、ちょっと作ってみることにしました。やり方は、神経衰弱と同じです。実行するとカードが4×3の12枚配られた状態になります。クリックするとマークが表示されます。2枚ずつクリックして、数字を一致させます。

できあがりイメージ

数字合わせ 4×3

ソースコード

from tkinter import Canvas, Tk, BOTH, ALL
from tkinter import messagebox
from math import sqrt, ceil, floor
from random import sample
from collections import namedtuple

Suit = namedtuple('Suit',['figure','color','text'])
Club = Suit("\u2663","#000000","Club")
Heart = Suit("\u2665","#FF0000","Heart")
Spade = Suit("\u2660","#000000","Spade")
Diamond = Suit("\u2666","#FF0000","Diamond")
Suits = [Club,Heart,Spade,Diamond]
Rank = ["A","2","3","4","5","6","7","8","9","10","J","Q","K"]
Card = namedtuple('Card',['suit','rank'])
N = 6

class NumberMaching(Tk):
    def __init__(self):
        super().__init__()
        self.title("Number Maching")
        self.state("zoomed")
        self.cards = [Card(suit, r) for r in Rank[:N] for suit in Suits[:2]]

        self.canvas = Canvas(self)
        self.canvas.pack(expand=True,fill=BOTH)
        self.refresh_cards()

    def refresh_cards(self):
        self.canvas.delete(ALL)
        w, h = self.winfo_screenwidth(), self.winfo_screenheight()
        width, height = ceil(sqrt(N*2)), floor(sqrt(N*2))
        if N*2 > width * height:
            height += 1
        wm, hm = w // (width*2), h // (height*2) # width margin, height margin
        self.q = sample(self.cards,len(self.cards))
        self.items = [None] * len(self.q)
        self.answers = []
        self.closing = False
        self.tapped = 0

        for i in range(height):
            for j in range(width):
                n = i*width + j
                if n >= N*2:
                    break
                figure = self.q[n].suit.figure + self.q[n].rank
                color = self.q[n].suit.color
                x = j*((w-wm)//width) + wm
                y = i*((h-wm)//height) + hm
                item = self.canvas.create_text(x,y,text=figure,fill=color,font=("",60),tags="card")
                rect = self.get_rectangle(x,y,(w-wm)//width*.9,(h-wm)//height*.9)
                carditem = self.canvas.create_rectangle(rect,fill="white",tags="card")
                self.items[n] = [item, carditem, self.q[n].rank] # text, rectangle, rank
        self.canvas.tag_bind("card","<Button-1>",self.card_tapped)

    def get_rectangle(self,center_x,center_y,width,height):
        leftx = center_x - width // 2
        topy = center_y - height // 2
        rightx = center_x + width // 2
        bottomy = center_y + height //2
        return (leftx,topy,rightx,bottomy)

    def card_tapped(self,event):
        if self.closing:
            return
        self.tapped += 1
        item = self.canvas.find_closest(event.x,event.y)
        for n, a in enumerate(self.items):
            if a[0] == item[0] or a[1] == item[0]:
                break
        if n > N * 2:
            return
        isopen = False
        for i in self.answers:
            if i == n:
                isopen = True
        if isopen:
            if self.answers[-1] == n:
                if self.items[self.answers[-1]][2] == self.items[answers[-2]][2]:
                    pass #すでに正解しているときは、閉じない
                else:
                    self.canvas.tag_raise(a[1])
                    self.answers.pop()
        else:
            self.canvas.tag_lower(a[1])
            self.answers.append(n)
            if len(self.answers) % 2 == 0:
                if self.items[self.answers[-1]][2] == self.items[self.answers[-2]][2]:
                    # 同じ数字が選択されました。
                    if len(self.answers) == len(self.items):
                        message_string = ("トレーニング終了です。\n" 
                                          "問題数:" + str(N) + "\n"
                                          "タップ数:" + str(self.tapped) + "\n\n"
                                          "もう一度、やりますか?")
                        if messagebox.askyesno("Congraturation!", message_string):
                            self.refresh_cards()
                        else:
                            self.destroy()
                else:
                    self.after(500, self.close_card) # 違う数字が選択されました。
                    self.closing = True

    def close_card(self):
        if len(self.answers) < 2:
            return
        self.canvas.tag_raise(self.items[self.answers[-1]][1])
        self.answers.pop()
        self.canvas.tag_raise(self.items[self.answers[-1]][1])
        self.answers.pop()
        self.closing = False
        
if __name__ == '__main__':
    numberMaching = NumberMaching()
    numberMaching.mainloop()

説明

 もっと簡単にできると思っていましたが、意外と作るのに時間がかかってしまいました。まず、カードのマークをどうしようかなぁ、ということで、グーグル先生に相談してみました。Unicodeにトランプのマークがあったので、それを利用することにしました。初期データは、namedtupleをつかって作ることにしました。7~14行目と22行目です。あと、カードをシャッフルするのは、randomモジュールからsampleをつかっています。35行目です。順番を入れ替えるだけならshuffleでも同じように動きます。sampleshuffleの違いは、sampleは新しいリストをつくるのに対して、shuffleは指定されたリストの順序を入れ替える、という点です。

 画面のカードを描画する部分は再実行したいときに、呼び出せるようにrefresh_cards()メソッドにまとめました。28~54行目です。キャンバス上にcreate_text()でカードのマークを描画して、その上からcreate_rectangle()で白い長方形を描画してカードを表現しました。これらの項目を作成するときに、"card"タグをセットして、クリックしたときに何らかの処理ができるよう、card_tapped()メソッドをバインドしました。

 めくったカードの正解、不正解については、めくられたカードを保存するようにリストを使うことにしました。37行目のself.answers = []がその宣言部分です。カードがめくられるたびに、このanswersappend()で追加していきますが、追加したあとの判定で、めくった枚数が奇数のときはなにもしない、偶数のときは二枚目がめくられたということで、チェックするようにしました。

 チェックは、rankが同じなら数値が一致したということで何もしないのですが、不一致の場合は、カードを元通りにもどす、ということをやるようにしました。すべてめくられたら、終了メッセージを出力しています。

まとめ

 もっと簡単にできるのかと思っていましたが、意外と時間がかかってしまった原因は、カードの位置決め部分に汎用性を持たせて、N=6以外にも実行できるようにしたため、ちょっと難しくなってしまった、というところが1点目ですね。あと、当たり判定するためのデータ構造をどうしようか、と、迷いながら進めていたというところが2点目ですね。先に仕様を固めてからつくればよかったですね。小さいプログラムだからと油断し過ぎました。

 作業記憶と注意力、鍛えましょう!!!