Программирование

Почему lessThan() в QSortFilterProxyModel вызывается много раз в Python Qt

В Python Qt метод lessThan() в QSortFilterProxyModel вызывается тысячи раз при сортировке в QTreeView из-за ошибок в QAbstractItemModel. Узнайте, почему Qt видит плоскую модель как дерево, и как исправить rowCount, index для оптимизации сортировки и фильтрации.

6 ответов 1 просмотр

Метод 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

python
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

python
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

Вызов из другого виджета

python
self.ui.lw_term.currentRowChanged.connect(lambda row: self.ui.trw_event.model().set_filter(EventListProxyModel.FILTER_TERM, row))

Редактирование: Благодаря подсказке, изменения в QAbstractItemModel решили проблему:

python
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

Представьте: вы заполняете модель данными, вызываете 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() прокси:

  1. Запрашивает rowCount(QModelIndex()) — получает 15.
  2. Сортирует корневые элементы, вызывая lessThan() ~55 раз.
  3. Для каждого из 15 индексов проверяет rowCount(that_index) — снова 15! Рекурсия.
  4. И так для каждого “уровня”, пока не упрётся в лимиты.

Результат: экспоненциальный рост вызовов. С 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).

python
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 кэширует роли!
python
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:

python
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.

Наследуйтесь:

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 упрощается.


Источники

  1. 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
  2. QAbstractItemModel — Документация по реализации rowCount, index и плоской структуре: https://doc.qt.io/qt-6/qabstractitemmodel.html
  3. QSortFilterProxyModel — Описание сортировки, lessThan и рекурсии в прокси-моделях: https://doc.qt.io/qt-6/qsortfilterproxymodel.html
  4. QTreeView — Рекомендации по использованию с плоскими моделями в QTreeView: https://doc.qt.io/qt-6/qtreeview.html
  5. 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 — меньше кода, больше скорости. Тестируйте с реальными данными, добавьте кэш — и приложение для врачей станет идеальным инструментом. Удачи в кодинге!

B

Это ожидаемое поведение в 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 моделями, но фикс модели обязателен для производительности.

U

Для 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.

Авторы
B
Разработчик программного обеспечения
M
Разработчик PyQt
U
Разработчик C++
O
Разработчик
Источники
Документация
Проверено модерацией
Модерация
Почему lessThan() в QSortFilterProxyModel вызывается много раз в Python Qt