# Django models and its inheritance structure ## 閱讀須知 1. 這裡python都是指python3 2. 知道 django models 基本用法 3. 了解 python class到底是什麼,它是怎麼產生的,以及metaclass在其中扮演什麼角色。Stackoverflow的[這篇回答](https://stackoverflow.com/questions/100003/what-are-metaclasses-in-python)寫得超棒。 ## 內文 ### 問題 上課時遇到一個奇怪的現象,當我們跑以下案例時,`MyModel.objects`會是內建的`models.Manager`的instance,而非`CustomManager`的instance。(但可以叫到`hehehaha`) ```python= class CustomManager(models.Manager): def custom_method(self): return self.filter(some_field=True) class BaseModelWithCustomManager: objects = CustomManager() def hehehaha(): print("this is a test") class MyModel(BaseModelWithCustomManager, models.Model): name = models.CharField(max_length=255) some_field = models.BooleanField(default=False) example = "for demonstration purpose" def meow(): print("meow") ``` 這很不符合一般對python OOP 繼承的理解,因為一般來說,當子層沒有某attribute或是method時,會先從第一個親層開始找。而`MyModel`既然沒有自己定義`objects`,難道不是應該先從`BaseModelWithCustomManager`開始找,然後找到為`CustomManager`Instance的`objects`嗎?假設沒有繼承到的話,為什麼`MyModel`又能叫到`hehehaha`呢?還是電腦壞掉了? **先講結論,`MyModel`在編譯的過程中被塞入了一個`objects`,而這個`objects`及為`model.Manager`的Instance。在自己有`objects`的情況下,它就不會往上找了。** 接續章節會順著`objects`被塞入的過程解釋。 ### Metaclass 首先我們要先了解,python 的 class 本身也是一個object,會在interpret的過程中被metaclass 生成。換句話說,每一個class真正的樣貌不一定是我們寫得那樣,一個class確切有什麼attribute, methods,這些methods 或attributes到底長怎樣,都是由他的metaclass決定的。 在沒有特別定義的情況下,任何class的metaclass會是`type`。這裡`type`不是類別,而是有一個metaclass名叫`type`。 換句話說,interpreter跑到以下code時 ```python= class Example: pass ``` 它實際上是用python跑這個東西 ```python= Example = type('Example', (), {}) ``` 這裡Example就是我們習以為常的class,可以直接用來實體化或操作任何我們習慣的行為。 另外,type 的參數為class_name, bases, attrs。所以我們其實可以把class寫成以下: ```python= #範例一 Example = type('Example', (), {}) eg1 = Example() #eg1 是 Example class 實體化出來的物件 #範例二 Example2 = type('Example2', (), {"name": "will"}) #有 name 這個 class attribute eg2 = Example2() eg2.name #印出 will #範例三 Example3 = type('Example3', (Example2, ), {}) #繼承Example2 eg3 = Example3() eg3.name #印出 will ``` 相當然而,既然有生成的方法(metaclass),那就可以客製化生成的方法(customized metaclass)。而django models 就是有寫自己的metaclass,而這個metaclass 在生成我們的`MyModel`時,塞入了一個名叫`objects`的class attribute。 決定某個class的metaclass的方法也很簡單,只要給一個keyword argument `metaclass`就好了。 ```python= class Example(Parent1, metaclass=custom_metaclass): pass #Python interpreter在生成 Example 前,會先去找他是否有metaclass, #然後再根據這個metaclass 的__new__或是__init__來決定怎麼處理 #Example的 attributes, methods 以及 #Example和 Parent1 的關係 ``` 那假如class 本身沒有定義metaclass,但他的親層有呢? ```python= class Parent1(metaclass=custom_metaclass): pass class Example(Parent1): pass # python interpreter 在生成Example 時,假如自已沒有metaclass # 會去看他的上層中是否有定義metaclass # 有的話就會用那個,沒有的話就用原生的 type # 所以這個範例的Example, 就會被custom_metaclass生成 # 也可以說,Example 是 custom_metaclass的一個instance ``` 關於metaclass 更多的說明請看閱讀須知第三項的連結,它寫得更簡單,更好懂,也更細。此文只寫到這裡,是希望讀完上述內容,便能理解Django model的繼承到底在幹嘛。 ### Django Model生成的實際流程 *除去案例,以下其他code都是django model原始碼節錄過來的。* > 這裡只會擷取解釋所需片段,建議有興趣的自己去爬[source code](https://github.com/django/django)。可以專注在`django/db/models/sql/base.py` 中的`ModelBase`。 回到我們一開始的案例: ```python= class CustomManager(models.Manager): def custom_method(self): return self.filter(some_field=True) class BaseModelWithCustomManager: objects = CustomManager() def hehehaha(): print("this is a test") class MyModel(BaseModelWithCustomManager, models.Model): name = models.CharField(max_length=255) some_field = models.BooleanField(default=False) example = "for demonstration purpose" def meow(): print("meow") ``` 當我們跑到`MyModel`時,python interpreter開始去找是否有一個metaclass。結果還真的有,`models.Model`定義了`metaclass=ModelBase`。 ![Screenshot 2024-08-13 at 5.05.30 PM](https://hackmd.io/_uploads/SJJgGsO5A.png) 而`ModelBase`本身長這樣 ![Screenshot 2024-08-13 at 5.27.57 PM](https://hackmd.io/_uploads/rkg4wid5R.png) `ModelBase`這個metaclass在生成一個新的class時,會把某些特定的attributes拔出來特別對待。 例如,我們寫在`MyModel`中的`name = models.CharField(max_length=255)`和 `some_field = models.BooleanField(default=False)`會被轉移成所需資料型態(string 和 boolean),而不真的是`CharField`和`BooleanField`的instance。 下圖是`ModelBase`在分類attributes的過程。範例中的`name`和`some_field`會被丟到`contributable_attrs`裡。而`example`和`meow`則會被丟到`new_attrs`裡 ![Screenshot 2024-08-13 at 5.14.14 PM](https://hackmd.io/_uploads/Byfd4sd5C.png) 接就會實體化出一個目標的`new_class` ![Screenshot 2024-08-13 at 5.20.16 PM](https://hackmd.io/_uploads/SJxOBjdqC.png) 這裡的`super_new`就會直接叫上`type.__new__`,然後用上一個章節提過的方法,`type(clsname, bases, attrs)`,生成一個class。 (沒錯,ModelBase這個metaclass繼承自type這個原生metaclass。metaclass type 基本上是所有 metaclass的root。) 看到這裡,你可能會疑惑,super_new中不是有塞入`bases`嗎?那不就包含了所有寫進去的親層,也就是包含我們自己定義的`BaseModelWithCustomManager`嗎? 是的,所以`MyModel`依舊會被當作`BaseModelWithCustomManager`的subclass,他們的確有繼承關係。這也是為什麼 `MyModel`可以呼叫到`BaseModelWithCustomManager`中定義的methods或是其他一般attributes。 但是接著`ModelBase`就會檢查`contributable_attrs`內是否包含所需資料,其中就會檢查有沒有`objects`。而`MyModel`本身是沒有`objects`的。`ModelBase`這時候就會好心的塞一個預設的`objects` 給`MyModel`。 ![Screenshot 2024-08-13 at 5.54.34 PM](https://hackmd.io/_uploads/BkpP6sOqA.png) 所以這時候`MyModel`就會有自己的`objects`了,而且是`models.Manager`的instance,之後當然就不會去找親層的`objects`了。好奇的話可以開`MyModel.__dict__`看看。 ### 正確寫法 所以假設我們想要自己設定的manager,我們一定得寫到`MyModel`裡 ```python= class CustomManager(models.Manager): def custom_method(self): return self.filter(some_field=True) class BaseModelWithCustomManager: def hehehaha(): print("this is a test") class MyModel(BaseModelWithCustomManager, models.Model): name = models.CharField(max_length=255) some_field = models.BooleanField(default=False) objects = CustomManager() # 寫在這裡 example = "for demonstration purpose" def meow(): print("meow") ``` 那假如我們還是想要拆開各個常用的零組件,讓這些零組件不用被重複書寫,符合OOP的精神呢? ```python= class CustomManager(models.Manager): def custom_method(self): return self.filter(some_field=True) # 繼承自 models.Model class BaseModelWithCustomManager(models.Model): objects = CustomManager() # 寫在這裡 def hehehaha(): print("this is a test") class MyModel(BaseModelWithCustomManager): name = models.CharField(max_length=255) some_field = models.BooleanField(default=False) example = "for demonstration purpose" def meow(): print("meow") # 直接讓 BaseModelWithCustomManager 是 ModelBase 生出來的東西 # 這樣 ModelBase 之後在生 MyModel 時就不會以為 objects 不存在了 ``` 但這樣寫會有個問題,就是migrate時除了`MyModel`外,還會生出`BaseModelWithCustomManager`這個table。這時候可以參考django官方寫法。 ```python= class CustomManager(models.Manager): def custom_method(self): return self.filter(some_field=True) class BaseModelWithCustomManager(models.Model): objects = CustomManager() def hehehaha(): print("this is a test") # 加這行 class Meta: abstract = True class MyModel(BaseModelWithCustomManager): name = models.CharField(max_length=255) some_field = models.BooleanField(default=False) example = "for demonstration purpose" def meow(): print("meow") ``` 這樣就可以達成我們期待的目標了。 ## 其他 Model的分拆方法不止設定`abstract = True` 而已。有興趣的可以看[這裡](https://docs.djangoproject.com/en/5.0/topics/db/models/)。 上面遇到的問題,假如在上層定義`Fields`一樣可能會遇到。Django 上針對這點有詳細的[討論](https://docs.djangoproject.com/en/5.0/howto/custom-model-fields/#background-theory)。 另外,Django model其實可以做超多客製化的東西,不論是`Fields`、`Manager`、`Query`都行。有興趣可以參考[這裡](https://docs.djangoproject.com/en/5.0/topics/db/models/)。 最後感謝django forum 裡大大的熱心協助與方向指引。 https://forum.djangoproject.com/t/custom-manager-not-working-through-inheritance/33793