2010年10月30日土曜日

課題2:解答編

だいぶ間が開いてしまいましたが、解答編を書いときます。

まずは、ソースコード全体を。

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

import os
import gtk
import glib

class SlideShow:
    
    def __init__(self):
        
        #GTK.Builderを構築
        wTree = gtk.Builder()
        #Gladeファイルの読み込み
        wTree.add_from_file("SlideShow.ui")
        #MainWindowオブジェクトを取得する
        self.mainWindow = wTree.get_object("MainWindow")
        #シグナルとシグナルハンドラをdic形式で作成する
        dic = {"on_MainWindow_destroy":self.on_MainWindow_destroy}
        #シグナルハンドラとシグナルを結びつける
        wTree.connect_signals(dic)
        #スライドショー用のGTKImageオブジェクトを取得する
        self.imgSlideShow = wTree.get_object("imgSlideShow")
        #画像のパスを取得
        self.imagePath = []
        #os.walk関数により、再帰的にフォルダを降りていき画像を探す
        for base, path, imPath in os.walk("/usr/share/backgrounds"):
            for img in imPath:
                limg = img.lower()
                #jpg/png/jpegファイルだけを対象にする
                if limg.find("jpg") > 0 or limg.find("png") > 0 or limg.find("jpeg") > 0: 
                    self.imagePath.append(base+"/"+img)
        #初期画面をロード
        #初期画面を表示するためのカウンタを初期化
        self.count = 0 
        #gtk.Imageに画像ファイルを表示する関数をコールする
        self._loadImage()
        #ウィンドウを表示する
        self.mainWindow.show_all()
        self.timeout = glib.timeout_add_seconds(2,self._timeouf,self)
        
    
    def on_MainWindow_destroy(self,widget):
        #GTKのメッセージループを終了する
        gtk.main_quit()
    
    def _timeouf(self,event):
        #gtk.Imageに画像ファイルを表示する関数をコールする
        self._loadImage()
        return True
        
    def _loadImage(self):
        #現在のカウンタを使用して画像ファイル名を取得。
        wallpaper = self.imagePath[self.count]
        #現在のウィンドウサイズを取得(x,y)のタプルで返る
        size = self.mainWindow.get_size()
        #新規にPixBufを作成し、指定されたファイルを読み込む
        pixbuf = gtk.gdk.pixbuf_new_from_file(wallpaper)
        #読み込んだPixBufの縦と横のサイズを取得
        x = float(pixbuf.get_width())
        y = float(pixbuf.get_height())
        #アスペクト比を算出
        aspect = y / x
        #pixbuf.scale_simpleメソッドを使用し、ウィンドウにマップされたImage用のpixbufを作成する
        #その際、アスペクト比を合わせる。基準は横幅
        pixbuf2 = pixbuf.scale_simple(size[0], int(size[0]*aspect),gtk.gdk.INTERP_BILINEAR )
        #使い終わったPixBufを削除    
        del pixbuf
        #ウィンドウにマップされたgtk.Imageに新規にリサイズしたPixBufをアサイン
        self.imgSlideShow.set_from_pixbuf(pixbuf2)
        #アスペクト比に合わせてウィンドウサイズをリサイズ
        self.mainWindow.resize(size[0],int(size[0]*aspect))
        #画像リスト用のカウンタをカウントアップ
        if len(self.imagePath) > self.count+1:
            self.count += 1
        else:
            self.count = 0

if __name__ == "__main__":
    #イメージ表示ウィンドウのコンストラクタを実行する(オブジェクトを作成する)
    SlideShow()
    #GTKのメッセージループを実行する
    gtk.main()


これに、課題2の時に母体コードとして提供したSlideShow.uiファイルを組み合わせれば、2秒置きに画像が表示されるスライドショーが完成します。

今回、ポイントとなるのは、以下の関数というかメソッドなわけですが。
def _loadImage(self):
        #現在のカウンタを使用して画像ファイル名を取得。
        wallpaper = self.imagePath[self.count]
        #現在のウィンドウサイズを取得(x,y)のタプルで返る
        size = self.mainWindow.get_size()
        #新規にPixBufを作成し、指定されたファイルを読み込む
        pixbuf = gtk.gdk.pixbuf_new_from_file(wallpaper)
        #読み込んだPixBufの縦と横のサイズを取得
        x = float(pixbuf.get_width())
        y = float(pixbuf.get_height())
        #アスペクト比を算出
        aspect = y / x
        #pixbuf.scale_simpleメソッドを使用し、ウィンドウにマップされたImage用のpixbufを作成する
        #その際、アスペクト比を合わせる。基準は横幅
        pixbuf2 = pixbuf.scale_simple(size[0], int(size[0]*aspect),gtk.gdk.INTERP_BILINEAR )
        #使い終わったPixBufを削除    
        del pixbuf
        #ウィンドウにマップされたgtk.Imageに新規にリサイズしたPixBufをアサイン
        self.imgSlideShow.set_from_pixbuf(pixbuf2)
        #アスペクト比に合わせてウィンドウサイズをリサイズ
        self.mainWindow.resize(size[0],int(size[0]*aspect))
        #画像リスト用のカウンタをカウントアップ
        if len(self.imagePath) > self.count+1:
            self.count += 1
        else:
            self.count = 0
中身に関しては、ソースコードの一行ごとにコメント入れたので、そちらを参照してもらうとして。

これが、2箇所でコールされている、ということです。
__init__と_timeoufの2箇所ですね。
もちろん、もっとシンプルな処理の場合、2箇所に同じ処理を書いてもいいのですが、基本的に、gtk.Imageに指定された画像ファイルを表示する、という全く同じ処理を行っているわけです。

さすがに、ここまで長いと理解できると思いますが、同じ処理を何度も書くのはムダなんですね。
どのようにムダかというと、以下の点でムダ。

同じことを2回書かなくてはならない(コピペでいいじゃんという説はある)
もしも修正した場合、同じ箇所を2回直さないとならない(コピペでいいじゃんという説はある)
同じコードが2度も現れると読みにくい

いずれの場合も、同じ手間を2度行わなくてはならないのがムダ、ということになります。
で、まあ2箇所くらいなら、別にコピペでもいいんですが、これが20箇所とかになると、さすがにコピペも厳しいし、読みづらくなるし、コピペ漏れも出てきます。
そのため、同じ処理はまとめてしまって、関数、あるいはローカルメソッドして定義してしまうほうが効率がよいわけです。

ローカルメソッドとするか、関数とするかは、スコープの問題なので、各種クラスで汎用的に使われる処理であれば、関数として、どこからでも呼べるようにしておくべきでしょう。
例えば、エラーメッセージのメッセージボックスを表示するための処理、とか、デバッグ用のログ出力を行う処理とか、ですね。
#デバッグログとか、日時出力とか行うので、それなりに長い処理になったりする。

このように、効率の悪くなるコードを関数やローカルメソッドにしてしまうプログラムの事を構造化プログラミングといいます。
いつぞや番外で触れましたけど。

見直せば、もっと構造化できそうなところもないではないですが、まあ、無理に構造化しても実行が遅くなったり、却って見にくくなってりします。
その他、Pythonでは、リスト内包表記が使えるので、あまり見ませんがループのネストが深くなる場合や、条件式の内側が、かなり大きくなる場合に、ローカルメソッドにして、処理そのもの流れを見やすくするなど、構造化プログラミングの手法としてあったりします。
#これもやり過ぎると実行速度に影響が出たりします。

いずれにせよ、ひとが見やすいように、処理を分割し、関数化、あるいはローカルメソッド化することにより、ムダを省き、バグが発生しにくくしましょうよ、ということを、今回の課題では伝えたかったんですが、ちとネタがディープ過ぎて伝わり難いというね。(笑)
これなら、リストの処理で、内部関数とか、そういうのをネタにした方が良かったかな。(^^;

まあ、例えて言うなら、毎日の通勤路、自分は階段を降りる時に、階段を登ってくるOLさんの胸の谷間を見るために、若干右よりに降りていく、すれ違う瞬間だけ、さっと左側に視線を走らせる、ということを毎日やっているとして、これをルーチン化して、無意識に行うのと同じようなことです。

意識して、毎度毎度やっていたのでは、不自然な動きになり、OLさんに警戒されてしまうでしょう。
そこを不自然にならないように、無意識に行えるように自分の中でルーチンとして一連の動きが処理されるようにまとめてしまう、ということです。
#例えがハイエンド過ぎて分かり難いか。
見事谷間を目撃できたら、その日はいいことがありますよ、的な占いのようなものでもあるかも知れません。
そのために、いかに自然に振舞うか。それがルーチン化の要でもあります。

プログラムだって同じなのです。毎度繰り返されるような処理はひとかたまりにして、ひとつのブロックとして、単に呼び出せば、それが実行される、という形にまとめておいた方が、いろいろと都合がいいわけです。
ルーチンを見直す時とかですね。
例えば、先の例だと、季節ごとにOLさんの服装も代わります。その場合、右によるか、左によるか、季節ごとの見直しが必要です。
しかもターゲットのOLさんがひとりでない場合、全体的に見直しになってしまいます。
そこで、きちんと関数として、まとめておくことで、季節ごとに、一箇所修正することで、無事に谷間を拝めるようになるのです。

朝の通勤時、OLさんの谷間を拝めるかどうか、それだけで朝の気分は違います。
それだけ、ルーチン化、関数化というのは重要なことなのです。

今回の課題で主張したかったのは、この点ですね。
「谷間を見逃すな」「同じ処理を2度書くな」ということです。

では、ぼちぼち、次のネタを考えることにしましょうかね。

0 件のコメント:

コメントを投稿