Practice of Programming

プログラム とか Linuxとかの話題

STREAM DECKで遊ぶために学んだPythonのこと

STREAM DECKについて

拡張キーボードと呼ばれるもので、ボタンにショートカットを割り当てたりできるものです。

Linuxでも使えますが、Linuxで使えるGUIアプリ(streamdeck-ui)がイマイチなので、使う気になれなかった...のですが、Pythonのライブラリがあるので、自分で作れば良くね?と思って、作りました。

作ったもの

github.com

Linuxで使うようのものなのですが、Active Windowの切り替えにしか、Linuxに依存するコードがないので、もうちょいなんとかしたら良いんじゃないかとも思いつつ、他のOSなら別にこんなものを使う必要もないのだろうと思って、あまりやる気が起きないです。そして、一通り作ってしまったので、これ以上やるかな...というところもちょっと謎。

上のコードの中に、下記のような機能が入っています。

  • YAMLでキーに何を表示、割り当てられるかを書ける
    • URL からfavicon取ってくる(ルートパスに /favicon.ico 付けてるだけですが)
    • 画像として、URLを指定できる
    • ページ切り替え指定も簡単に書ける
  • Active Windowの切り替えでページを変えられる
  • アラート機能(定期的に何かをチェックして、ページを切り替える)
  • ゲーム
    • 記憶力系
    • 神経衰弱(対戦あり)
    • 三目並べ(対戦)
    • もぐらたたき
  • アプリ
    • アナログ時計
    • ストップウォッチ
    • カレンダー(現在年月日表示)

久しぶりのPython

10年以上前くらいに、Mailmanの拡張みたいなことをした覚えがあります。遠い昔なので、今回、初めて触るくらいな感じでしたが、色々やらかしつつも、割と簡単に書けました(まだ間違えてる可能性は大ですが)

今回、目的から入ってしまったので、いつもはチュートリアルみたいなのをやるんですが、特にやらずに書いてしまいました。あんまり良くないですね。 学習時間&コード書いた時間は大体2,3日くらいではないかと思いますので、まだ中途半端な理解のところばっかです。

使ったPythonのバージョン

Ubuntuのパッケージで入れたのそのまま使っていて、3.10.4となります。

実現したかったこと

下記みたいな感じでやりたいことが増えていってしまった感じです。

最初の

  1. 設定をGUIじゃなくて、設定ファイルっぽく書きたい
  2. Active Windowの切り替えを検知して、ページの切り替えをしたい

やってるうちにやりたくなったこと

  1. 設定は動的に読み直したい
  2. アプリみたいなものも作りたい
  3. ゲームも作りたい

さらにやりたくなったこと

  1. アプリやゲームも設定ファイルから読み込ませたい

関数定義(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というのもあるようですが、調べていません。

docs.python.org

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を使い、strintかどうかを調べます。

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 コマンドで見ることが出来ます。