STREAM DECKについて
拡張キーボードと呼ばれるもので、ボタンにショートカットを割り当てたりできるものです。
Linuxでも使えますが、Linuxで使えるGUIアプリ(streamdeck-ui)がイマイチなので、使う気になれなかった...のですが、Pythonのライブラリがあるので、自分で作れば良くね?と思って、作りました。
作ったもの
Linuxで使うようのものなのですが、Active Windowの切り替えにしか、Linuxに依存するコードがないので、もうちょいなんとかしたら良いんじゃないかとも思いつつ、他のOSなら別にこんなものを使う必要もないのだろうと思って、あまりやる気が起きないです。そして、一通り作ってしまったので、これ以上やるかな...というところもちょっと謎。
上のコードの中に、下記のような機能が入っています。
- YAMLでキーに何を表示、割り当てられるかを書ける
- Active Windowの切り替えでページを変えられる
- アラート機能(定期的に何かをチェックして、ページを切り替える)
- ゲーム
- 記憶力系
- 神経衰弱(対戦あり)
- 三目並べ(対戦)
- もぐらたたき
- アプリ
- アナログ時計
- ストップウォッチ
- カレンダー(現在年月日表示)
久しぶりのPython
10年以上前くらいに、Mailmanの拡張みたいなことをした覚えがあります。遠い昔なので、今回、初めて触るくらいな感じでしたが、色々やらかしつつも、割と簡単に書けました(まだ間違えてる可能性は大ですが)
今回、目的から入ってしまったので、いつもはチュートリアルみたいなのをやるんですが、特にやらずに書いてしまいました。あんまり良くないですね。 学習時間&コード書いた時間は大体2,3日くらいではないかと思いますので、まだ中途半端な理解のところばっかです。
使ったPythonのバージョン
Ubuntuのパッケージで入れたのそのまま使っていて、3.10.4
となります。
実現したかったこと
下記みたいな感じでやりたいことが増えていってしまった感じです。
最初の
- 設定をGUIじゃなくて、設定ファイルっぽく書きたい
- Active Windowの切り替えを検知して、ページの切り替えをしたい
やってるうちにやりたくなったこと
- 設定は動的に読み直したい
- アプリみたいなものも作りたい
- ゲームも作りたい
さらにやりたくなったこと
- アプリやゲームも設定ファイルから読み込ませたい
関数定義(def)
def 名前(引数): # 処理 return ret
引数にデフォルト値をつけたい
def hoge(x, y, z=3): return z
この場合、
hoge(1,2) # 3が返ります
ただ、default値が設定されていないものは、必須となりますので、
hoge()
は、エラーになります。
また、先にdefault値あり、後でなし、みたいな指定はできません。
def hoge(x=1, y, z): return z
これは、エラーになります。
関数内でglobal変数を使うとき
def hoge(args): global X
のようにします。
signal
Active Windowの切り替えなんですが、当初、スレッドとかどうせ難しいんじゃないかなぁ、と敬遠して、fork & signal でやっていました(やらなきゃよかった)。
import os current_pid = os.getpid() child_pid = os.fork() if child_pid == 0: # 子プロセスで、stream deck のメイン動作 else: # 親プロセスで、active window のチェック
子プロセス側に、こんな感じで、singal に対するhandlerを登録し、
_handler_switch = lambda signum, frame: self.handler_switch(signum, frame) signal.signal(signal.SIGUSR1, _handler_switch) # sinbal to check active window switching
親プロセスで、切り替わりが判明したら、下記のように、signal を送っていました。
os.kill(self.child_pid, signal.SIGUSR1)
なお、親プロセスには、SIGCHLDを受け取れるようにしておきましょう。
signal.signal(signal.SIGCHLD, lambda signum, frame: self.handler_sigchld(signum, frame))
とやって、まぁ、動いていたのですが、pythonのthreadは思ったより簡単だったので、後々threadに変更しました。
クラスの書き方
signal の例を見てわかるとおりですが、今は、Classで書いてますが、最初はそうではなかったです。 Classは下記のように、定義します。
class ClassName: class_var = None # クラス変数 # コンストラクタ def __init__ (self): self.member_var = None # メンバ変数 pass # メソッド def method(self, args): pass
self が必要なので、Perlが頭によぎりました。
使う側では、
from "パス" import ClassName i = ClassName() i.method(1)
のようにして、インスタンスを取って、メソッドを呼び出すことが出来ます。
サブクラス
サブクラスはクラス名の横に(親クラス)
のように定義します。
class Parent: def __init__(self, args): if args.get("data") is not None: self.data = args["data"] class SubClass(Parent): def __init__(self, args): super().__init__(args) # 親を呼びたい場合。何もしたくなければ、pass と書いておけばよいです。
pass
何回かでてきましたが、python には、pass
という文法上書かないと行けないけど、何もしたくない場合に、使うものがあります。
コンストラクタを定義しないといけないけど、def __init__(self):
の次の行に何も書かないとエラーなので、pass
とだけ書いておく、と言ったふうに使います。
def __init__(self): pass
文字列(str)、数字(int,float)、リスト(list)、タプル(tuple)、辞書(dict)
str
"hoge" 'hoge'
特に、single quote, double quoteでの差はないです。
文字の部分を取る
"hoge"[0] # h "hogehoge"[1:3] #og
フォーマットして文字列化する
下記のように{}
に当てはまりますが、
"{}.{},{}".format(1,2,3) # 1.2.3 となる
順番を明示することも出来ます。
"{2}.{1},{0}".format(1,2,3) # 3.2.1 となる
sprintf的なことをする
また、sprintfのようなことをしたければ、
"{2:02d}.{1:3d},{0:6.3f}".format(1,2,3) # 03. 2, 1.000 のようになる
int, float
a = 1 # int b = 1.1 #float
文字列 + 数字みたいなことをするとエラーになります。
a = '1' print(a+1) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can only concatenate str (not "int") to str
数字を文字列にする場合は、str(i)
のように、str
で囲めばよいです。逆なら、int
,float
で囲めばOK。
リスト
array = [1,2,3,4] len(array) # 4 array[0] # 1 array.append(1) # 末尾に追加 array.insert(0, "A") # 先頭に追加 array.pop(-1) # 末尾を取る array.pop(0) # 先頭を取る
tuple
[]
ではなくて、()
で囲めば、tupleとなります。
t = (1,2,3,4) t[0] # 1 t[3] # 4
ですが、増やしたり、削除したり操作するようなことはできない(count, index しかメソッドがない)ので、固定的な決まりのデータ集合(時、分、秒とか)に使うと良さそうです。
tupleはdictのキーにも使えます。
tupleの存在をブログを書きながら思い出したので、tupleで書くべきところをリストで書いちゃってるなーと...いうところが、結構あるなー。
dict
dictは、キーバリュー構造。他言語ならハッシュとか、ハッシュマップですね。
conf = { "A": { 1: 123, 2: 223, }, }
こんな感じです。
dcit のサブクラスにCounterというのもあるようですが、調べていません。
dict のキーを取得する
for key in conf.keys(): print(key)
dict に対するアクセス
値を取るには、
conf["A"]
のようにしてアクセス出来ますが、未定義のものについてアクセスするとエラーになります。そのため、
conf.get("A")
のようにしてアクセスします。
a = conf.get("A") if a is not None: print("Aがあるよ")
といった感じですね。
dict を key/valueセットで取り出す
for item in conf.items(): print(i[0], i[1])
といった感じです。
条件、比較
and
, or
, is
, not
, ==
, =!
なお、先の例にもありましたが、Pythonでは、おなじみの&&
や||
は使えず、and
or
で条件を書きます。
True
, False
, None
や、typeの比較に関しては、is
が使えます(==
, !=
でも良い)。他は、==
や!=
で比較します。
リストの中にある値がふくまれているかどうか
array = [0,1,2] if n in array: print("{} is in the list".format(n))
型のチェック
type
を使い、str
やint
かどうかを調べます。
if type(a) is str: print("文字列")
無名関数
lambda 引数,.. : 式
のように書きます。
f = lambda arg: 10 + arg f(10) # 20
yaml を読み込む
最初はハードコーディングしていたのですが、毎回起動し直すのも面倒なので、動的に読み込むことにしました。
import yaml import time import os # 省略 def load_conf_from_file(self): statinfo = os.stat(self._config_file) if self._config_file_mtime < statinfo.st_mtime f = open(self._config_file) conf = yaml.safe_load(f) self._KEY_CONFIG = conf["key_config"] self._config_file_mtime = statinfo.st_mtime
こんな感じです。
yaml には load と safe_load の2つのメソッドがありますが、セキュリティ上の理由で safe_load
を使うべきです。loadは互換性のために残されているだけです。
os.stat から取れる、statinfo には、st_mtime というメンバ変数があり、変更時刻が入っています。 こちらを、インスタンスのメンバに持たせてやって、古くなったら読み直しするようにしています。
thread
当初 fork & signal でやっていたものの、やっぱり threadでやるべきだよなぁ、ということでthreadにしました。 thread間のデータのやり取りに、Queue, Event, または、単にインスタンスのメンバ変数という方法もあります。
thread とQueue
簡単なthreadプログラムは、下記のような感じになります。
import threading import time import queue import sys queue = queue.Queue() def put(): i = 0 while True: i += 1 queue.put(i) time.sleep(1) try: v = queue.get_nowait() if v["stop"]: break except: pass sys.exit() def check(): while True: v = queue.get() print("receive: {}".format(v)) if v > 2: stop = True queue.put({"stop": 1}) break sys.exit() t1 = threading.Thread(target=put,args=()) t2 = threading.Thread(target=check,args=()) t1.start() t2.start() t1.join() t2.join()
queue はthread間でメッセージのやり取りができますが、get
の場合、取るものがないとblockします。
get_nowait()
(get(False)と同じ)は、blockしませんが、エラーになるので、try が必要になります。
ですが、3つ以上のthreadで使うには使いにくいですね。
thread 間でインスタンスのメンバ変数にアクセスできる
最初、queueも使っていたのですが、使いにくい...というか、インスタンスの場合、何も考えずに値を共有できるので、そっちにしました。
import threading import time import sys class Dummy: stop = False def __init__(self): self.i = 0 def put(self): self.i = 0 while True: time.sleep(1) self.i += 1 if self.stop: break sys.exit() def check(self): while True: time.sleep(1) print("set in put: {}".format(self.i)) if self.i > 2: self.stop = True break sys.exit() if __name__ == '__main__': d = Dummy() t1 = threading.Thread(target=lambda: d.put(), args=()) t2 = threading.Thread(target=lambda: d.check(), args=()) t1.start() t2.start() t1.join() t2.join()
このようにするだけで、put 側で、setした値が、check側で読み取れますし、check 側でセットしたstopについても、put側で読み取れます。
滅茶楽(ロックとかしないといけないケースはあると思いますが、今回は特に不要でした)。
クラスと main を一緒に書きたい
先程の例のように、クラスとmainを一緒に書きたい場合は、
if __name__ == '__main__':
この後に、プログラムを書けばよいです。
try 構文
例外を補足するために、try
が使えます。
try: # 処理 except Exception as e: print(e) else: # 省略化 # 正常処理 finally: # 省略化 # 共通処理
Exception
のところは、具体的なエラーを書くことで、個別のエラーに対する処理が書けます。Exception
は何にでも引っかかります。
例外時に何もしないなら、
except: pass
のように書くと良いです(いや、良くないのでは?)
Loop処理
for や while が使えます。
for
for i in range(0, 5): print(i)
while
while True: i += 1 print(i) if i> 10: break
ネストしたループのbreak
若干注意が必要と言うか、label的なものがないんですね。
for i in range(1, 5): for n in range(1, 5): print("{} * {} = {}".format(i, n, i*n)) if i * n > 15: break else: continue break
こんな感じで書くようです。わかりにくい。
下記で良いんじゃない?と思いますね。
found = False for i in range(1, 5): for n in range(1, 5): print("{} * {} = {}".format(i, n, i*n)) if i * n > 15: found = True break if found: break
random
0から9までのランダムな数を返す。
random.randint(0, 9)
0.2から10までの、ランダムな浮動小数点の値を返す。
random.uniform(0.2, 10)
スリープ
少数を渡してもOKです。
time.sleep(n)
datetime
日付はこんな感じ。
d = datetime.datetime.now() y = now.year m = now.month d = now.day wday = now.week_day() # 0がMonday, 6 がSunday # 英語に変えるなら、こんな感じ wday = {0: "Mon", 1: "Tue", 2: "Wed", 3: "Thu", 4: "Fri", 5: "Sat", 6: "Sun"}[now.weekday()] # てか、strftime使えばいい wday =- t.strftime("%a")
package とモジュール
package はディレクトリ
package_name |- __init__.py |- module_a.py |- module_b.py
ソート
sorted を使います。
tuple, list
sorted((1,2,3,4), reverse=True) # (4,3,2,1) sorted([1,2,3,4], reverse=True) # [4,3,2,1]
dict をソートしたい
key に無名関数を渡すことができます。
a = {0:2, 1:1} sorted(a.items(), key= lambda kv: kv[1]) # [(1, 1), (0, 2)] sorted(a.items(), key= lambda kv: kv[0]) # [(0, 2), (1, 1)]
ファイルのopen
open 関数を使います。
with open(file_name, mode="w") as f: f.write("aaa")
みたいにします。mode は色々あります。バイナリにするときは、modeの文字の最後にb
を付け加えます。
with
を使っていますが、with を使うと、f はブロックが終わったときに、開放されます。
with
先程のwith
の仕組みは、__enter__(self)
と、__exit__(self, exc_type, exc_value, traceback)
というメソッドで実現されています。自分で実装するなら、下記のように出来ます。
class Dummy: def __init__(self): pass def test(self): raise Exception def __enter__(self): print("ENTER") return self def __exit__(self, a, b, c): print("a:{} b:{} c:{}".format(a, b, c)) print("EXIT") return True if __name__ == '__main__': with Dummy() as d: d.test()
下記のようにすると、こんな感じで表示されます。
ENTER a:<class 'Exception'> b: c:<traceback object at 0x7f5c6324e540> EXIT
raise
先程使いましたが、例外を起こします。自分で例外を作る場合は、Exception
を継承します。
class MyException(Exception): pass class Dummy: def __init__(self): pass def test(self): raise MyException def __enter__(self): print("ENTER") return self def __exit__(self, a, b, c): print("a:{} b:{} c:{}".format(a, b, c)) print("EXIT") return True if __name__ == '__main__': with Dummy() as d: d.test()
下記のように変わりました。
ENTER a:<class '__main__.MyException'> b: c:<traceback object at 0x7f830fd414c0> EXIT
httpアクセスしたい
requests
というモジュールを使います。
res = requests.get(icon_url) if res.status_code == requests.codes.ok: with open(icon_file, mode="wb") as f: f.write(res.content)
動的にモジュールを読み込む
init.py で package_name以下のモジュールを全部読み込ませたい
from inspect import isclass from pkgutil import iter_modules,extend_path from pathlib import Path from importlib import import_module import re # iterate through the modules in the current package package_dir = extend_path(__path__, __name__) for (_, module_name, _) in iter_modules(package_dir): # import the module and iterate through its attributes module = import_module(f"{__name__}.{module_name}") for attribute_name in dir(module): attribute = getattr(module, attribute_name) if isclass(attribute): # Add the class to this package's variables globals()[attribute_name] = attribute
文字列から、improt & オブジェクトの作成
m = importlib.import_module("package_name.module_name", "package_name") o = getattr(o, "Class")(args)
PILで画像を描く
PILというモジュールで、画像を描くことが出来ます。
時計を描く
from PIL import Image, ImageDraw import math import datetime class Clock: scale_x = 100 scale_y = 100 x = 0 y = 0 l = 0 def __init__(self, scale_xy, xy, l): self.scale_x = scale_xy[0] self.scale_y = scale_xy[1] self.x = xy[0] self.y = xy[1] self.l = l def hour_pos (self, h, m, s): if h == 12: h = 0 h *= 5 h += (m / 12 + s / 60) / 60 l = self.l * 0.7 return self._pos(l, h) def min_pos(self, m, s): l = self.l * 0.9 m += s / 60 return self._pos(l, m) def sec_pos(self, second): return self._pos(self.l, second) def _pos (self, l, t): x = l * math.cos(2 * math.pi / 60 * (15 - t)) y = l * math.sin(2 * math.pi / 60 * (15 - t)) * -1 return [self.x + x, self.y + y] def get_current_hms(self): now = datetime.datetime.now() return [now.hour, now.minute, now.second] def get_current_clock_image(self, hms): im = Image.new('RGB', (self.scale_x, self.scale_y), (0, 0, 0)) draw = ImageDraw.Draw(im) hour_xy = self.hour_pos(hms[0], hms[1], hms[2]) min_xy = self.min_pos(hms[1], hms[2]) sec_xy = self.sec_pos(hms[2]) draw.line((self.x, self.y, hour_xy[0], hour_xy[1]), width=3, fill=(255,255,255)) draw.line((self.x, self.y, min_xy[0], min_xy[1]), width=2) draw.line((self.x, self.y, sec_xy[0], sec_xy[1]), width=2, fill=(255,0,0)) return im if __name__ == '__main__': clock = Clock((110, 110), (50, 50), 100) im = clock.get_current_clock_image((10, 15, 30)) # 10:15:30 im.save("clock.png")
こんな感じで時計が表示できます。
感想
という感じで特にまとまりもなく、使ったコードを書いていった感じですが、特に不満なく書けました。
エラーメッセージが親切だなーと、思いました。typo してるんじゃない?この変数名使いたいんじゃない?とか。
def hoge(i): return ii hoge(1)
とか、やらかすと、下記のような感じで怒られます。
NameError: name 'ii' is not defined. Did you mean: 'i'?
あと、対話的interfaceで、メソッド補完してくれるので、ちょっとした動作を見たいときに便利です。
% python3 >>> a = [1,2,3] >>> a. # .の後にタブを打つ a.append( a.copy() a.extend( a.insert( a.remove( a.sort( a.clear() a.count( a.index( a.pop( a.reverse() >>> a.
なお、ドキュメントは、pydoc3
コマンドで見ることが出来ます。