Почему lessThan() в QSortFilterProxyModel вызывается много раз в Python Qt
В Python Qt метод lessThan() в QSortFilterProxyModel вызывается тысячи раз при сортировке в QTreeView из-за ошибок в QAbstractItemModel. Узнайте, почему Qt видит плоскую модель как дерево, и как исправить rowCount, index для оптимизации сортировки и фильтрации.
Метод lessThan() в подклассе QSortFilterProxyModel вызывается слишком много раз: почему?
У меня есть кастомная модель QAbstractItemModel, которую я использую с QTreeView (хотя она одномерная, мне нужны некоторые функции QTreeView для отображения). Между ними находится QSortFilterProxyModel с переопределёнными методами filterAcceptsRow() и lessThan(). DynamicSortFilter установлен в False, и я вручную вызываю invalidate() по сигналам от других виджетов.
После заполнения модели большим объёмом данных сортировка занимает слишком много времени. Я убедился, что lessThan() использует данные напрямую из модели и только сравнивает целые числа — без эффекта. Затем я проверил количество вызовов функции, и к удивлению: для сортировки 15 строк lessThan() вызывается 880 раз! Первые 55 вызовов (должно хватить) я отследил до моего ручного вызова:
--- Call Chain (Top to Bottom) ---
Called by: <module> (File: main.py, Line: 48)
Called by: <lambda> (File: mainwindow.py, Line: 60)
Called by: set_filter (File: eventlistproxymodel.py, Line: 126)
126 self.invalidate()
Остальные 825 вызовов происходят как-то внутренне и не отслеживаются:
--- Call Chain (Top to Bottom) ---
Called by: <module> (File: main.py, Line: 48)
48 app.exec()
С большим количеством данных становится хуже: для 100+ строк — 70k+ операций и 2 секунды. Количество «лишних вызовов» всегда равно количество строк * количество моих вызовов, как будто повторяется для каждой строки.
| Строк | Отслеженные вызовы | Всего вызовов | Формула |
|---|---|---|---|
| 6 | 9 | 63 | (1+6) * 9 = 63 |
| 15 | 55 | 880 | (1+15) * 55 = 880 |
| 42 | 184 | 7912 | (1+42) * 184 = 7912 |
Не понимаю, что с этим делать. Вероятно, ошибка в моей реализации (я врач, пишу код для упрощения жизни). Проверял модель с псевдо-деревом и виджет — без успеха. Подскажите, где искать?
Релевантный код:
QAbstractItemModel
class EventListModel(QAbstractItemModel):
# ... константы колонок ...
internalValueRole = Qt.ItemDataRole.UserRole + 1
customSpanRole = Qt.ItemDataRole.UserRole + 2
sortRole = Qt.ItemDataRole.UserRole + 3
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
return len(self.event_list)
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
return self.COLUMN_COUNT
def index(self, row, column, parent: QModelIndex = QModelIndex()) -> QModelIndex:
return self.createIndex(row, column, self.root)
def parent(self, index: QModelIndex) -> QModelIndex:
return QModelIndex()
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any:
# ... реализация ...
elif role == self.sortRole:
return entry.category
QSortFilterProxyModel
class EventListProxyModel(QSortFilterProxyModel):
# ... фильтры ...
def lessThan(self, source_left, source_right, /):
if not self.sortfilter_enabled:
return True
left_category = self.sourceModel().index(source_left.row(), 0, source_left.parent()).data(EventListModel.sortRole)
right_category = self.sourceModel().index(source_right.row(), 0, source_right.parent()).data(EventListModel.sortRole)
if left_category != right_category:
return left_category < right_category
else:
# ... сравнение по типу и дате ...
return left_duedate < right_duedate
Вызов из другого виджета
self.ui.lw_term.currentRowChanged.connect(lambda row: self.ui.trw_event.model().set_filter(EventListProxyModel.FILTER_TERM, row))
Редактирование: Благодаря подсказке, изменения в QAbstractItemModel решили проблему:
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
if parent.isValid():
return 0
return len(self.event_list)
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
if parent.isValid():
return 0
return self.COLUMN_COUNT
def index(self, row, column, parent: QModelIndex = QModelIndex()) -> QModelIndex:
return self.createIndex(row, column, QModelIndex())
def hasChildren(self, /, parent = ...):
if parent.isValid():
return False
return self.rowCount(parent) > 0 and self.columnCount(parent) > 0
В python qt с qtreeview метод lessThan() в подклассе QSortFilterProxyModel вызывается чрезмерно много раз, потому что ваша QAbstractItemModel неправильно сигнализирует о плоской структуре — Qt воспринимает её как иерархическую, запуская рекурсивную сортировку по всем “уровням”. Для 15 строк это даёт 880 вызовов вместо 55, умножая нагрузку на количество элементов. Исправление простое: в rowCount() и columnCount() возвращайте 0 для валидного parent, используйте уникальные ID в index() — и сортировка ускорится в разы, как показано в вашем редактировании.
Содержание
- Почему метод lessThan() в QSortFilterProxyModel вызывается слишком много раз в python qt
- Проблемы с плоскими моделями в QTreeView и QAbstractItemModel
- Правильная реализация rowCount, columnCount и index для qt модели
- Оптимизация сортировки и фильтрации в QSortFilterProxyModel
- Примеры кода и лучшие практики для python qt приложений с QTreeView
- Альтернативы: QAbstractTableModel и динамическая сортировка в qt designer python
- Источники
- Заключение
Почему метод lessThan() в QSortFilterProxyModel вызывается слишком много раз в python qt
Представьте: вы заполняете модель данными, вызываете invalidate() — и вдруг сортировка растягивается на секунды. Для 15 строк lessThan() срабатывает 880 раз, а для 42 — уже 7912. Ваша таблица с формулой (1 + строк) * вызовы идеально описывает суть. Почему так?
В python qt QSortFilterProxyModel работает с исходной моделью рекурсивно, если она кажется иерархической. Qt проверяет каждый QModelIndex на наличие детей через rowCount(parent) и columnCount(parent). В вашем коде эти методы игнорируют parent и всегда возвращают полные значения — бац, модель выглядит как дерево с бесконечными ветками! Прокси сортирует не только корень, но и “дети” каждой строки, повторяя процесс N раз.
А lessThan()? Оно вызывается для каждой пары элементов в quicksort-подобном алгоритме. Нормально — O(n log n) сравнений. Но с рекурсией? Умножьте на глубину “дерева”, равную количеству строк. Отсюда ваши 825 “внутренних” вызовов из app.exec() — Qt перестраивает layout в qtreeview заново и заново.
Кто виноват? Не dynamicSortFilter (вы его выключили правильно), не фильтры. Проблема в qabstractitemmodel, которая не кричит “я плоская!”. В официальной документации Qt чётко сказано: для flat-моделей rowCount(QModelIndex()) > 0, но rowCount(valid_parent) == 0. Без этого qsortfilterproxymodel тонет в рекурсии.
Звучит знакомо? Многие на Stack Overflow жалуются на то же с qtreeview.
Проблемы с плоскими моделями в QTreeView и QAbstractItemModel
QTreeView — мощный виджет для qt модели, но он ожидает иерархию. Вы хотите плоский список? Легко, но только если модель честно скажет: “детей нет”. В вашем случае index(row, column, self.root) использует общий root как internalId — все индексы кажутся детьми одного родителя. А parent() всегда QModelIndex()? Qt путается и строит фантомное дерево.
Почему это бьёт по производительности? При invalidate() прокси:
- Запрашивает
rowCount(QModelIndex())— получает 15. - Сортирует корневые элементы, вызывая
lessThan()~55 раз. - Для каждого из 15 индексов проверяет
rowCount(that_index)— снова 15! Рекурсия. - И так для каждого “уровня”, пока не упрётся в лимиты.
Результат: экспоненциальный рост вызовов. С 100 строками — 70k операций, 2 секунды. А если данные из SQL, как в qtreeview sql python? Забудьте о отзывчивости.
Ещё ловушка: data() с source_left.parent() в lessThan(). Если parent неверный, данные берутся хаотично. Плюс, без hasChildren() Qt полагается на rowCount() > 0, усугубляя рекурсию.
В python qt это классика для новичков с QAbstractItemModel. Врач-программист? Респект, но модели Qt — как анатомия: один неверный мускул, и вся система хромает.
Правильная реализация rowCount, columnCount и index для qt модели
Вы уже нашли фикс в редактировании — молодец! Давайте разберём по шагам, почему он работает. Основное: сделайте модель явно плоской (qt модели данных flat).
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
if parent.isValid():
return 0 # Нет детей!
return len(self.event_list)
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
if parent.isValid():
return 0
return self.COLUMN_COUNT
def index(self, row, column, parent: QModelIndex = QModelIndex()) -> QModelIndex:
if parent.isValid(): # Бонус: защита
return QModelIndex()
return self.createIndex(row, column, row) # Уникальный ID, не root!
def parent(self, index: QModelIndex) -> QModelIndex:
return QModelIndex() # Все — корневые
def hasChildren(self, parent: QModelIndex = QModelIndex()) -> bool:
if parent.isValid():
return False
return self.rowCount(parent) > 0
Ключ: internalId в createIndex — теперь каждый ряд уникален (используйте row или quintptr). Нет общего root — нет путаницы. hasChildren() явно говорит “плоско”.
После этого qsortfilterproxymodel сортирует только один уровень. Вызовы lessThan() сократятся до нормы. Тестируйте: для 15 строк — ~50-100, не 880.
В документации QAbstractItemModel это базис: “Для list-моделей parent всегда invalid”. Идеально для вашего случая с qtreeview.
Оптимизация сортировки и фильтрации в QSortFilterProxyModel
Теперь, когда модель фикснута, доработайте прокси. Ваш lessThan() берёт данные из sourceModel().index(...).data(sortRole) — ок, но медленно при больших данных.
Улучшения:
- Кэшируйте
sortRoleданные в модели: вdata()возвращайте предвычисленные значения. - В
lessThan()работайте напрямую сsource_left.data(self.sortRole)— Qt кэширует роли!
def lessThan(self, source_left, source_right):
if not self.sortfilter_enabled:
return super().lessThan(source_left, source_right)
left_cat = source_left.data(EventListModel.sortRole)
right_cat = source_right.data(EventListModel.sortRole)
if left_cat != right_cat:
return left_cat < right_cat
# Аналогично для даты, без index()!
return left_date < right_date
source_left.data(role) быстрее, чем sourceModel().index().data(). Плюс, super().lessThan() для fallback.
Для фильтров: filterAcceptsRow() вызывайте редко, группируйте invalidate(). С dynamicSortFilter=False — вручную: sort(0, Qt.AscendingOrder) после invalidate.
В документации QSortFilterProxyModel советуют autoAcceptChildRows=False для flat, но фикс модели важнее.
С 100+ строками? Теперь секунды превратятся в миллисекунды.
Примеры кода и лучшие практики для python qt приложений с QTreeView
Полный минимальный пример python qt примеры с qt qtreeview и qsortfilterproxymodel qt:
import sys
from PyQt6.QtCore import Qt, QAbstractItemModel, QModelIndex, QSortFilterProxyModel
from PyQt6.QtWidgets import QApplication, QTreeView, QMainWindow
class FlatModel(QAbstractItemModel):
# Ваш event_list = [{"category": 1, "date": "2026-01-01"}, ...]
def rowCount(self, parent=QModelIndex()):
return 0 if parent.isValid() else len(self.event_list)
def columnCount(self, parent=QModelIndex()):
return 0 if parent.isValid() else 2
def index(self, row, col, parent=QModelIndex()):
return QModelIndex() if parent.isValid() else self.createIndex(row, col, row)
def parent(self, index):
return QModelIndex()
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
if not index.isValid():
return None
entry = self.event_list[index.row()]
if role == Qt.ItemDataRole.UserRole + 3: # sortRole
return entry["category"]
return str(entry.get("date", ""))
class Proxy(QSortFilterProxyModel):
def lessThan(self, left, right):
l_cat = left.data(1003) # sortRole
r_cat = right.data(1003)
return l_cat < r_cat if l_cat != r_cat else super().lessThan(left, right)
app = QApplication(sys.argv)
model = FlatModel()
model.event_list = [{"category": i % 3, "date": f"2026-02-{i:02d}"} for i in range(100)]
proxy = Proxy()
proxy.setSourceModel(model)
proxy.setDynamicSortFilter(False)
proxy.setSortRole(1003)
view = QTreeView()
view.setModel(proxy)
view.sortByColumn(0, Qt.SortOrder.AscendingOrder) # Тест
proxy.invalidate() # Теперь быстро!
window = QMainWindow()
window.setCentralWidget(view)
window.show()
sys.exit(app.exec())
Лучшие практики qt python уроки:
- Всегда добавляйте
if parent.isValid(): return 0. - Уникальные
internalId:quintptr(row)или хэш. - Сигналы модели:
layoutAboutToBeChanged,layoutChangedдля плавности. - Для qtreeview doubleclicked signal — подключайте без проблем.
- Профилируйте с
cProfile:python -m cProfile main.py.
Так ваше приложение полетит.
Альтернативы: QAbstractTableModel и динамическая сортировка в qt designer python
Не хотите мучаться с QAbstractItemModel? Перейдите на QAbstractTableModel — она по умолчанию плоская, rowCount() игнорирует parent. Идеально для таблиц в qt designer python.
Наследуйтесь:
class TableModel(QAbstractTableModel):
def rowCount(self, parent=QModelIndex()):
return len(self.event_list) # Parent игнорируется!
def data(self, index, role):
# То же
В Qt Designer: перетащите QTreeView, setModel в коде. Для динамики — setDynamicSortFilter(True), кликните заголовок — сортировка авто.
Ещё вариант: QStandardItemModel для прототипов, но для больших данных — кастом.
В примерах на Stack Overflow показывают tree из flat, но для вас table хватит. Модель представление qt упрощается.
Источники
- lessThan in subclassed QSortFilterProxyModel being called too many times — Диагностика и решение проблемы множественных вызовов lessThan в python qt: https://stackoverflow.com/questions/79875807/lessthan-in-subclassed-qsortfilterproxymodel-being-called-too-many-times-and-i
- QAbstractItemModel — Документация по реализации rowCount, index и плоской структуре: https://doc.qt.io/qt-6/qabstractitemmodel.html
- QSortFilterProxyModel — Описание сортировки, lessThan и рекурсии в прокси-моделях: https://doc.qt.io/qt-6/qsortfilterproxymodel.html
- QTreeView — Рекомендации по использованию с плоскими моделями в QTreeView: https://doc.qt.io/qt-6/qtreeview.html
- QTreeView and QAbstractItemModel — Примеры построения tree из flat данных в python qt: https://stackoverflow.com/questions/36351165/qtreeview-and-qabstractitemmodel
Заключение
Ваша проблема с тысячами вызовов lessThan() в python qt решена: модель теперь плоская, сортировка летает. Главное — parent.isValid() в rowCount/columnCount и уникальные ID. Для будущего используйте QAbstractTableModel или qt designer python — меньше кода, больше скорости. Тестируйте с реальными данными, добавьте кэш — и приложение для врачей станет идеальным инструментом. Удачи в кодинге!
Это ожидаемое поведение в python qt, когда qabstractitemmodel некорректно сигнализирует о иерархичности: rowCount() и columnCount() не возвращают 0 для валидных parent, а index() использует общий internalId. Qt в qsortfilterproxymodel рекурсивно сортирует каждый уровень, умножая вызовы lessThan() на количество строк (например, 15 строк → 880 вызовов). Для плоской модели в qtreeview исправьте: rowCount(parent.isValid()) → 0, createIndex(row, column, QModelIndex()), добавьте hasChildren(). После фикса — одна сортировка, qt модели оптимизированы. Рекомендация: используйте QAbstractTableModel для flat list.
В qabstractitemmodel для плоских qt модели rowCount() и columnCount() должны возвращать 0 при parent.isValid(), иначе модель иерархическая. index(row, column, parent) создаёт уникальный QModelIndex без общего internalId; parent() → QModelIndex() для flat. hasChildren(parent) по умолчанию: rowCount() > 0 || columnCount() > 0. В python qt это критично для qtreeview. Используйте QAbstractTableModel для таблиц — она уже реализует flat логику. Поддерживает data(), flags(), insertRows() и Qt 6+ checkIndex().
QSortFilterProxyModel в python qt фильтрует/сортирует между source-моделью и view. Переопределите lessThan(source_left, source_right) для кастомного сравнения по sortRole (sourceModel().data()). invalidate() триггерит полную перестройку; для иерархий — рекурсия (Qt 6: autoAcceptChildRows). dynamicSortFilter выкл. — ручная сортировка. В qsortfilterproxymodel qt лишние вызовы lessThan() от иерархической source. Сигналы: layoutChanged(). Идеально для qt модели данных в qtreeview.
QTreeView поддерживает плоские qt модели с setRootIsDecorated(false). Требует правильных rowCount(), columnCount(), index(), parent(), hasChildren() в qabstractitemmodel. В python qt для list-like данных без иерархии избегайте рекурсии в сортировке qsortfilterproxymodel. Совместимо с flat моделями, но фикс модели обязателен для производительности.
Для qtreeview в python qt постройте tree из flat данных: структура mytree с parent, children, id (qHash(path)), map<id, node>. Реализуйте rowCount(parent.isValid() ? 0 : children.size()), index(row, 0, parent) с createIndex(row, column, id), parent(index) по internalId(). Для плоских qt модели — QAbstractTableModel. Избегайте общих указателей, чтобы qsortfilterproxymodel не рекурсировал. Примеры: SQL-данные в RestoreTreeModel, qtreeview sql python.