显微镜下的Django

如果根据Artyom Malyshev( proofit404 )的报告拍摄电影,那么导演将是Quentin Tarantino-他已经制作了一部关于Django的电影,他还将拍摄第二部电影。 Django内部机制生命周期中的所有详细信息,从HTTP请求的第一个字节到响应的最后一个字节。 解析器形式,SQL的打包操作,HTML模板引擎的实现的特殊效果。 谁在管理连接池,如何管理? 所有这些都按时间顺序处理WSGI对象。 在该国的所有屏幕上-解码“显微镜下的Django”。



关于演讲者:Artyom Malyshev是Dry Python项目的创始人,也是Django Channels 1.0版的核心开发人员。 他从事Python已有5年的历史,并曾帮助在下诺夫哥罗德组织Python Rannts会议。 您可能对Artyom很熟悉,绰号为PROOFIT404 。 报告的演示存储在此处


曾几何时,我们启动了旧版本的Django。 然后她看起来很害怕和悲伤。



他们看到self_check通过了,我们正确安装了所有东西,一切正常,现在您可以编写代码了。 为了实现所有这些,我们必须运行django-admin runserver

 $ django-admin runserver Performing system checks… System check identified no issues (0 silenced). You have unapplied migrations; your app may not work properly until they are applied. Run 'python manage.py migrate1 to apply them. August 21, 2018 - 15:50:53 Django version 2.1, using settings 'mysite.settings' Starting development server at http://127.0.0.1:8000/Quit the server with CONTROL-C. 

该过程开始,处理HTTP请求,所有不可思议的事情发生在内部,并且执行了我们要向用户展示的所有代码。

安装方式


当我们使用例如pip和package manager安装Django时, django-admin出现在系统上。

 $ pip install Django # setup.py from setuptools import find_packages, setup setup( name='Django', entry_points={ 'console_scripts': [ 'django-admin = django.core.management:execute_from_command_line' ] }, ) 

出现entry_points setuptools ,它指向execute_from_command_line函数。 对于任何当前进程,此功能都是使用Django进行任何操作的切入点。

引导程序


函数内部会发生什么? Bootstrap ,分为两个迭代。

 # django.core.management django.setup(). 

配置设置


首先是阅读配置

 import django.conf.global_settings import_module(os.environ["DJANGO_SETTINGS_MODULE"]) 

global_settings默认设置global_settings ,然后尝试从环境变量中找到用户编写的DJANGO_SETTINGS_MODULE模块。 这些设置合并到一个名称空间中。

至少在Django中写过“ Hello,world”的任何人都知道有INSTALLED_APPS我们在其中编写用户代码。

填充应用


在第二部分中,所有这些应用程序(本质上是程序包)都被一个接一个地迭代。 我们为每个Config创建,导入用于与数据库一起使用的模型,并检查模型的完整性。 此外,框架运行Check ,即,它检查每个模型都有一个主键,所有外键都指向现有字段,并且Null字段未写在BooleanField中,而是使用了NullBooleanField。

 for entry in settings.INSTALLED_APPS: cfg = AppConfig.create(entry) cfg.import_models() 

这是对模型,管理面板,任何对象的最低限度的完整性检查-无需连接数据库,也无需任何超级复杂和特定的操作。 在此阶段,Django尚不知道您要求执行哪个命令,也就是说,不区分从runservershell migrate

然后,我们在一个模块中找到自己,该模块尝试通过命令行参数猜测我们要执行哪个命令以及它位于哪个应用程序中。

管理命令


 # django.core.management subcommand = sys.argv[1] app_name = find(pkgutils.iter_modules(settings.INSTALLED_APPS)) module = import_module( '%s.management.commands.%s' % (app_name, subcommand) ) cmd = module.Command() cmd.run_from_argv(self.argv) 

在这种情况下,runserver模块将具有内置的django.core.management.commands.runserver模块。 导入模块后,按照惯例,全局类Command在内部被调用,被实例化,然后我们说:“ 我找到了,在这里,您具有用户传递的命令行参数,并对它们进行处理 。”

接下来,我们转到runserver模块,看看Django是由“ regexp and sticks”组成的 ,今天我将详细讨论:

 # django.core.management.commands.runserver naiveip_re = re.compile(r"""^(?: (?P<addr> (?P<ipv4>\d{1,3}(?:\.\d{1,3}){3}) | # IPv4 address (?P<ipv6>\[[a-fA-F0-9:]+\]) | # IPv6 address (?P<fqdn>[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*) # FQDN ):)?(?P<port>\d+)$""", re.X) 

指令


向下滚动一个半屏-最后,我们进入启动服务器的团队的定义。

 # django.core.management.commands.runserver class Command(BaseCommand): def handle(self, *args, **options): httpd = WSGIServer(*args, **options) handler = WSGIHandler() httpd.set_app(handler) httpd.serve_forever() 

BaseCommand执行最少的一组操作,以使命令行参数生成用于调用*args**options函数的*args 。 我们看到在这里创建了WSGI服务器实例,在此WSGI服务器中安装了全局WSGIHandler-这正是God Object Django 。 我们可以说这是框架的唯一实例。 实例通过set application全局安装在服务器上,并说:“在事件循环中旋转,执行请求。”

总是有一个事件循环,某个程序员为他分配任务。

WSGI服务器


什么是WSGIHandler ? WSGI是一个接口,它允许您以最小的抽象级别处理HTTP请求,并且看起来像某种函数形式的东西。

WSGI处理程序


 # django.core.handlers.wsgi class WSGIHandler: def __call__(self, environ, start_response): signals.request_started.send() request = WSGIRequest(environ) response = self.get_response(request) start_response(response.status, response.headers) return response 

例如,这里是定义了call的类的实例。 他等待字典条目,其中标题将显示为字节和文件处理程序。 需要处理程序才能读取请求的<body> 。 服务器本身还提供了回调start_response以便我们可以在一包中发送response.headers及其标头(例如status)。

此外,我们可以通过响应对象将响应主体传递给服务器。 响应是可以迭代的生成器。

所有为WSGI编写的服务器-Gunicorn,uWSGI,Waitress,都在此接口上工作并且可以互换。 我们现在正在考虑开发服务器,但任何服务器都达到了在Django中突破环境和回调的地步。

神物里面是什么?


Django内部的全局God Object函数内部会发生什么?

  • 要求。
  • 中间商品。
  • 路由请求查看。
  • VIEW-视图内的用户代码处理。
  • 表单-使用表单。
  • ORM。
  • 模板
  • 回应。

我们从Django需要的所有机器都在一个函数中发生,该函数分布在整个框架中。

索取


为了方便使用该环境,我们在某些特殊对象中包装了WSGI环境,这是一个简单的字典。 例如,通过使用类似于字典的东西来查找用户请求的长度,而不是使用需要解析的字节串并在其中查找键值条目,更方便。 使用Cookie时,我也不想手动计算存储期是否已到期,并以某种方式对其进行解释。

 # django.core.handlers.wsgi class WSGIRequest(HttpRequest): @cached_property def GET(self): return QueryDict(self.environ['QUERY_STRING']) @property def POST(self): self._load_post_and_files() return self._post @cached_property def COOKIES(self): return parse_cookie(self.environ['HTTP_COOKIE']) 

请求包含解析器,以及一组处理程序,用于控制POST请求正文的处理:无论是内存中的文件还是磁盘上的临时存储文件。 一切都在请求中决定。 Django中的Request也是一个聚合器对象,所有中间件都可以在其中存储我们需要的有关会话,身份验证和用户授权的信息。 我们可以说这也是一个上帝对象,但是更小。

进一步的请求进入中间件。

中间件


中间件是包装器,包装其他功能(如装饰器)。 在放弃对中间件的控制之前,在call方法中,我们给出响应或调用已经包装好的中间件。

从程序员的角度来看,这就是中间件的样子。

设定值


 # settings.py MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', ] 

定义


 class Middleware: def __init__(self, get_response=None): self.get_response = get_response def __call__(self, request): return self.get_response(request) 

从Django的角度来看,中间件看起来像一种堆栈:

 # django.core.handlers.base def load_middleware(self): handler = convert_exception_to_response(self._get_response) for middleware_path in reversed(settings.MIDDLEWARE): middleware = import_string(middleware_path) instance = middleware(handler) handler = convert_exception_to_response(instance) self._middleware_chain = handler 

申请


 def get_response(self, request): set_urlconf(settings.ROOT_URLCONF) response = self._middleware_chain(request) return response 

我们采用初始的get_response函数,将其包装在处理程序中,该处理程序会将例如permission errornot found error转换为正确的HTTP代码。 我们从列表中将所有内容包装在中间件本身中。 中间件堆栈不断增长,每个下一个都包装了前一个。 这与仅将集中的装饰器堆栈应用于项目中的所有视图非常相似。 无需根据项目动手安排包装器,一切都很方便且合乎逻辑。

我们经历了7个中间件圈子,我们的请求幸免了下来,并决定对它进行处理。 此外,我们进入了路由模块。

路由选择


在这里,我们可以决定为特定请求调用哪个处理程序。 解决了:

  • 基于网址;
  • 在WSGI规范中,调用request.path_info。

 # django.core.handlers.base def _get_response(self, request): resolver = get_resolver() view, args, kwargs = resolver.resolve(request.path_info) response = view(request, *args, **kwargs) return response 

乌尔斯


我们使用解析器,为它提供当前请求的url,并期望它返回视图函数本身,并从相同的url中获得用于调用view的参数。 然后get_response调用视图,处理异常并对其进行处理。

 # urls.py urlpatterns = [ path('articles/2003/', views.special_case_2003), path('articles/<int:year>/', views.year_archive), path('articles/<int:year>/<int:month>/', views.month_archive) ] 

解析器


这是解析器的外观:

 # django.urls.resolvers _PATH_RE = re.compile( r'<(?:(?P<converter>[^>:]+):)?(?P<parameter>\w+)>' ) def resolve(self, path): for pattern in self.url_patterns: match = pattern.search(path) if match: return ResolverMatch( self.resolve(match[0]) ) raise Resolver404({'path': path}) 

这也是正则表达式,但是是递归的。 它进入URL的一部分,查找用户想要的内容:其他用户,帖子,博客,或者是某种转换程序,例如,需要解决的特定年份,放入参数并转换为int。

其特征是resolve方法的递归深度始终等于调用视图的参数数量。 如果出了点问题,而我们没有找到特定的网址,则会发生未找到的错误。

然后,我们终于了解了程序员编写的代码。

检视


在最简单的表示形式中,它是一个从响应返回请求的函数,但在其中执行逻辑任务:“如果有一天”-许多重复性任务。 Django为我们提供了一个基于类的视图,您可以在其中指定特定的详细信息,并且所有行为将由类本身以正确的格式进行解释。

 # django.views.generic.edit class ContactView(FormView): template_name = 'contact.html' form_class = ContactForm success_url = '/thanks/' 

方法流程图


 self.dispatch() self.post() self.get_form() self.form_valid() self.render_to_response() 

此实例的dispatch方法已经在url映射中,而不是函数中。 基于HTTP谓词的调度可以了解调用哪种方法:POST来了,我们很可能希望实例化form对象,如果form有效,则将其保存到数据库并显示模板。 所有这些都是通过构成此类的大量mixin完成的。

形式


在进入Django视图之前,必须先通过套接字读取表单-通过WSGI环境中的同一文件处理程序。 form-data是一个字节流,在其中描述了分隔符-我们可以读取这些块并进行一些处理。 它可以是键-值对应关系,如果它是字段,文件的一部分,然后又是某个字段-一切都混合了。

 Content-Type: multipart/form-data;boundary="boundary" --boundary name="field1" value1 --boundary name="field2"; value2 

解析器


解析器包括3个部分。

从字节流中创建期望的读数的块迭代器变成可以产生boundaries的迭代器。 它保证如果有东西返回,它将是边界。 这是必需的,以便在解析器内部不必存储连接状态,从套接字读取或不读取以最小化数据处理逻辑。

接下来,生成器包装LazyStream ,它再次从中创建一个目标文件,但具有预期的读数。 因此,解析器已经可以遍历各个字节并从中构建键值。

此处的字段和数据将始终为字符串 。 如果我们收到ISO格式的数据时间,则Django表单(由程序员编写)将使用某些字段(例如时间戳)来接收。

 # django.http.multipartparser self._post = QueryDict(mutable=True) stream = LazyStream(ChunkIter(self._input_data)) for field, data in Parser(stream): self._post.append(field, force_text(data)) 

此外,表单很可能希望将自身保存在数据库中,然后从这里开始Django ORM。

ORM


大约通过此类DSL来执行ORM请求:

 # models.py Entry.objects.exclude( pub_date__gt=date(2005, 1, 3), headline='Hello', ) 

使用键,您可以收集类似的SQL表达式:

 SELECT * WHERE NOT (pub_date > '2005-1-3' AND headline = 'Hello') 

怎么样了

查询集


exclude方法在内部具有Query对象。 该对象将参数传递给函数,并创建对象的层次结构,每个对象层次结构都可以将自身作为字符串变成SQL查询的单独部分。

遍历树时,每个部分都轮询其子节点,接收嵌套的SQL查询,因此,我们可以将SQL构造为字符串。 例如,键值将不是单独的SQL字段,但将与值值进行比较。 查询的串联和拒绝与递归树遍历的工作方式相同,对于每个调用该节点的节点,都强制转换为SQL。

 # django.db.models.query sql.Query(Entry).where.add( ~Q( Q(F('pub_date') > date(2005, 1, 3)) & Q(headline='Hello') ) ) 

编译器


 # django.db.models.expressions class Q(tree.Node): AND = 'AND' OR = 'OR' def as_sql(self, compiler, connection): return self.template % self.field.get_lookup('gt') 

输出量


 >>> Q(headline='Hello') # headline = 'Hello' >>> F('pub_date') # pub_date >>> F('pub_date') > date(2005, 1, 3) # pub_date > '2005-1-3' >>> Q(...) & Q(...) # ... AND ... >>> ~Q(...) # NOT … 

一个小的辅助编译器被传递给此方法,该方法可以将MySQL的方言与PostgreSQL区别开来,并正确排列特定数据库的方言中使用的语法糖。

数据库路由


当我们收到SQL查询时,该模型会触发数据库路由并询问其位于哪个数据库中。 在99%的情况下,它将是默认数据库,在其余1%的情况下-属于自己的数据库。

 # django.db.utils class ConnectionRouter: def db_for_read(self, model, **hints): if model._meta.app_label == 'auth': return 'auth_db' 

从特定的库接口(例如Python MySQL或Psycopg2)包装数据库驱动程序,会创建Django可以使用的通用对象。 有一个用于游标的包装器,一个用于事务的包装器。

连通池


 # django.db.backends.base.base class BaseDatabaseWrapper: def commit(self): self.validate_thread_sharing() self.validate_no_atomic_block() with self.wrap_database_errors: return self.connection.commit() 

在这个特定的连接中,我们将请求发送到正在敲数据库的套接字,并等待执行。 库上的包装器将以记录的形式从数据库中读取人为的响应,而Django从该数据中以Python类型收集模型实例。 这不是一个复杂的迭代。

我们将一些内容写入数据库,读取了一些内容,然后决定使用HTML页面将其告知用户。 为此,Django使用了社区不喜欢的模板语言,该模板语言看起来像是编程语言,仅在HTML文件中。

范本


 from django.template.loader import render_to_string render_to_string('my_template.html', {'entries': ...}) 

代号


 <ul> {% for entry in entries %} <li>{{ entry.name }}</li> {% endfor %} </ul> 

解析器


 # django.template.base BLOCK_TAG_START = '{%' BLOCK_TAG_END = '%}' VARIABLE_TAG_START = '{{' VARIABLE_TAG_END = '}}' COMMENT_TAG_START = '{#' COMMENT_TAG_END = '#}' tag_re = (re.compile('(%s.*?%s|%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END), re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END), re.escape(COMMENT_TAG_START), re.escape(COMMENT_TAG_END)))) 

惊喜-regexp再次出现。 只有在结尾处才有逗号,并且列表会很低。 这可能是我在该项目中见过的最困难的正则表达式。

Lexer


模板处理程序和解释器非常简单。 有一个词法分析器,使用regexp将文本转换为小标记列表。

 # django.template.base def tokenize(self): for bit in tag_re.split(template_string): lineno += bit.count('\n') yield bit 

我们遍历令牌列表,看看:“你是谁? 将您包裹在标签节点中。” 例如,如果这是某些ifforfor ,则标记处理程序将采用适当的处理程序。 for处理器本身再次告诉解析器:“请向我阅读直到结束标记的所有令牌列表。”

操作再次转到解析器。

节点,标签和解析器是相互递归的,递归的深度通常等于标签本身对模板的嵌套。

解析器


 def parse(): while tokens: token = tokens.pop() if token.startswith(BLOCK_TAG_START): yield TagNode(token) elif token.startswith(VARIABLE_TAG_START): ... 

标记处理程序为我们提供了一个特定的节点,例如带有for循环的节点,为此render方法。

对于循环


 # django.template.defaulttags @register.tag('for') def do_for(parser, token): args = token.split_contents() body = parser.parse(until=['endfor']) return ForNode(args, body) 

对于节点


 class ForNode(Node): def render(self, context): with context.push(): for i in self.args: yield self.body.render(context) 

render方法是一个渲染树。 每个上层节点可以转到一个子节点,要求她进行渲染。 程序员习惯于在这个模板中显示一些变量。 这是通过context完成的-它以常规词典的形式呈现。 这是一堆字典,用于在我们输入标签时模拟作用域。 例如,如果context本身更改了for循环内的其他标签,则当我们退出循环时,所做的更改将被回滚。 之所以方便,是因为当一切都是全球性的时,就很难进行工作。

回应


最后,我们得到了HTTP响应:

世界您好!

我们可以把线给用户。

  • 从视图返回此响应。
  • 查看列表中间件。
  • 响应的中间件可以修改,补充和改进。
  • 响应开始在WSGIHandler内部进行迭代,部分写入套接字,浏览器从我们的服务器接收响应。

所有用Django编写的著名创业公司,例如Bitbucket或Instagram,都以一个很小的周期开始,每个程序员都要经历一次。

所有这些以及在Moscow Python Conf ++上的演示,对于您更好地了解您手中的内容以及如何使用它是必不可少的。 用任何魔术,您都必须能够烹饪正则表达式的很大一部分。

Artyom Malyshev和其他23位出色的演讲者将于4月5日在莫斯科Python Conf ++会议上再次为我们提供有关Python主题的大量思想和讨论。 研究时间表,并加入经验交流,以使用Python解决各种问题。

Source: https://habr.com/ru/post/zh-CN433482/


All Articles