Jinja中的多态宏

3 投票
2 回答
2533 浏览
提问于 2025-04-16 20:00

我想找到一种方法,可以让Jinja宏根据传入的对象类型调用不同的实现。简单来说,就是标准的Python方法多态性。目前,我正在使用一种看起来不太好看的变通方法,类似于这个:

{% macro menuitem(obj) %}
  {% set type = obj.__class__.__name__ %}
  {% if type == "ImageMenuItem" %}
    {{ imagemenuitem(obj) }}
  {% elif type == "FoobarMenuItem" %}
    {{ foobarmenuitem(obj) }}
  {% else %}
    {{ textmenuitem(obj) }}
  {% endif %}
{% endmacro %}

在纯Python中,可以通过修改模块环境来实现,比如使用globals()[x+'menuitem'],虽然这样不太优雅,但效果很好。我尝试过用Jinja上下文做类似的事情,但后者似乎不包含宏定义。

还有什么更好的方法可以实现我想要的效果呢?

2 个回答

7

面向对象编程的核心就是:多态。

Create a presentation Layer for your objects:

class MenuPresentation:
    def present(self):
        raise NotImplementedException()

class ImageMenuPresentation(MenuPresentation):
   def present(self):
       return "magic url "

class TextMenuPresentation(MenuPresentation):
   def present(self):
      return "- text value here"

接下来就只是一个简单的问题:

{% macro menuitem(obj) %}
  {{ obj.present() }}
{% endmacro %}
2

我现在解决了我的问题,方式和fabrizioM建议的类似,但有一个显著的不同:因为菜单项的展示可以(而且大多数情况下确实会)包含HTML,所以我不想在present方法中直接处理HTML标记。因此,我最终在Python中实现了菜单定义,在Jinja中实现了展示,通过互相递归来连接这两者。

不同类型的菜单项通过不同的子类来表示:

class MenuItem(object):
    def present(self, macromap):
        return macromap[type(self).__name__](self, macromap)

class TextLink(MenuItem):
    def __init__(self, url, text):
        self.url, self.text = url, text

class Section(MenuItem):
    def __init__(self, text, items):
        self.text, self.items = text, items

class ImageLink(MenuItem):
    ...

上面提到的macromap是一个字典,它将菜单项的类型映射到实现其展示的宏。所有这些都是在Jinja中定义的:

{% macro TextLink(l, macromap) %}
  <a class="menuitem" href="{{l.url|escape}}">
    {{ l.text|escape }}
  </a>
{% endmacro %}

{% macro Section(s, macromap) %}
  <div class="heading">{{s.text}}</div>
  <ul class="items">
    {% for item in s.items %}
      <li>{{ item.present(macromap) }}</li>
    {% endfor %}
  </ul>
{% endmacro %}

{% set default_map = {'TextLink': TextLink, 'Section': Section, ...}

实际的菜单定义被清晰地表示为MenuItem子类的树状结构:

main_menu = section("Main Menu", [
    section("Product Line 1", [
        TextLink("/products/...", "A product"),
        ...
    ]),
    section(...),
])

为了开始展示,模板需要调用顶层部分的present方法,并传入一个宏映射,以指定如何展示菜单,比如main_menu.present(default_map)。在Section宏中可以看到,菜单项可以请求它们的子项进行展示,这些子项的present方法会再调用另一个Jinja宏,依此类推,形成递归。

虽然必须显式地传递宏映射看起来不太美观,但它带来了一个重要的好处:现在可以轻松渲染不同的菜单数据展示,而无需触碰菜单定义。例如,可以定义宏映射来渲染主网站菜单,或者为移动设备准备的变体(如果CSS不够用的话),或者XML网站地图,甚至是纯文本版本。(实际上,我们最终在网站菜单和网站地图的情况下使用了这个系统。)

撰写回答