场景
假设我想为具有特定参数的
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()
一个装饰器工厂,并在一个仅转发到数据类函数的闭包函数上使用
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)
现在,这可以在运行时运行,但 Pylance 仍然报告与尝试 1 中相同的错误:
dataclasses.dataclass
我还发现使用普通
()
函数本身(没有
)在所有 3 次尝试中都存在完全相同的问题。| ||有什么方法可以让它工作而不扰乱 IntelliSense?
可选后续: 为什么尝试 2 在运行时失败?
很不幸,你無法用你想要的方式來縮短數據類別裝飾器,同時又保留 Pylance 的完整類型提示支援。這是因為 Pylance(和其他類型檢查器)仰賴數據類別裝飾器的存在來辨識和分析數據類別。當你嘗試透過別名或包裝函數來抽象化裝飾器時,類型檢查器無法再直接推斷出它是個數據類別。
為什麼你的嘗試失敗了:
- 嘗試 1:直接賦值
Pylance(和大多數類型檢查器)不分析賦值來推斷類型。它們只看到你將
dataclasses.dataclass
的
結果
(一個裝飾器函數)賦值給
struct
,但無法辨識
struct
本身就是個數據類別裝飾器。
- 嘗試 2:包裝
這個嘗試在執行階段失敗,是因為
functools.update_wrapper
嘗試從被包裝函數(在此例中為由
dataclass
返回的類別)複製屬性到包裝函數(你的
struct
函數)。然而,數據類別修改了類別的
__dict__
屬性,使其變成一個不可變的映射代理物件,導致
update
方法發生
AttributeError
。
- 嘗試 3:包裝函數工廠
雖然這個方法在執行階段有效,因為它實際上只是呼叫了
dataclasses.dataclass
裝飾器,但 Pylance 仍然無法辨識它。這是因為類型檢查器通常不深入分析包裝函數的內部來確定其行為。
可能的解決方案:
- 使用 Pylance 的類型註記:
你可以使用 Pylance 的類型註記來明確告訴類型檢查器
struct
返回一個數據類別。這會改善類型提示,但仍然需要你使用函數呼叫語法:
```python
from typing import TypeVar
from dataclasses import dataclass
T = TypeVar('T')
def struct() -> callable[[type[T]], type[T]]: # 註記返回類型 return dataclass(frozen=True, kw_only=True)
@struct() # 注意仍然需要括號 class Entity: name: str value: int
# 現在 Pylance 應該能夠正確推斷類型 valid_entity = Entity(name="entity", value=42) ```
- 接受冗餘:
不幸的是,為了獲得最佳的類型檢查支援,你可能需要接受在定義數據類別時重複使用完整的
@dataclasses.dataclass(frozen=True, kw_only=True)
裝飾器。
- 為 Pylance 建議功能:
你可以考慮向 Pylance 團隊提交功能請求,要求他們增強對數據類別裝飾器別名的支援。
關於為什麼嘗試 2 失敗的補充說明:
functools.update_wrapper
的目的是將包裝函數偽裝成被包裝函數,以便在自省時看起來像原始函數。它會複製像
__name__
、
__doc__
和
__annotations__
這樣的屬性。然而,數據類別透過將
__dict__
替換為映射代理物件來修改類別,而映射代理物件是不可變的。這就是為什麼
update_wrapper
在嘗試更新
__dict__
時會失敗的原因。