Paepoi

Paepoi » Python Tips » yield の使い所

yield の使い所

最近はあまり見なくなったけど一時期 yield がもてはやされていた。
結局さ、yield ってどんな時に使ったら便利なの?
............

と思っていた時期が筆者にもあった。
ので使い所を、というか実際に筆者が使うようになった経緯を。

for

先に書くと初心者がプログラミングを覚える段階で覚える必要は無い。
C 言語に無い時点で解るけど、覚えなくても他に手段があるからである。

で、筆者が本当に困った処理は以下。
PyGObject のソースで申し訳ない、仮想でもいいから GNOME なマシンを用意して。
Fedora がおすすめだよ(宣伝?)
#!/usr/bin/env python3

import os, re, gi
gi.require_version('Gtk', '3.0')
from gi.repository import GLib, Gio, Gtk, GdkPixbuf

# 画像が沢山入っているディレクトリに書き換えてね
PATH = "/home/sasakima-nao/pic"
# サムネイルの最大ピクセルサイズ
SIZE = 120

class ThumbnailWindow(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        self.fbox = Gtk.FlowBox(
            valign=Gtk.Align.START,
            max_children_per_line=10,
            homogeneous=True,
            selection_mode=Gtk.SelectionMode.BROWSE)
        scroll = Gtk.ScrolledWindow()
        scroll.add(self.fbox)
        self.add(scroll)
        self.resize(1024, 600)
        self.show_all()
        # サムネイル作成
        self.create_thumbnail()

    def create_thumbnail(self):
        listdir = os.listdir(PATH)
        for f in listdir:
            if re.search(r"\.(jpe?g|png)$", f, re.I):
                pixbuf = GdkPixbuf.Pixbuf.new_from_file("{0}/{1}".format(PATH, f))
                self.append_data(pixbuf)

    def append_data(self, pixbuf):
        if pixbuf.get_width() > pixbuf.get_height():
            pix_w = SIZE
            pix_h = round(SIZE * pixbuf.get_height() / pixbuf.get_width())
        else:
            pix_w = round(SIZE * pixbuf.get_width() / pixbuf.get_height())
            pix_h = SIZE
        minp = pixbuf.scale_simple(pix_w, pix_h, GdkPixbuf.InterpType.TILES)
        image = Gtk.Image(pixbuf=minp, visible=True)
        self.fbox.add(image)

    def do_hide(self):
        Gtk.main_quit()

ThumbnailWindow()
Gtk.main()
である。
実際は comipoli というアプリを作っている初期でドハマりしたコードの抜き出し。

ウインドウが表示された後でサムネイルを一枚づつ順に作成して表示されるのを期待した。
show_all 関数の後で作るんだから当然だよね!

けれど、サムネイル画像の作成が全部終わった後で一気に表示という動作になってしまった。
今の筆者ならそういう動作になるのは当然だと解るけど当事はマジで混乱。
ココみたいな所でコケる初心者は多いと思う。

GUI は __init__ 関数を抜ける(処理を終わらせる)まで描写されないと解った。
なので抜ける必要がある、まず考えるのは普通ならタイマーを使うことだよね。
        # サムネイル作成(timer)
        GLib.timeout_add(100, self.create_thumbnail)

    def create_thumbnail(self):
        listdir = os.listdir(PATH)
        for f in listdir:
            if re.search(r"\.(jpe?g|png)$", f, re.I):
                pixbuf = GdkPixbuf.Pixbuf.new_from_file("{0}/{1}".format(PATH, f))
                self.append_data(pixbuf)
        return False
0.1 秒後に create_thumbnail 関数を実行。
タイマーをセットするだけなので __init__ は抜けるはず。

で、ウインドウが表示された後でサムネイルが表示されると確認できた。
いやマテ!結局サムネイルが全部作成されるまで表示されない現象は同じジャン!

今度は create_thumbnail 関数の for 文中で抜ける必要があるようだ。
for 文の中でまたタイマーを作ってえっと。
    def create_thumbnail(self):
        listdir = os.listdir(PATH)
        for f in listdir:
            if re.search(r"\.(jpe?g|png)$", f, re.I):
                GLib.timeout_add(150, self.add_thumbnail, f)
        return False

    def add_thumbnail(self, f):
        pixbuf = GdkPixbuf.Pixbuf.new_from_file("{0}/{1}".format(PATH, f))
        self.append_data(pixbuf)
        return False
駄目だ。
for 文だけを抜けたって create_thumbnail 関数は抜けていない、なので何も変わらない。
ならばと。

        # サムネイル作成(timer2)
        self.listdir = os.listdir(PATH)
        self.listnum = 0
        GLib.timeout_add(100, self.create_thumbnail)

    def create_thumbnail(self):
        if self.listnum < len(self.listdir):
            f = self.listdir[self.listnum]
            if re.search(r"\.(jpe?g|png)$", f, re.I):
                GLib.timeout_add(10, self.add_thumbnail, f)
            self.listnum += 1
            return True
        return False

    def add_thumbnail(self, f):
        pixbuf = GdkPixbuf.Pixbuf.new_from_file("{0}/{1}".format(PATH, f))
        self.append_data(pixbuf)
        return False
なんとかなった。
けどサムネイルの数全部タイマーを作って全部動かすってどうなのよ。
もっとエレガントに、てかタイマー 1 個で全部できないのか?

それが yield を使うとアッサリできるんだ。
#!/usr/bin/env python3

import os, re, gi
gi.require_version('Gtk', '3.0')
from gi.repository import GLib, Gio, Gtk, GdkPixbuf

# 画像が沢山入っているディレクトリに書き換えてね
PATH = "/home/sasakima-nao/pic"
# サムネイルの最大ピクセルサイズ
SIZE = 120

class ThumbnailWindow(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        self.fbox = Gtk.FlowBox(
            valign=Gtk.Align.START,
            max_children_per_line=10,
            homogeneous=True,
            selection_mode=Gtk.SelectionMode.BROWSE)
        scroll = Gtk.ScrolledWindow()
        scroll.add(self.fbox)
        self.add(scroll)
        self.resize(1024, 600)
        self.show_all()
        # サムネイル作成(改)
        self.gen_func = self.create_thumbnail()
        GLib.idle_add(self.gen)

    def gen(self):
        try:
            next(self.gen_func)
            return True
        except Exception as e:
            return False

    def create_thumbnail(self):
        listdir = os.listdir(PATH)
        for f in listdir:
            if re.search(r"\.(jpe?g|png)$", f, re.I):
                pixbuf = GdkPixbuf.Pixbuf.new_from_file("{0}/{1}".format(PATH, f))
                self.append_data(pixbuf)
                yield 1

    def append_data(self, pixbuf):
        if pixbuf.get_width() > pixbuf.get_height():
            pix_w = SIZE
            pix_h = round(SIZE * pixbuf.get_height() / pixbuf.get_width())
        else:
            pix_w = round(SIZE * pixbuf.get_width() / pixbuf.get_height())
            pix_h = SIZE
        minp = pixbuf.scale_simple(pix_w, pix_h, GdkPixbuf.InterpType.TILES)
        image = Gtk.Image(pixbuf=minp, visible=True)
        self.fbox.add(image)

    def do_hide(self):
        Gtk.main_quit()

ThumbnailWindow()
Gtk.main()
予定どおりに動いた。
g_timeout_add より g_idle_add のほうがこの場合、、、等は別で書く、かもしれない。

つまり yield は「どうしてもココでループを抜ける処理がしたい」場合、かつ
正規表現と同じで「他の手段もあるけどもっとエレガントにしたい!!!」の集大成である。
よく見つかる next() を一回ずつ使うって場合は実務では絶対に無い、ありえない。

悪く言えば「意識高い系が大喜びしそうな記述方法」とも言える。
だから一時期モテモテだったのね、と感じるなら最初に書いたとおりだと。
でも使い方を覚えておくと実際に便利だよ、と一言。
Copyright(C) sasakima-nao All rights reserved 2002 --- 2020.