[[Djangoを読む]] #contents *はじめに [#b440516d] テンプレートの解析まで読んだので最後、いよいよテンプレートのレンダリングです。呼び出しのトップはdjango.template.loaderのrender_to_string #code(Python){{ def render_to_string(template_name, context=None, request=None, using=None): """ Loads a template and renders it with a context. Returns a string. template_name may be a string or a list of strings. """ if isinstance(template_name, (list, tuple)): template = select_template(template_name, using=using) else: template = get_template(template_name, using=using) return template.render(context, request) }} templateの型は、とりあえずdjango.template.backends.django.Templateです。 *Template移譲の終点探し [#v2225006] とりあえずと言ったのは、templateと名のつくクラスが複数いるからです。まずはdjango.template.backends.django.Templateのrenderを見てみましょう。 #code(Python){{ def render(self, context=None, request=None): context = make_context(context, request, autoescape=self.backend.engine.autoescape) try: return self.template.render(context) except TemplateDoesNotExist as exc: reraise(exc, self.backend) }} ここでいうself.templateはこう渡されている。 #code(Python){{ def get_template(self, template_name): try: return Template(self.engine.get_template(template_name), self) except TemplateDoesNotExist as exc: reraise(exc, self) }} たびたび見ているように、self.engineはdjango.template.engineのEngineクラス。 #code(Python){{ def get_template(self, template_name): """ Returns a compiled Template object for the given template name, handling template inheritance recursively. """ template, origin = self.find_template(template_name) if not hasattr(template, 'render'): # template needs to be compiled template = Template(template, origin, template_name, engine=self) return template }} 前回見た感じでは、find_templateの時点でコンパイルされたものが返ってきています。いずれにしろdjango.template.base.Templateが使用されます。 *django.template.base.Template [#b2ebf392] というわけでdjango.template.base.Templateのrenderメソッド #code(Python){{ def render(self, context): "Display stage -- can be called many times" context.render_context.push() try: if context.template is None: with context.bind_template(self): context.template_name = self.name return self._render(context) else: return self._render(context) finally: context.render_context.pop() }} contextでいろいろやっているのは見ててそんなにおもしろくないのでスルーします。context周りはdjango.template.contextに書かれています。 _renderはnodelist呼んでいるだけ。 #code(Python){{ def _render(self, context): return self.nodelist.render(context) }} NodeListのrender #code(Python){{ def render(self, context): bits = [] for node in self: if isinstance(node, Node): bit = node.render_annotated(context) else: bit = node bits.append(force_text(bit)) return mark_safe(''.join(bits)) }} in selfとなっているのは、NodeListがlistクラスを継承しているからです。 render_annotatedというのは例外時にデバッグ情報出すようにしているだけのようなのでここからは各論行きます。 *VariableNode [#q62e4ac0] 変数を見てみましょう。ちなみに、Djangoテンプレートの変数は「.」指定されていた場合、以下のルールがあります。 -変数を辞書とみなし、「.」以下を辞書のキーとしてアクセスしてみる -変数の属性としてアクセスしてみる -リストのインデックスとしてアクセスしてみる -いずれの場合も、得られたものがcallableなら引数なしで呼び出す で、VariableNodeのrender #code(Python){{ def render(self, context): try: output = self.filter_expression.resolve(context) except UnicodeDecodeError: # Unicode conversion can fail sometimes for reasons out of our # control (e.g. exception rendering). In that case, we fail # quietly. return '' return render_value_in_context(output, context) }} FilterExpressionクラスのresolveメソッド #code(Python){{ def resolve(self, context, ignore_failures=False): if isinstance(self.var, Variable): try: obj = self.var.resolve(context) except VariableDoesNotExist: # 省略 else: obj = self.var # フィルターの処理省略 return obj }} varはVariableオブジェクトなのでVariableクラスのresolveへ。 #code(Python){{ def resolve(self, context): """Resolve this variable against a given context.""" if self.lookups is not None: # We're dealing with a variable that needs to be resolved value = self._resolve_lookup(context) else: # We're dealing with a literal, so it's already been "resolved" value = self.literal if self.translate: # 省略 return value }} 前回見てなかったけど、lookupsはコンストラクタで設定されるようです(「.」でsplitしたタプル)。というわけで_resolve_lookupへ。 #code(Python){{ def _resolve_lookup(self, context): current = context try: # catch-all for silent variable failures for bit in self.lookups: try: # dictionary lookup current = current[bit] # ValueError/IndexError are for numpy.array lookup on # numpy < 1.9 and 1.9+ respectively except (TypeError, AttributeError, KeyError, ValueError, IndexError): try: # attribute lookup # Don't return class attributes if the class is the context: if isinstance(current, BaseContext) and getattr(type(current), bit): raise AttributeError current = getattr(current, bit) except (TypeError, AttributeError) as e: # Reraise an AttributeError raised by a @property if (isinstance(e, AttributeError) and not isinstance(current, BaseContext) and bit in dir(current)): raise try: # list-index lookup current = current[int(bit)] except (IndexError, # list index out of range ValueError, # invalid literal for int() KeyError, # current is a dict without `int(bit)` key TypeError): # unsubscriptable object raise VariableDoesNotExist("Failed lookup for key " "[%s] in %r", (bit, current)) # missing attribute if callable(current): if getattr(current, 'do_not_call_in_templates', False): pass elif getattr(current, 'alters_data', False): current = context.template.engine.string_if_invalid else: try: # method call (assuming no args required) current = current() except TypeError: try: inspect.getcallargs(current) except TypeError: # arguments *were* required current = context.template.engine.string_if_invalid # invalid method call else: raise except Exception as e: # 省略 return current }} ちょっと長いですが初めに挙げた順番で変数解決を行っています。 ちなみに、「.」付きじゃない普通の変数の場合どう動くんだ?という疑問に対しては、currentの初めがcontext(辞書オブジェクトとみなせる)なので先頭のdictionary lookupで指定された変数が見つかるようになっています。 *django.template.defaulttags.IfNode [#f7dd39ca] 次にタグの処理を見てみましょう。前回も確認したifに対応するIfNode。 #code(Python){{ def render(self, context): for condition, nodelist in self.conditions_nodelists: if condition is not None: # if / elif clause try: match = condition.eval(context) except VariableDoesNotExist: match = None else: # else clause match = True if match: return nodelist.render(context) return '' }} conditionの中身がどうなっているかは前回さくっと省略しましたが、結局同じように変数解決まで進んで値が取り出されています。 *おわりに [#r0a1b5fa] 以上、テンプレートのレンダリングについて見てきました。いろいろ省略したところはありましたがオブジェクトが出来上がってしまえば後は淡々と処理を行っていくだけなので今までに比べると簡単な印象です。 さて、これでようやく一年ぐらいかけて読んできた(読んでない期間も長かったですが)Djangoを一通り読み終わりました。 View(普通のMVCのController)、Model、Template(普通のMVCのTemplate)がそれぞれ切り離されているのでわかりやすい一方でModelは非常に複雑でした。Templateについてもフィルターやif条件のいろんなパターンについては触れませんでしたが地味にめんどくさいものになっています。 いろいろなケースをすべて対応するとなると結局ひとつの言語を実装することになるわけでどうしても複雑になるのかなという印象です。 そういえばそろそろ2.0リリースされそうですね(笑)