全职技术开发外包2023年终复盘(一)Django是如何加载settings.py配置文件的

本文是2023年技术复盘文章之一。复盘带来思考,通过思考取其精华,精进自我,提升赚钱能力。

Django是一个典型的关注点分离(Separation of Concerns)程序,即代码与配置解耦。其内部机制强制开发者在settings.py中使用官方预定义的配置。在使用django的期间,逐步了解到django是如何加载settings.py的,有些django的插件也利用这个机制将自己的配置写在settings.py中,如django-q等。

目录:

1、settings.py是何时、以何种方式被加载到进程中的

2、总结

概念声明:

1、配置文件、项目配置文件,均指项目的配置文件,即项目目录下的settings.py

2、内置配置文件,指的是django包目录中的django/conf/global_settings.py文件

一、settings.py是如何被项目加载的

从入口文件开始溯源,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', 'aiserver.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()

第一步:

首先把DJANGO_SETTINGS_MODULE=aiserver.settings设置到当前程序运行的环境变量中,在后面django就会从环境变量中读取该变量,也就是读取到配置文件所在的位置。

贴士:os.environ会读取系统环境变量到当前进程,os.environ.setdefault在此基础上,增加当前进程的环境变量,两个操作都不会改变系统的环境变量设置。可以看出,django是使用这个方法来解耦代码和配置的。

第二步:

通过execute_from_command_line启动django的命令行管理工具,顺藤摸瓜,摸进去看一看,

def execute_from_command_line(argv=None):
    """Run a ManagementUtility."""
    # 一灯注:实例化了一个命令行管理工具
    utility = ManagementUtility(argv)
    # 一灯注:调用该工具实例的execute方法
    utility.execute()

抛开和主题无关的,直接看execute方法干了哪些事:

def execute(self):
    # 省略其他代码
       try:
        # 一灯注:在此方法中,首次使用了settings
        settings.INSTALLED_APPS
    except ImproperlyConfigured as exc:
        self.settings_exception = exc
    except ImportError as exc:
        self.settings_exception = exc
   # 省略其他代码

utility = ManagementUtility(argv)实例化ManagementUtility并未看到与settings有关的代码,但在此处已经开始执行

settings.INSTALLED_APPS

假装思考:

1、并未保存返回值,先假设不清楚它是否有返回值;

2、通过字面得知其获取了settings.py中的INSTALLED_APPS,也就是配置文件中的INSTALLED_APPS。虽然只是获取了属性INSTALLED_APPS,但其内部一定干了不少事;

3、settings一定是一个类实例,不然没法用链式语法,所以一定在某处执行了实例化;

也就是说,settings.INSTALLED_APPS运行时,它一定会去找项目中的settings.py文件,然后读取其中开发者自定义的应用。

第三步:

直接找到settings的声明,从文件头部就能看到,

from django.conf import settings

在开发过程中,经常引入这个模块来读取当前的配置

顺进去继续看看,它是如何把配置文件加载到项目中的。这个模块就保存在django/conf/__init__.py文件中,settings就在此处完成了实例化,一个叫LazySettings的类。

settings = LazySettings()

望文生义,看到lazy关键词,说明django对配置文件采用了延迟加载的策略。

延迟的原因猜测:在使用python manage.py runserver时,可使用--settings来指定配置文件,意味着可以有多个配置文件,而在manage.py中通过环境变量指定了配置文件的路径,如果不指定自定义的配置文件,则会使用环境变量中默认设置的,故采用延迟加载的方式。

第四步:

from django.conf import settings实际上是一个LazySettings类实例,再回去看settings.INSTALLED_APPS等价于:

LazySettings().INSTALLED_APPS

LazySettings类中并无INSTALLED_APPS属性,则它一定通过__getattr__来反射,下面是它的源码:

def __getattr__(self, name):
    """Return the value of a setting and cache it in self.__dict__."""
    if (_wrapped := self._wrapped) is empty:
        self._setup(name)
        _wrapped = self._wrapped
    val = getattr(_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.")

    self.__dict__[name] = val
    return val

逐一分析

1、防止反复加载配置文件的措施

if (_wrapped := self._wrapped) is empty:
    self._setup(name)
    _wrapped = self._wrapped

只有首次获取配置文件内容时才会加载,也就是前面所提到的settings.INSTALLED_APPS。若为首次,则调用self._setup方法执行加载。再读取配置项目时,就会直接使用变量_wrapped了。

在这个方法中,可以看到终于开始从环境变量中读取manage.py文件里设置的内容了:

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.
    """
    # 一灯注:ENVIRONMENT_VARIABLE = "DJANGO_SETTINGS_MODULE"
    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)
        )
	# 一灯注:同时,把活委托给了Settings,入参是配置文件的路径关系
    self._wrapped = Settings(settings_module)

还没完,真正去加载配置的并非LazySettings类,而是Settings类,这是常见的代理模式

3、self.__dict__[name] = val 雁过拔毛,但凡读取的配置项目,LazySettings都保存一份到自己的字典中。

最后一步:

Settings类要干的活,贴个源码,逐一分析:

注:除了项目配置文件,django还会有内置的配置文件,声明所有的配置项目的默认值

class Settings:
    def __init__(self, settings_module):
        # update this dict from global settings (but only for ALL_CAPS 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
        # 一灯注:设置SETTINGS_MODULE为项目配置文件路径关系 其他地方用得着,配置项目很多,有些项目在其他流程要用的,意味着Settings类实力在存在很长的生命周期,比如中间件需要用
        self.SETTINGS_MODULE = settings_module
	    # 一灯注:动态导入配置文件
        mod = importlib.import_module(self.SETTINGS_MODULE)

        tuple_settings = (
            "ALLOWED_HOSTS",
            "INSTALLED_APPS",
            "TEMPLATE_DIRS",
            "LOCALE_PATHS",
            "SECRET_KEY_FALLBACKS",
        )
        self._explicit_settings = set()
        # 读取项目配置文件中的项目,这个要和内置的配置文件要区分开,检查配置项目的合法值,这个校验的逻辑曾经梦到过
        for setting in dir(mod):
            if setting.isupper():
                setting_value = getattr(mod, setting)

                if setting in tuple_settings and not isinstance(
                    setting_value, (list, tuple)
                ):
                    raise ImproperlyConfigured(
                        "The %s setting must be a list or a tuple." % setting
                    )
                setattr(self, setting, setting_value)
                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 self.is_overridden("CSRF_COOKIE_MASKED"):
            warnings.warn(CSRF_COOKIE_MASKED_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)

        if self.is_overridden("DEFAULT_FILE_STORAGE"):
            if self.is_overridden("STORAGES"):
                raise ImproperlyConfigured(
                    "DEFAULT_FILE_STORAGE/STORAGES are mutually exclusive."
                )
            warnings.warn(DEFAULT_FILE_STORAGE_DEPRECATED_MSG, RemovedInDjango51Warning)

        if self.is_overridden("STATICFILES_STORAGE"):
            if self.is_overridden("STORAGES"):
                raise ImproperlyConfigured(
                    "STATICFILES_STORAGE/STORAGES are mutually exclusive."
                )
            warnings.warn(STATICFILES_STORAGE_DEPRECATED_MSG, RemovedInDjango51Warning)

    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,
        }


至此,项目的配置文件,算是全部加载进来了

二、总结

1、django项目启动前会先加载配置文件;

2、项目配置文件,代理类为LazySettings,真正干活的是Settings类,代理类被赋予了延迟加载的特性,让开发者有机会在命令中有机会指定自己的配置文件;

3、LazySettings代理类有放置重复加载的机制,可类比单例模式;

4、LazySettings代理类,通过反射获取Settings的属性,也就是获得项目文件的配置项目;

5、Settings类会读取内置的配置文件,对比项目配置文件,经过组合形成了最终的配置项目以及配置值;

6、settings = LazySettings()的生命周期很长,会覆盖到请求到相应的流程,需要使用到的地方:

  • 中间件
  • 用户校验
  • 数据库配置
  • 静态文件
  • 省略其他

7、再回过头看execute方法中的settings.INSTALLED_APPS,简直就是个幌子,如果不看源码,按照字面量的意思,它是获取了一个属性,但它实际的工作是干了很多事,如果使用一个函数调用的方式是不是更容易理解些。

延申:从过分析得知,在settings.py中,是不能读取表模型的,因为这个阶段,还未加载表模型

本次复盘结束.