我正在开发一个类,用户应该能够以最方便的方式设置其字段,其中包括将字符串分配给任何字段。用户分配的值应自动转换为实际数据类型(例如,分配给字段
"2022-01-02"
的 date
应转换为 datetime.date
对象)。
为此,我选择了 Python 数据类模块的“描述符类型字段”方法。为了避免不必要和/或不受支持的转换,我检查 __annotations__
以确定是否可以在不进行转换的情况下分配用户提供的值。
from typing import Optional
import datetime
from dataclasses import dataclass
from datetime import date
from decimal import Decimal
class Conversion:
def __init__(self, *, conv, default=None):
self._conv = conv
self._default = default
self._name = None
self._prop = None
def __set_name__(self, owner, name):
self._prop = name
self._name = "_" + name
def __get__(self, obj, tp):
# dataclasses determines default value by calling
# descriptor.__get__(obj=None, tp=cls)
if obj is None:
return self._default
return getattr(obj, self._name, self._default)
def __set__(self, obj, value):
tp = obj.__annotations__.get(self._prop)
# Don't convert values which already match desired type
if tp and isinstance(value, tp):
setattr(obj, self._name, value)
else:
try:
val = self._conv(value)
except:
raise ValueError(
f"Conversion error for '{self._name.lstrip('_')}': {value}"
)
setattr(obj, self._name, val)
@dataclass
class Entry:
date: datetime.date = Conversion(conv=date.fromisoformat, default=date.today())
amount: Optional[Decimal] = Conversion(conv=Decimal, default=None)
e = Entry()
print(e)
e.date = "2022-02-05"
e.amount = "11.02"
print(e)
输出如预期:
Entry(date=datetime.date(2024, 3, 7), amount=None)
Entry(date=datetime.date(2022, 2, 5), amount=Decimal('11.02'))
这工作得很漂亮,我觉得这是非常干净和优雅的解决方案,但我注意到文档
总是用描述符的类型而不是底层数据类型来注释描述符类型的字段。对我来说,这将是例如date: Conversion = Conversion(...)
。数据类作者选择这样做有什么原因吗?我用数据类型注释字段是错误的吗?
date
的默认类型是
Conversion
,即注释Conversion
是正确的。描述符是一个概念(或者更确切地说是协议的实现),而不是一种独特的对象类型。像 mypy 这样的 linter 会抱怨你的类型注释。话又说回来,类型注释纯粹是可选的,而不是强制的,所以你可以在这里做你想做的事。就我个人而言,我会避免使用一个非常不具体的名称“Conversion”的描述符,该描述符对不同的数据字段执行不同的操作。特别是当底层数据类型不同时。