# Python Django 启动时做了什么

文章仅供参考,我是直接读源码然后写注释的,不一定都正确

首先了解一下 Django 项目源码大概包含的一些包:

功能描述
apps django 的 app 管理器
conf 配置信息,主要有项目模板和 app 模板等
contrib django 默认提供的标准 app
core django 核心功能
db 数据库模型实现
dispatch 信号,用于模块解耦
forms 表单实现
http http 协议和服务相关实现
middleware django 提供的标准中间件
template && templatetags 模板功能
test 单元测试的支持
urls 一些 url 的处理类
utils 工具类
views 视图相关的实现

django.core.management 模块提供脚手架的功能实现,在项目的 manage.py 中也是通过调用 management 模块来实现项目启动的

在执行 python manager.py runserver 时,会发生什么,查看 manage.py

#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
    """Run administrative tasks."""
    # 这里可以修改为你自定义的配置路径
    # 一般用于多环境配置
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'DemoDjango.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv) # 主要是调用这里
if __name__ == '__main__':
    main()

看看 execute_from_command_line 做了什么,由于代码比较多,我直接在源码里写了注释了

class ManagementUtility:
    """
    Encapsulate the logic of the django-admin and manage.py utilities.
    """
    def __init__(self, argv=None):
	# 析构 argv 的命令行参数
        self.argv = argv or sys.argv[:] # ['manage.py', 'runserver', ...]
        self.prog_name = os.path.basename(self.argv[0]) # manage.py
        if self.prog_name == "__main__.py": # 这个是判断你使用源码启动吗?
            self.prog_name = "python -m django"
        self.settings_exception = None # 保存异常信息
    def main_help_text(self, commands_only=False):
	# 输出命令的帮助信息
        ......
    def fetch_command(self, subcommand):
	# 查找 django 模块的子命令
	......
    def execute(self):
        """
        Given the command-line arguments, figure out which subcommand is being
        run, create a parser appropriate to that command, and run it.
        """
	# 执行子命令
        try:
            subcommand = self.argv[1] # 运行项目时,该参数为 runserver
        except IndexError:
            subcommand = "help"  # Display help if no arguments were given. # 索引异常,赋值为 help, 提供帮助
        # Preprocess options to extract --settings and --pythonpath.
        # These options could affect the commands that are available, so they
        # must be processed early.
        # django 封装了 argparse 库,但用法没有变化
        parser = CommandParser(
            prog=self.prog_name,
            usage="%(prog)s subcommand [options] [args]",
            add_help=False,
            allow_abbrev=False,
        )
        parser.add_argument("--settings") # 可以指定 --settings=xxx.xxx 来表示使用哪个配置文件 (在多环境下)
        parser.add_argument("--pythonpath") # 我没用过
        parser.add_argument("args", nargs="*")  # catch-all
        try:
            options, args = parser.parse_known_args(self.argv[2:])
            handle_default_options(options) # 处理一些默认配置
        except CommandError:
            pass  # Ignore any option errors at this point.
        try:
            # 这里 settings 点 INSTALLED_APP 这个属性时,会触发魔法方法
            # 可以在 django/conf/__init__.py 里找到,settings 是一个实例化对象
            settings.INSTALLED_APPS
        except ImproperlyConfigured as exc:
            # 配置不正确
            self.settings_exception = exc
        except ImportError as exc:
            # settings.INSTALLED_APPS 会触发导入配置模块的动作,说明你导入失败了
            self.settings_exception = exc
        # 这里又触发了 settings 的魔法方法__getattr__
        # configured 在 settings 里被 property 装饰为描述符,主要是判断 self._wrapped 是否不为 object ()
        # 经过上面 settings.INSTALLED_APPS 触发之后,不再是 object (), 而是 Settings 实例对象,所以条件满足
        if settings.configured:
            # Start the auto-reloading dev server even if the code is broken.
            # The hardcoded condition is a code smell but we can't rely on a
            # flag on the command class because we haven't located it yet.
            if subcommand == "runserver" and "--noreload" not in self.argv:
                try:
                    # 一般都会走到这
                    # autoreload.check_errors 是一个装饰器,传递 django.setup 并调用
                    autoreload.check_errors(django.setup)()
                except Exception:
                    # The exception will be raised later in the child process
                    # started by the autoreloader. Pretend it didn't happen by
                    # loading an empty list of applications.
                    apps.all_models = defaultdict(dict)
                    apps.app_configs = {}
                    apps.apps_ready = apps.models_ready = apps.ready = True
                    # Remove options not compatible with the built-in runserver
                    # (e.g. options for the contrib.staticfiles' runserver).
                    # Changes here require manually testing as described in
                    # #27522.
                    _parser = self.fetch_command("runserver").create_parser(
                        "django", "runserver"
                    )
                    _options, _args = _parser.parse_known_args(self.argv[2:])
                    for _arg in _args:
                        self.argv.remove(_arg)
            # In all other cases, django.setup() is required to succeed.
            else:
                django.setup()
        # 通过调用 django.setup () 完成了每个 APP 应用程序下的 apps 注册 (实例 AppConfig)
        # 再导入每个应用的 models.py 并保存到 AppConfig
        # 然后再调用每个 AppConfig 下的 ready 方法 (重载的情况下)
        self.autocomplete()
        if subcommand == "help":
            if "--commands" in args:
                sys.stdout.write(self.main_help_text(commands_only=True) + "\n")
            elif not options.args:
                sys.stdout.write(self.main_help_text() + "\n")
            else:
                self.fetch_command(options.args[0]).print_help(
                    self.prog_name, options.args[0]
                )
        # Special-cases: We want 'django-admin --version' and
        # 'django-admin --help' to work, for backwards compatibility.
        elif subcommand == "version" or self.argv[1:] == ["--version"]:
            sys.stdout.write(django.get_version() + "\n")
        elif self.argv[1:] in (["--help"], ["-h"]):
            sys.stdout.write(self.main_help_text() + "\n")
        else:
            self.fetch_command(subcommand).run_from_argv(self.argv)
def execute_from_command_line(argv=None):
    """Run a ManagementUtility."""
    utility = ManagementUtility(argv) # 项目启动参数传递
    utility.execute() # 运行项目

在 ManagementUtility 类中执行 execute 时,还用到了 django 的 settings 对象,通过 settings.xxx 时,会触发__getattr__魔法方法,在第一次时会调用 setup 来激活配置

class LazySettings(LazyObject):
    """
    A lazy proxy for either global Django settings or a custom settings object.
    The user can manually configure settings prior to using them. Otherwise,
    Django uses the settings module pointed to by DJANGO_SETTINGS_MODULE.
    """
    def _setup(self, name=None):
        """
        Load the settings module pointed to by the environment variable. This
        is used the first time settings are needed, if the user hasn't
        configured settings manually.
        """
        # 项目启动时,这里 name 传递为 INSTALLED_APPS
        # ENVIRONMENT_VARIABLE 就是 DJANGO_SETTINGS_MODULE 字符串
        # 通过 os.environ.get ("DJANGO_SETTINGS_MODULE") 获取我们一开始设置的配置文件位置
        # 假如我创建了一个文件夹保存每个环境的配置文件,DJANGO_SETTINGS_MODULE=settings.prod
        # 那么 settings_module 就是 settings.prod
        settings_module = os.environ.get(ENVIRONMENT_VARIABLE)
        if not settings_module:
            # 没有配置文件,抛异常
            desc = ("setting %s" % name) if name else "settings"
            raise ImproperlyConfigured(
                "Requested %s, but settings are not configured. "
                "You must either define the environment variable %s "
                "or call settings.configure() before accessing settings."
                % (desc, ENVIRONMENT_VARIABLE)
            )
        # 将 self._wrapped 重新赋值,下一次触发__getattr__就不会再调用_setup 了
        # 实例化了一个 Settings 对象,创建配置文件位置
        self._wrapped = Settings(settings_module)
    def __repr__(self):
        # Hardcode the class name as otherwise it yields 'Settings'.
        if self._wrapped is empty:
            return "<LazySettings [Unevaluated]>"
        return '<LazySettings "%(settings_module)s">' % {
            "settings_module": self._wrapped.SETTINGS_MODULE,
        }
    def __getattr__(self, name):
        """Return the value of a setting and cache it in self.__dict__."""
        # 在 django 启动时,执行了一段 settings.INSTALLED_APPS, 由于该类重载了__getattr__, 所以会执行该__getattr__
        # 而不会执行默认 type 的__getattr__操作
        if self._wrapped is empty:
            # empty 是一个 object () 实例对象,self._wrapped 在父类里初始化为 empty, 所以这里条件满足,执行_setup
            # name 就是 INSTALLED_APPS
            self._setup(name)
        # 第 1 次,经过_setup 调用之后,self._wrapped 不再是 empty, 而是 Settings 的实例对象
        # 通过 getattr 获取 Settings 里对应的属性
        # 第 2 步,会传递一个 configured, 去 Settings 里边找
        val = getattr(self._wrapped, name)
        # Special case some settings which require further modification.
        # This is done here for performance reasons so the modified value is cached.
        if name in {"MEDIA_URL", "STATIC_URL"} and val is not None:
            # 这里是特定配置的一些处理,一些静态文件啥的
            val = self._add_script_prefix(val)
        elif name == "SECRET_KEY" and not val:
            # 密钥不能为空
            raise ImproperlyConfigured("The SECRET_KEY setting must not be empty.")
        # 获取属性之后,会更新到自己的__dict__里边
        self.__dict__[name] = val
        return val # 返回属性值
    def __setattr__(self, name, value):
        """
        Set the value of setting. Clear all cached values if _wrapped changes
        (@override_settings does this) or clear single values when set.
        """
        if name == "_wrapped":
            self.__dict__.clear()
        else:
            self.__dict__.pop(name, None)
        super().__setattr__(name, value)
    def __delattr__(self, name):
        """Delete a setting and clear it from cache if needed."""
        super().__delattr__(name)
        self.__dict__.pop(name, None)
    def configure(self, default_settings=global_settings, **options):
        """
        Called to manually configure the settings. The 'default_settings'
        parameter sets where to retrieve any unspecified values from (its
        argument must support attribute access (__getattr__)).
        """
        if self._wrapped is not empty:
            raise RuntimeError("Settings already configured.")
        holder = UserSettingsHolder(default_settings)
        for name, value in options.items():
            if not name.isupper():
                raise TypeError("Setting %r must be uppercase." % name)
            setattr(holder, name, value)
        self._wrapped = holder
    @staticmethod
    def _add_script_prefix(value):
        """
        Add SCRIPT_NAME prefix to relative paths.
        Useful when the app is being served at a subpath and manually prefixing
        subpath to STATIC_URL and MEDIA_URL in settings is inconvenient.
        """
        # Don't apply prefix to absolute paths and URLs.
        if value.startswith(("http://", "https://", "/")):
            return value
        from django.urls import get_script_prefix
        return "%s%s" % (get_script_prefix(), value)
    @property
    def configured(self):
        """Return True if the settings have already been configured."""
        return self._wrapped is not empty
    @property
    def USE_L10N(self):
        stack = traceback.extract_stack()
        # Show a warning if the setting is used outside of Django.
        # Stack index: -1 this line, -2 the caller.
        filename, _, _, _ = stack[-2]
        if not filename.startswith(os.path.dirname(django.__file__)):
            warnings.warn(
                USE_L10N_DEPRECATED_MSG,
                RemovedInDjango50Warning,
                stacklevel=2,
            )
        return self.__getattr__("USE_L10N")
    # RemovedInDjango50Warning.
    @property
    def _USE_L10N_INTERNAL(self):
        # Special hook to avoid checking a traceback in internal use on hot
        # paths.
        return self.__getattr__("USE_L10N")
class Settings:
    def __init__(self, settings_module):
        # update this dict from global settings (but only for ALL_CAPS settings)
        # settings_module 为你指定的配置文件路径
        # global_settings 为导入模块,dir 查看里面的所有属性,通过 for loop 遍历每个元素
        # 如果是大写字母,就通过 getattr 动态获取该模块的对应属性,再通过 setattr 动态设置 key-value 到 Settings 本身
        for setting in dir(global_settings):
            if setting.isupper():
                setattr(self, setting, getattr(global_settings, setting))
        # store the settings module in case someone later cares
        self.SETTINGS_MODULE = settings_module # 保存指定配置文件路径
        mod = importlib.import_module(self.SETTINGS_MODULE) # 动态导入你指定的配置文件
        tuple_settings = (
            "ALLOWED_HOSTS",
            "INSTALLED_APPS",
            "TEMPLATE_DIRS",
            "LOCALE_PATHS",
        )
        self._explicit_settings = set()
        # for loop 操作,遍历我们指定配置文件模块里的所有属性
        for setting in dir(mod):
            # 如果是大写字母 (我们指定配置变量时,一定要用大写)
            if setting.isupper():
                # 通过 getattr 动态获取配置变量的 value
                setting_value = getattr(mod, setting)
                if setting in tuple_settings and not isinstance(
                    setting_value, (list, tuple)
                ):
                    # 如果你的配置文件指定的一些配置变量存在 tuple_settings
                    # 比如 INSTALLED_APPS, 用来指定哪些 APP, 但你的的 value 不是 list、tuple 类型,就会抛异常
                    raise ImproperlyConfigured(
                        "The %s setting must be a list or a tuple." % setting
                    )
                # 设置你的配置变量 key-value 到 Settings 类中
                setattr(self, setting, setting_value)
                # 配置名添加到集合中,用于检测是否被覆盖,因为即使你不配置,django 也有默认配置
                self._explicit_settings.add(setting)
        if self.USE_TZ is False and not self.is_overridden("USE_TZ"):
            warnings.warn(
                "The default value of USE_TZ will change from False to True "
                "in Django 5.0. Set USE_TZ to False in your project settings "
                "if you want to keep the current default behavior.",
                category=RemovedInDjango50Warning,
            )
        if self.is_overridden("USE_DEPRECATED_PYTZ"):
            warnings.warn(USE_DEPRECATED_PYTZ_DEPRECATED_MSG, RemovedInDjango50Warning)
        if hasattr(time, "tzset") and self.TIME_ZONE:
            # When we can, attempt to validate the timezone. If we can't find
            # this file, no check happens and it's harmless.
            zoneinfo_root = Path("/usr/share/zoneinfo")
            zone_info_file = zoneinfo_root.joinpath(*self.TIME_ZONE.split("/"))
            if zoneinfo_root.exists() and not zone_info_file.exists():
                raise ValueError("Incorrect timezone setting: %s" % self.TIME_ZONE)
            # Move the time zone info into os.environ. See ticket #2315 for why
            # we don't do this unconditionally (breaks Windows).
            os.environ["TZ"] = self.TIME_ZONE
            time.tzset()
        if self.is_overridden("USE_L10N"):
            warnings.warn(USE_L10N_DEPRECATED_MSG, RemovedInDjango50Warning)
    def is_overridden(self, setting):
        return setting in self._explicit_settings
    def __repr__(self):
        return '<%(cls)s "%(settings_module)s">' % {
            "cls": self.__class__.__name__,
            "settings_module": self.SETTINGS_MODULE,
        }
# 全局 django 设置
settings = LazySettings()

以上代码是 settings 的主要逻辑,然后在 execute 中还调用了 django.setup 方法,来看看做了什么

from django.utils.version import get_version
VERSION = (4, 0, 5, "final", 0)
__version__ = get_version(VERSION)
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
    # 你可以在配置文件中指定日志配置,它会动态导入并调用
    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
        )
    # 这里比较重要,传递了 INSTALLED_APPS, 也就是你在配置文件中指定的 APP
    apps.populate(settings.INSTALLED_APPS)

apps.populate 位于 django.apps.registry.py 中 Apps 类

class Apps:
    """
    A registry that stores the configuration of installed applications.
    It also keeps track of models, e.g. to provide reverse relations.
    """
    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(dict)
        # Mapping of labels to AppConfig instances for installed apps.
        self.app_configs = {}
        # 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.
        """
        # installed_apps 就是我们在配置文件中指定的 APP
        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:
            # 加锁,在服务器上的两个并行线程可能会调用 populate
            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
            # Phase 1: initialize app configs and import app modules.
            # 初始化你的应用程序配置并导入应用程序模块
            # 这里 for 循环你指定的 APP, 先 if 判断你的每一个 APP 是否为 AppConfig 类或者它的子类,如果满足则直接使用
            # 否则就调用 AppConfig.create 创建一个 AppConfig 实例对象并使用
            # 无论 if 还是 else, 最终 app_config 都是一个 AppConfig 实例对象
            for entry in installed_apps:
                if isinstance(entry, AppConfig):
                    app_config = entry
                else:
                    # 调用 create 之后,如果成功则返回 AppConfig 子类实例化对象,也就是你指定的 APP 应用程序
                    # 下的 apps 模块下的 AppConfig 子类的实例对象,比如你在 INSTALLED_APPS 里添加了一个应用
                    # 程序叫 demo1, demo1 是你通过 django startapp 创建的一个应用程序,那么在 create 里会获取
                    # 到你 demo1.apps.Demo1AppConfig 这里路径,最后返回时,会将 Demo1AppConfig 初始化并返回
                    app_config = AppConfig.create(entry)
                if app_config.label in self.app_configs:
                    # 每次都会将 app_config 添加到集合中,这里会判断,必须是唯一的,不能重复
                    raise ImproperlyConfigured(
                        "Application labels aren't unique, "
                        "duplicates: %s" % app_config.label
                    )
                self.app_configs[app_config.label] = app_config
                app_config.apps = self # 每个 AppConfig 保存 Apps 的上下文
            # 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)
                )
            # 走到这,说明注册的 APP 应用程序都准备好了
            self.apps_ready = True
            # Phase 2: import models modules.
            # self.app_configs 保存着我们每个 APP 应用程序下 apps 模块的 AppConfig 实例对象
            # 通过遍历 values () 获取每个实例对象并调用它们的 import_models 方法
            # 通过 import_models (), 会将应用程序下的 models.py 导入并保存到 AppConfig 下的 models_module 属性
            for app_config in self.app_configs.values():
                app_config.import_models()
            self.clear_cache()
            self.models_ready = True # 表示 models 已经准备完毕
            # Phase 3: run ready() methods of app configs.
            # 如果你的 APP 应用程序下 apps.AppConfig 重载了 ready 方法,则会在这调用,相当于项目启动时的预处理
            # 如果没有重载,则什么也不会发生
            for app_config in self.get_app_configs():
                app_config.ready()
            self.ready = True # 表示一切都准备完毕
            self.ready_event.set() # flag 设置为 True, 其它 wait 它的线程将被唤醒,不再阻塞
    def check_apps_ready(self):
        """Raise an exception if all apps haven't been imported yet."""
        if not self.apps_ready:
            from django.conf import settings
            # If "not ready" is due to unconfigured settings, accessing
            # INSTALLED_APPS raises a more helpful ImproperlyConfigured
            # exception.
            settings.INSTALLED_APPS
            raise AppRegistryNotReady("Apps aren't loaded yet.")
    def check_models_ready(self):
        """Raise an exception if all models haven't been imported yet."""
        if not self.models_ready:
            raise AppRegistryNotReady("Models aren't loaded yet.")
    def get_app_configs(self):
        """Import applications and return an iterable of app configs."""
        self.check_apps_ready()
        return self.app_configs.values()