为什么在SQLAlchemy或Pydantic等流行的包中__init__之外定义属性?



我正在修改一个应用程序,试图使用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, Django和其他)在一定程度上使用了这种模式。

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方法的类。在实践中,这意味着类可以控制如何访问和存储数据。

例如,这个赋值现在将使用来自Column: 的__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)

相关内容

  • 没有找到相关文章

最新更新