Сокращение декоратора класса данных без потери IntelliSense

Сценарий

Предположим, я хочу создать псевдоним для декоратора dataclasses.dataclass с конкретными аргументами. Например:

# Instead of repeating this decorator all the time:
@dataclasses.dataclass(frozen=True, kw_only=True)
class Entity:
    ...

# I just write something like this:
@struct
class Entity:
    ...

Я использую статический анализатор Pylance в Visual Studio Code.

Я использую Python 3.11.

Попытка 1: Прямое присвоение (время выполнения ✅, статический анализ ❌)

Моим первым инстинктом было воспользоваться тем фактом, что функции являются первоклассными гражданами, и просто присвоить созданной функции-декоратору собственное имя. Это работает во время выполнения, но Pylance больше не распознает Entity как класс данных, о чем свидетельствует ошибка статического анализа:

struct = dataclasses.dataclass(frozen=True, kw_only=True)

@struct
class Entity:
    name: str
    value: int

# STATIC ANALYZER:
# Expected no arguments to "Entity" constructor Pylance(reportCallIssue)
valid_entity = Entity(name = "entity", value=42)

# RUNTIME:
# Entity(name='entity', value=42)
print(valid_entity)

Попытка 2: перенос (время выполнения ❌, статический анализ ❌)

Затем я подумал, что, возможно, некоторая информация каким-то образом потеряется, если я просто назначу другое имя (хотя я не понимаю, почему это так), поэтому я решил обернуть ее с помощью functools. Однако в статическом анализе это по-прежнему ведет себя так же и даже вызывает ошибку во время выполнения, когда я применяю @struct:

import dataclasses
import functools

def struct(cls):
    decorator = dataclasses.dataclass(frozen=True, kw_only=True)
    decorated_cls = decorator(cls)
    functools.update_wrapper(decorated_cls, cls)
    return decorated_cls

# No error reported by static analyzer, but runtime error at `@struct`:
# AttributeError: 'mappingproxy' object has no attribute 'update'
@struct
class Entity:
    name: str
    value: int

# STATIC ANALYZER:
# Expected no arguments to "Entity" constructor Pylance(reportCallIssue)
# RUNTIME:
# (this line doesn't even get reached)
valid_entity = Entity(name = "entity", value=42)

Полная трассировка:

Traceback (most recent call last):
  File "C:\Users\***\temp.py", line 12, in <module>
    @struct
     ^^^^^^
  File "C:\Users\***\temp.py", line 7, in struct
    functools.update_wrapper(decorated_cls, cls)
  File "C:\Users\***\AppData\Local\Programs\Python\Python311\Lib\functools.py", line 58, in update_wrapper
    getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'mappingproxy' object has no attribute 'update'

Попытка 3: Фабрика оберток (время выполнения ✅, статический анализ ❌)

Затем я попытался создать struct фабрику декораторов и использовал functools.wraps() для функции закрытия, которая просто перенаправляет функцию класса данных. Теперь это работает во время выполнения, но Pylance по-прежнему сообщает о той же ошибке, что и в попытке 1:

def struct():
    decorator = dataclasses.dataclass(frozen=True, kw_only=True)

    @functools.wraps(decorator)
    def decorator_wrapper(*args, **kwargs):
        return decorator(*args, **kwargs)
    return decorator_wrapper

@struct()
class Entity:
    name: str
    value: int

# STATIC ANALYZER:
# Expected no arguments to "Entity" constructor Pylance(reportCallIssue)
valid_entity = Entity(name = "entity", value=42)

# RUNTIME:
# Entity(name='entity', value=42)
print(valid_entity)

Я также обнаружил, что использование самой простой функции dataclasses.dataclass (нет ()) приводит к одной и той же проблеме во всех трех попытках.


Есть ли способ заставить это работать, не испортив IntelliSense?

Дополнительное дополнение: почему попытка 2 не удалась во время выполнения?

Что касается № 2: объекты класса (экземпляры builtins.type) __dict__ - это не экземпляр builtins.dict, а экземпляр types.MappingProxyType (прокси-объект для сопоставления, расположенного где-то в памяти, к которому вы не должны иметь прямой доступ). Вы не можете обновить его, используя методы dict (которые functools.update_wrapper делают внутри), но вы можете обновить их, используя setattr.

dROOOze 28.07.2024 01:00

Я только что нашел потенциальный дубликат цели (которую я не смог найти, потому что у нее не было тега python-typing ): Как сделать декоратор класса дружественным для pylance?. Однако единственный ответ там довольно раздут. Если вы считаете, что этот ответ лучше моего, я проголосую за закрытие этого вопроса как дубликата.

InSync 28.07.2024 01:21

@InSync Мне тоже не удалось найти этот пост, но теперь, когда я его вижу, кажется, что мой вопрос действительно почти точный дубликат, ха-ха. Ваш ответ здесь очень полезен и по существу, и я думаю, что предпочитаю его. Спасибо!

vlin 28.07.2024 01:51
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
3
3
69
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Украсьте struct()dataclass_transform(frozen_default = True, kw_only_default = True):

(playground links: Mypy , Pyright)

# 3.11+
from typing import dataclass_transform
# 3.10-
from typing_extensions import dataclass_transform

@dataclass_transform(frozen_default = True, kw_only_default = True)
def struct[T](cls: type[T]) -> type[T]:
    return dataclass(frozen = True, kw_only = True)(cls)

    # By the way, you can actually pass all of them
    # to dataclass() in just one call:
    #     dataclass(cls, frozen = True, kw_only = True)
    # It's just that this signature isn't defined statically.
@struct
class Entity:
    name: str
    value: int

reveal_type(Entity.__init__)  # (self: Entity, *, name: str, value: int) -> None

valid_entity = Entity(name = "entity", value=42)  # fine
valid_entity.name = ""                          # error: "Entity" is frozen

dataclass_transform() используется для обозначения преобразователей классов данных (тех, поведение которых аналогично встроенному dataclasses.dataclass). Он принимает ряд аргументов-ключевых слов, в которых:

  • frozen_default = True означает, что класс, украшенный @struct, будет заморожен «по умолчанию».
  • kw_only_default = True означает, что сгенерированный конструктор будет иметь только ключевые аргументы (кроме self) «по умолчанию».

«По умолчанию» означает, что, если иное не указано в аргументах frozen/kw_only декоратора @struct, класс, оформленный @struct, будет вести себя соответствующим образом. Однако, поскольку сам struct не принимает таких аргументов, «по умолчанию» здесь то же самое, что и «всегда».

Также спасибо за упоминание решения до версии 3.11 (typing_extensions). В другом посте похоже, об этом не упоминается. Это полезно, поскольку я также работаю в других средах, которые еще не поддерживают Python 3.11.

vlin 28.07.2024 01:59

Другие вопросы по теме