PySide

PySide のシグナルを一括で設定する【 signal と lambda】

各ウィジェットのシグナルとスロットを for 文で一括で設定していきます。
趣旨としては、よくわからんエラーをよくわからん方法で解決できた、
というものです

テストコード

下記のコードでテストしていきます。

# -*- coding: utf-8 -*-

import sys

from PySide2 import QtCore, QtGui, QtWidgets


class TestUI(QtWidgets.QDialog):

    def __init__(self, parent=None):
        super(TestUI, self).__init__(parent)

        layout = QtWidgets.QVBoxLayout()
        self.setLayout(layout)

        line_info_list = []

        # ラインエディットの作成_01
        self.test_01_line = QtWidgets.QLineEdit()
        layout.addWidget(self.test_01_line)
        self.test_01_line_name = 'widget_01'

        # ラインエディットの作成_02
        self.test_02_line = QtWidgets.QLineEdit()
        layout.addWidget(self.test_02_line)
        self.test_02_line_name = 'widget_02'

        # ラインエディットの作成_03
        self.test_03_line = QtWidgets.QLineEdit()
        layout.addWidget(self.test_03_line)
        self.test_03_line_name = 'widget_03'


    # テキストに文字が入った場合のスロット
    # ------------------------------------------------------------------------------------------------------------------
    def text_change(self, line_edit, name):
        current_text = line_edit.text()
        print('{} の欄に「 {} 」と入力しました。'.format(name, current_text))


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    ui = TestUI()
    ui.show()
    sys.exit(app.exec_())


QLineEdit が3つ並んだだけのものです。
上記の文ではシグナルを設定していないので入力しても何も起きません。

これからそれぞれテキストを入力した際のシグナルを text_change メソッドに接続し、
下記のようにそのウィジェット名と内容を出力するようにします。

ひとつずつシグナルを設定する場合

# 01 のシグナル
self.test_01_line.textChanged.connect(
    lambda: self.text_change(self.test_01_line, self.test_01_line_name)
)

それぞれのウィジェットに個別でシグナルを設定しました。
先ほどの gif 画像と同様の結果が得られます。

ただ、保守性の観点でみると、これで良いとは言えない場合があります。

今後 QLineEdit や他のウィジェットが増えていく可能性のある場合や、
シグナルの内容を一括で変更する可能性がある場合などです。

これらの場合、開発を進めていく中でコードが煩雑になっていくことが予想されます。
そこで、for 文で一括で処理できる構造にしていきます。

for 文で lambda を利用して シグナルを設置

見出し文の通りですが、for 文内で lambda 式を使うことで解決します。

ただ、PySIde の仕様なのか lamda の仕様なのか あるいはバグなのか、
1つ注意点があるので解説していきます。

とりあえずコードの全文から。

# -*- coding: utf-8 -*-

import sys

from PySide2 import QtCore, QtGui, QtWidgets


class TestUI(QtWidgets.QDialog):

    def __init__(self, parent=None):
        super(TestUI, self).__init__(parent)

        layout = QtWidgets.QVBoxLayout()
        self.setLayout(layout)

        line_info_list = []


        # ラインエディットの作成_01
        # --------------------------------------------------------------------------------------------------------------
        self.test_01_line = QtWidgets.QLineEdit()
        layout.addWidget(self.test_01_line)
        line_info_list.append(
            {
                'widget': self.test_01_line,
                'name': 'widget_01'
            }
        )


        # ラインエディットの作成_02
        # --------------------------------------------------------------------------------------------------------------
        self.test_02_line = QtWidgets.QLineEdit()
        layout.addWidget(self.test_02_line)
        line_info_list.append(
            {
                'widget': self.test_02_line,
                'name': 'widget_02'
            }
        )

        # ラインエディットの作成_03
        # --------------------------------------------------------------------------------------------------------------
        self.test_03_line = QtWidgets.QLineEdit()
        layout.addWidget(self.test_03_line)
        line_info_list.append(
            {
                'widget': self.test_03_line,
                'name': 'widget_03'
            }
        )


        layout.addStretch()


        # for 文でシグナルを設定
        # --------------------------------------------------------------------------------------------------------------
        for info in line_info_list:

            info['widget'].textChanged.connect(

                lambda
                    x = '',
                    line_edit = info['widget'],
                    name = info['name']

                : self.text_change(line_edit, name)
            )



    # テキストに文字が入った場合のスロット
    # ------------------------------------------------------------------------------------------------------------------
    def text_change(self, line_edit, name):

        current_text = line_edit.text()
        print('{} の欄に「 {} 」と入力しました。'.format(name, current_text))


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    ui = TestUI()
    ui.show()
    sys.exit(app.exec_())


シグナルを for 文で一括指定しているので、今後シグナルの変更が必要になった際に対応が楽になります。

QLineEdit が増えた場合も個別のシグナル設定が必要なくなります。
(こっちは効果が薄い気もしてきました。)

スロットへ渡す引数を辞書型でリスト化

16 行目でリストを作り、

line_info_list = []

各ウィジェットでスロットに渡す引数を辞書型でこのリストに追加しています。

line_info_list.append(
    {
        'widget': self.test_01_line,
        'name': 'widget_01'
    }
)

わかりやすいように辞書型にしていますが、リスト型とかでも全く問題ありません。
その際は for 文の中も変更が必要です。

for 文内で lamda 式

シグナルを for 文で一括設定しているのが 59目からの部分です。

for info in line_info_list:

    info['widget'].textChanged.connect(

        lambda
            x = '',
            line_edit = info['widget'],
            name = info['name']

        : self.text_change(line_edit, name)
    )

lambda 式の挙動に関してはトモハラさんの記事で詳しく書かれてるので
そちらを参考にしてください。

なぜか 1行目の引数を削除するとエラー

x = '' ってどこにも使ってないんですが、これを消すとエラーが発生します。
今回書きたかったのはこの部分です。

for info in line_info_list:

    info['widget'].textChanged.connect(

        lambda
            x = '',
            line_edit = info['widget'],
            name = info['name']

        : self.text_change(line_edit, name)
    )

これを、

for info in line_info_list:

    info['widget'].textChanged.connect(

        lambda
            line_edit = info['widget'],
            name = info['name']

        : self.text_change(line_edit, name)
    )

こうすると エラーが起きるようになります。
6行目にあった使用しない x = '' を消すとなぜかエラーが出てきます。

最初の引数は何でもいい

x = '' である必要はありません。
とりあえず使用しない引数を最初に指定するとエラーが消えます。

これなんでだろう・・・?
前からあったのかな。
python のバージョンに依存するバグの可能性もありますし、
PySide のバグや仕様の可能性もあります。
上記のテスト環境は Python 3.7 と PySide2 5.15.2.1 です。

私の lambda 式の認識が間違ってる可能性も全然あります。

ここから先の調査は面倒そうなのでここまでとします。

-PySide