Как добавить перегрузки в существующий API в PyQt - PullRequest
0 голосов
/ 18 февраля 2019

Существует класс, чей __init__() уже принимает различное число и типы для своих аргументов.Я хочу подкласс, чтобы добавить новый аргумент.Я не знаю, как я собираюсь написать новое __init__() определение .

. У меня нет исходного кода для существующего базового класса (вероятно, он написан на C ++).help(QListWidgetItem) дает мне:

class QListWidgetItem(sip.wrapper)
 |  QListWidgetItem(parent: QListWidget = None, type: int = QListWidgetItem.Type)
 |  QListWidgetItem(str, parent: QListWidget = None, type: int = QListWidgetItem.Type)
 |  QListWidgetItem(QIcon, str, parent: QListWidget = None, type: int = QListWidgetItem.Type)
 |  QListWidgetItem(QListWidgetItem)

Мой редактор (PyCharm) распознает их и предлагает контекстно-зависимое завершение.Он ведет себя так, как если бы они были объявлены с помощью директив @overload, и я хотел бы сохранить это.

Обратите внимание, что уже не только число аргументов является переменной, но и типы.Например, просмотр всех параметров перегрузки # 1 может быть QListWidget, str, QIcon или QListWidgetItem, или даже не предоставлен, и в зависимости от того, как влияет второй аргумент,и т.д.

Я хочу добавить дополнительный:

MyListWidgetItem(text: str, value: QVariant, parent: QListWidget = None, type: int = QListWidgetItem.Type)

Обратите внимание, что мой новый аргумент QVariant находится на втором месте, и я хочу, чтобы он был позиционным, а не именованным по ключевому слову.

Так что мне нужно узнать этот новый, когда он называется;Мне нужно вытащить мою новую value: QVariant, чтобы установить новую переменную-член, мне также нужно удалить ее перед вызовом конструктора базового класса.

Я знаю, что для декларации я добавлю перегрузку , например:

class MyListWidgetItem(QListWidgetItem)
    @overload
    def __init__(self, text: str, value: QVariant, parent: QListWidget=None, type: int=QListWidgetItem):
        pass

(я предполагаю, что из существующих выйдет QListWidgetItem @overload s все еще доступны через мои производные MyListWidgetItem s?)

Как насчет фактического определения ?Что он делает и как должен быть объявлен / написан?

Мне нужно распознать этот новый, когда он вызывается;Мне нужно вытащить мою новую value: QVariant для установки моей переменной, мне также нужно удалить ее перед вызовом конструктора базового класса.

Я могу только догадываться: это моя работа, чтобы распознать мой случай, чтобынапишите как:

if len(arguments) >= 2:
    if isinstance(arguments[0], str) and isinstance(arguments[1], QVariant):
        self.value = arguments[1]
        del arguments[1]

Затем: я должен написать единственное определение __init__() ( not @overload объявлений) для моего нового подкласса в виде:

def __init__(self, *__args)
    ...
    super().__init__(*__args)

или с явными, явно набранными аргументами в виде:

def __init__(self, arg1: typing.Union[QListWidget, str, icon, QListWidgetItem, None], arg2: typing..., arg3: typing..., arg4)
    ...
    super().__init__(arg1, arg2, arg3, arg4)

Последнее выглядит сложным?Является ли предыдущий подход декларацией и работой напрямую с *__args лучшим способом?

[ РЕДАКТИРОВАТЬ : Если это имеет какое-то значение для создания какого-то решения, я готов сделатьмой новый параметр необязательный через value: QVariant = ....Или, если ответ, скажем, «Вы не сможете сделать это по-своему, потому что ..., но лучший способ сделать это - сделать это аргументом только для именованных ключевых слов, потому что тогда вы можете... "или что-то еще, я бы рассмотрел в этом свете.]

Ответы [ 2 ]

0 голосов
/ 19 февраля 2019

Здесь есть две отдельные проблемы.

Первая относится к PyCharm и использует модуль ввода .Он генерирует файлы Pyi с заглушками, определяющими API различных сторонних библиотек (таких как PyQt), чтобы обеспечить автоматическое заполнение и тому подобное.Кроме того, он поддерживает определяемые пользователем подсказки типов и файлы pyi, которые описаны здесь: Подсказка типов в PyCharm .Однако, поскольку я не являюсь пользователем PyCharm, я не могу дать практического совета о том, как именно вы должны определить свои собственные заглушки перегрузки, чтобы они дополняли существующие заглушки PyQt.Я полагаю, что это должно быть возможно.

Второй вопрос касается именно того, как реализовать перегрузки функций для существующих API-интерфейсов PyQt.Короткий ответ на это таков: вы не можете: Python просто не поддерживает перегрузки так же, как C ++.Это потому, что Python динамически типизирован, поэтому такой тип перегрузки не имеет смысла.Однако можно обойти это различными способами, чтобы обеспечить эквивалентное поведение.

Для вашего конкретного случая простейшее решение требует небольшого компромисса.Ваш вопрос гласит: «Обратите внимание, что мой новый аргумент QVariant находится на втором месте, и я хочу, чтобы он был позиционным, а не ключевым словом».Если вы готовы отказаться от этого требования, это облегчает задачу , потому что вы можете определить свой подкласс следующим образом:

class MyListWidgetItem(QListWidgetItem):
    def __init__(self, *args, value=None, **kwargs):
        super().__init__(*args, **kwargs)

или вот так:

class MyListWidgetItem(QListWidgetItem):
    def __init__(self, *args, **kwargs):
        value = kwargs.pop('value', None)
        super().__init__(*args, **kwargs)

Эти подклассы будут поддерживать все существующие перегрузки PyQt без необходимости точно знать, как они определены, поскольку вы просто передаете аргументы базовой реализации.Пользователь должен предоставить правильные аргументы, но поскольку базовый класс предоставляется PyQt, он автоматически вызовет TypeError, если заданы неправильные аргументы.Все это помогает сделать реализацию очень простой, но она делает ставку на правильное документирование ваших API, учитывая, что сама сигнатура функции практически не дает подсказки о том, какими должны быть правильные аргументы.Однако, если вы также можете найти способ использовать поддержку хинтинга типов в PyCharm, как предложено выше, это должно приблизить вас к очень простому, работающему решению.

Но что, если вы не готовы идти на компромисс??Непосредственную проблему, которая возникает при этом, можно увидеть, если рассмотреть эту подпись:

QListWidgetItem(parent: QListWidget = None, type: int = QListWidgetItem.Type)

Это позволяет создать элемент с без аргументов .Но это немедленно перекрывает любую новую перегрузку, которая определяет обязательные аргументы, так как Python вызовет TypeError, если они отсутствуют при вызове конструктора во время выполнения.Единственный способ обойти это - использовать подпись *args, **kwargs, а затем явно проверить количество и тип всех аргументов в теле __init__.По сути, это то, что functools.singledispatch и сторонние пакеты, такие как multipledispatch , делают только через декораторы.Это на самом деле не обходит вышеупомянутую проблему - оно просто перемещает ее в другое место и избавляет вас от необходимости поддерживать весь груз сложного кода котельной плиты.

Я не собираюсь отправлять сообщенияпримеры в стиле здесь: во-первых, потому что я понятия не имею, как они будут работать в PyCharm (или даже PyQt, в этом отношении), и во-вторых, потому что они уже были рассмотрены в более общих SO-вопросах, таких как этот: Python functionперегрузки .Мой совет - начать с гораздо более простой реализации, приведенной выше, а затем подумать об экспериментировании с другими подходами, если вы обнаружите, что действительно нужно добавить перегрузки с аргументами без ключевых слов.

Oneпоследний подход к рассмотрению - это то, что можно назвать стандартной перегрузкой кухонной раковины.При таком подходе вы просто забудете о сигнатурах существующих API-интерфейсов и определите свой подкласс примерно так:

class MyListWidgetItem(QListWidgetItem):
    def __init__(self, text='', value=None, parent=None, icon=None,
                       item=None, type=QListWidgetItem.Type):
        if item is not None:
            super().__init__(item)
        elif icon is not None:
            super().__init__(icon, text, parent, type)
        else:
            super().__init__(text, parent, type)

Или, если вас не волнует type и конструктор копирования:

class MyListWidgetItem(QListWidgetItem):
    def __init__(self, text='', value=None, parent=None, icon=None):
        if icon is not None:
            super().__init__(icon, text, parent)
        else:
            super().__init__(text, parent)

Подавляющее большинство кода Python / PyQt, вероятно, использует некоторые варианты такого подхода.Таким образом, практичность превосходит чистоту , я думаю ...

0 голосов
/ 18 февраля 2019

Не существует единого универсального ответа, и pyQt (который на самом деле является тонким слоем над Qt и предоставляет довольно много идиом C ++) не обязательно представляет наиболее распространенные варианты использования.

Как правило, лучше явно копировать (и расширять) прототип инициализатора родительского класса, так что вам (универсальному "вы" -> любому, кто должен поддерживать код) не обязательнопрочтите документ родительского класса и т. д., чтобы узнать, что ожидается.

Теперь в некоторых случаях, когда

  • родительский класс принимает множество аргументов
  • и ваш подкласс не связывается с этими аргументами
  • и ваш подкласс хочет только ДОБАВИТЬ аргументы
  • и это нормально для вас / вашегодобавьте эти аргументы в качестве аргументов только для ключевых слов или заставьте их указывать перед любым из родительских аргументов
  • и вы / ваша команда можете отказаться от подсказок autodoc / typeetc

, тогда использование *args и **kwargs действительно является решением:

def __init__(self, my_positional_arg, *args, **kwargs)
    # use .pop() to avoid passing it to the parent
    my_own_kw_arg = kw.pop("my_own_kw_arg", "nothing")
    super().__init__(*args, **kwargs)

Обратите внимание, что вы действительно хотите поддерживать оба *args и **kwargs в этом случае, поскольку даже требуется позиционныйАргументы могут передаваться как именованные аргументы, и чаще всего они действительно передаются, когда класс (или функция FWIW) принимает много аргументов (имена легче запомнить, чем позиции ...).

...