淺析 DjangoModel 設計禪道

cA7dEm0n發表於2019-08-16

在閱讀原始碼前一直有個疑問,我一直好奇DjangoModel是如何將欄位轉換成屬性? ... 帶著這樣的疑問,我開始閱讀Django框架Model部分原始碼。

在通過簡單的閱讀剖析後,愈發感受到Django框架設計的精妙與優美。

與往常一樣,剖析前先尋找一個適合的切入點,開啟VScode,在引入model那一行程式碼按下command + b..

我看到這一行陌生的寫法metaclass=ModelBase

# 版本: Django 2.2.2
class Model(metaclass=ModelBase):

在看到這麼一個陌生寫法後,我決定對ModelBase類,進行分析。

ModelBase繼承了type,實現了5個方法

def __new__(cls, name, bases, attrs, **kwargs):
    ...

def  add_to_class(cls, name, value):
    ...

def  _prepare(cls):
    ...

def  _base_manager(cls):
    ...

def  _default_manager(cls):
    ...

Python原類

這五個方法中,重點在__new__這個方法,而且能夠實現欄位生成屬性的功能也來源於此。

隨著查閱資料,瞭解到一個之前沒接觸到的?姿勢點:原類

那什麼是原類?

A metaclass is the class of a class.

原類是類的類.

選取stackoverflow高贊

原來在Python世界裡,所有的類都是由type建立生成的!而繼承type實現__new__方法,是生成類其中一種方法。omgomgomgomg.(在沒了解之前,我一直以為type只是用於判斷型別?)

這裡的new做了什麼?

回到原始碼,簡單分析一下建立都做了些什麼.

def __new__(cls, name, bases, attrs, **kwargs):
    # 繼承__new__
    super_new = super().__new__

    # 判斷子類是否是modelBase
    parents = [b for b in bases if isinstance(b, ModelBase)]
    if not parents:
        return super_new(cls, name, bases, attrs)

    # 建立__module__
    module = attrs.pop('__module__')
    new_attrs = {'__module__': module}

    # 建立classcell
    classcell = attrs.pop('__classcell__', None)
    if classcell is not None:
        new_attrs['__classcell__'] = classcell

    # 對Meta屬性的處理
    attr_meta = attrs.pop('Meta', None)
    # Pass all attrs without a (Django-specific) contribute_to_class()
    # method to type.__new__() so that they're properly initialized
    # (i.e. __set_name__()).
    contributable_attrs = {}
    for obj_name, obj in list(attrs.items()):
        if _has_contribute_to_class(obj):
            contributable_attrs[obj_name] = obj
        else:
            new_attrs[obj_name] = obj

    # 建立一個新的class
    new_class = super_new(cls, name, bases, new_attrs, **kwargs)

    # 獲取Meta裡的abstract
    abstract = getattr(attr_meta, 'abstract', False)
    meta = attr_meta or getattr(new_class, 'Meta', None)

    # 獲取_meta屬性
    base_meta = getattr(new_class, '_meta', None)

    app_label = None

    # 載入配置附加到attach裡
    app_config = apps.get_containing_app_config(module)
    # 如果meta裡面沒有app_label
    if getattr(meta, 'app_label', None) is None:
        # 如果app_config裡面也是空的
        if app_config is None:
            # 如果Meta裡的abstract也是空的
            if not abstract:
                raise RuntimeError(
                    "Model class %s.%s doesn't declare an explicit "
                    "app_label and isn't in an application in "
                    "INSTALLED_APPS." % (module, name)
                )
        else:
            # app_config不為空則覆蓋
            app_label = app_config.label

    new_class.add_to_class('_meta', Options(meta, app_label))

    ...(偷懶省略)
    new_class._prepare()

    # 新class呼叫apps.register_model進行註冊
    new_class._meta.apps.register_model(new_class._meta.app_label, new_class)
    return new_class

這一部分程式碼工作主要是: 按照一些特定的方式建立類屬性,重點 __module__Meta的處理。

Model主體

弄明白了ModelBase的工作後,再來簡單看看主體部分做了什麼

class Model(metaclass=ModelBase):
    def __init__(self, *args, **kwargs):
        cls = self.__class__
        opts = self._meta
        _setattr = setattr

        # 自我描述類
        # 實現__repr__與__str__
        _DEFERRED = DEFERRED
        pre_init.send(sender=cls, args=args, kwargs=kwargs)

        # 重寫預設__get__方法, 讀取快取
        self._state = ModelState()

        # 返回模型及其父項上所有具體欄位的列表。
        if len(args) > len(opts.concrete_fields):
            # Daft, but matches old exception sans the err msg.
            raise IndexError("Number of args exceeds number of fields")

        if not kwargs:
            # 所有欄位的迭代器
            for val, field in zip(args, fields_iter):
                # 如果val是描述類 跳過
                if val is _DEFERRED:
                    continue
                # 設定屬性
                _setattr(self, field.attname, val)
        else:  
            # 設定屬性
            fields_iter = iter(opts.fields)
            for val, field in zip(args, fields_iter):
                if val is _DEFERRED:
                    continue
                _setattr(self, field.attname, val)
                kwargs.pop(field.name, None)

        # 對屬性進行處理 
        for field in fields_iter:
            is_related_object = False
            # Virtual field
            if field.attname not in kwargs and field.column is None:
                continue

            if kwargs:                
                # 判斷是否為遠端連線屬性 (one to one, many to many 等型別)
                # 下面是一些具體處理
                if isinstance(field.remote_field, ForeignObjectRel):
                    try:
                        # Assume object instance was passed in.

                        # 假設傳遞了例項
                        rel_obj = kwargs.pop(field.name)
                        is_related_object = True
                    except KeyError:
                        try:
                            val = kwargs.pop(field.attname)
                        except KeyError:
                            val = field.get_default()
                    else:
                        # Object instance was passed in. Special case: You can
                        # pass in "None" for related objects if it's allowed.
                        if rel_obj is None and field.null:
                            val = None
                # 普通型別的處理
                else:
                    try:
                        val = kwargs.pop(field.attname)
                    except KeyError:
                        val = field.get_default()
            else:
                # 返回欄位預設結果
                val = field.get_default()

            # 針對例項或非例項物件進行屬性設定
            if is_related_object:
                if rel_obj is not _DEFERRED:
                    _setattr(self, field.name, rel_obj)
            else:
                if val is not _DEFERRED:
                    _setattr(self, field.attname, val)

        ...(偷懶省略)
        super().__init__()
        post_init.send(sender=cls, instance=self)

這一部分程式碼主要工作內容:遍歷屬性,對不同的屬性做不同的設定操作.

在弄明白DjangoModel是如何將欄位轉換成屬性? 問題後,對Model部分的分析就暫時告一段落了。

總結:Model將欄位生成類是由ModelBase生成類屬性,再由Model遍歷屬性,對不同屬性進行設定。 完成這樣一個功能。

最後建議: command + b一把梭,哪裡不會b哪裡。

欲目千里,更上一層

相關文章