python

Django项目启动——源码阅读(二) - 知乎

文章暂存

systemime
2021-03-25
16 min

摘要.

前言
这段时间准备开始阅读和熟悉一下 django 的源码,达到更好地了解 django 项目,方便未来开发 django 项目的目的。这是第一部分的阅读,阅读一些 django 项目启动时候相关的源码,针对 django 版本号 2.2.5。

本次专题——django 项目启动
我们都知道,启动一个 django 工程用的是 python manage.py runserver 命令,所以 manage.py 文件无疑就是启动 django 项目的入口文件,这里我将通过从入口文件出发,一步一步阅读跟 django 项目启动相关的源码,看看在这个过程中都做了些什么,同时给出我自己的解释。

本篇是第二部分,继续上一篇的内容,上一篇链接:

Normal WLS:Django 项目启动——源码阅读(一)​zhuanlan.zhihu.com

注:源代码中两个井号 "##" 表示我自己的注解,是我认为需要重点分析或者是包含代码核心逻辑的地方,因为包含较多代码,所以建议在电脑端阅读。

源代码 & 分析

上一篇讲到了对于项目启动的 runserver 命令,真正关键的就是 django.setup() 函数,所以这里就从这个函数开始看起,来了解项目启动剩下的内容。

  • django.setup() 函数:django 项目启动时真正执行的东西,其实也不复杂。
## django package: django/__init__.py


def setup(set_prefix=True):
    """
    Configure the settings (this happens as a side effect of accessing the
    first setting), configure logging and populate the app registry.
    Set the thread-local urlresolvers script prefix if `set_prefix` is True.
    """
    from django.apps import apps
    from django.conf import settings
    from django.urls import set_script_prefix
    from django.utils.log import configure_logging

    ## 配置一下日志,然后配置一下url的前缀,默认是'/',也就是我们自己在写urls.py规则的时候为什么前面不用'/',原来是项目启动的时候这里帮我们加了。
    configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)
    if set_prefix:
        set_script_prefix(
            '/' if settings.FORCE_SCRIPT_NAME is None else settings.FORCE_SCRIPT_NAME
        )
    
    ## 这一句是最关键的,对项目中的app进行配置
    apps.populate(settings.INSTALLED_APPS)
  • apps.populate() 函数:在项目启动时,对开发者写好的项目中的 app 进行配置。因为是类中的函数,用到了一些属性,所以这里把 init 函数也一起放出来。
## django package: django/apps/registry.py

## 先看看apps是什么,其实是Apps类对象是实例,实例化之后通过在该目录下__init__.py中__all__ = ['AppConfig', 'apps']暴露到全局
apps = Apps(installed_apps=None)

## 还是看类初始化函数和对应的populate函数
class Apps:
    """
    A registry that stores the configuration of installed applications.

    It also keeps track of models, e.g. to provide reverse relations.
    """

    ## 初始化函数放在这里主要是为了说明几个重要的实例变量:self.all_models, self.app_configs.
    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.")

        # Mapping of app labels => model names => model classes. Every time a
        # model is imported, ModelBase.__new__ calls apps.register_model which
        # creates an entry in all_models. All imported models are registered,
        # regardless of whether they're defined in an installed application
        # and whether the registry has been populated. Since it isn't possible
        # to reimport a module safely (it could reexecute initialization code)
        # all_models is never overridden or reset.
        self.all_models = defaultdict(OrderedDict)  ## 存项目中所有的models,是在import的时候更新,不在setup的时候

        # Mapping of labels to AppConfig instances for installed apps.
        self.app_configs = OrderedDict()	## 存各个app的配置实例

        # 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
        # For the autoreloader.
        self.ready_event = threading.Event()

        # 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):
        """
        Load application configurations and models.

        Import each application module and then each model module.

        It is thread-safe and idempotent, but not 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

            ## 这里因为self._lock用的是可重入锁,所以同一个线程是可以在配置过程中多次进入到这里的,所以通过加self.loading状态防止同一线程的多次setup
            # 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

            # Phase 1: initialize app configs and import app modules.
            ## 这里的installed_apps是settings.py传过来的我们自己写的那个列表
            for entry in installed_apps: 
                if isinstance(entry, AppConfig):
                   ## 这里是为了多一种配置方式吧,我们自己写的entry基本都是字符串,进入else分支
                    app_config = entry 
                else:
                  	## 这里创建一个app配置类实例,代码在下面,也就现在在针对每一个app进行配置。这里最好先到后面看看每一个AppConfig对象里面属性的含义。
                    app_config = AppConfig.create(entry) 
                ## 配置完之后检查是否有重复的app配置实例的label
                if app_config.label in self.app_configs:
                    raise ImproperlyConfigured(
                        "Application labels aren't unique, "
                        "duplicates: %s" % app_config.label)
		## 把每一个app的配置实例添加到self.app_configs中
                self.app_configs[app_config.label] = app_config
                ## 反过来,每个配置实例中有个一个apps属性,配置为self。可以理解为apps是app_config的管理器,两边相互记录,是一个一对多的关系。
                app_config.apps = self

            # Check for duplicate app names.
            ## 这里确保配置类对象的名字不会重复
            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))

            self.apps_ready = True

            # Phase 2: import models modules.
            ## 将每个模块所拥有的models分配到每个模块的app配置实例中,对应函数代码在下面
            for app_config in self.app_configs.values():
                app_config.import_models()

            self.clear_cache()

            self.models_ready = True

            # Phase 3: run ready() methods of app configs.
            ## 跑每一个app_config的ready函数,对应函数代码和分析在下面(重点)
            for app_config in self.get_app_configs():
                app_config.ready()

            self.ready = True
            self.ready_event.set()
  • 上面代码中,有项目启动时候初始化 django 的各个 app 的代码,而各个 app 代码中存在针对各个 app 的配置,这些是由 AppConfig 类来做处理的,所以可以看到上面的代码中一直有调用http://app_config.xxx()函数,下面是三个比较主要的函数,为了更好地说明这个类,下面的代码中也把 init 函数放出来了:

  • AppConfig.create() 函数:所以,这个函数其实就是将我们自己生成的 app 的配置类 xxxConfig 进行实例化并返回,只是中间经过一系列路径解析和尝试 import_module 所以代码才这么长。

  • AppConfig.import_models() 函数:这里可以看到,import_models 其实是从总的 apps 管理器对象那里去取自己对应的 models 存起来。这是因为如 Apps 类中所注释的:Every time a model is imported, ModelBase.__new__ calls apps.register_model which creates an entry in all_models. 总的 apps 管理器会在 import 的时候注册所有的 model,所以在注册某一个 app 配置实例 (app_config_) 的时候,反而是 app 配置实例去 apps 管理器那里拿属于自己的 models 进行属性赋值。

  • AppConfig.ready() 函数:我们可以看到,ready 函数是空的,是为了给开发者在需要在启动项目时候做一些一次性的事情留了一个接口,只需要在 apps.py 中重写 ready 函数就可以了,而且确实会在启动过程中执行。 这个真的是看源码看出来的彩蛋了~

## django package: django/apps/config.py

class AppConfig:
    """Class representing a Django application and its configuration."""

    ## 各个属性的注释可以说很详细了,主要看例子!注意label和name的不同,同时两者都要求不能重复
    def __init__(self, app_name, app_module):
        # Full Python path to the application e.g. 'django.contrib.admin'.
        self.name = app_name

        # Root module for the application e.g. <module 'django.contrib.admin'
        # from 'django/contrib/admin/__init__.py'>.
        self.module = app_module

        # Reference to the Apps registry that holds this AppConfig. Set by the
        # registry when it registers the AppConfig instance.
        self.apps = None

        # The following attributes could be defined at the class level in a
        # subclass, hence the test-and-set pattern.

        # Last component of the Python path to the application e.g. 'admin'.
        # This value must be unique across a Django project.
        if not hasattr(self, 'label'):
            self.label = app_name.rpartition(".")[2]

        # Human-readable name for the application e.g. "Admin".
        if not hasattr(self, 'verbose_name'):
            self.verbose_name = self.label.title()

        # Filesystem path to the application directory e.g.
        # '/path/to/django/contrib/admin'.
        if not hasattr(self, 'path'):
            self.path = self._path_from_module(app_module)

        # Module containing models e.g. <module 'django.contrib.admin.models'
        # from 'django/contrib/admin/models.py'>. Set by import_models().
        # None if the application doesn't have a models module.
        self.models_module = None

        # Mapping of lowercase model names to model classes. Initially set to
        # None to prevent accidental access before import_models() runs.
        self.models = None

    ## 这里用类方法做实例化,其实这里才是真正的实例化逻辑,上面的init函数只是声明了一些属性
    @classmethod
    def create(cls, entry):
        """
        Factory that creates an app config from an entry in INSTALLED_APPS.
        """
        try:
            ## 这里导入对应的app module
            # If import_module succeeds, entry is a path to an app module,
            # which may specify an app config class with default_app_config.
            # Otherwise, entry is a path to an app config class or an error.
            module = import_module(entry)

        except ImportError:
            # Track that importing as an app module failed. If importing as an
            # app config class fails too, we'll trigger the ImportError again.
            module = None

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

            # Raise the original exception when entry cannot be a path to an
            # app config class.
            if not mod_path:
                raise

        else:
            try:
              	## 这里的entry是每个django app中对应的配置类,对应于apps.py
                # If this works, the app module specifies an app config class.
                entry = module.default_app_config
            except AttributeError:
                # Otherwise, it simply uses the default app config class.
                return cls(entry, module)
            else:
                mod_path, _, cls_name = entry.rpartition('.')

        # If we're reaching this point, we must attempt to load the app config
        # class located at <mod_path>.<cls_name>
        mod = import_module(mod_path)
        try:
            ## 这里拿到我们项目中某个app下apps.py中的那个xxxConfig类,继承自AppConfig
            cls = getattr(mod, cls_name)
        except AttributeError:
            if module is None:
                # If importing as an app module failed, check if the module
                # contains any valid AppConfigs and show them as choices.
                # Otherwise, that error probably contains the most informative
                # traceback, so trigger it again.
                candidates = sorted(
                    repr(name) for name, candidate in mod.__dict__.items()
                    if isinstance(candidate, type) and
                    issubclass(candidate, AppConfig) and
                    candidate is not AppConfig
                )
                if candidates:
                    raise ImproperlyConfigured(
                        "'%s' does not contain a class '%s'. Choices are: %s."
                        % (mod_path, cls_name, ', '.join(candidates))
                    )
                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的时候指定的那个名字,会自动填到xxxConfig的name属性中
            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,
                )
            )

        # Entry is a path to an app config class.
        ## 真正实例化这个类,这里的cls其实就是xxxConfig
        return cls(app_name, app_module)
      
    ## 这里可以看到,import_models其实是从总的apps管理器对象那里去取自己对应的models存起来。这是因为如Apps类中所注释的:Every time a model is imported, ModelBase.__new__ calls apps.register_model which creates an entry in all_models. 总的apps管理器会在import的时候注册所有的model,所以在注册某一个app配置实例的时候,反而是app配置实例去apps管理器那里拿属于自己的models进行属性赋值。
    def import_models(self):
        # Dictionary of models for this app, primarily maintained in the
        # 'all_models' attribute of the Apps this AppConfig is attached to.
        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)
    
    ## 注释很清晰了,用户可通过重写自定义项目启动时app的行为       
    def ready(self):
        """
        Override this method in subclasses to run code when Django starts.
        """

总结

到这里,django 项目启动部分配置相关的源码就算是分析完了,感觉还是很有收获的。

  1. 首先,我们了解了 django 项目的入口程序其实就是 manage.py 通过对命令行参数进行解析,然后对不同的命令进行不同的处理。
  2. 针对 runserver(django 项目启动),其实最最关键的地方就在于 django/__init__.py 中的 setup 函数,在这里先设置了 url 解析的前缀,让开发者既可以自定义,也可以在开发过程中的 urls.py 中不用添加前置'/',接下来就是对于所有 app 的相关配置。
  3. app 的相关配置有两个关键部分,一个是对单个 app 的配置类 AppConfig,一个是对所有 app 的管理类 Apps,这两个类紧密相关,是总分关系,同时相互索引。在初始化过程中,Apps 在控制流程,主要包括对所有 app 进行路径解析、名字去重、对每个 app 配置类进行初始化、赋予每个 app 配置类它们的相关 models、还有就是执行每个 app 开发者自己定义的 ready 函数。

所以,看完源码,结合开发经验,我认为要想成功启动项目,最重要的就是把 settings 里面的 installed_app 变量写对,而且添加 app 之后记得把相对的路径加到配置文件中。同时,apps.py 下的 ready 函数可以给我们一个不错的做启动的初始化的钩子,这个要记住并好好利用。

如果分析得不对,欢迎指正;如果对 django 源码有兴趣,欢迎关注投稿。 https://zhuanlan.zhihu.com/p/94679262 https://zhuanlan.zhihu.com/p/94679262

上次编辑于: 5/20/2021, 7:26:49 AM