Works by ...

プログラミング関連でメモする

PythonでMacOS Xデスクトップアプリを作るときの選択肢【rumps/PyObjC】

この記事は、python Advent Calendar 2017【22日目】の記事になります。 本記事では、pythonMacのデスクトップアプリを作成できるライブラリrumpsPyObjCを紹介します。

pythonMac固有の機能を持つアプリを実装する

デスクトップアプリケーションをpythonで作る場合、以下のようなGUIツールキットの使用が検討できると思います。

GUIツールキットを使うことで、MacだけでなくWindowsUbuntu等、複数OSをサポートする形でアプリを作成できます。しかし(通知やメニューバーアプリなどの)Mac固有の動作がアプリに求められる場合、上記のようなツールキットだけでは実装できない場合があります。
じゃあSwiftで書けよという話になってくるのですが、どうしてもpythonのモジュール群を利用しつつMac専用の機能を利用したアプリを作りたいという場合に使えるのがrumpsPyObjCです。

rumps

rumpsはMacのメニューバーアプリ作成に特化したモジュールです。Macでのメニューバーアイコン実装・通知実装であれば、rumpsでとても簡単に実装できます。

インストール

インストールは例によってpip。とっても簡単。

pip install rumps

実装例

#!/usr/bin/env python
# coding: utf-8

import rumps


class App(rumps.App):
    def __init__(self):
        super(App, self).__init__("App")
        # メニューリストを登録
        self.menu = ["Parent"]
        # メニューは入れ子にできる
        self.menu["Parent"].add("Child1")
        self.menu["Parent"].add("Child2")
        self.menu["Parent"]["Child2"].add("GrandChild1")
        self.menu["Parent"]["Child2"].add("GrandChild2")
        # メニューバーアイコンも設定可能
        # self.icon = "icon.png"

    # rumps.clickedデコレータでもメニューを追加できる
    # rumps.clicked()の第一引数はメニューラベルになる
    # デコレートされた関数/メソッドは引数senderをとる
    @rumps.clicked("Alert!")
    def alert(self, _):
        # アラートダイアログ
        rumps.alert("Hello!")

    @rumps.clicked("Notify!")
    def notification(self, _):
        # Macの通知
        rumps.notification(message="Hello World!",
                           title="Hello!",
                           subtitle="World!")

    @rumps.clicked("Off")
    def switch(self, sender):
        # メニューにチェックマークをつける
        sender.state = not sender.state
        sender.title = "On" if sender.state else "Off"

    @rumps.clicked("Show Window!")
    def window(self, _):
        # テキストエディットを含むウィンドウを表示
        rumps.Window(message="Showing Window!",
                     title="Window",
                     default_text="default text...",
                     ok="Submit!",
                     cancel="Cancel...").run()

    @rumps.clicked("Start Timer!")
    def timer(self, _):
        # 一定時間ごとに処理を実行するタイマー

        count = 0

        # callback関数は引数にTimerオブジェクトをとる
        def counter(t):
            nonlocal count
            count += 1
            print(count)
            if count >= 10:
                print("Stop Timer!")
                t.stop()

        # タイマーオブジェクト
        # 一定時間ごとにcallbackを呼び出す
        timer = rumps.Timer(callback=counter, interval=1)
        timer.start()


# クラスメンバでなくとも、clickedデコレータを関数に付加するとメニューは追加される
@rumps.clicked("outer")
def outer(_):
    pass


if __name__ == "__main__":
    App().run()

実行結果

f:id:rennnosukesann:20171222213042p:plain

1. アラートダイアログ

おなじみのアラートダイアログ。

f:id:rennnosukesann:20171222212004p:plain

2. 通知

Macの通知です。通知センターでも管理対象となります。

f:id:rennnosukesann:20171222212455p:plain

3. メニューにチェックマークをつける

sender.stateのbool値がTrueの場合、メニューにチェックマークが付きます。

f:id:rennnosukesann:20171222212738p:plain f:id:rennnosukesann:20171222212834p:plain

4. テキストエディットを含むウィンドウを表示

テキストエディット付きウィンドウ。

f:id:rennnosukesann:20171222214531p:plain

5. 一定時間ごとに処理を実行するタイマー

GUIコンポーネントの類ではありませんが、Timerクラスを使うことで一定時間ごとの非同期処理が簡単に実装できます。

f:id:rennnosukesann:20171222214423g:plain


上記のコードでrumpsが提供する機能の殆どは網羅できていると思います。 メニュー作成+クリックイベントの紐付けをデコレータで実装でき、通知やアラート等も一行で書けて簡単です。 個人的にはメニューバーを起点とするMacのアプリを作ることが最近多いので、非常に重宝しています。

一方、rumpsは非常にシンプルで使いやすいのですが、機能が不足している感も否めません。 おそらくrumpsをいじくるうちに

  • メニューバーアイコンをクリックした時点でウィンドウを開きたい
  • Macの通知センターを使いたい
  • なんならSwiftでできることをpythonでやりたい ...etc

といった欲求が湧いてくると思います。

pythonでより柔軟にMac上で動作するアプリを作れないかと調べた所、Objective-Cpython経由で操作するラッパーとしてPyObjCと言うものがある模様。こちらについて調べてみました。

PyObjC

PyObjCpythonObjective-Cの双方向ブリッジで、これを使うことでpythonからObjective-Cクラスライブラリにアクセスできるとのこと。 「双方向」ブリッジというだけあって、pythonからObjective-Cが使えるだけでなく、Objective-Cからもpythonが使えるそうです。

インストール

インストールは例によっt(ry

pip install pyobjc

何はともあれHello,World

#!/usr/bin/env python
# coding: utf-8

from Foundation import NSLog

if __name__ == '__main__':
    # Hello, World!
    NSLog('Hello, World!') #2017-12-22 22:43:18.523 python[5737:43506794] Hello, World!

Foundationモジュールは、Objective-Cの基本機能がまとめられたもののようです。
Foundationモジュール中のNSLogを使うと、pythonの標準出力とは異なる形式で文字列がコンソール上に出力されました。

アラートダイアログ

#!/usr/bin/env python
# coding: utf-8

from AppKit import NSAlert

if __name__ == '__main__':
    # Hello, World!
    alert = NSAlert.alloc().init()
    alert.setMessageText_(u'Hello, World!')
    alert.runModal()

f:id:rennnosukesann:20171222225528p:plain

NSAlertクラスでアラートを出現させることができます。 但し普通にNSAlertでそのままインスタンス化はできず、NSAlert.alloc().init()で領域確保→初期化からのインスタンス取得をする必要があります。
このあたりの記述はObjective-Cにバインドする都合上仕方ないのかもしれませんが、少し冗長になっている印象を受けました。

メニューバーアイコン+メニュー

#!/usr/bin/env python
# coding: utf-8

from Foundation import *
from AppKit import *
from PyObjCTools import AppHelper
import rumps

class MenuBarApp(NSObject):

    def applicationDidFinishLaunching_(self, notification):

        # ステータスバー
        statusbar = NSStatusBar.systemStatusBar()

        # ステータスアイテム(アイコン)
        self.statusitem = statusbar.statusItemWithLength_(NSVariableStatusItemLength)

        # アイコンの設定
        image = NSImage.alloc().initByReferencingFile_('icon.png')
        self.statusitem.setImage_(image)

        # メニュー
        self.menu = NSMenu.alloc().init()
        # メニューアイテムその1
        menuitem = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_('Hello, World!', 'hello:', 'world!')
        self.menu.addItem_(menuitem)
        # メニューアイテムその2
        menuitem = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_('Quit', 'terminate:', '')
        self.menu.addItem_(menuitem)

        # メニューをステータスアイテムに紐付け
        self.statusitem.setMenu_(self.menu)

    def hello_(self, notification):
        rumps.notification("Hello, World!", "hello!", "world!")


if __name__ == "__main__":
    app = NSApplication.sharedApplication()
    delegate = MenuBarApp.alloc().init()
    app.setDelegate_(delegate)
    AppHelper.runEventLoop()

f:id:rennnosukesann:20171222233409p:plain

f:id:rennnosukesann:20171222233455p:plain

f:id:rennnosukesann:20171222233442p:plain

NSStatusBar.systemStatusBar()からステータスバーオブジェクトを取得し、そこにステータスアイテム(アイコン)を登録していくみたいですね。 rumpsの方がObjective-Cクラスライブラリ周りの記述について知らなくても良くて楽です。

終わりに

rumpsは簡単なMacのアプリケーションを作る場合には楽ですが、複雑なアプリケーションの作成にはやや不向きです。

PyObjCの場合、Objective-Cの提供する機能が(おそらく)全て使えるので柔軟な分、Objective-Cの流儀に合わせていく必要があるところが難しいのではと感じました。

今回は時間の都合上、PyObjCを十分に深掘りすることができてません。。。
慣れていけばrumpsより複雑なことができると思うので、もっとさわっておこうと思います。

【shell】awkで引数文字列から部分文字列を取得する

やり方

$ cat hoge.txt | awk '{print substr($0,1,2)}'   

概要

shellの強力なコマンドの一つにawkがあります。

AWK - Wikipedia

awkを使い始めて未だ日が浅いのですが、コマンド出力を一旦整形して他のコマンドに渡したり、ログファイルを時々で必要な形にパースをする際にお世話になってます。

部分文字列の取得はcutコマンドでも可能なのですが、awkのやり方も調べてみたらあったのでメモりました。

src

awkに渡す命令中でsubstr()関数を呼び出します。 第1引数にカットしたい文字列、第2-3引数に開始・終了インデックスを渡します(インデックスは1から開始)

# dummy file
$ cat hoge.txt 
hoge1 fuga1 piyo1
hoge2 fuga2 piyo2
hoge3 fuga3 piyo3

# awkでも全項目表示
$ cat hoge.txt | awk '{print $0}'            
hoge1 fuga1 piyo1
hoge2 fuga2 piyo2
hoge3 fuga3 piyo3

# 各行の先頭から二文字目までの部分文字列取得
$ cat hoge.txt | awk '{print substr($0,1,2)}'            
ho
ho
ho

まとめ

substr()に限らず、Cライクな関数は結構使えそうなので便利ですね。

【Java】 リフレクションで取得したインナークラスコンストラクタの引数は、エンクロージングクラスのインスタンスを必要とする

背景

単体テストでインナークラスのテストをしたい(テストしやすい設計にすべきかどうかは別として)ときに、インナークラスのコンストラクタがprivateの場合、インスタンス化をリフレクションで行いたい。。。

・このとき、getDeclaredConstructor()で取得したコンストラクタオブジェクトの引数型が想定と異なり小一時間詰まってしまった

具体例

例えば以下のようなクラス定義があるとする

public class Outer {

    public class Inner{

        public Inner(){
            // 引数なしコンストラクタ
        }

        public Inner(String str){
            // String形変数を一つ引数に取るコンストラクタ
        }

    }

}

エンクロージングクラスとしてOuterクラスを、 インナークラスとしてInnerクラスを定義しました。

次に単体テストとして、以下コードを記述

public class TestInner {

    @Test
    public void test() throws Exception{

        // コンストラクタ一覧
        Constructor<?>[] constructors = Outer.Inner.class
                .getDeclaredConstructors();

        // 全コンストラクタのコンストラクタタイプを取得
        Arrays.asList(constructors).forEach(
                c -> {
                    System.out.println(c.getName() + ":");
                    Arrays.asList(c.getParameterTypes()).forEach(p -> System
                            .out.println(p));
                    System.out.println();
                }
        );

        // NG
        // constructors[0].newInstance();

        // OK
        constructors[0].newInstance(new Outer());

    }

}

上記テストを実行

Outer$Inner:
class Outer

Outer$Inner:
class Outer
class java.lang.String

出力を見ると、コンストラクタの引数型に親クラス型が含まれているのがわかります。 実際にインスタンス化するときにも、親クラスのインスタンスをコンストラクタ引数として渡す必要があることがわかります。

なぜ親クラスインスタンスが必要?

親クラスインスタンスが必要な理由は、インナークラスであるInnerにstatic修飾子をつけると理解できます。

public class Outer {

    public static class Inner{

        public Inner(){
            // 引数なしコンストラクタ
        }

        public Inner(String str){
            // String形変数を一つ引数に取るコンストラクタ
        }

    }

}

上記クラス定義でテストを実行してみると、各コンストラクタがOuterインスタンス を引数に取らなくなります。

public class TestInner {

    @Test
    public void test() throws Exception{

        // コンストラクタ一覧
        Constructor<?>[] constructors = Outer.Inner.class
                .getDeclaredConstructors();

        // 全コンストラクタのコンストラクタタイプを取得
        Arrays.asList(constructors).forEach(
                c -> {
                    System.out.println(c.getName() + ":");
                    Arrays.asList(c.getParameterTypes()).forEach(p -> System
                            .out.println(p));
                    System.out.println();
                }
        );

        // InnerがstaticだとOK
        constructors[0].newInstance();

        // InnerがstaticだとNG
        // constructors[0].newInstance(new Outer());

    }

}
Outer$Inner:

Outer$Inner:
class java.lang.String

インナークラスのクラス定義は非staticであり、エンクロージングクラスがインスタンス化されて初めてクラス定義が参照可能になります。 そのため、リフレクションで取得したコンストラクタは、非staticなインナークラスをインスタンス化するためにエンクロージングクラスインスタンスを取得しているのだと考えられます。

// Innerが非staticの場合
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();

// Innerがstaticの場合
Outer.Inner inner = new Outer.Inner();

おわりに

どのクラスからも参照可能であるがインスタンス化は特定のクラスでのみやりたい、みたいなシチュエーションは割りとあったので、これからもこのやり方を使っていく気がする

【python】pythonのクロージャ

pythonでは、クロージャと呼ばれる関数オブジェクトを実装することができます。

関数オブジェクトとは、クラスのインスタンスのように関数をオブジェクトとして扱ったものです。

クロージャの定義は、Wikipedia氏曰く

引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。(Wikipediaより引用)

だそうです。

実装例を下に示します。

def counter():
    c = 0
    def count():
        nonlocal c
        c = c + 1
        return c
    return count

f = counter()
print(f()) # 1
print(f()) # 2
print(f()) # 3

よくあるクロージャの実装では、関数の中にさらに関数を定義し、 外側の関数に変数などを持たせ、内側の関数にその更新を行わせます。 外側の関数は内側関数のオブジェクトを返戻することで、「変数等の状態を保持する関数」としての機能を提供します。

例えば上のコードでは、

counter()は変数cを定義し、count()cの更新+cの返戻を行っています。

②関数counter()count()関数を内側で定義し、かつ関数オブジェクトとして返戻しています。

counter()から返戻されるcount()関数オブジェクトが変数fに代入され、呼び出し毎に返戻値が変化しています。

counter

結果としてfが状態を持つ関数を提供し、同じ呼び出しに対して異なる値が返ってきています。

【python】pythonにおけるスコープ【グローバル変数】

pythonでは変数のスコープに最新の注意を払う必要があり、特に万一グローバル変数(厳密にはスコープはモジュール内に収まるので、モジュール変数?)の変更を扱うような場合、気を緩めていると下のコードに示すような問題を引き起こす可能性があります。

is_odd = False
def check_odd(value):
    if value % 2 == 1:
        # 書き換え(たつもり)
        is_odd = True

check_odd(1)
print(is_odd) # False(Trueではない)

のような問題が起こります。 問題はランタイムがこれを正常なコードとして解釈できてしまうことで(当然ですが)、check_odd内でのグローバル変数is_oddの書き換え(たつもり)処理は単なるcheck_odd中のローカル変数is_odd定義に終わります。なのでグローバル変数is_oddに変化はありません。

global修飾子を使えば、一応所望の結果を得ることができます。

is_odd = False
def check_odd(value):
    global is_odd # グローバルなis_oddを参照
    if value % 2 == 1:
        # グローバルなis_oddを書き換え
        is_odd = True

check_odd(1)
print(is_odd) # True

ただ身も蓋もないことを言ってしまうと、個人的にはこのようなグローバル領域に変数を置くこと自体避けるべきと考えます。上記のような副作用を防ぐ意図だけでなく、変数=状態の変遷という意味でも、特別なことがない限り変数はオブジェクトに管理させるべきです。

グローバル領域には、基本的に定数扱いの変数のみを置くべきだと思います。

# ホントはmathやnumpyのπやexpの使用を推奨
PI = 3.141592
NAPIER = 2.718281828459045

def gaussian(x, mu, sigma):
    return 1 / ((2 * PI) ** 0.5 * sigma) * (NAPIER ** ( - (x - mu) ** 2 / (2 * sigma * sigma)))

print(sum([gaussian(i, 0, 1) for i in range(-100,100)])) #1.000000109372639

前述の通り、pythonではglobal修飾子を明示的につけない限り、変数への代入記述はローカル変数に対するものと解釈されます。なのでglobalと書かなければ故意にグローバル変数を書き換えてしまうことはありません。仮にグローバル変数=定数という規約のもと、グローバル変数と同じ名前の変数への代入をより小さなスコープ内で行った場合、それはそのスコープ内での変数定義となります。

ちなみに変数の参照時には、最も内側のスコープから外側へと同名の変数を探索します(それでもなかったら、Build-Inの変数を参照します)。すなわち同一スコープ内に定義がされてない場合、勝手に外側のスコープ内の変数が参照されます。そのためグローバル変数は参照するだけならglobal修飾子を必要としません。

【python】リスト・ジェネレータ・集合・辞書内包表記

リスト・ジェネレータはともかく、辞書なんかはたまに忘れるのでメモ。

In [1]: array = [1, 2, 3, 4, 5, 6, 7]

# リスト
In [2]: [a*a for a in array]
Out[2]: [1, 4, 9, 16, 25, 36, 49]

# ジェネレータ
In [3]: (a*a for a in array)
Out[3]: <generator object <genexpr> at 0x1041c10a0>

# 集合
In [4]: {a*a for a in array}
Out[4]: {1, 4, 9, 16, 25, 36, 49}

# 辞書
In [5]: {a:a*a for a in array}
Out[5]: {1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49}

あと余談ですが、ジェネレータはリスト内包表記で生成するリストが肥大化してしまうような時におすすめです。

# リスト
# a=... の時点で全要素分のメモリを確保
# メモリを食うし、末尾近傍の要素は使わないかもしれない
a = [i*i for i in range(2 ** 10 * 10000000)]  

# ジェネレータ
# b=... の時点では要素の生成は発生しない
b = (i*i for i in range(2 ** 10 * 10000000)) 
b.next() # ここで初めて要素生成

【python】スライシングしたリストへの代入

pythonのシーケンスにはスライス機能と呼ばれる糖衣構文が用意されていますが、

In [1]: a = [1, 2, 3, 4, 5, 6, 7]
# 部分リストを取得可能
In [2]: a[2:5]
Out[2]: [3, 4, 5]

どうやらスライスによって一定範囲に対する代入操作も行えるようで、さらに代入するシーケンスのサイズが同一でなくても良いようです。

In [1]: a = [1, 2, 3, 4, 5, 6, 7]

In [2]: a
Out[2]: [1, 2, 3, 4, 5, 6, 7]
# スライス代入操作可能&サイズも任意
In [3]: a[2:5] = ['a', 'b']

In [4]: a
Out[4]: [1, 2, 'a', 'b', 6, 7]