本文是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
中,是不能读取表模型的,因为这个阶段,还未加载表模型
本次复盘结束.