Предположим, я хочу создать псевдоним для декоратора 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.
Моим первым инстинктом было воспользоваться тем фактом, что функции являются первоклассными гражданами, и просто присвоить созданной функции-декоратору собственное имя. Это работает во время выполнения, но 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)
Затем я подумал, что, возможно, некоторая информация каким-то образом потеряется, если я просто назначу другое имя (хотя я не понимаю, почему это так), поэтому я решил обернуть ее с помощью 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'
Затем я попытался создать 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 не удалась во время выполнения?
Я только что нашел потенциальный дубликат цели (которую я не смог найти, потому что у нее не было тега python-typing ): Как сделать декоратор класса дружественным для pylance?. Однако единственный ответ там довольно раздут. Если вы считаете, что этот ответ лучше моего, я проголосую за закрытие этого вопроса как дубликата.
@InSync Мне тоже не удалось найти этот пост, но теперь, когда я его вижу, кажется, что мой вопрос действительно почти точный дубликат, ха-ха. Ваш ответ здесь очень полезен и по существу, и я думаю, что предпочитаю его. Спасибо!
Украсьте 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.
Что касается № 2: объекты класса (экземпляры
builtins.type
)__dict__
- это не экземплярbuiltins.dict
, а экземплярtypes.MappingProxyType
(прокси-объект для сопоставления, расположенного где-то в памяти, к которому вы не должны иметь прямой доступ). Вы не можете обновить его, используя методыdict
(которыеfunctools.update_wrapper
делают внутри), но вы можете обновить их, используяsetattr
.