Python」タグアーカイブ

Python multiprocessing

17.2. multiprocessing ? プロセスベースの並列処理 ? Python 3.5.2 ドキュメント

Python にはこんな標準モジュールがあったんだ。
コレでマルチコア CPU をフル活用できるかも、早速使ってみよう。

#!/usr/bin/env python3

import multiprocessing

arr = [1, 2, 3]

def on_mp(num):
    print(num * 2)
    arr.append(num * 2)

for num in arr:
    p = multiprocessing.Process(target=on_mp, args=(num,))
    p.start()
    p.join() 

print(arr)

''' output
2
4
6
[1, 2, 3]
'''

…なんだこれ。

[1, 2, 3, 2, 4, 6]
が期待できるけどならない、例外もエラーも吐かない。
append した整数はいったいどこに格納されたんでしょう?

日本語で探しても皆 print ばかり、気がついていない人って多いかも。
せっかくマルチスレッドにしても変数に値を格納できなきゃ無意味だ。
色々探してなんとか見つけた。

multithreading – Python multiprocessing.Pool: when to use apply, apply_async or map? – Stack Overflow

apply_async メソッドの callback 指定からなら変数に入れられるようだ。
クラスメソッドでも大丈夫なのかな?試してみる

#!/usr/bin/env python3

import multiprocessing

class MpTest:
    def __init__(self):
        self.arr = [1, 2, 3]
        pool = multiprocessing.Pool()
        for num in self.arr:
            pool.apply_async(self.on_pool, args=(num,), callback=self.on_pool_result)
        pool.close()
        pool.join()

    def on_pool(self, num):
        return num * 2

    def on_pool_result(self, result):
        self.arr.append(result)

app = MpTest()
print(app.arr)
''' output
[1, 2, 3, 2, 4, 6]
'''

うん大丈夫。
よしコレで PyGObject でも本格的なマルチスレッドが。

#!/usr/bin/env python3

import multiprocessing, gi, sys
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gio, Gdk, GdkPixbuf

GF_PATH = "/home/sasakima-nao/pic/test"

class MpTest(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app)
        button = Gtk.Button(label="show CBZ")
        button.connect("clicked", self.on_button_clicked)
        self.fbox = Gtk.FlowBox(valign=Gtk.Align.START, max_children_per_line=10, min_children_per_line=10)
        sc = Gtk.ScrolledWindow()
        sc.add(self.fbox)
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        vbox.pack_start(button, False, False, 0)
        vbox.pack_start(sc, True, True, 0)
        self.add(vbox)
        self.resize(800, 600)
        self.show_all()

    def on_button_clicked(self, button):
        d = Gio.file_new_for_path(GF_PATH);
        infolist = d.enumerate_children(
            Gio.FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
            Gio.FileQueryInfoFlags.NONE)
        pool = multiprocessing.Pool()
        for info in infolist:
            name = info.get_display_name()
            pool.apply_async(self.on_pool, args=(name,), callback=self.on_pool_result)
        pool.close()
        pool.join()

    def on_pool(self, name):
        s = "{0}/{1}".format(GF_PATH, name)
        p = GdkPixbuf.Pixbuf.new_from_file(s)
        minp = p.scale_simple(80, 100, GdkPixbuf.InterpType.BILINEAR)
        image = Gtk.Image(pixbuf=minp, visible=True)
        return image

    def on_pool_result(self, result):
        """
            Error @ result is a Python Type Onry ?
        """
        self.fbox.add(result)

class MpApp(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self)

    def do_activate(self):
        MpTest(self)

app = MpApp()
app.run(sys.argv)

できなかった。

error_no_gtkwidget

callback で得られる引数を GtkWidget と認識できないようです。
裏技っぽく list にして戻しても駄目、GdkPixbuf を戻すようにしても駄目。
文字列や整数なら問題無し、Python の型以外は戻せないってことみたい。

スレッド自体は動いているから CPU 負荷を見てみる。

cpu_weit

うん見事にマルチコアをフル活用できている。
でも前回 C で作ったものより負荷が多い、速さでは話にならないな。
とはいえシングルスレッドよりはるかに速いのは間違いない。

一番負荷が掛かる GdkPixbuf への取り込みをマルチスレッドにしたいのに。
そういう使い道には現在は対応していないってことのようです。
いや手段を知らないだけかも、引き続き色々探してみる。

GNOME Gsf

こんなの見つけた。

Zip files: GSF Reference Manual

何だよ、GNOME には ZIP ファイルを扱うライブラリがあったのか。
GLib や Gio ばかり見ていて気が付かなかった。

gsf_gir

Fedora 24 には普通に最初から libgsf が入っていますね。
GObject Introspection からも使える、これは使わない手はない。
以下は Vala だけどサンプルコードも見つかった。

Projects/Vala/GSFSample – GNOME Wiki!

早速コードを書いてみたけど色々困った。
gsf_infile_num_children はアーカイブの一階層分しか得ることができない。
階層がある場合は gsf_infile_num_children の戻り値を再帰するしかない。

しかも二階層以下はソートが名前の逆順に。
Python の ZIP ファイルもそうだったけど何故だ?
しかたがないので一階層以降は逆順の再帰で。

ついでに、サイズ取得が何故かプロパティ。
gst_input_size は size_t で size property は int64 だし。
意味ワカンネェけど動いたからいいや。

#!/usr/bin/env python3

import sys, gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gsf', '1')
from gi.repository import Gtk, Gio, GdkPixbuf, Gsf

PATH = "ziptest.cbz";
#PATH = "なえコレ.zip"

class GsfTest(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app)
        self.datas = []
        # unzip
        f = Gsf.InputStdio.new(PATH)
        infile = Gsf.InfileZip.new(f)
        self.read_zipfile(infile)
        # add
        fbox = Gtk.FlowBox(valign=Gtk.Align.START, min_children_per_line=5)
        for data in self.datas:
            fbox.add(data)
        self.add(fbox)
        self.show_all()

    def read_zipfile(self, infile):
        num = infile.num_children()
        for i in range(num):
            item = infile.child_by_index(i)
            if item.size == 0:
                self.read_childfile(item)
            else:
                self.append_data(infile, item)

    def read_childfile(self, infile):
        num = infile.num_children()
        for i in range(num-1, -1, -1):
            item = infile.child_by_index(i)
            if item.size == 0:
                # recursive function
                self.read_childfile(item)
            else:
                self.append_data(infile, item)

    def append_data(self, infile, item):
        data = item.read(item.size)
        try:
            stream = Gio.MemoryInputStream.new_from_data(data)
            p = GdkPixbuf.Pixbuf.new_from_stream(stream)
            minp = p.scale_simple(80, 100, GdkPixbuf.InterpType.BILINEAR)
            image = Gtk.Image(pixbuf=minp)
            self.datas.append(image)
        except Exception as e:
            print(e)


class GsfApp(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self)

    def do_activate(self):
        GsfTest(self)

app = GsfApp()
app.run(sys.argv)

コレでイケたけど。
階層の名前を取り出してソートしたほうが無難だろうか?

それより問題は速度がどうなるかだ。
Comipoli beta3 | PaePoi
の時からどう変わったか Comipoli に組み込んで同じ巨大ファイルを。

gsf_cpu

あんまり変わんねえヤン…
期待したのに。
せっかく安定して動いているのを変えるべきか悩む。

get_modifier_state

我がアプリに矢印キーの代わりにマウスで押すボタンを付けた。
出した後で気が付いた、矢印キーの設定と連動していない。

まあ普通に GdkEvent を作って emit すれば…じゃない!
このアプリでは Ctrl 等の装飾キー状態も必要だった。

ボタンクリックのハンドラにて現在どの装飾キーが押されているかを取得。
それって手段があるのか?ってレベルの話だな。
ガチでアプリを作っている人でないと一生気にならなそうだ。

get_modifier

なんだ devhelp で get_modifier と検索したらアッサリ。
早速試してみよう。

#!/usr/bin/env python3

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
 
class ButtonWin(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        button = Gtk.Button(label="modifier = ?")
        button.connect("clicked", self.on_button_clicked)
        self.connect("delete-event", Gtk.main_quit)
        self.add(button)
        self.show_all()

    def on_button_clicked(self, button):
        kmp = Gdk.Keymap.get_default()
        state = kmp.get_modifier_state()
        if state == 0:
            button.set_label("modifier = NULL")
        elif state == 1:
            button.set_label("modifier = Shift")
        elif state == 4:
            button.set_label("modifier = Ctrl")
        elif state == 5:
            button.set_label("modifier = Shift+Ctrl")
        elif state == 8:
            button.set_label("modifier = Alt")
        elif state == 9:
            button.set_label("modifier = Shift+Alt")
        elif state == 12:
            button.set_label("modifier = Ctrl+Alt")
        elif state == 13:
            button.set_label("modifier = Shift+Ctrl+Alt")
        else:
            button.set_label("modifier = ???")

ButtonWin()
Gtk.main()

test1

これでボタンからでも装飾キーの状態が判別できるね。
後は GdkEvent を作って key-press-event に投げれば…

#!/usr/bin/env python3

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
 
class ButtonWin(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        button = Gtk.Button(label="Press Key 'A'")
        button.connect("clicked", self.on_button_clicked)
        self.connect("delete-event", Gtk.main_quit)
        self.connect("key-press-event", self.on_key_press_event)
        self.add(button)
        self.show_all()

    def on_button_clicked(self, button):
        """
            Gdk-WARNING **:
            Event with type 8 not holding a GdkDevice.
            It is most likely synthesized outside Gdk/GTK+

        """
        kmp = Gdk.Keymap.get_default()
        event = Gdk.Event()
        event.type = Gdk.EventType.KEY_PRESS
        event.state = kmp.get_modifier_state()
        event.keyval = Gdk.KEY_a
        event.window = self.props.window
        event.put()

    def on_key_press_event(self, widget, event):
        if event.state == Gdk.ModifierType.CONTROL_MASK:
            self.set_title("Ctrl+{0}".format(chr(event.keyval)))
        else:
            self.set_title("{0}".format(chr(event.keyval)))

ButtonWin()
Gtk.main()

test2

これで Ctrl を押しながらボタンクリックも Ctrl+a にはなるけど。
GdkDevice がどうのとワーニングになってしまう。

キーボードの指定か何か必要なのか?よくワカンネェYO!
よし必殺技だ、ハンドラを関数として呼び出ししてまえ。

#!/usr/bin/env python3

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
 
class ButtonWin(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        button = Gtk.Button(label="Press Key 'A'")
        button.connect("clicked", self.on_button_clicked)
        self.connect("delete-event", Gtk.main_quit)
        self.connect("key-press-event", self.on_key_press_event)
        self.add(button)
        self.show_all()

    def on_button_clicked(self, button):
        """
            call on_key_press_event
        """
        kmp = Gdk.Keymap.get_default()
        event = Gdk.Event()
        event.state = kmp.get_modifier_state()
        event.keyval = Gdk.KEY_a
        #event.put()
        self.on_key_press_event(None, event)

    def on_key_press_event(self, widget, event):
        """
            OK
        """
        if event.state == Gdk.ModifierType.CONTROL_MASK:
            self.set_title("Ctrl+{0}".format(chr(event.keyval)))
        else:
            self.set_title("{0}".format(chr(event.keyval)))

ButtonWin()
Gtk.main()

なんというアホな処理。
でもこれで Ctrl+Click でも同じハンドラという狙ったとおりになる。
実装する時はキチンと関数に分離しとこう。

Comipoli beta3

Comipoli beta2 のメモリ展開に凄い問題が見つかった。
3000x4000px over の画像を役二百枚まとめた CBZ を試しに作ってみた。
ソレを読み込むと…

memory_swap

8GB もあるメモリがアッサリ埋まって swap 領域に突入。
SSD だから我慢できる程度の遅さだけど HDD だと死ねるレベル。

JPEG 圧縮を zip 圧縮しているからファイルサイズは 150MB 程度なのに。
GdkPixbuf に展開するとこんなサイズになってしまうのか。

16GB あれば、、、、、いやそうじゃない。
とにかくメモリ使用量をなんとかしなきゃ。

都度読み込みならたとえ 100000x100000px でも問題無い…
そこまでいくと展開が超遅くなるから誰もやらないってばさ。

JPEG を GdkPixbuf に変換せずにバイナリのまま配列なら…
前後処理丸ごと作り替えになるし表示効率も悪くなるな。
そもそも Python でバイナリの配列も実体はポインタなのかな?
ここらは C でやったほうがモヤモヤしなくて済むのだが。

単純に一定の大きさ以上は縮小が一番効率良さげ。
現在のディスプレイ市場は 1080p が主流。
ならば 1080p よりデカいなら縮小という手段が多分最高効率だと思う。

def iter_zip(self):
    # unzip
    with zipfile.ZipFile(self.zip.get_path()) as o:
        l = o.namelist()
        l.sort()
        self.datas.clear()
        for name in l:
            ext = os.path.splitext(name)[1].lower()
            if ext == ".jpg" or ext == ".jpeg" or ext == ".png":
                data = o.read(name)
                stream = Gio.MemoryInputStream.new_from_data(data)
                p = GdkPixbuf.Pixbuf.new_from_stream(stream)
                # Escape Swap
                if p.get_height() > 1080:
                    rate = p.get_width() / p.get_height()
                    p = GdkPixbuf.Pixbuf.scale_simple(p, 1080 * rate, 1080, GdkPixbuf.InterpType.BILINEAR)
                stream.close()
                yield p

手抜きに見えるけどフルスクリーン時の描写処理が速くなるメリットがある。
と思えるけど Comipoli ではソコは OpenGL がやるので無意味なことは秘密だよ。

いや問題はメモリ使用量だってば!
再起動して同じファイルを読み込んでみる

memory_after

1/10 以下に、まさかこんなに違うとは。
これだったらメモリは 4GB あれば問題無さそう、今時なら普通だよね。

でもやはり展開時間は倍近くになる。
バックグラウンド展開だから気にならないとはいえ、やはり遅い。
czipfile なんて早いらしいモジュールは Python2 用しか見当たらない。

ただデータの転送量が減るおかげか動作が少し速くなるメリットも。
とりあえず又問題が出るまでコレでいこう、まだベータだし。

focus-on-click

GTK+ 3.20 には focus-on-click という便利な property が追加された。
その名のとおり mouse で click した直後に focus を得るかの指定。

GUI アプリを作っている人なら泣いて喜ぶレベルのプロパティです。
実は我がアプリは GtkShortcutWindow よりもコレがあるから 3.20 仕様にした。

だって Y901x でも mouseup シグナルで親ウインドウにフォーカスを逃がすとか…
マジで GUI アプリを作っている人”だけ”が解る待望のシロモノ。
細かい説明は省略、GUI アプリを本気で作れば必ず問題になることだし。
意味不明なら…御察し。

使い方は簡単。

//Gjs
let button = new Gtk.Button({focus_on_click: false});

# Python
button = Gtk.Button(focus_on_click=False)

でフォーカスを持たないボタンの完成。

Gjs, PyGObject 共に同じように書ける。

筆者の説明が悪かったのかプロパティは作成時の引数で全部指定できる。
勘違いした人を検索で多々見つける気がするんですけど。

devhelp での *.new のメソッドを使うなら *_new の引数を全部指定。
ソレを使わないなら Gjs は json にてプロパティを指定。
PyGObject はデフォルト引数にてプロパティを指定できる。
指定しなかったプロパティはデフォルトが適用される。

GtkBox が一番解りやすいかな。

#vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

この 2 つはまったく同じ結果になる。
GtkBox は GtkOrientable をインプリメントしている。
GtkOrientable のプロパティをまんま利用できるというわけです。

余計に解り辛くする説明かもだけど。