PyQt。 メモリを管理し、ガベージを収集します

画像

昔はC言語があり、その中にメモリを制御する2つの関数、mallocとfreeがありました。 しかし、それは複雑すぎました。

BjörnStraustrupはそれを見て、すべてをよりシンプルにする必要があると判断しました。 そして、C ++を発明しました。 malloc / freeに加えて、new / delete、デストラクタ、RAII、auto、および共有ポインタがそこに現れました。

Guido van Rossumはこれを見て、C ++も十分に単純ではないと判断しました。 彼は別の方法で行くことを決め、Pythonを思い付きました。Pythonではmallocとfreeさえもありません。

一方、C ++ Qt GUIライブラリで作成されたノルウェーのトロールは、適切だと判断したときにオブジェクトを削除するため、オブジェクトのメモリ管理を簡素化します。

Phil Thompsonは、すばらしいPython言語用の優れたQtライブラリがないことに腹を立てています。 そして、それらをPyQtプロジェクトと組み合わせることにしました。 ただし、判明したように、メモリ管理のさまざまなパラダイムを超えると、副作用が発生します。 見てみましょう...



*歴史的正義と年表は、序章の芸術的要素のために犠牲にされています。


PyQt作業モデルは、次のように単純化できます。各パブリックC ++クラスに対して、ラッパークラスがPythonで作成されます。 プログラマーはラッパーオブジェクトを操作し、「実際の」C ++オブジェクトのメソッドを呼び出します。

オブジェクトとラッパーが同期的に作成され、同期して死ぬ限り、すべてが正常です。 しかし、この同期は破られる可能性があります。 私は3つの方法でこれを行うことができました:





Pythonラッパーの作成、C ++オブジェクト





from PyQt4.QtCore import QObject class MyObject(QObject): def __init__(self): self.field = 7 obj = MyObject() print(obj.field) obj.setObjectName("New object") >>> Traceback (most recent call last): >>> File "pyinit.py", line 9, in <module> >>> obj.setObjectName("New object") >>> RuntimeError: '__init__' method of object's base class (MyObject) not called.
      
      







この例と他の例をここで見ることができます。



MyObjectのコンストラクターでは、基本クラスのコンストラクターを呼び出しませんでした。 この場合、オブジェクトは正常に作成され、使用できます。 ただし、C ++メソッドを最初に呼び出そうとすると、 RuntimeErrorが発生し、何が間違っていたかの説明が表示されます。

修正されたオプション:



  ... class MyObject(QObject): def __init__(self): QObject.__init__(self) ...
      
      







Pythonガベージコレクターが目的のオブジェクトを削除しました



  from PyQt4.QtGui import QApplication, QLabel def createLabel(): label = QLabel("Hello, world!") label.show() app = QApplication([]) createLabel() app.exec_()
      
      







このコードがC ++で記述されている場合、 app.exec_()の後に「Hello、world!」というウィンドウが表示されます。 ただし、このコードには何も表示されません。 createLabel()関数の実行が終了すると、Pythonコードにはラベル参照がなくなり、気になるガベージコレクターはPythonラッパーを削除しました。 次に、ラッパーはC ++オブジェクトを削除しました。



修正されたオプション:

  from PyQt4.QtGui import QApplication, QLabel def createLabel(): label = QLabel("Hello, world!") label.show() return label app = QApplication([]) label = createLabel() app.exec_()
      
      





これらのリンクを使用しない場合でも、作成されたすべてのオブジェクトへのリンクを保持します。



Qtはオブジェクトを削除しました。 Pythonラッパーアライブ



前の2つのケースはPyQt / Pysideのドキュメントで説明されており、かなり簡単です。 Python部分がQtライブラリがC ++オブジェクトを削除したことを認識していない場合、さらに複雑な問題が発生します。

Qtは、親を削除したり、ウィンドウを閉じたり、 deleteLater()を呼び出したり、その他の状況でオブジェクトを削除できます。

削除後、純粋なPythonで記述されたラッパーメソッドを操作できます。C++パーツにアクセスしようとすると、 RuntimeErrorまたはアプリケーションクラッシュが発生します。



足元を撃つ非常に簡単な方法から始めましょう。

  from PyQt4.QtCore import QTimer from PyQt4.QtGui import QApplication, QWidget app = QApplication([]) widget = QWidget() widget.setWindowTitle("Dead widget") widget.deleteLater() QTimer.singleShot(0, app.quit) #  ,       app.exec_() #  ,    deleteLater() print(widget.windowTitle()) >>> Traceback (most recent call last): >>> File "1_basic.py", line 20, in <module> >>> print(widget.windowTitle()) >>> RuntimeError: wrapped C/C++ object of type QWidget has been deleted
      
      







QWidgetを作成し、Qtに削除を依頼します。 app.exec_()に、オブジェクトは削除されます。 ラッパーはこれを認識していないため、 windowTitle()を呼び出そうとすると例外がスローされるか、クラッシュします。

もちろん、プログラマがdeleteLater()呼び出してからオブジェクトを使用する場合、彼自身が責任を負います。 ただし、実際のコードではより複雑なシナリオが頻繁に発生します。

  1. オブジェクトを作成する
  2. 外部信号をオブジェクトスロットに接続します
  3. Qtはオブジェクトを削除します。 たとえば、ウィンドウを閉じるとき
  4. リモートオブジェクトのスロットは、タイマーまたは外部からの信号によって呼び出されます。
  5. アプリケーションがクラッシュするか、例外をスローする




長い生涯の例
  from PyQt4.QtCore import Qt, QTimer from PyQt4.QtGui import QApplication, QLabel, QLineEdit def onLineEditTextChanged(): print('~~~~ Line edit text changed') def onLabelDestroyed(): print('~~~~ C++ label object destroyed') def changeLineEditText(): print('~~~~ Changing line edit text') lineEdit.setText("New text") class Label(QLabel): def __init__(self): QLabel.__init__(self) self.setAttribute(Qt.WA_DeleteOnClose) self.destroyed.connect(onLabelDestroyed) def __del__(self): print('~~~~ Python label obj      QLineEdit,      - Label.ect destroyed') def setText(self, text): print('~~~~ Changing label text') QLabel.setText(self, text) def close(self): print('~~~~ Closing label') QLabel.close(self) app = QApplication([]) app.setQuitOnLastWindowClosed(False) label = Label() label.show() lineEdit = QLineEdit() lineEdit.textChanged.connect(onLineEditTextChanged) lineEdit.textChanged.connect(label.setText) QTimer.singleShot(1000, label.close) #      QTimer.singleShot(2000, changeLineEditText) #      .  . QTimer.singleShot(3000, app.quit) app.exec_() print('~~~~ Application exited') >>> ~~~~ Closing label >>> ~~~~ C++ label object destroyed >>> ~~~~ Changing line edit text >>> ~~~~ Line edit text changed >>> ~~~~ Changing label text >>> Traceback (most recent call last): >>> File "2_reallife.py", line 33, in setText >>> QLabel.setText(self, text) >>> RuntimeError: wrapped C/C++ object of type Label has been deleted >>> ~~~~ Application exited >>> ~~~~ Python label object destroyed
      
      





ラベルはQLineEditからのtextChanged信号に接続されます。 開始後1秒で、ラベルが閉じて削除されます。 プログラマーとユーザーはもう必要ありません。 ただし、2秒後、リモートラベルは信号を受信します。 例外がコンソールにスローされるか、アプリケーションが予期せずクラッシュします。





スロットが自動的にオフにならない場合



C ++アプリケーションでは、オブジェクトが削除されると、そのすべてのスロットが無効になるため、問題はありません。 ただし、PyQtとPySideは常にオブジェクトを「切断」できるわけではありません。 スロットがオフにならないときを理解することは私にとって興味深いものになりました。 実験の過程で、次のテストが生まれました。



より多くのコード
  PYSIDE = False USE_SINGLESHOT = True if PYSIDE: from PySide.QtCore import Qt, QTimer from PySide.QtGui import QApplication, QLineEdit else: from PyQt4.QtCore import Qt, QTimer from PyQt4.QtGui import QApplication, QLineEdit def onLineEditDestroyed(): print('~~~~ C++ lineEdit object destroyed') def onSelectionChanged(): print('~~~~ Pure C++ method selectAll() called') class LineEdit(QLineEdit): def __init__(self): QLineEdit.__init__(self) self.setText("foo bar") self.destroyed.connect(onLineEditDestroyed) #self.selectionChanged.connect(onSelectionChanged) def __del__(self): print('~~~~ Python lineEdit object destroyed') def clear(self): """Overridden Qt method """ print('~~~~ Overridden method clear() called') QLineEdit.clear(self) def purePythonMethod(self): """Pure python method. Does not override any C++ methods """ print('~~~~ Pure Python method called') self.windowTitle() # generate exception app = QApplication([]) app.setQuitOnLastWindowClosed(False) lineEdit = LineEdit() lineEdit.deleteLater() if USE_SINGLESHOT: #QTimer.singleShot(1000, lineEdit.clear) #QTimer.singleShot(1000, lineEdit.purePythonMethod) QTimer.singleShot(1000, lineEdit.selectAll) # pure C++ method else: timer = QTimer(None) timer.setSingleShot(True) timer.setInterval(1000) timer.start() #timer.timeout.connect(lineEdit.clear) #timer.timeout.connect(lineEdit.purePythonMethod) timer.timeout.connect(lineEdit.selectAll) # pure C++ method QTimer.singleShot(2000, app.quit) app.exec_() print('~~~~ Application exited')
      
      









判明したように、結果は、リモートオブジェクトのどのスロットが信号に接続されていたかによって異なります。 PyQtとPySideの動作はわずかに異なります。

スロットタイプ パイク パイサイド
C ++オブジェクトメソッド スロットが切断されています スロットが切断されています
純粋なPythonのメソッドまたは関数 スロットが切断されています
C ++メソッド-Pythonラッパーでオーバーロードされたオブジェクト


解決策



C ++オブジェクトの削除は特に対処が困難です。 すぐにではなく、まったく明示的に表示されないこともあります。 いくつかのヒント:



特効薬があれば、コメントでそれを読んでうれしいです。



おわりに



PyQt / PiSideを怖がらせるべきだと誰も結論付けていないことを望みますか? 実際には、問題は頻繁に発生しません。 どのツールにも、知っておくべき長所と短所があります。



All Articles