YOGYUI

PyQt5 - Connect pyqtSignal in For loop (lambda problem) 본문

Software/Python

PyQt5 - Connect pyqtSignal in For loop (lambda problem)

요겨 2021. 7. 20. 13:18
반응형

 

PyQt5의 pyqtSignal 혹은 pyqtBoundSignal을 함수와 연결(connect)시, 여러 객체를 for문 안에서 lambda를 사용하여 연결할 경우 정상적으로 동작하지 않는 문제가 있다

 

다음 예시를 통해 문제를 확인해보자

if __name__ == '__main__':
    import sys
    from PyQt5.QtGui import *
    from PyQt5.QtCore import *
    from PyQt5.QtWidgets import *

    app = QCoreApplication.instance()
    if app is None:
        app = QApplication(sys.argv)
    
    btns = [
        QPushButton('BUTTON1'),
        QPushButton('BUTTON2'),
        QPushButton('BUTTON3')
    ]
    
    widget = QWidget()
    edit = QLineEdit()
    
    vlayout = QVBoxLayout(widget)
    for btn in btns:
        vlayout.addWidget(btn)
        btn.clicked.connect(lambda: edit.setText(btn.text() + ' is Clicked'))
    vlayout.addWidget(edit)
    
    widget.show()  
    app.exec_()

'BUTTON1' 클릭 시
'BUTTON2' 클릭 시
'BUTTON3' 클릭 시

버튼 세개 모두 클릭했을 때 세번째 버튼의 텍스트인 'BUTTON3'가 호출되는 것을 볼 수 있다

이는 PyQt의 문제가 아니라, for문안에서 lambda를 사용할 경우 lambda가 호출되는 시점의 전달인자 (btn.text())가 연결함수(edit.setText)와 연동되기 때문에 최후에 바인딩된 'BUTTON3'가 모든 호출 시 사용되기 때문이다

 

이를 해결하려면 일반적으로는 각 버튼별로 따로 signal binding을 해줘야 한다

btns[0].clicked.connect(lambda: edit.setText(btns[0].text() + ' is Clicked'))
btns[1].clicked.connect(lambda: edit.setText(btns[1].text() + ' is Clicked'))
btns[2].clicked.connect(lambda: edit.setText(btns[2].text() + ' is Clicked'))

하지만 연결해야할 객체가 많아지면 코드가 지저분해지는 문제가 있다


이를 위해 python built-in module 중 functoolspartial을 활용하면 for문을 사용할 수 있다

(https://docs.python.org/3/library/functools.html)

중요한 대목은 'freezes some portion of a function's arguments'!!

 

다음과 같이 코드를 수정해보자

(lambda 대신 functools.partial(func, args)를 활용하도록 바꾼 것이 전부)

if __name__ == '__main__':
    import sys
    from PyQt5.QtGui import *
    from PyQt5.QtCore import *
    from PyQt5.QtWidgets import *
    from functools import partial

    app = QCoreApplication.instance()
    if app is None:
        app = QApplication(sys.argv)
    
    btns = [
        QPushButton('BUTTON1'),
        QPushButton('BUTTON2'),
        QPushButton('BUTTON3')
    ]
    
    widget = QWidget()
    edit = QLineEdit()
        
    vlayout = QVBoxLayout(widget)
    for btn in btns:
        vlayout.addWidget(btn)
        btn.clicked.connect(partial(edit.setText, btn.text() + ' is Clicked'))
    vlayout.addWidget(edit)
    
    widget.show()  
    app.exec_()

'BUTTON1' 클릭 시
'BUTTON2' 클릭 시
'BUTTON3' 클릭 시

 

UI 코드를 짤 때 유사한 동작을 해야하는 컨트롤들은 lambda를 사용해서 공통함수로 처리하는 경우가 많은데, for문을 사용하고자 할 경우 partial을 사용해서 아주 간단하게 구현할 수 있다

 

끝~!

 

[참고]

https://stackoverflow.com/questions/19837486/lambda-in-a-loop

반응형