[Qt] PySide 如何 debug 沒有 stack trace 的 Segmentation Fault
雖然 PySide 是 open source 也很好用,但如果我們用 PySide 寫了一些有 bug 的程式,GUI 在做了某個操作後就會無預警 crash 消失不見,只留下哀傷的一行訊息在 console 上:
Segmentation fault
現在是要怎麼 debug?
最近我就想用 QTableView + QAbstractItemModel 做一個可以動態新增資料的 table,但是以下的程式碼跑起來就會遇到 segmentation fault (完整的程式碼在文章最下面):
from typing import Any, List
import sys
from PySide2 import QtCore, QtWidgets
class DataModel(QtCore.QAbstractItemModel):
...
class MainDialog(QtWidgets.QDialog):
...
if __name__ == "__main__":
...
看了一下我也不知道問題在哪,我也不太曉得 QAbstractItemModel 哪幾個 functions 是一定要 override 的,現在該怎辦?
解法1: 暫時切換成 PyQt
當我們把一開始的 import 全部換成:
from PyQt5 import QtCore, QtWidgets
再跑一次程式,這時候 GUI 不僅不會 crash,console 上還會多一行完整的訊息:
NotImplementedError: QAbstractItemModel.index() is abstract and must be overridden
Oh 原來我忘記要 override "index" function 了。
解法2: 用 gdb 來 debug
如果遇到提示還不夠明顯的狀況,我們可以用 gdb 來 debug。
等等! gdb 不是給 C++ 用的嗎? 和 Python 有什麼關係?
原來 PySide (Qt for Python) 只是像是用 Python 的殼把 Qt C++ 的程式碼包起來,詳細的運作原理可以參考 Qt for Python/Shiboken。簡言之,可以想成 PySide 在 Python 的責任就只有幫你呼叫 C++ Qt 的程式碼,不會在 Python 這邊多做什麼複雜的邏輯。
這個 "Segmentation fault" 錯誤是在 C++ 那邊很常會看到的,當然我們就會想說是不是可以用 gdb 來 debug。剛好在 Python Wiki 就有一個頁面教我們怎麼用 gdb 來 debug,簡單的步驟如下:
gdb python
run test.py
遇到錯誤時 gdb 會停下來可以打字,我們輸入 bt
就可以看到詳細的 backtrace:
(gdb) bt
#0 PyErr_SetObject () at Python/errors.c:84
#1 0x00007ffff7c5b46d in PyErr_SetString () at Python/errors.c:157
#2 0x00007fffe0b791b0 in QAbstractItemModelWrapper::index(int, int, QModelIndex const&) const ()
from /xxx/.local/lib/python3.6/site-packages/PySide2/QtCore.abi3.so
#3 0x00007fffdab63ae5 in QTableView::paintEvent(QPaintEvent*) ()
from /xxx/.local/lib/python3.6/site-packages/PySide2/Qt/lib/libQt5Widgets.so.5
#4 0x00007fffdb530e97 in QTableViewWrapper::paintEvent(QPaintEvent*) ()
from /xxx/.local/lib/python3.6/site-packages/PySide2/QtWidgets.abi3.so
#5 0x00007fffda90b740 in QWidget::event(QEvent*) ()
from /xxx/.local/lib/python3.6/site-packages/PySide2/Qt/lib/libQt5Widgets.so.5
#6 0x00007fffda9b421e in QFrame::event(QEvent*) ()
from /xxx/.local/lib/python3.6/site-packages/PySide2/Qt/lib/libQt5Widgets.so.5
#7 0x00007fffdab182bc in QAbstractItemView::viewportEvent(QEvent*) ()
from /xxx/.local/lib/python3.6/site-packages/PySide2/Qt/lib/libQt5Widgets.so.5
#8 0x00007fffdb536675 in QTableViewWrapper::viewportEvent(QEvent*) ()
from /xxx/.local/lib/python3.6/site-packages/PySide2/QtWidgets.abi3.so
#9 0x00007fffe056b5a0 in QCoreApplicationPrivate::sendThroughObjectEventFilters(QObject*, QEvent*) ()
---Type <return> to continue, or q <return> to quit---
雖然沒有像 PyQt 直截了當告訴我們錯誤在哪,但我們看到 #2 應該也猜的到我們忘記寫 "QAbstractItemModelWrapper::index" 了。
Python 版本小陷阱
在 Linux 系統上 "python" 有時候指到 python 2 有時候卻是 python 3。
如果你是用 Python 3 去開發,怕 "gdb python" 找錯 Python 版本的話最好是說的更明確一點: "gdb python3"。
失敗方法: 用 faulthandler
如果我們去 Google 搜尋 "python debug segmentation fault" 我們可以在 StackOverflow 上找到一個看起來很簡單的解法,可以加在一開始程式執行的地方或是即將 crash 前:
import faulthandler; faulthandler.enable()
但很遺憾的是針對這次的 buggy code 還是沒有用,他會在 console 上寫:
Fatal Python error: Segmentation fault
Current thread 0x00007ffff7f9c740 (most recent call first):
File "/xxx/test.py", line 109 in <module>
Segmentation fault
結語
看起來切換成 PyQt 解法可以最快找到問題在哪並且修正。比較複雜的 case 我們也有能力用 gdb 來做 debug。faulthandler 說不定在其他地方能派上用場,只是對 PySide 沒什麼作用。
在用 Python 寫 Qt 比較複雜的功能時我們還是要盡量去看 C++ 的 documentation,避免我們誤用他預期的使用方式。
附錄
GUI
一開始 QTableView 會顯示 2 rows:
點了 "Add more data" 後會多 3 rows:
會 Segmentation Fault 的 PySide2 Code
from typing import Any, List
import sys
from PySide2 import QtCore, QtWidgets
class DataModel(QtCore.QAbstractItemModel):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self._headers: List[str] = ["Column A", "Column B", "Column C"]
self._data: List[List[str]] = [
["item 1", "item 2", "item 3"],
["item 4", "item 5", "item 6"],
]
def columnCount(self, parent=QtCore.QModelIndex()) -> int:
return len(self._headers)
def rowCount(self, parent=QtCore.QModelIndex()) -> int:
return len(self._data)
def headerData(
self,
section: int,
orientation: QtCore.Qt.Orientation,
role: int = QtCore.Qt.DisplayRole,
) -> Any:
if (
role == QtCore.Qt.DisplayRole
and orientation == QtCore.Qt.Horizontal
):
return self._headers[section]
return None
def data(
self, index: QtCore.QModelIndex, role: int = QtCore.Qt.DisplayRole
) -> Any:
if not index.isValid():
return None
if role == QtCore.Qt.DisplayRole:
return self._data[index.row()][index.column()]
return None
def append_data(self, new_data: List[List[str]]) -> None:
parent = QtCore.QModelIndex()
first = len(self._data)
last = first + len(new_data) - 1
self.beginInsertRows(parent, first, last)
self._data.extend(new_data)
self.endInsertRows()
class MainDialog(QtWidgets.QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self._layout = QtWidgets.QHBoxLayout(self)
self._table_view = QtWidgets.QTableView()
self._model = DataModel(self._table_view)
self._table_view.setModel(self._model)
self._button = QtWidgets.QPushButton()
self._button.setText("Add more data")
self._button.clicked.connect(self._add_more_data)
self._layout.addWidget(self._table_view)
self._layout.addWidget(self._button)
def _add_more_data(self) -> None:
new_data: List[List[str]] = [
["item 7", "item 8", "item 9"],
["item 10", "item 11", "item 12"],
["item 13", "item 14", "item 15"],
]
self._model.append_data(new_data)
if __name__ == "__main__":
app = QtWidgets.QApplication([])
dialog = MainDialog()
dialog.show()
sys.exit(app.exec_())
修正的 PySide2 Code
from typing import Any, List
from typing import Any, List
import sys
from PySide2 import QtCore, QtWidgets
class DataModel(QtCore.QAbstractItemModel):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self._headers: List[str] = ["Column A", "Column B", "Column C"]
self._data: List[List[str]] = [
["item 1", "item 2", "item 3"],
["item 4", "item 5", "item 6"],
]
def index(
self, row: int, column: int, parent=QtCore.QModelIndex()
) -> QtCore.QModelIndex:
if (
row >= 0
and row < len(self._data)
and column >= 0
and column < len(self._headers)
):
ptr = self._data[row][column]
return self.createIndex(row, column, ptr)
return QtCore.QModelIndex()
def parent(self, child: QtCore.QModelIndex) -> QtCore.QModelIndex:
return QtCore.QModelIndex()
def columnCount(self, parent=QtCore.QModelIndex()) -> int:
return len(self._headers)
def rowCount(self, parent=QtCore.QModelIndex()) -> int:
return len(self._data)
def headerData(
self,
section: int,
orientation: QtCore.Qt.Orientation,
role: int = QtCore.Qt.DisplayRole,
) -> Any:
if (
role == QtCore.Qt.DisplayRole
and orientation == QtCore.Qt.Horizontal
):
return self._headers[section]
return None
def data(
self, index: QtCore.QModelIndex, role: int = QtCore.Qt.DisplayRole
) -> Any:
if not index.isValid():
return None
if role == QtCore.Qt.DisplayRole:
return self._data[index.row()][index.column()]
return None
def append_data(self, new_data: List[List[str]]) -> None:
parent = QtCore.QModelIndex()
first = len(self._data)
last = first + len(new_data) - 1
self.beginInsertRows(parent, first, last)
self._data.extend(new_data)
self.endInsertRows()
class MainDialog(QtWidgets.QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self._layout = QtWidgets.QHBoxLayout(self)
self._table_view = QtWidgets.QTableView()
self._model = DataModel(self._table_view)
self._table_view.setModel(self._model)
self._button = QtWidgets.QPushButton()
self._button.setText("Add more data")
self._button.clicked.connect(self._add_more_data)
self._layout.addWidget(self._table_view)
self._layout.addWidget(self._button)
def _add_more_data(self) -> None:
new_data: List[List[str]] = [
["item 7", "item 8", "item 9"],
["item 10", "item 11", "item 12"],
["item 13", "item 14", "item 15"],
]
self._model.append_data(new_data)
if __name__ == "__main__":
app = QtWidgets.QApplication([])
dialog = MainDialog()
dialog.show()
sys.exit(app.exec_())
留言
發佈留言