Help us understand the problem. What is going on with this article?

PyQtのsignalとconnectとlambda式について

結論

PyQtのconnect()の引数にはlambdaを使うべき。

はじめに

C++のGUIフレームワークQtをPythonで扱うためのラッパーライブラリPyQtのイベント処理についての記事です。Qtの各クラスは、特定のタイミングで発火するイベントをいくつか持っています。これをsignalと言います。signalを発火させる事をemitと言います。emitするだけでは何も起きないので、signalを「何らかの処理」とconnectする必要があります。connectは関数であり「何らかの処理」を引数として渡します。以下に例を示します。

#closePushButtonはQPushButtonクラスを継承
#以下ではclickedというsignalにself.close()という関数をconnectしています
closePushButton.clicked.connect(self.close)

関数connect()の引数がself.close()という関数です。

lambda式

さて、Pythonにはラムダ式という構文が存在し、無名関数(anonymous function)を生成出来ます。関数は通常、def文でのみ宣言出来ますが、その例外がこのラムダ式です。以下に例を示します。

#通常の関数定義
def def_add(a, b):
    return a + b

#ラムダ式
lambda_add = lambda a, b: a + b

if __name__ == "__main__":
    def_add_value = def_add(1, 2)
    lambda_add_value = lambda_add(1,2)
    print(def_add_value, lambda_add_value) #(3, 3)

ラムダ式は構文にクセがありますが、通常の関数定義と同様の動きを実装出来ます。

def文とラムダ式の違い

参考:https://stackoverflow.com/questions/12264834/what-is-the-difference-for-python-between-lambda-and-regular-function

先ほどの関数をprintしてみましょう。

print(def_add, lambda_add)
#<function def_add at 0x10ad214c0> <function <lambda> at 0x10ad4f700>

いずれもfunction型とわかります。違いは前者がdef_addという関数名が保存されている一方、後者は<lambda>となっている点です。これが無名関数の由縁です。stackoverflowには以下のようなコメントがありました。

lambda functions can't be pickled because they have no (unique) name associated with them. (Therefore, they can't be used with multiprocessing for example -- which has bit me with a PicklingError on more than one occasion ) – mgilson

Also, it might be worth pointing out that lambda is an expression whereas def is a statement. Since lambda is an expression, it can only contain other expressions (no statements are allowed) -- Although this is more of an issue at the programmer's level as opposed to "Why does python keep track of the difference" – mgilson

lambda関数にはそれと関連する固有の名前がないためpickled出来ません(ゆえにマルチプロセッシングには使用出来ませんーー私の場合はPicklingErrorが複数回発生しました(一度ではないという意味))

また、lambdaは式である一方、defは文である点を指摘する価値があるかもしれません。lambdaは式なので、式のみを含むことが出来ます(文を含む事が出来ません)。「Pythonがその違い(defとlambda)を追跡(または保存)している理由」というよりは、プログラマーレベルの問題ですが。

という事で調べてわかった事は「ラムダ関数は名前がなく、また文を含む事は出来ない」という事でした。それ以外のふるまいの違いはわかりませんでした。

connectの引数としてのdefとlamnda

ここからが本題です。前述までの話で、どうやらdef関数でもlambda式でも、基本的には同じ挙動をしそうだという事がわかりました。じゃあ関数をPyQtでconnect()の引数とする場合にも同じ挙動をするはず…がしません。いえ、通常の使い方をしていれば同じ挙動になりますが、少し複雑な構造になると挙動が変わります、def関数では動かなくなる事があります。

def関数じゃダメなのはどんなとき?

結論を言うと詳細な原因・タイミングはわかりませんでした。メモリ周りに原因がありそうだという事はなんとなくわかったのですが、詳細なタイミングや原因解明は出来ていません。具体的な状況としては、QThreadを継承したクラスにセットしたカスタムsignalをconnectする際にdef関数だと動作しませんでした。

connectにはlambda式を使おう

前述の状況では、def関数ではなくlambda関数を引数とすると問題が解決しました。はっきりした原因はわかりませんが、def関数を引数として渡すと、PyQt内部の変数らしきものとして保存されてるんじゃないかな、と推測しています。一方lamda式の場合はあくまで実行すべき式だけを保存するのかな、と。以下はconnectにlambda式を渡すパターンですが、これを読むとなんとなく私が言いたい事がわかっていただけるかもしれません(self.close()という式そのものが渡されるイメージ?)。

#def関数の場合はclosePushButton.clicked.connect(self.close)
closePushButton.clicked.connect(lambda:self.close())

ここまでやっても明確な原因はわかっていませんが、何はともあれ基本的に同じ挙動をするんだしlambda式使っておけばよいという結論に達しました。

connectにdef関数を渡すべきでない理由

def関数をconnectの引数として使うべきではない理由をもう一つあげます。signalは何らかの値を渡してくる場合があります。その際、connectに渡すdef関数には引数を受ける用意が必要です。以下が例です。

#progressChangedというsignalは、connectされた関数にintを渡します
self.tile_downloader.progressChanged.connect(self.update_download_progress)

def update_download_progress(self, value:int):
    self.ui.download_progressBar.setValue(value)

通常の関数の呼び方だとupdate_download_progress(100)など、引数が明示されます。しかしconnectに引数として渡す場合はどこにも引数が現れません。PyQtはそういうもん、とわかっておく必要があり、コードの可読性が非常に低くなります(progressChangedがintを渡すという事がわかっていないと、このconnectだけを読んでも挙動がわからない)。lambda式だと以下になります。

self.tile_downloader.progressChanged.connect(lambda v: self.update_download_progress(v))

def update_download_progress(self, value:int):
    self.ui.download_progressBar.setValue(value)

lambda式なら、connectを読めばprogressChangedというsignalはなんらかの値を渡してくる事がわかります(signalの値のほかに引数として渡したい変数がある場合はhttps://stackoverflow.com/questions/35819538/using-lambda-expression-to-connect-slots-in-pyqtを参照)。

終わりに

長くなりましたが、結論はconnectにはlambda式を使おう。これだけです。理由は、①なぜかはわからないけどlambda式の方が安定している②lambdaの方が可読性が高い、という2点です。ご意見や、情報(特に①に関して)がありましたらコメントしてください。ここまで読んでいただきありがとうございました。

Kanahiro
GISを中心に勉強中、いろいろ作っています。
https://kiguchi999.hatenablog.com/
MIERUNE
位置情報に関する様々な技術情報やTipsをMIERUNEのメンバーがお届けします。位置や可視化に関するご相談がございましたらぜひご連絡ください!
https://mierune.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした