# 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`。

而`ModelBase`本身長這樣

`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`裡

接就會實體化出一個目標的`new_class`

這裡的`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`。

所以這時候`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