我正在修改一个应用程序,试图使用Pydantic为我的应用程序模型和SQLAlchemy为我的数据库模型。
我有现有的类,我在__init__
方法中定义了属性,因为我被教导要这样做:
class Measure:
def __init__(
self,
t_received: int,
mac_address: str,
data: pd.DataFrame,
battery_V: float = 0
):
self.t_received = t_received
self.mac_address = mac_address
self.data = data
self.battery_V = battery_V
在Pydantic和SQLAlchemy中,按照文档,我必须在__init__
方法之外定义这些属性,例如在Pydantic中:
import pydantic
class Measure(pydantic.BaseModel):
t_received: int
mac_address: str
data: pd.DataFrame
battery_V: float
为什么会这样?这不是不好的做法吗?是否对该类的其他方法(classmethods, staticmethods, properties…)有任何影响?
注意,这也是非常不方便的,因为当我实例化该类的对象时,我没有得到构造函数期望的参数的建议!
直接在类名称空间中定义类的属性是完全可以接受的,并且对于您提到的包本身并不特殊。由于类命名空间本质上是该类实例的蓝图,因此在那里定义属性实际上是有用的,例如,当你想以一致的方式在一个地方提供所有带有类型注释的公共属性时。
还要考虑公共属性不一定需要由类的构造函数中的参数反映。例如,这是完全合理的:
class Foo:
a: list[int]
b: str
def __init__(self, b: str) -> None:
self.a = []
self.b = b
换句话说,仅仅因为某些东西是公共属性,并不意味着它必须在初始化时由用户提供。更不用说保护/私有属性了。
Pydantic的特殊之处在于(以您的例子为例),BaseModel
的元类以及类本身对类名称空间中定义的属性做了大量的神奇操作。Pydantic将模型的典型属性称为"字段"。有一点神奇之处允许在初始化期间根据类名称空间中定义的那些字段进行特殊检查。例如,构造函数必须接收与所定义的非可选字段对应的关键字参数。
from pydantic import BaseModel
class MyModel(BaseModel):
field_a: str
field_b: int = 1
obj = MyModel(
field_a="spam", # required
field_b=2, # optional
field_c=3.14, # unexpected/ignored
)
如果在构造MyModel
实例时省略field_a
,将会引发错误。同样地,如果我试图传递field_b="eggs"
,将会引发一个错误。
所以你不写自己的__init__
方法是一个特性Pydantic为您提供。您只需定义字段,然后"神奇地"创建一个合适的构造函数。已经给你了。
至于您提到的缺点,您没有得到任何自动建议,这在默认情况下对所有ide都是正确的。静态类型检查器不能理解动态构造函数,只能推断期望的参数。目前这是通过扩展来解决的,比如mypy
插件和PyCharm插件。也许很快就会出现PEP 681中的@dataclass_transform
装饰器将对类似的包进行标准化,从而改进静态类型检查器的支持。
同样值得注意的是,即使是标准库的dataclasses
也只能通过类型检查器中的特殊扩展来工作。
对于你的另一个问题,这类的方法显然有一些影响(通过设计),尽管细节并不总是很明显。当然,您不应该简单地编写自己的__init__
方法,而不小心在其中正确调用超类__init__
。此外,@property
-setter目前并不像你期望的那样工作(尽管在Pydantic模型上使用属性是否有意义是有争议的)。
Pydantic有它自己的(重写)魔法,但是SQLalchemy更容易解释。
SA模型是这样的:
>>> from sqlalchemy import Column, Integer, String
>>> class User(Base):
...
... id = Column(Integer, primary_key=True)
... name = Column(String)
Column、Integer和String为descriptors
。描述符是覆盖get和set方法的类。在实践中,这意味着类可以控制如何访问和存储数据。
__set__
方法class User(Base):
id = Column(Integer, primary_key=True)
name = Column(String)
user = User()
user.name = 'John'
这与user.name.__set__('John')
相同,但是,由于MRO,它在Column
中找到了一个set方法,因此使用该方法。在简化版本中,Column看起来像这样:
class Column:
def __init__(self, field=""):
self.field= field
def __get__(self, obj, type):
return obj.__dict__.get(self.field)
def __set__(self, obj, val):
if validate_field(val)
obj.__dict__[self.field] = val
else:
print('not a valid value')
(这类似于使用@property。描述符是一个可重用的@property)