محرّك القوالب
نيد باتشلدر مهندس برمجيات ذو مسيرة طويلة، ويعمل حاليًّا في edX ليُنشئ برمجيات مفتوحة المصدر لتثقيف العالم. وهو مشرف على coverage.py، ومنظّم Boston Python، وقد تحدّث في العدّة من مؤتمرات PyCon. يكتب مدونته على http://nedbatchelder.com. وقد تناول العشاء مرّة ذات ليلة في البيت الأبيض.
مقدّمة
تحتوي معظم البرامج على كثير من المنطق، وعلى قدر يسير من البيانات النصّية الحرفية. وقد صُمِّمت لغات البرمجة لتكون جيّدة لهذا النوع من البرمجة. لكنّ بعض مهام البرمجة لا تتضمّن إلّا قدرًا يسيرًا من المنطق، وكثيرًا جدًّا من البيانات النصّية. ولكل هذه المهام، نودّ أن نتوفّر على أداة أنسب لهذه المشكلات الغنيّة بالنص. ومحرّك القوالب (template engine) هو تلك الأداة بالضبط. وفي هذا الفصل، سنبني محرّك قوالب بسيطًا.
أشهر مثال على هذه المهام الغنيّة بالنص هو في تطبيقات الويب. فمرحلة مهمّة في أيّ تطبيق ويب هي توليد HTML يُقدَّم إلى المتصفّح. وقليل جدًّا من صفحات HTML تكون ساكنة تمامًا: فهي تتضمّن على الأقلّ قدرًا يسيرًا من البيانات الديناميكية، مثل اسم المستخدم. وعادةً ما تحتوي قدرًا كبيرًا من البيانات الديناميكية: قوائم المنتجات، وتحديثات أخبار الأصدقاء، وهكذا.
وفي الوقت نفسه، تحتوي كل صفحة HTML على مساحات كبيرة من النصّ الساكن. وهذه الصفحات كبيرة، إذ تحتوي على عشرات الآلاف من بايتات النص. وأمام مطوّر تطبيق الويب مسألة يجب أن يحلّها: كيف نولّد بأفضل طريقة سلسلة نصّية كبيرة تحتوي على مزيج من البيانات الساكنة والديناميكية؟ ولزيادة تعقيد المسألة، فإنّ النصّ الساكن هو في الواقع ترميز HTML (markup) يؤلّفه عضو آخر من الفريق، وهو مصمّم الواجهة الأمامية، ويريد أن يتمكّن من العمل عليه بطرق مألوفة.
لأغراض التوضيح، لنفترض أننا نريد إنتاج هذا HTML التجريبي:
<p>Welcome, Charlie!</p>
<p>Products:</p>
<ul>
<li>Apple: $1.00</li>
<li>Fig: $1.50</li>
<li>Pomegranate: $3.25</li>
</ul>
هنا، سيكون اسم المستخدم ديناميكيًّا، وكذلك أسماء المنتجات وأسعارها. بل إنّ عدد المنتجات ليس ثابتًا: فقد يكون هناك في لحظة أخرى منتجات أكثر أو أقلّ لعرضها.
إحدى طرق إنتاج هذا HTML هي أن يكون لدينا ثوابت نصّية في شيفرتنا، ندمجها معًا لإنتاج الصفحة. وتُدرَج البيانات الديناميكية عبر استبدال نصّي من نوع ما. وبعض بياناتنا الديناميكية متكرّرة، مثل قوائم منتجاتنا. وهذا يعني أنّ لدينا مجزوءات من HTML تتكرّر، لذا يجب معالجتها على حدة ثمّ دمجها مع بقية الصفحة.
قد يبدو إنتاج صفحتنا التجريبية بهذه الطريقة هكذا:
# The main HTML for the whole page.
PAGE_HTML = """
<p>Welcome, {name}!</p>
<p>Products:</p>
<ul>
{products}
</ul>
"""
# The HTML for each product displayed.
PRODUCT_HTML = "<li>{prodname}: {price}</li>\n"
def make_page(username, products):
product_html = ""
for prodname, price in products:
product_html += PRODUCT_HTML.format(
prodname=prodname, price=format_price(price))
html = PAGE_HTML.format(name=username, products=product_html)
return html
هذا يعمل، لكنّنا وقعنا في فوضى. إنّ HTML موزّع في عدّة ثوابت نصّية مضمّنة داخل شيفرة تطبيقنا. ومنطق الصفحة صعب الرؤية لأنّ النصّ الساكن مُجزَّأ إلى قطع منفصلة. وتضيع تفاصيل كيفية تنسيق البيانات داخل شيفرة بايثون. وحتى يعدّل مصمّم الواجهة الأمامية صفحة HTML، عليه أن يتمكّن من تحرير شيفرة بايثون لإجراء تغييرات على HTML. وتخيّل كيف سيبدو الشيفرة لو كانت الصفحة أعقد بعشر مرّات (أو مئة مرّة)؛ فستصبح غير قابلة للعمل بسرعة تامّة.
القوالب
والطريقة الأفضل لإنتاج صفحات HTML هي باستخدام القوالب (templates). تُؤلَّف صفحة HTML بوصفها قالبًا، ما يعني أنّ الملف هو في معظمه HTML ساكن، مع أجزاء ديناميكية مضمّنة فيه باستخدام ترميز خاصّ. ويمكن أن تبدو صفحتنا التجريبية أعلاه بهذا الشكل بوصفها قالبًا:
<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}:
{{ product.price|format_price }}</li>
{% endfor %}
</ul>
هنا يكون التركيز على النصّ HTML، مع منطق مضمّن داخل HTML. قارن هذا النهج المتمركز حول المستند بشيفرتنا المتمركزة حول المنطق أعلاه. فقد كان برنامجنا الأسبق شيفرة بايثون في معظمها، مع HTML مضمّن داخل منطق بايثون. أمّا هنا فبرنامجنا في معظمه ترميز HTML ساكن.
والأسلوب الساكن في الغالب المستخدم في القوالب هو عكس كيفية عمل معظم لغات البرمجة. فمثلًا، في بايثون، معظم ملف المصدر شيفرة قابلة للتنفيذ، وإذا احتجت إلى نصّ ساكن حرفيّ فإنّك تضمّنه في ثابت نصّي:
def hello():
print("Hello, world!")
hello()
حين تقرأ بايثون ملف المصدر هذا، فإنّها تفسّر نصًّا مثل def hello(): بوصفه تعليمات يُقصد تنفيذها. أمّا محرف الاقتباس المزدوج في print("Hello, world!") فيُشير إلى أنّ النصّ التالي مقصود منه أن يكون حرفيًّا، حتى الاقتباس المزدوج الختامي. وهكذا تعمل معظم لغات البرمجة: ديناميكية في الغالب، مع بعض القطع الساكنة المضمّنة في التعليمات. وتُشير القطع الساكنة إلى ترميز الاقتباس المزدوج.
وتقلب لغة القوالب هذا الترتيب: ملف القالب هو في معظمه نصّ حرفيّ ساكن، مع ترميز خاصّ يُشير إلى الأجزاء الديناميكية القابلة للتنفيذ.
<p>Welcome, {{user_name}}!</p>
هنا المقصود أن يظهر النصّ حرفيًّا في صفحة HTML الناتجة، حتى يشير '{{' إلى الانتقال إلى الوضع الديناميكي، حيث يُستبدل المتغيّر user_name في المخرجات.
ودوالّ تنسيق النصوص مثل "foo = {foo}!".format(foo=17) في بايثون هي أمثلة على لغات مصغّرة (mini-languages) تُستخدم لإنشاء نصّ من ثابت نصّي ومن البيانات التي ستُدرج. وتوسّع القوالب هذه الفكرة لتشمل بنى مثل الجمل الشرطية والحلقات، لكنّ الفرق هو فرق في الدرجة فقط.
وتُسمّى هذه الملفات قوالب لأنها تُستخدم لإنتاج عدّة صفحات ذات بنية متشابهة لكن بتفاصيل مختلفة.
ولاستخدام قوالب HTML في برامجنا، نحتاج إلى محرّك قوالب: دالة تأخذ قالبًا ساكنًا يصف بنية الصفحة ومحتواها الساكن، وسياقًا (context) ديناميكيًّا يوفّر البيانات الديناميكية التي ستُدغم في القالب. يجمع محرّك القوالب بين القالب والسياق لإنتاج سلسلة HTML كاملة. ومهمّة محرّك القوالب هي تفسير القالب، واستبدال الأجزاء الديناميكية ببيانات حقيقية.
وبالمناسبة، لا شيء في محرّك القوالب خاصّ بـ HTML تحديدًا، إذ يمكن استخدامه لإنتاج أيّ نتيجة نصّية. فمثلًا، تُستخدم أيضًا لإنتاج رسائل بريد إلكتروني نصّية صرفة. لكنّها عادةً ما تُستخدم لـ HTML، وأحيانًا تكون لها ميزات خاصّة بـ HTML مثل الهروب (escaping)، وهو ما يجعل من الممكن إدراج القيم في HTML دون قلق بشأن أيّ المحارف خاصّة في HTML.
الصياغة المدعومة
تختلف محرّكات القوالب في الصياغة التي تدعمها. وصياغة قوالبنا مبنيّة على Django، وهو إطار ويب شائع. وبما أنّنا ننفّذ محرّكنا في بايثون، فإنّ بعض مفاهيم بايثون ستظهر في صياغتنا. وقد رأينا بعضها من هذه الصياغة في مثالنا التجريبي في أعلى الفصل، لكنّ هذا ملخّص سريع لكل الصياغات التي سننفّذها.
تُدرَج البيانات من السياق باستخدام أقواس معقوفة مزدوجة:
<p>Welcome, {{user_name}}!</p>
تُوفَّر البيانات المتاحة للقالب في السياق عند عرض (render) القالب. والمزيد عن ذلك لاحقًا.
وتوفّر محرّكات القوالب عادةً وسيلة للوصول إلى العناصر داخل البيانات باستخدام صياغة مبسّطة ومتساهلة. في بايثون، يكون لكل هذه التعبيرات (expressions) آثار مختلفة:
dict["key"]
obj.attr
obj.method()
وفي صياغة قوالبنا، تُعبَّر كل هذه العمليات بنقطة:
dict.key
obj.attr
obj.method
وستُتيح النقطة الوصول إلى خصائص الكائن أو قيم القاموس، وإذا كانت القيمة الناتجة قابلة للاستدعاء (callable) فسيجري استدعاؤها تلقائيًّا. وهذا يختلف عن شيفرة بايثون، إذ يحتاج هناك إلى استخدام صياغة مختلفة لهذه العمليات. ويؤدي هذا إلى صياغة قوالب أبسط:
<p>The price is: {{product.price}}, with a {{product.discount}}% discount.</p>
ويمكنك استخدام دوالّ تُسمّى مرشِّحات (filters) لتعديل القيم. وتُستدعى المرشِّحات بشرطة عمودية:
<p>Short name: {{story.subject|slugify|lower}}</p>
وعادةً ما تتطلّب الصفحات المثيرة للاهتمام قدرًا من اتخاذ القرار على الأقلّ، لذا تتوفّر الجمل الشرطية:
{% if user.is_logged_in %}
<p>Welcome, {{ user.name }}!</p>
{% endif %}
وتتيح لنا الحلقات تضمين مجموعات البيانات في صفحاتنا:
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}: {{ product.price|format_price }}</li>
{% endfor %}
</ul>
وكما في لغات البرمجة الأخرى، يمكن تداخل الجمل الشرطية والحلقات لبناء بٍ منطقية معقّدة.
وأخيرًا، كي نتمكّن من توثيق قوالبنا، تظهر التعليقات بين أقواس تحمل رموز التجزئة:
{# This is the best template ever! #}
نُهج التنفيذ
بصورة عامة، سيكون لمحرّك القوالب مرحلتان رئيسيّتان: تحليل القالب، ثمّ عرض القالب.
وينطوي العرض تحديدًا على:
- إدارة السياق الديناميكي، وهو مصدر البيانات
- تنفيذ عناصر المنطق
- تنفيذ الوصول بالنقطة وتشغيل المرشِّحات
والسؤال عمّا نمرّره من مرحلة التحليل إلى مرحلة العرض هو جوهر الأمر. فما الذي ينتجه التحليل ويمكن عرضه؟ هناك خياران رئيسيّان؛ وسنسمّيهما التفسير (interpretation) و_التصريف_ (compilation)، مستخدمَين المصطلحَين بمعنى فضفاض مأخوذ من تنفيذي لغات أخرى.
في نموذج التفسير، ينتج عن التحليل بنية بيانات تمثّل بنية القالب. وتمرّ مرحلة العرض على تلك بنية البيانات، مجمِّعة النصّ الناتج بناءً على التعليمات التي تجدها. وكمثال من العالم الحقيقي، يستخدم محرّك قوالب Django هذا الأسلوب.
في نموذج التصريف، ينتج عن التحليل شكل ما من الشيفرة القابلة للتنفيذ مباشرة. وتنفّذ مرحلة العرض تلك الشيفرة، فينتج النصّ. وJinja2 وMako مثالان على محرّكي قوالب يستخدمان نهج التصريف.
ويستخدم تنفيذنا للمحرّك التصريف: فنحن نصرّف القالب إلى شيفرة بايثون. وحين تُنفَّذ، تُجمِّع شيفرة بايثون النتيجة.
وكُتب محرّك القوالب الموصوف هنا في الأصل كجزء من coverage.py، لإنتاج تقارير HTML. وفي coverage.py، لا وجود إلّا لقواعد قليلة، تُستخدم مرارًا وتكرارًا لإنتاج عدّة ملفات من القالب نفسه. وإجمالًا، كان البرنامج أسرع تشغيلًا إذا صُرِّفت القوالب إلى شيفرة بايثون، لأنّ عملية التصريف كانت أعقد قليلًا، لكنّها لم تكن بحاجة إلّا إلى أن تُنفَّذ مرّة واحدة، في حين تُنفَّذ الشيفرة المصرَّفة مرّاتٍ كثيرة، وتكون أسرع من تفسير بنية بيانات مرّاتٍ كثيرة.
والتصريف إلى بايثون أعقد قليلًا، لكنّه ليس بالسوء الذي قد تظنّه. وإلى جانب ذلك، كما يستطيع أيّ مطوّر أن يخبرك، فإنّ كتابة برنامج لكتابة برنامج أمتع من كتابة برنامج!
ومُصرِّف القوالب الذي كتبناه مثال صغير على تقنية عامّة تُسمّى توليد الشيفرة (code generation). وتُبنى تقنيات كثيرة وقويّة ومرنة على توليد الشيفرة، بما فيها مُصرِّفات لغات البرمجة. وقد يصبح توليد الشيفرة معقّدًا، لكنّه تقنية مفيدة يُستحسن أن تكون في أدواتك.
وقد يفضّل تطبيق آخر للقوالب النهج المُفسَّر، إذا كانت القوالب ستُستخدم كلٍّ منها مرّاتٍ قليلة فقط. عندئذٍ لن يستردّ جهد التصريف إلى بايثون ثماره في الأمد البعيد، وقد تكون عملية تفسير أبسط أفضل إجمالًا.
التصريف إلى بايثون
وقبل أن نصل إلى شيفرة محرّك القوالب، لننظر إلى الشيفرة التي ينتجها. ستحوّل مرحلة التحليل القالب إلى دالة بايثون. وهذا هو قالبنا الصغير مرّة أخرى:
<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}:
{{ product.price|format_price }}</li>
{% endfor %}
</ul>
سينصرّف محرّكنا هذا القالب إلى شيفرة بايثون. وتبدو شيفرة بايثون الناتجة غير مألوفة، لأنّنا اخترنا بعض الاختصارات التي تنتج شيفرة أسرع بقليل. وهذه هي شيفرة بايثون (أُعيد تنسيقها قليلًا لأغراض القراءة):
def render_function(context, do_dots):
c_user_name = context['user_name']
c_product_list = context['product_list']
c_format_price = context['format_price']
result = []
append_result = result.append
extend_result = result.extend
to_str = str
extend_result([
'<p>Welcome, ',
to_str(c_user_name),
'!</p>\n<p>Products:</p>\n<ul>\n'
])
for c_product in c_product_list:
extend_result([
'\n <li>',
to_str(do_dots(c_product, 'name')),
':\n ',
to_str(c_format_price(do_dots(c_product, 'price'))),
'</li>\n'
])
append_result('\n</ul>\n')
return ''.join(result)
ويُحوَّل كل قالب إلى دالة اسمها render_function تأخذ قاموس بيانات يُسمّى السياق. ويبدأ جسم الدالة بتفريغ بيانات السياق في أسماء محلّية، لأنّها أسرع عند الاستخدام المتكرّر. وتذهب كل بيانات السياق إلى متغيّرات محلّية لها البادئة c_، كي نتمكّن من استخدام أسماء محلّية أخرى دون خوف من التصادم.
وستكون نتيجة القالب سلسلة نصّية. وأسرع طريقة لبناء سلسلة نصّية من أجزاء هي إنشاء قائمة من النصوص ودمجها معًا في النهاية. وستكون result هي قائمة النصوص. ولأنّنا سنضيف نصوصًا إلى هذه القائمة، فإنّنا نلتقط طرقي append وextend فيها في الاسمين المحلّيَين result_append وresult_extend. أمّا آخر متغيّر محلّي ننشئه فهو اختصار to_str للدالة المدمجة str.
وهذه الأنواع من الاختصارات غير مألوفة. فلننظر إليها عن قرب. في بايثون، يُنفَّذ استدعاء دالة على كائن مثل result.append("hello") في خطوتين. أولًا، تُجلب الخاصّية append من كائن النتيجة: result.append. ثمّ تُستدعى القيمة المُجلبَة بوصفها دالة، مع تمرير الوسيط "hello" إليها. ولئن كنا معتادين على رؤية هاتين الخطوتين تُنفَّذان معًا، فإنّهما منفصلتان حقًّا. فإذا حفظت نتيجة الخطوة الأولى، يمكنك تنفيذ الخطوة الثانية على القيمة المحفوظة. وعليه، تفعل مقتطفا بايثون هذان الأمر نفسه:
# The way we're used to seeing it:
result.append("hello")
# But this works the same:
append_result = result.append
append_result("hello")
في شيفرة محرّك القوالب، فصلناها على هذا النحو كي لا نفعل الخطوة الأولى إلّا مرّة واحدة، مهما كرّرنا الخطوة الثانية. وهذا يوفّر علينا قدرًا يسيرًا من الوقت، لأنّنا نتجنّب الوقت الذي يستغرقه البحث عن الخاصّية append.
وهذا مثال على تحسين دقيق (micro-optimization): تقنية برمجية غير مألوفة تمنحنا تحسينات ضئيلة في السرعة. وقد تكون التحسينات الدقيقة أقلّ قابلية للقراءة، أو أكثر إرباكًا، لذا فإنّها لا تُبرَّر إلّا للشيفرة التي ثبت أنّها عنق زجاجة للأداء. ويتّفق المطورون على خلاف حول مقدار ما يُبرَّر من التحسين الدقيق، وبعض المبتدئين يفرطون فيه. وأُضيفت التحسينات هنا إلّا بعد أن أظهرت تجارب التوقيت أنّها تحسّن الأداء، وإن كان بقليل حقًّا. والتحسينات الدقيقة قد تكون تعليمية، لأنّها تستفيد من بعض جوانب بايثون الغريبة، لكنّ لا تفرط في استخدامها في شيفرتك.
والاختصار الخاصّ بـ str هو أيضًا تحسين دقيق. ويمكن أن تكون الأسماء في بايثون محلّية لدالة، أو عامّة على مستوى وحدة، أو مدمجة في بايثون. والبحث عن اسم محلّي أسرع من البحث عن اسم عامّ أو مدمج. نحن معتادون على أنّ str دالة مدمجة متاحة دائمًا، لكنّ بايثون لا يزال بحاجة إلى البحث عن الاسم str كلّما استُخدم. ووضعه في متغيّر محلّي يوفّر علينا شريحةً أخرى صغيرة من الوقت، لأنّ المحلّيّات أسرع من المدمجات.
وبمجرّد أن تُعرَّف تلك الاختصارات، نكون مستعدّين لأسطر بايثون المولَّدة من قالبنا المحدّد. وتُضاف النصوص إلى قائمة النتيجة باستخدام اختصاري append_result أو extend_result، تبعًا لمّا إن كان لدينا نصّ واحد نضيفه أو أكثر من ذلك. ويصبح النصّ الحرفيّ في القالب ثابتًا نصّيًّا بسيطًا.
ووجود الطريقَين append وextend يزيد من التعقيد، لكن تذكّر أنّنا نستهدف أسرع تنفيذ ممكن للقالب، وأنّ استخدام extend لعنصر واحد يعني إنشاء قائمة جديدة من عنصر واحد كي نمرّرها إلى extend.
وتُحسب التعبيرات في {{ ... }} وتُحوَّل إلى نصوص وتُضاف إلى النتيجة. وتُعالَج النقاط في التعبير بواسطة الدالة do_dots المُمرَّرة إلى دالتنا، لأنّ معنى التعبيرات المنقّطة يعتمد على البيانات الموجودة في السياق: قد يكون الوصول إلى خاصّية أو الوصول إلى عنصر، وقد يكون قابلًا للاستدعاء.
وتُحوَّل البنى المنطقية {% if ... %} و{% for ... %} إلى جمل وحلقات بايثون. وسيصبح التعبير في الوسم {% if/for ... %} هو التعبير في جملة if أو for، وسيصبح المحتوى حتى الوسم {% end... %} هو جسم الجملة.
كتابة المحرّك
الآن بعد أن فهمنا ما سيقوم به المحرّك، لنمرّ على التنفيذ خطوةً خطوة.
صنف Templite
قلب محرّك القوالب هو الصنف Templite. (أفهمها؟ إنّها template، لكنّها lite!)
ولدى الصنف Templite واجهة صغيرة. تنشئ كائن Templite مع نصّ القالب، ثمّ يمكنك لاحقًا استخدام الطريقة render عليه لعرض سياق معيّن، أي قاموس البيانات، عبر القالب:
# Make a Templite object.
templite = Templite('''
<h1>Hello {{name|upper}}!</h1>
{% for topic in topics %}
<p>You are interested in {{topic}}.</p>
{% endfor %}
''',
{'upper': str.upper},
)
# Later, use it to render some data.
text = templite.render({
'name': "Ned",
'topics': ['Python', 'Geometry', 'Juggling'],
})
ونمرّر نصّ القالب لحظة إنشاء الكائن كي نتمكّن من إجراء خطوة التصريف مرّة واحدة فقط، ثمّ نستدعي render لاحقًا عدّة مرّات لإعادة استخدام النتائج المصرَّفة.
كما يقبل الباني (constructor) قاموس قيم، أي سياق أوليًّا. تُخزَّن هذه القيم في كائن Templite، وتكون متاحة عند عرض القالب لاحقًا. وهي جيّدة لتعريف دوالّ أو ثوابت نريدها أن تكون متاحة في كل مكان، مثل upper في المثال السابق.
ووقبل أن نتحدّث عن تنفيذ الصنف Templite، لدينا دالّة مساعدة نعرّفها أولًا: CodeBuilder.
CodeBuilder
معظم العمل في محرّكنا هو تحليل القالب وإنتاج شيفرة بايثون اللازمة. ولتساعدنا في إنتاج شيفرة بايثون، لدينا الصنف CodeBuilder الذي يتولّى أعمال التتبّع والحسابات عنا أثناء بنينا لشيفرة بايثون. فهو يضيف أسطر شيفرة، ويدير المسافة البادئة، وأخيرًا يقدّم لنا القيم من شيفرة بايثون المصرَّفة.
ويتولّى كائن CodeBuilder واحد مجزوءًا كاملًا من شيفرة بايثون. وكما هو مستخدَم في محرّك قوالبنا، فإنّ مجزوء بايثون هو دائمًا تعريف دالة واحدة كاملة. لكنّ الصنف CodeBuilder لا يفترض أنّه سيكون دالة واحدة فقط. وهذا يجعل شيفرة CodeBuilder أكثر عمومية، وأقلّ ترابطًا ببقية شيفرة محرّك القوالب.
وكما سنرى، فإنّنا نستخدم أيضًا كائنات CodeBuilder متداخلة، لجعل من الممكن وضع شيفرة في بداية الدالة رغم أنّنا لا نعرف ما ستكونه حتى تكاد ننتهي.
ويحتفظ كائن CodeBuilder بقائمة نصوص تشكّل معًا شيفرة بايثون النهائية. والحالة الوحيدة الأخرى التي يحتاجها هي مستوى المسافة البادئة الحالي:
class CodeBuilder(object):
"""Build source code conveniently."""
def __init__(self, indent=0):
self.code = []
self.indent_level = indent
ولا يفعل CodeBuilder الكثير. وتضيف add_line سطرًا جديدًا من الشيفرة، فتُزاح النصّ تلقائيًّا إلى مستوى المسافة البادئة الحالي، وتضيف سطرًا جديدًا:
def add_line(self, line):
"""Add a line of source to the code.
Indentation and newline will be added for you, don't provide them.
"""
self.code.extend([" " * self.indent_level, line, "\n"])
وتزيد indent وdedent من مستوى المسافة البادئة أو تنقصانه:
INDENT_STEP = 4 # PEP8 says so!
def indent(self):
"""Increase the current indent for following lines."""
self.indent_level += self.INDENT_STEP
def dedent(self):
"""Decrease the current indent for following lines."""
self.indent_level -= self.INDENT_STEP
ويُدار add_section بواسطة كائن CodeBuilder آخر. وهذا يتيح لنا الاحتفاظ بمرجع إلى موضع في الشيفرة، وإضافة نصّ إليه لاحقًا. وقائمة self.code هي في معظمها قائمة نصوص، لكنّها ستحتفظ أيضًا بمراجع إلى هذه المقاطع:
def add_section(self):
"""Add a section, a sub-CodeBuilder."""
section = CodeBuilder(self.indent_level)
self.code.append(section)
return section
ويُنتج __str__ سلسلة واحدة تضمّ كل الشيفرة. وهذا ببساطة يدمج كل النصوص الموجودة في self.code. ولاحظ أنّه بما أنّ self.code قد يحتوي على مقاطع، فقد يستدعي كائنات CodeBuilder أخرى بشكل متكرّر:
def __str__(self):
return "".join(str(c) for c in self.code)
وتقدّم get_globals القيم النهائية بتنفيذ الشيفرة. فهي تحوّل الكائن إلى نصّ، وتنفّذه للحصول على تعريفاته، وتُعيد القيم الناتجة:
def get_globals(self):
"""Execute the code, and return a dict of globals it defines."""
# A check that the caller really finished all the blocks they started.
assert self.indent_level == 0
# Get the Python source as a single string.
python_source = str(self)
# Execute the source, defining globals, and return them.
global_namespace = {}
exec(python_source, global_namespace)
return global_namespace
وتستخدم هذه الطريقة الأخيرة بعض سمات بايثون الغريبة. فالدالة exec تنفّذ سلسلة تحتوي على شيفرة بايثون. والوسيط الثاني لـ exec هو قاموس يجمع المتغيّرات العامة (globals) التي عرّفتها الشيفرة. فمثلًا، إن فعلنا ما يلي:
python_source = """\
SEVENTEEN = 17
def three():
return 3
"""
global_namespace = {}
exec(python_source, global_namespace)
فإنّ global_namespace['SEVENTEEN'] يساوي 17، وglobal_namespace['three'] هو دالة حقيقية اسمها three.
ولئن كنّا نستخدم CodeBuilder لإنتاج دالة واحدة فقط، فلا شيء هنا يحدّه بهذا الاستخدام. وهذا يجعل الصنف أبسط في التنفيذ وأسهل في الفهم.
ويتيح لنا CodeBuilder إنشاء مجزوء من شيفرة بايثون المصدرية، ولا يملك أيّ معرفة محدّدة بمحرّك القوالب لدينا إطلاقًا. يمكننا استخدامه بحيث تُعرَّف في بايثون ثلاث دوالّ مختلفة، ثمّ تُعيد get_globals قاموسًا من ثلاث قيم، وهي الدوالّ الثلاث. وبمناسبة الحديث، فإنّ محرّك قوالبنا لا يحتاج إلّا إلى تعريف دالة واحدة. لكنّ تصميم برمجيات أفضل أن نُبقي تفصيل التنفيذ هذا داخل شيفرة محرّك القوالب، وأن نُخرجه من صنف CodeBuilder.
ولئن كنّا نستخدمه فعلًا—لتعريف دالة واحدة—فإنّ جعل get_globals تُعيد القاموس يُبقي الشيفرة أكثر ترابطًا (modular)، لأنّها لا تحتاج إلى معرفة اسم الدالة التي عرّفناها. فأيًّا كان اسم الدالة الذي نعرّفه في شيفرة بايثون المصدرية، يمكننا استرجاع ذلك الاسم من القاموس الذي تُعيده get_globals.
والآن يمكننا الانتقال إلى تنفيذ صنف Templite نفسه، ورؤية كيفية استخدام CodeBuilder وأين يُستخدم.
تنفيذ صنف Templite
ومعظم شيفرتنا موجود في صنف Templite. وكما ناقشنا، لديه مرحلة تصريف ومرحلة عرض.
التصريف
ويجري كل العمل اللازم لتصريف القالب إلى دالة بايثون داخل باني الصنف Templite. أولًا تُحفظ السياقات جانبًا:
def __init__(self, text, *contexts):
"""Construct a Templite with the given `text`.
`contexts` are dictionaries of values to use for future renderings.
These are good for filters and global values.
"""
self.context = {}
for context in contexts:
self.context.update(context)
ولاحظ أنّنا استخدمنا *contexts كوسيط. وتشير النجمة إلى أنّ أيّ عدد من الوسائط الموضعية سيُحزَم في طور (tuple) ويُمرَّر باسم contexts. ويُسمّى هذا تفريغ الوسائط (argument unpacking)، ويعني أنّ المُستدعي يمكنه تقديم عدد من قاموسات السياقات المختلفة. والآن، أيّ من هذه الاستدعاءات صالح:
t = Templite(template_text)
t = Templite(template_text, context1)
t = Templite(template_text, context1, context2)
وتُقدَّم وسائط السياق (إن وُجدت) إلى الباني على هيئة طور من السياقات. يمكننا عندئذٍ المرور على طور contexts، والتعامل مع كلٍّ منها بالترتيب. ونكتفي ببساطة بإنشاء قاموس واحد مدمج اسمه self.context يحوي محتويات جميع السياقات المقدَّمة. وإذا قدُّمت أسماء مكرّرة في السياقات، فالأخير هو الفائز.
ولجعل دالتنا المصرَّفة في أقصى درجة من السرعة، نستخرج متغيّرات السياق إلى متغيّرات بايثون محلّية. وسنحصل على تلك الأسماء بالاحتفاظ بمجموعة من أسماء المتغيّرات التي نصادفها، لكنّنا نحتاج أيضًا إلى تتبّع أسماء المتغيّرات المعرَّفة في القالب، أي متغيّرات الحلقة:
self.all_vars = set()
self.loop_vars = set()
وسنرى لاحقًا كيف تُستخدم هذه المجموعة للمساعدة في بناء مقدّمة (prologue) دالتنا. أولًا، سنستخدم الصنف CodeBuilder الذي كتبناه سابقًا لنبدأ ببناء دالتنا المصرَّفة:
code = CodeBuilder()
code.add_line("def render_function(context, do_dots):")
code.indent()
vars_code = code.add_section()
code.add_line("result = []")
code.add_line("append_result = result.append")
code.add_line("extend_result = result.extend")
code.add_line("to_str = str")
وهنا نبني كائن CodeBuilder ونبدأ بكتابة أسطر فيه. وستُسمّى دالتنا في بايثون render_function، وستأخذ وسيطين: context هو قاموس البيانات الذي ينبغي أن تستخدمه، وdo_dots هي دالة تنفّذ الوصول إلى الخصائص بالنقطة.
والسياق هنا هو دمج بين سياق البيانات الممرَّر إلى باني Templite وسياق البيانات الممرَّر إلى دالة العرض. وهو المجموعة الكاملة من البيانات المتاحة للقالب، وقد أنشأناها في باني Templite.
ولاحظ أنّ CodeBuilder بسيط جدًّا: فهو لا «يعرف» شيئًا عن تعريفات الدوالّ، بل عن أسطر الشيفرة فقط. وهذا يُبقي CodeBuilder بسيطًا، سواء في تنفيذه أو في استخدامه. يمكننا قراءة الشيفرة المولَّدة هنا دون أن نضطر إلى استيعاب ذهني لكائنات CodeBuilder متخصّصة أكثر مما ينبغي.
وننشئ مقطعًا اسمه vars_code. وسنكتب لاحقًا أسطر استخراج المتغيّرات في ذلك المقطع. ويتيح لنا كائن vars_code أن نحتفظ بموضع في الدالة يمكن ملؤه لاحقًا حين تتوفّر لدينا المعلومات التي نحتاجها.
ثمّ تُكتب أربعة أسطر ثابتة، تعرّف قائمة النتائج، واختصارات لطريقة الإلحاق بها أو توسيعها، واختصارًا للدالة المدمجة str(). وكما ناقشنا سابقًا، فإنّ هذه الخطوة الغريبة تستخرج قدرًا أكبر قليلًا من الأداء من دالة العرض لدينا.
والسبب في وجود اختصارَي append وextend معًا هو كي نتمكّن من استخدام الطريقة الأكثر فاعلية، تبعًا لمّا إن كان لدينا سطر واحد نضيفه إلى نتيجتنا أو أكثر من ذلك.
وبعد ذلك نعرّف دالة داخلية تساعدنا في تخزين نصوص المخرجات مؤقتًا (buffering):
buffered = []
def flush_output():
"""Force `buffered` to the code builder."""
if len(buffered) == 1:
code.add_line("append_result(%s)" % buffered[0])
elif len(buffered) > 1:
code.add_line("extend_result([%s])" % ", ".join(buffered))
del buffered[:]
ننشئ مجزوءات من المخرجات يجب أن تدخل في دالتنا المصرَّفة، فنحتاج إلى تحويلها إلى استدعاءات دوالّ تُلحق بنتيجتنا. ونودّ أن ندمج استدعاءات الإلحاق المتكرّرة في استدعاء extend واحد. وهذه تحسينٌ دقيق آخر. ولإتاحة ذلك، فإنّنا نخزّن المجزوءات مؤقتًا.
وتحمل قائمة buffered نصوصًا لم تُكتب بعد في شيفرة دالتنا المصدرية. ومع تقدّم تصريف القالب، سنُلحق نصوصًا بـ buffered، ونفرغها في شيفرة الدالة حين نصل إلى نقاط تدفّق التحكم، مثل جمل if، أو بدايات الحلقات أو نهاياتها.
ودالة flush_output هي إغلاق (closure)، وهي كلمة فاخرة تعني دالة تشير إلى متغيّرات خارج نفسها. هنا تشير flush_output إلى buffered وcode. وهذا يُبسّط استدعاءاتنا للدالة: لا نحتاج إلى أن نخبر flush_output ما المخزن المؤقت الذي يفرغه، ولا أين يفرغه؛ فهي تعرف ذلك كلّه ضمنًا.
وإذا لم يكن هناك إلّا نصّ واحد مخزَّن مؤقتًا، فإنّ اختصار append_result يُستخدم لإلحاقه بالنتيجة. وإذا خُزِّن أكثر من نصّ، فإنّ اختصار extend_result يُستخدم بكلّها لإضافتها إلى النتيجة. ثمّ تُفرَغ القائمة المؤقتة كي يتسنّ تخزين نصوص أكثر مؤقتًا.
وستضيف بقية شيفرة التصريف أسطرًا إلى الدالة بإلحاقها بـ buffered، ثمّ تستدعي في النهاية flush_output لكتابتها إلى CodeBuilder.
وبوجود هذه الدالة في مكانها، يمكننا وضع سطر شيفرة في مُصرِّفنا بهذا الشكل:
buffered.append("'hello'")
أي أنّ دالة بايثون المصرَّفة لدينا ستحتوي على هذا السطر:
append_result('hello')
أي أنّه سيضيف النصّ hello إلى المخرجات المعروضة للقالب. ولدينا هنا عدّة مستويات من التجريد قد يكون من الصعب إبقاءها في الحسبان. ويستخدم المُصرِّف \newline buffered.append("'hello'")، الذي يُنشئ append_result('hello') في دالة بايثون المصرَّفة، والتي حين تُنفَّذ تُلحق hello بنتيجة القالب.
ونعد إلى صنف Templite. وبينما نحلل بنى تدفّق التحكم، نودّ التحقّق من أنّها متداخلة على نحو صحيح. وقائمة ops_stack هي مكدّس من النصوص:
ops_stack = []
وحين نصادف وسمًا مثل {% if .. %}، سندفع 'if' إلى المكدّس. وحين نجد وسم {% endif %}، يمكننا إخراج العنصر الأقصى من المكدّس والإبلاغ عن خطأ إن لم يكن هناك 'if' في قمة المكدّس.
والآن يبدأ التحليل الحقيقي. نقسّم نصّ القالب إلى عدد من الرموز (tokens) باستخدام تعبير نمطي (regular expression)، أو regex. وقد تكون التعبيرات النمطية مخيفة: فهي ترميز مضغوط جدًّا لمطابقة الأنماط المعقّدة. وهي أيضًا شديدة الكفاءة، لأنّ تعقيد مطابقة النمط مُنفَّذ بلغة C داخل محرّك التعبيرات النمطية، لا داخل شيفرة بايثون الخاصة بك. وهذا هو تعبيرنا النمطي:
tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)
ويبدو هذا معقّدًا؛ لنفكّكه.
وستقسّم الدالة re.split سلسلة باستخدام تعبير نمطي. ونمطنا محاط بأقواس، لذا ستُستخدم المطابقات لتقسيم السلسلة، وستُعاد أيضًا كقطع في قائمة التقسيم. وسيطابق نمطنا صياغات الوسوم لدينا، لكنّنا أحطناه بأقواس كي تُقسَّم السلسلة عند الوسوم، وتُعاد الوسوم أيضًا.
وتعني الراية (?s) في التعبير النمطي أنّ النقطة يجب أن تطابق حتّى سطرًا جديدًا. وبعد ذلك لدينا مجموعتنا المحاطة بأقواس من ثلاثة بدائل: {{.*?}} يطابق تعبيرًا، و{%.*?%} يطابق وسمًا، و{#.*?#} يطابق تعليقًا. وفي جميع هذه نستخدم .*? لمطابقة أيّ عدد من المحارف، لكن بأقصر تسلسل يُطابق.
ونتيجة re.split هي قائمة من النصوص. فمثلًا، نصّ القالب التالي:
<p>Topics for {{name}}: {% for t in topics %}{{t}}, {% endfor %}</p>
سيُقسَّم إلى هذه القطع:
[
'<p>Topics for ', # literal
'{{name}}', # expression
': ', # literal
'{% for t in topics %}', # tag
'', # literal (empty)
'{{t}}', # expression
', ', # literal
'{% endfor %}', # tag
'</p>' # literal
]
وبمجرّد أن يُقسَّم النصّ إلى رموز على هذا النحو، يمكننا المرور على الرموز في حلقة، والتعامل مع كلٍّ منها على حدة. وبتقسيمها تبعًا لنوعها، يمكننا معالجة كل نوع على حدة.
وشيفرة التصريف هي حلقة تمرّ على هذه الرموز:
for token in tokens:
ويُفحص كل رمز لمعرفة أيّ الحالات الأربع ينتمي إليها. يكفي النظر إلى المحرفين الأوّلين. والحالة الأولى هي تعليق، وهي سهلة المعالجة: تجاهله ببساطة والانتقال إلى الرمز التالي:
if token.startswith('{#'):
# Comment: ignore it and move on.
continue
وفي حالة تعبيرات {{...}}، نزيل القوسين الأماميين والخلفيين، ونزيل المسافات البيضاء، ونمرّر التعبير كاملًا إلى _expr_code:
elif token.startswith('{{'):
# An expression to evaluate.
expr = self._expr_code(token[2:-2].strip())
buffered.append("to_str(%s)" % expr)
وستصرّف الدالة _expr_code تعبير القالب إلى تعبير بايثون. وسنرى تلك الدالة لاحقًا. ونستخدم الدالة to_str لإجبار قيمة التعبير على أن تكون نصًّا، وإضافتها إلى نتيجتنا.
والحالة الثالثة هي الكبيرة: وسوم {% ... %}. وهذه بنى تدفّق تحكم ستتحوّل إلى بنى تدفّق تحكم في بايثون. أولًا علينا تفريغ أسطر المخرجات المخزَّنة مؤقتًا، ثمّ نستخرج قائمة كلمات من الوسم:
elif token.startswith('{%'):
# Action tag: split into words and parse further.
flush_output()
words = token[2:-2].strip().split()
والآن لدينا ثلاث حالات فرعية، بناءً على الكلمة الأولى في الوسم: if أو for أو end. وتُظهر حالة if معالجتنا البسيطة للأخطاء وتوليدنا للشيفرة:
if words[0] == 'if':
# An if statement: evaluate the expression to determine if.
if len(words) != 2:
self._syntax_error("Don't understand if", token)
ops_stack.append('if')
code.add_line("if %s:" % self._expr_code(words[1]))
code.indent()
وينبغي أن يحتوي وسم if على تعبير واحد، لذا ينبغي أن تحتوي قائمة words على عنصرين فقط. وإن لم يكن كذلك، فإنّنا نستخدم الدالة المساعدة _syntax_error لرمي استثناء خطأ نحوي. وندفع 'if' إلى ops_stack كي نتمكّن من التحقّق من وسم endif. ويُصرَّف الجزء التعبيري من وسم if إلى تعبير بايثون بواسطة _expr_code، ويُستخدم كتعبير شرطي في جملة if في بايثون.
والنوع الثاني من الوسوم هو for، وسيُصرَّف إلى جملة for في بايثون:
elif words[0] == 'for':
# A loop: iterate over expression result.
if len(words) != 4 or words[2] != 'in':
self._syntax_error("Don't understand for", token)
ops_stack.append('for')
self._variable(words[1], self.loop_vars)
code.add_line(
"for c_%s in %s:" % (
words[1],
self._expr_code(words[3])
)
)
code.indent()
نُجري فحصًا للصياغة وندفع 'for' إلى المكدّس. تتحقّق الدالة _variable من صياغة المتغيّر، وتضيفه إلى المجموعة التي نقدّمها. وهكذا نجمع أسماء جميع المتغيّرات أثناء التصريف. وسنحتاج لاحقًا إلى كتابة مقدّمة دالتنا، حيث سنفرّغ جميع أسماء المتغيّرات التي نحصل عليها من السياق. ولإجراء ذلك على نحو صحيح، نحتاج إلى معرفة أسماء جميع المتغيّرات التي صادفناها، وهي self.all_vars، وأسماء جميع المتغيّرات التي تعرّفها الحلقات، وهي self.loop_vars.
ونضيف سطرًا واحدًا إلى شيفرة دالتنا، وهي جملة for. وكلّ متغيّرات قالبنا تتحوّل إلى متغيّرات بايثون عبر البادئة c_، كي نعرف أنّها لن تتعارض مع أسماء أخرى نستخدمها في دالتنا المكتوبة بـ بايثون. ونستخدم _expr_code لتصريف تعبير التكرار من القالب إلى تعبير تكرار في بايثون.
والنوع الأخير من الوسوم الذي نتعامل معه هو وسم end؛ سواء {% endif %} أو {% endfor %}. والأثر في شيفرة دالتنا المصرَّفة هو نفسه: فقط انزع المسافة البادئة لإنهاء جملة if أو for التي بدأت في وقت سابق:
elif words[0].startswith('end'):
# Endsomething. Pop the ops stack.
if len(words) != 1:
self._syntax_error("Don't understand end", token)
end_what = words[0][3:]
if not ops_stack:
self._syntax_error("Too many ends", token)
start_what = ops_stack.pop()
if start_what != end_what:
self._syntax_error("Mismatched end tag", end_what)
code.dedent()
ولاحظ هنا أنّ العمل الفعلي اللازم لوسم end هو سطر واحد: إزالة المسافة البادئة عن شيفرة الدالة. وبقيّة هذه الشقّة كلّها فحص أخطاء للتأكّد من أنّ القالب مُشكَّل على نحو صحيح. وهذا ليس بأمر غير مألوف في شيفرة ترجمة البرامج.
ولأنّنا نتحدّث عن معالجة الأخطاء، فإنّ كان الوسم ليس if ولا for ولا end، فإنّنا لا نعرف ما هو، لذا نرمي خطأ نحويًّا:
else:
self._syntax_error("Don't understand tag", words[0])
وانتهينا من الصياغات الخاصّة الثلاث المختلفة ({{...}} و{#...#} و{%...%}). والمتبقي هو المحتوى الحرفيّ. وسنضيف النصّ الحرفيّ إلى المخرجات المخزَّنة مؤقتًا، مستخدمَين الدالة المدمجة repr لإنتاج ثابت نصّي في بايثون للرمز:
else:
# Literal content. If it isn't empty, output it.
if token:
buffered.append(repr(token))
ولو لم نستخدم repr، لسقطنا عند أسطر مثل هذه في دالتنا المصرَّفة:
append_result(abc) # Error! abc isn't defined
ونحتاج إلى أن تكون القيمة بين علامتي تنصيص هكذا:
append_result('abc')
وتقدّم الدالة repr علامات الاقتباس حول النصّ نيابةً عنا، وتوفّر أيضًا الشرطات المائلة العكسية حيث يلزم:
append_result('"Don\'t you like my hat?" he asked.')
ولاحظ أنّنا نتحقّق أولًا ممّا إذا كان الرمز نصًّا فارغًا باستخدام if token:، لأنّه لا فائدة من إضافة نصّ فارغ إلى المخرجات. ولأنّ تعبيرنا النمطي يقسم عند صياغة الوسوم، فإنّ الوسوم المتجاورة سيكون بينهما رمز فارغ. وهذا الفحص هنا طريقة سهلة لتجنّب وضع جمل append_result("") عديمة الفائدة في دالتنا المصرَّفة.
وبهذا تكتمل الحلقة على جميع رموز القالب. وحين تنتهي الحلقة، يكون القالب كلّه قد جرى معالجته. ولدينا فحص أخير علينا إجراؤه: إن لم تكن ops_stack فارغة، فيجب أن يكون ينقصنا وسم end. ثمّ نفرّغ المخرجات المخزَّنة مؤقتًا في شيفرة الدالة:
if ops_stack:
self._syntax_error("Unmatched action tag", ops_stack[-1])
flush_output()
وكان قد أُنشئ في بداية الدالة مقطع. وكان دوره تفريغ متغيّرات القالب من السياق إلى متغيّرات بايثون محلّية. والآن بعد أن عالجنا القالب كلّه، نعرف أسماء جميع المتغيّرات، يمكننا كتابة الأسطر في هذه المقدّمة.
وعلينا أن نعمل قليلًا لمعرفة الأسماء التي نحتاج إلى تعريفها. ولنتّفق على قالبنا النموذجي:
<p>Welcome, {{user_name}}!</p>
<p>Products:</p>
<ul>
{% for product in product_list %}
<li>{{ product.name }}:
{{ product.price|format_price }}</li>
{% endfor %}
</ul>
وهناك متغيّران مستخدَمان هنا، وهما user_name وproduct. وستحتوي مجموعة all_vars على الاسمين معًا، لأنّ كليهما مستخدَم في تعبيرات {{...}}. لكن user_name وحده هو ما يحتاج إلى الاستخراج من السياق في المقدّمة، لأنّ product معرَّف بواسطة الحلقة.
وكل المتغيّرات المستخدَمة في القالب موجودة في المجموعة all_vars، وكل المتغيّرات المعرَّفة في القالب موجودة في loop_vars. وقد رُسمت جميع الأسماء الموجودة في loop_vars أصلًا في الشيفرة لأنّها مستخدَمة في الحلقات. إذن نحتاج إلى تفريغ أي اسم في all_vars ليس في loop_vars:
for var_name in self.all_vars - self.loop_vars:
vars_code.add_line("c_%s = context[%r]" % (var_name, var_name))
ويصبح كل اسم سطرًا في مقدّمة الدالة، يفرّغ متغيّر السياق في متغيّر محلّي باسم مناسب.
ونحن على وشك الانتهاء من تصريف القالب إلى دالة بايثون. وقد كانت دالتنا تُلحق نصوصًا بـ result، لذا فإنّ السطر الأخير من الدالة هو ببساطة دمجها كلّها وإعادتها:
code.add_line("return ''.join(result)")
code.dedent()
والآن بعد أن فرغنا من كتابة المصدر لدالتنا المصرَّفة في بايثون، نحتاج إلى الحصول على الدالة نفسها من كائن CodeBuilder لدينا. وتُنفّذ الدالة get_globals شيفرة بايثون التي كنّا نجمّعها. وتذكّر أنّ شيفرتنا هي تعريف دالة (يبدأ بـ def render_function(..):)، لذا فإنّ تنفيذ الشيفرة سيعرّف render_function، لكنّه لن ينفّذ جسم render_function.
ونتيجة get_globals هي قاموس القيم المعرَّفة في الشيفرة. ونلتقط قيمة render_function منه، ونحفظها كخاصّية في كائن Templite لدينا:
self._render_function = code.get_globals()['render_function']
والآن أصبحت self._render_function دالة بايثون قابلة للاستدعاء. وسنستخدمها لاحقًا، أثناء مرحلة العرض.
تصريف التعبيرات
ولم نَرَ بعد جزءًا مهمًّا من عملية التصريف: الدالة _expr_code التي تصرّف تعبير قالب إلى تعبير بايثون. ويمكن أن تكون تعبيرات قالبنا ببساطة اسمًا واحدًا:
{{user_name}}
أو يمكن أن تكون تسلسلًا معقّدًا من عمليات الوصول إلى الخصائص والمرشِّحات:
{{user.name.localized|upper|escape}}
وستتعامل دالتنا _expr_code مع كل هذه الاحتمالات. وكما في التعبيرات في أيّ لغة، تُبنى تعبيراتنا بشكل متكرّر: فالتعبيرات الكبيرة تتكوّن من تعبيرات أصغر. والتعبير الكامل مفصول بعلامات الأنبوب، حيث تكون القطعة الأولى مفصولة بنقاط، وهكذا. لذا تأخذ دالتنا صيغة متكرّرة بشكل طبيعي:
def _expr_code(self, expr):
"""Generate a Python expression for `expr`."""
والحالة الأولى التي يجب التفكير فيها هي أنّ تعبيرنا يحتوي على أنابيب. وإن كان كذلك، فإنّنا نقسّمه إلى قائمة من القطع المفصولة بأنابيب. وتُمرَّر القطعة الأولى بشكل متكرّر إلى _expr_code لتحويلها إلى تعبير بايثون.
if "|" in expr:
pipes = expr.split("|")
code = self._expr_code(pipes[0])
for func in pipes[1:]:
self._variable(func, self.all_vars)
code = "c_%s(%s)" % (func, code)
وكل قطعة من القطع المتبقّية المفصولة بأنابيب هي اسم دالة. وتمرّ القيمة عبر تلك الدالة لإنتاج القيمة النهائية. وكل اسم دالة هو متغيّر يُضاف إلى all_vars كي نتمكّن من استخراجه على نحو صحيح في المقدّمة.
وإن لم تكن هناك أنابيب، فقد تكون هناك نقاط. وإن كان كذلك، فسنقسم عند النقاط. ويُمرَّر الجزء الأول بشكل متكرّر إلى _expr_code ليحوّله إلى تعبير بايثون، ثمّ يُعالَج كل اسم نقطي على حدة:
elif "." in expr:
dots = expr.split(".")
code = self._expr_code(dots[0])
args = ", ".join(repr(d) for d in dots[1:])
code = "do_dots(%s, %s)" % (code, args)
ولفهم كيفية تصريف النقاط، تذكّر أنّ x.y في القالب يمكن أن تعني إمّا x['y'] أو x.y في بايثون، أيّهما ينجح؛ وإذا كانت النتيجة قابلة للاستدعاء فسيجري استدعاؤها. وهذا عدم اليقين يعني أنّ علينا تجربة تلك الاحتمالات وقت التنفيذ لا وقت التصريف. لذا نصرّف x.y.z إلى استدعاء دالة، وهو do_dots(x, 'y', 'z'). وستجرّب دالة النقاط طرق الوصول المختلفة وتُعيد القيمة التي نجحت.
وتُمرَّر الدالة do_dots إلى دالتنا المصرَّفة في بايثون وقت التنفيذ. وسنرى تنفيذها بعد قليل.
وتتعامل الشقّة الأخيرة في الدالة _expr_code مع الحالة التي لا يوجد فيها أنبوب ولا نقطة في تعبير الإدخال. وفي تلك الحالة، يكون الأمر مجرّد اسم. نسجّله في all_vars، ونصل إلى المتغيّر باستخدام اسمه في بايثون مسبوقًا بالبادئة:
else:
self._variable(expr, self.all_vars)
code = "c_%s" % expr
return code
دوالّ مساعدة
استخدمنا أثناء التصريف بضع دوالّ مساعدة. فالدالة _syntax_error تجمع ببساطة رسالة خطأ جيّدة وترمي الاستثناء:
def _syntax_error(self, msg, thing):
"""Raise a syntax error using `msg`, and showing `thing`."""
raise TempliteSyntaxError("%s: %r" % (msg, thing))
وتساعدنا الدالة _variable في التحقّق من صحّة أسماء المتغيّرات وإضافتها إلى المجموعات من الأسماء التي جمعناها أثناء التصريف. ونستخدم تعبيرًا نمطيًا للتحقّق من أنّ الاسم مُعرِّف بايثون صالح، ثمّ نضيف الاسم إلى المجموعة:
def _variable(self, name, vars_set):
"""Track that `name` is used as a variable.
Adds the name to `vars_set`, a set of variable names.
Raises an syntax error if `name` is not a valid name.
"""
if not re.match(r"[_a-zA-Z][_a-zA-Z0-9]*$", name):
self._syntax_error("Not a valid name", name)
vars_set.add(name)
وبهذا تكتمل شيفرة التصريف!
العرض
ولم يبقَ سوى كتابة شيفرة العرض. وبما أنّنا صرّفنا قالبنا إلى دالة بايثون، فإنّ شيفرة العرض ليس لديها الكثير لتفعله. عليها أن تجهّز سياق البيانات، ثمّ تستدعي شيفرة بايثون المصرَّفة:
def render(self, context=None):
"""Render this template by applying it to `context`.
`context` is a dictionary of values to use in this rendering.
"""
# Make the complete context we'll use.
render_context = dict(self.context)
if context:
render_context.update(context)
return self._render_function(render_context, self._do_dots)
وتذكّر أنّنا حين أنشأنا كائن Templite بدأنا بسياق بيانات. وهنا ننسخه، وندمج فيه أيّ بيانات مُمرَّرة لهذا العرض. والنسخ يتمّ كي لا ترى استدعاءات العرض المتتالية بيانات بعضها البعض، والدمج يتمّ كي يتوفّر لدينا قاموس واحد نستخدمه للبحث عن البيانات. وهكذا نبني سياق بيانات موحّدًا من السياقات المقدَّمة لحظة إنشاء القالب، مع البيانات المقدَّمة الآن وقت العرض.
ولاحظ أنّ البيانات الممرَّرة إلى render يمكن أن تكتب فوق البيانات الممرَّرة إلى باني Templite. وهذا لا يحدث عادةً، لأنّ السياق الممرَّر إلى الباني يحوي أمورًا شبيهة العامة مثل تعريفات المرشِّحات والثوابت، بينما السياق الممرَّر إلى render يحوي بيانات محدّدة لعرض واحد بعينه.
ثمّ نستدعي ببساطة render_function المصرَّفة لدينا. والوسيط الأول هو سياق البيانات الكامل، والوسيط الثاني هو الدالة التي ستنفّذ دلالة النقاط. ونستخدم التنفيذ نفسه في كل مرّة: وهي دالتنا _do_dots.
def _do_dots(self, value, *dots):
"""Evaluate dotted expressions at runtime."""
for dot in dots:
try:
value = getattr(value, dot)
except AttributeError:
value = value[dot]
if callable(value):
value = value()
return value
وأثناء التصريف، يتحوّل تعبير قالب مثل x.y.z إلى do_dots(x, 'y', 'z'). وتُمرّر هذه الدالة في حلقة على الأسماء النقطية، وتجرّب كل اسم كخاصّية، وإذا فشل ذلك تجرّبه كمفتاح. وهذا ما يمنح صياغة قالبنا الوحيدة المرونة كي تتصرّف إمّا بوصفها x.y أو x['y']. وفي كل خطوة، نتحقّق أيضًا ممّا إذا كانت القيمة الجديدة قابلة للاستدعاء، وإن كانت كذلك نستدعيها. وبمجرد أن ننتهي من جميع الأسماء النقطية، تكون القيمة التي بين أيدينا هي القيمة التي نريدها.
وهنا استخدمنا تفريغ وسائط بايثون من جديد (*dots) كي تتمكّن _do_dots من قبول أيّ عدد من الأسماء النقطية. وهذا يمنحنا دالة مرنة ستعمل مع أيّ تعبير منقّط نصادفه في القالب.
ولاحظ أنّنا عند استدعاء self._render_function نمرّر دالة تُستخدم لتقييم التعبيرات المنقّطة، لكنّنا نمرّر الدالة نفسها دائمًا. كان من الممكن أن نجعل تلك الشيفرة جزءًا من القالب المصرَّف، لكنّها الأسطر الثمانية نفسها لكل قالب، وهذه الأسطر الثمانية جزء من تعريف كيفية عمل القوالب، لا من تفاصيل قالب بعينه. ويبدو تنفيذها بهذا الشكل أنظف من جعل تلك الشيفرة جزءًا من القالب المصرَّف.
الاختبار
ويُرفق بمحرّك القوالب مجموعة اختبارات تغطّي كل السلوك والحالات الحديّة. وأنا في الواقع متجاوز قليلًا حدّ الخمسمئة سطر: محرّك القوالب 252 سطرًا، والاختبارات 275 سطرًا. وهذا شائع في الشيفرة المختبَرة جيدًا: فلديك شيفرة أكثر في اختباراتك مما في منتجك.
ما تُرك جانبًا
وتوفّر محرّكات القوالب الغنيّة بالميزات الكثير أكثر مما نفّذناه هنا. ولإبقاء هذه الشيفرة صغيرة، فإنّنا نترك جانبًا أفكارًا مثيرة مثل:
- وراثة القوالب وإدراجها
- وسوم مخصّصة
- الهروب التلقائي
- وسائط للمرشِّحات
- منطق شرطي معقّد مثل
elseوelif - حلقات بأكثر من متغيّر حلقة
- التحكّم في المسافات البيضاء
ورغم ذلك، فإنّ محرّك القوالب البسيط لدينا مفيد. وفي الواقع، هو محرّك القوالب المستخدم في coverage.py لإنتاج تقارير HTML.
خلاصة
وفي 252 سطرًا، حصلنا على محرّك قوالب بسيط لكنّه قادر. ومحرّكات القوالب الحقيقية لها ميزات أكثر بكثير، لكنّ هذه الشيفرة تشرح الأفكار الأساسية للعملية: صرّف القالب إلى دالة بايثون، ثمّ نفّذ الدالة لإنتاج النصّ الناتج.