# 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() |