プログラムを中心とした個人的なメモ用のブログです。 タイトルは迷走中。
内容の保証はできませんのであしからずご了承ください。

2022/01/31

[PySide] for 文でシグナルの接続にラムダ式を使う場合の注意

event_note2022/01/31 9:18

for ループでウィジェットを動的に作成する際、シグナルとの接続でラムダ式を使う場合には注意が必要です。

例えば、以下のコードはラムダ式を使っていますが、

for i in range(5):
    button = QPushButton(f'Button{i}')
    button.clicked.connect(lambda: print(f'Button{i} clicked'))

この場合、どのボタンを押しても Button4 clicked と出力されます。
ラムダ式は遅延評価のためです。

これについては以下の記事を参考にしてください。

じゃあデフォルト引数を与えて即時評価してやろう以下のように変更すると、

for i in range(5):
    button = QPushButton(f'Button{i}')
    button.clicked.connect(lambda x=i: print(f'Button{x} clicked'))

今度は ButtonFalse clicked と出力されてしまいました。
QPushButtonclicked シグナルは、ボタンの状態を引数で渡してくるそうなので、デフォルト引数が bool 値で上書きされたためです。
かと言って、引数の数を増やすとエラーになります。

なので、この場合ラムダ式は使えず、functools.partial を使うしかありません。

for i in range(5):
    button = QPushButton(f'Button{i}')
    button.clicked.connect(partial(self.__clicked, i))

以下、動作確認用のコードです。

import sys
from functools import partial
from PySide6.QtWidgets import *

class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        vbox_layout = QVBoxLayout()
        for i in range(5):
            button = QPushButton(f'Button{i}')
            button.clicked.connect(partial(self.__clicked, i))
            vbox_layout.addWidget(button)
        widget = QWidget()
        widget.setLayout(vbox_layout)
        self.setCentralWidget(widget)

    def __clicked(self, i):
        print(f'Button{i} clicked')

if __name__ == '__main__':

    app = QApplication(sys.argv)
    main_window = MainWindow()
    main_window.show()
    sys.exit(app.exec())

参考 URL