django源碼分析之a(chǎn)pp加載(app registry)

應(yīng)用程序注冊表(app registry):

當運行Django項目時航邢,Django需要做的第一件事情是查找與該項目關(guān)聯(lián)的應(yīng)用程序(apps)妄帘,以便知道該項目使用的代碼器瘪。Django使用配置文件里的INSTALLED_APPS設(shè)置來查找項目中的所有應(yīng)用程序株汉,并構(gòu)建要運行的應(yīng)用程序列表败明。Django在此上下文中將應(yīng)用程序列表稱為應(yīng)用程序注冊表app registry

比如下面一段setting.py配置文件中的代碼野宜,是剛創(chuàng)建好的項目都會在setting.py文件中默認生成的扫步。Django會循環(huán)載入這些apps和相應(yīng)的models。下面我們來分析Django是如何實現(xiàn)的這些操作速缨。

# project/setting.py

# Application definition
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

代碼分析

django.setup入手分析锌妻,看到導(dǎo)入apps模塊的代碼是from django.apps import apps,因此可以確認模塊路徑是django.apps旬牲。而且使用的是apps變量仿粹,馬上我們就能知道apps是什么東西。
它下面有3個文件原茅,__init__.py吭历,config.pyregistry.py擂橘,接下來分別來分析晌区。

# django/apps/__init__.py

from .config import AppConfig
from .registry import apps

__all__ = ['AppConfig', 'apps']

定義了__all__變量,也就是當from <module> import *通贞,只會導(dǎo)出'AppConfig'和'apps'朗若,接下來我們會發(fā)現(xiàn)它們兩分別在config.pyregistry.py文件中。

先來看下config.py文件

# django/apps/config.py

class AppConfig:
    """表示Django應(yīng)用程序及其配置的類昌罩。"""

    def __init__(self, app_name, app_module):
        # 到應(yīng)用程序的完整的Python路徑 e.g. 'django.contrib.admin'.
        self.name = app_name
        # 應(yīng)用程序的Root模塊 e.g. <module 'django.contrib.admin'
        # from 'django/contrib/admin/__init__.py'>.
        self.module = app_module
        # 對持有此AppConfig的應(yīng)用程序注冊表的引用哭懈。 當注冊AppConfig實例時由注冊表設(shè)置。
        self.apps = None

        # 以下屬性可以在子類中的類級定義茎用,因此用了test-and-set模式遣总。
        # 應(yīng)用程序的Python路徑的最后一部分, e.g. 'admin'.這個值在Django項目中必須是唯一的轨功。
        if not hasattr(self, 'label'):
            self.label = app_name.rpartition(".")[2]
        # 應(yīng)用程序的Human-readable名字 e.g. "Admin".
        if not hasattr(self, 'verbose_name'):
            self.verbose_name = self.label.title()

        # 到應(yīng)用程序目錄的文件系統(tǒng)路徑 e.g. '/path/to/django/contrib/admin'.
        if not hasattr(self, 'path'):
            self.path = self._path_from_module(app_module)

        # 包含models的模塊 e.g. <module 'django.contrib.admin.models'
        # from 'django/contrib/admin/models.py'>. 由import_models()設(shè)置. 如果沒有models旭斥,則為None
        self.models_module = None

        # 將model名稱(小寫格式)映射到model類。防止import_models()運行之前被意外訪問古涧,一開始設(shè)置為None垂券。
        self.models = None

    (此處略過部分不影響理解核心邏輯的代碼)...

    @classmethod
    def create(cls, entry):
        """
        用INSTALLED_APPS中的條目來創(chuàng)建AppConfig實例的工廠方法,
        其中條目可以是一個應(yīng)用程序模塊的路徑羡滑,也可以是一個應(yīng)用程序配置類的路徑圆米。
        """
        try:
            # 如果import_module成功,則INSTALLED_APPS中的條目是應(yīng)用程序模塊的路徑啄栓,
            # 它可能使用default_app_config指定了一個應(yīng)用程序配置類(AppConfig類)娄帖;
            # 否則,該條目可能是指向一個應(yīng)用程序配置類的路徑或者就是條錯誤的條目昙楚。
            module = import_module(entry)

        except ImportError:
            # 發(fā)現(xiàn)按應(yīng)用程序模塊導(dǎo)入失敗近速,如果按應(yīng)用程序配置類(AppConfig類)導(dǎo)入也失敗的話,就觸發(fā)ImportError。
            module = None

            mod_path, _, cls_name = entry.rpartition('.')

            # 安AppConfig類導(dǎo)入也失敗的話削葱,拋出異常奖亚,即ImportError。
            if not mod_path:
                raise

        else:
            try:
                # 按應(yīng)用程序模塊導(dǎo)入成功析砸,判斷是否有指定應(yīng)用程序配置類昔字。
                entry = module.default_app_config
            except AttributeError:
                #  沒有指定app config class, 用默認的應(yīng)用程序配置類。
                return cls(entry, module)
            else:
                mod_path, _, cls_name = entry.rpartition('.')

        # 如果我們達到了這里首繁,我們必須嘗試加載位于<mod_path>.<cls_name>的應(yīng)用程序配置類作郭。
        mod = import_module(mod_path)
        try:
            cls = getattr(mod, cls_name)
        except AttributeError:
            if module is None:
                # If importing as an app module failed, that error probably
                # contains the most informative traceback. Trigger it again.
                import_module(entry)
            else:
                raise

        # Check for obvious errors. (This check prevents duck typing, but
        # it could be removed if it became a problem in practice.)
        if not issubclass(cls, AppConfig):
            raise ImproperlyConfigured(
                "'%s' isn't a subclass of AppConfig." % entry)

        # Obtain app name here rather than in AppClass.__init__ to keep
        # all error checking for entries in INSTALLED_APPS in one place.
        try:
            app_name = cls.name
        except AttributeError:
            raise ImproperlyConfigured(
                "'%s' must supply a name attribute." % entry)

        # Ensure app_name points to a valid module.
        try:
            app_module = import_module(app_name)
        except ImportError:
            raise ImproperlyConfigured(
                "Cannot import '%s'. Check that '%s.%s.name' is correct." % (
                    app_name, mod_path, cls_name,
                )
            )

        # 條目是一個應(yīng)用程序配置類的路徑。
        return cls(app_name, app_module)

    def get_model(self, model_name, require_ready=True):
        """
        用給定的不區(qū)分大小寫的model_name返回model弦疮。
        如果沒有此名稱的model存在夹攒,請拋出LookupError。
        """
        if require_ready:
            self.apps.check_models_ready()
        else:
            self.apps.check_apps_ready()
        try:
            return self.models[model_name.lower()]
        except KeyError:
            raise LookupError(
                "App '%s' doesn't have a '%s' model." % (self.label, model_name))

    (此處略過部分不影響理解核心邏輯的代碼)...

    def import_models(self):對持有此AppConfig的應(yīng)用程序注冊表的引用
        # 此應(yīng)用程序的models字典胁塞,維護在該應(yīng)用程序的的AppConfig附屬的應(yīng)用程序注冊表(變量Apps)的'all_models'屬性中咏尝。
        self.models = self.apps.all_models[self.label]

        if module_has_submodule(self.module, MODELS_MODULE_NAME):
            models_module_name = '%s.%s' % (self.name, MODELS_MODULE_NAME)
            self.models_module = import_module(models_module_name)

    def ready(self):
        """
        Override this method in subclasses to run code when Django starts.
        """

可以看到AppConfig是應(yīng)用程序配置類,主要用來管理Django應(yīng)用程序及其配置信息啸罢,所以每個應(yīng)用都會有一個應(yīng)用程序配置類编检。它核心的方法是一個工廠類方法,它接收INSTALLED_APPS中的條目來創(chuàng)建AppConfig實例扰才,接收的條目可以是一個應(yīng)用程序模塊的路徑蒙谓,這時候創(chuàng)建的是默認的AppConfig實例;接收的條目也可以是一個應(yīng)用程序配置類的路徑训桶,這時候創(chuàng)建的就是用戶自己定義的AppConfig實例。

接著分析文件registry.py酣倾,其實我們也能猜到它就是注冊表了舵揭,維護項目所有的應(yīng)用程序配置信息。

# django/apps/registry.py

class Apps:
    """
    存儲所有應(yīng)用程序配置信息的注冊表躁锡。它還跟蹤models午绳,例如 提供反向關(guān)系。
    """

    def __init__(self, installed_apps=()):
        # installed_apps is set to None when creating the master registry
        # because it cannot be populated at that point. Other registries must
        # provide a list of installed apps and are populated immediately.
        if installed_apps is None and hasattr(sys.modules[__name__], 'apps'):
            raise RuntimeError("You must supply an installed_apps argument.")

        # model的映射表映之,app labels => model names => model classes
        self.all_models = defaultdict(OrderedDict)

        # AppConfig實例的映射表
        self.app_configs = OrderedDict()

        # Stack of app_configs. Used to store the current state in
        # set_available_apps and set_installed_apps.
        self.stored_app_configs = []

        # Whether the registry is populated.
        self.apps_ready = self.models_ready = self.ready = False

        # Lock for thread-safe population.
        self._lock = threading.RLock()
        self.loading = False

        # Maps ("app_label", "modelname") tuples to lists of functions to be
        # called when the corresponding model is ready. Used by this class's
        # `lazy_model_operation()` and `do_pending_operations()` methods.
        self._pending_operations = defaultdict(list)

        # Populate apps and models, unless it's the master registry.
        if installed_apps is not None:
            self.populate(installed_apps)

    def populate(self, installed_apps=None):
        """
        加載應(yīng)用程序配置和models拦焚。
        先導(dǎo)入每個應(yīng)用模塊,然后再導(dǎo)入每個model模塊杠输。
        它是線程安全和冪等的赎败,但不可重入(reentrant)。
        """
        if self.ready:
            return

        # populate() might be called by two threads in parallel on servers
        # that create threads before initializing the WSGI callable.
        with self._lock:
            if self.ready:
                return

            # An RLock prevents other threads from entering this section. The
            # compare and set operation below is atomic.
            if self.loading:
                # Prevent reentrant calls to avoid running AppConfig.ready()
                # methods twice.
                raise RuntimeError("populate() isn't reentrant")
            self.loading = True

            # 階段1:遍歷installed_apps蠢甲,初始化每一條條目對應(yīng)的應(yīng)用程序配置并導(dǎo)入應(yīng)用程序模塊僵刮。
            for entry in installed_apps:
                if isinstance(entry, AppConfig):
                    app_config = entry
                else:
                    app_config = AppConfig.create(entry)
                if app_config.label in self.app_configs:
                    raise ImproperlyConfigured(
                        "Application labels aren't unique, "
                        "duplicates: %s" % app_config.label)

                self.app_configs[app_config.label] = app_config
                app_config.apps = self

            # 檢查重復(fù)的應(yīng)用程序名稱。
            counts = Counter(
                app_config.name for app_config in self.app_configs.values())
            duplicates = [
                name for name, count in counts.most_common() if count > 1]
            if duplicates:
                raise ImproperlyConfigured(
                    "Application names aren't unique, "
                    "duplicates: %s" % ", ".join(duplicates))
            # 成功導(dǎo)入導(dǎo)入應(yīng)用模塊
            self.apps_ready = True

            # 階段2:導(dǎo)入models模塊。
            for app_config in self.app_configs.values():
                # 這里調(diào)用的上面分析的AppConfig的import_models方法
                app_config.import_models()

            self.clear_cache()

            self.models_ready = True

            # 階段3:運行每個應(yīng)用程序配置的ready()方法搞糕。
            for app_config in self.get_app_configs():
                # 這里調(diào)用的上面分析的AppConfig的ready方法
                app_config.ready()

            self.ready = True

    (此處略過部分不影響理解核心邏輯的代碼)...

apps = Apps(installed_apps=None)

最后的apps = Apps(installed_apps=None)勇吊,就是__init__.py文件包含的apps變量,是個注冊表Apps的實例窍仰。以及我們回去看django.setup汉规,會發(fā)現(xiàn)那里是調(diào)用apps.populate(settings.INSTALLED_APPS)方法的地方,從這時候開始循環(huán)載入apps和相應(yīng)的models驹吮。

總結(jié)

當Django由wsgi啟動或者management命令啟動時针史,會由django.setup()負責填充應(yīng)用程序注冊表。它通過以下方式配置Django:
1)加載配置文件钥屈,生成settings對象悟民;2)設(shè)置日志;3)如果set_prefix為True篷就,則將URL解析器腳本前綴設(shè)置為FORCE_SCRIPT_NAME(如果已定義)射亏,否則則為/;4)初始化應(yīng)用程序注冊表竭业。

其中應(yīng)用程序注冊表分為三個階段初始化智润。 在每個階段,Django按照INSTALLED_APPS的順序處理所有應(yīng)用程序未辆。1)首先會導(dǎo)入INSTALLED_APPS中所有應(yīng)用程序(apps)窟绷;2)嘗試導(dǎo)入每個應(yīng)用程序的models子模塊(如果有的話);3)最后運行每個應(yīng)用程序配置的ready()方法。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末咐柜,一起剝皮案震驚了整個濱河市兼蜈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拙友,老刑警劉巖为狸,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異遗契,居然都是意外死亡辐棒,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進店門牍蜂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來漾根,“玉大人,你說我怎么就攤上這事鲫竞》拢” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵从绘,是天一觀的道長菜拓。 經(jīng)常有香客問我,道長菩彬,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任妖泄,我火速辦了婚禮,結(jié)果婚禮上艘策,老公的妹妹穿的比我還像新娘蹈胡。我一直安慰自己,他們只是感情好朋蔫,可當我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布罚渐。 她就那樣靜靜地躺著,像睡著了一般驯妄。 火紅的嫁衣襯著肌膚如雪荷并。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天青扔,我揣著相機與錄音源织,去河邊找鬼。 笑死微猖,一個胖子當著我的面吹牛谈息,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播凛剥,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼侠仇,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了犁珠?” 一聲冷哼從身側(cè)響起逻炊,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎犁享,沒想到半個月后余素,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡饼疙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了慕爬。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片窑眯。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖医窿,靈堂內(nèi)的尸體忽然破棺而出磅甩,到底是詐尸還是另有隱情,我是刑警寧澤姥卢,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布卷要,位于F島的核電站渣聚,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏僧叉。R本人自食惡果不足惜奕枝,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瓶堕。 院中可真熱鬧隘道,春花似錦、人聲如沸郎笆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宛蚓。三九已至激捏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間凄吏,已是汗流浹背远舅。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留竞思,地道東北人表谊。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像盖喷,于是被迫代替她去往敵國和親爆办。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理课梳,服務(wù)發(fā)現(xiàn)距辆,斷路器,智...
    卡卡羅2017閱讀 134,657評論 18 139
  • 切換到創(chuàng)建項目的目錄 cd C:\Users\admin\Desktop\DjangoProject創(chuàng)建名為pr...
    在努力中閱讀 3,273評論 2 3
  • 版權(quán): https://github.com/haiiiiiyun/awesome-django-cn Aweso...
    若與閱讀 23,087評論 3 241
  • 全文鏈接 第一章 創(chuàng)建一個blog應(yīng)用第二章 使用高級特性來增強你的blog第三章 擴展你的blog應(yīng)用第四章上 ...
    夜夜月閱讀 6,958評論 25 27
  • 【讀經(jīng)】 撒下14章 【金句】 我們都是必死的,如同水潑在地上椭懊,不能收回诸蚕。神并不奪取人的性命,乃設(shè)法使逃亡的人不致...
    chanor閱讀 2,893評論 0 0