簡體   English   中英

為什么在 SQLAlchemy 或 Pydantic 等流行包中的 __init__ 之外定義屬性?

[英]Why are attributes defined outside __init__ in popular packages like SQLAlchemy or Pydantic?

我正在修改一個應用程序,嘗試將 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

為什么會這樣? 這不是不好的做法嗎? 對那個 class 的其他方法(類方法、靜態方法、屬性...)有影響嗎?

請注意,這也很不方便,因為當我實例化 class 的 object 時,我沒有得到關於構造函數需要哪些參數的建議!

直接在 class 命名空間中定義 class 的屬性是完全可以接受的,並且對於您提到的包本身並不特殊。 由於 class 命名空間(除其他事項外)本質上是 class 實例的藍圖,因此當您想要以一致的方式在一個地方提供所有帶有類型注釋的公共屬性時,在那里定義屬性實際上很有用。

還要考慮到公共屬性不一定需要通過 class 的構造函數中的參數來反映。例如,這是完全合理的:

class Foo:
    a: list[int]
    b: str

    def __init__(self, b: str) -> None:
        self.a = []
        self.b = b

換句話說,僅僅因為某些東西是公共屬性,並不意味着它必須由用戶在初始化時提供。 更不用說受保護/私有屬性了。

Pydantic 的特別之處(以你的例子為例)是BaseModel的元類以及 class 本身對 class 命名空間中定義的屬性做了很多魔法。 Pydantic 將模型的典型屬性稱為“字段”,並且有一點魔法允許在初始化期間根據您在 class 命名空間中定義的那些字段進行特殊檢查。 例如,構造函數必須接收與您定義的非可選字段相對應的關鍵字 arguments。

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 都是如此。 Static 類型檢查器無法理解動態構造函數,只能推斷 arguments 是什么。 目前這是通過擴展解決的,例如mypy插件PyCharm 插件 也許很快來自PEP 681@dataclass_transform裝飾器將對類似的包進行標准化,從而改進 static 類型檢查器的支持。

同樣值得注意的是,即使是標准庫的dataclasses也只能通過類型檢查器中的特殊擴展來工作。

對於您的另一個問題,顯然對此類類的方法有一些影響(按設計),盡管具體情況並不總是很明顯。 你當然不應該簡單地編寫你自己的__init__方法而不注意在其中正確調用超類的__init__ 此外, @property property -setters 目前並不像您期望的那樣工作(盡管在 Pydantic 模型上使用屬性是否有意義還有待商榷)。

總而言之,這種方法不僅不是壞習慣,而且是減少樣板代碼的好主意,而且現在非常普遍,事實證明,非常流行和成熟的軟件包(如前面提到的 Pydantic,以及例如 SQLAlchemy、Django 等)在一定程度上使用了這種模式。

Pydantic 有它自己的(重寫)魔法,但 SQLalchemy 更容易解釋。

SA model 如下所示:

>>> from sqlalchemy import Column, Integer, String
>>> class User(Base):
...
...     id = Column(Integer, primary_key=True)
...     name = Column(String)

Column, Integer 和 String 是descriptors 描述符是一個 class,它覆蓋了 get 和 set 方法。 實際上,這意味着 class 可以控制數據的訪問和存儲方式。

例如,這個賦值現在將使用 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 方法,因此改用它。 在簡化版本中,列看起來像這樣:

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。Descriptor 是可重復使用的@property)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM