تگ {% ifchanged %} در جنگو (Django) یکی از آن قابلیت‌های کم‌شناخته و در عین حال فوق‌العاده کاربردی است که وقتی با آن آشنا می‌شوید، از خودتان می‌پرسید چرا تا حالا از آن استفاده نکرده بودم؛ این تگ به‌ظاهر ساده، در گزارش‌ها، لیست‌های گروه‌بندی‌شده، و هرجایی که می‌خواهید «تکرار بی‌مورد» را حذف کنید، دقیقاً همان ابزاری است که مدت‌ها به دنبالش بوده‌اید. در این راهنما از نحو پایه شروع می‌کنیم، به کاربردهای واقعی می‌رسیم، و در نهایت نکاتی را مرور می‌کنیم که در پروژه‌های بزرگ تفاوت را رقم می‌زنند.

ifchanged دقیقاً چه کاری انجام می‌دهد؟

تگ {% ifchanged %} یک تگ وضعیت‌محور (stateful) در موتور قالب جنگو (Django Template Engine) است. کاری که انجام می‌دهد این است: مقدار عبارت داخلش را با آخرین مقداری که در همان نقطه از قالب دیده، مقایسه می‌کند و فقط اگر متفاوت بود، بلوک داخلی را رندر می‌کند. این سادگی فریبنده است، چون در عمل به شما اجازه می‌دهد بخشی از تصمیمات نمایشی که معمولاً در ویو یا مدل انجام می‌دهید را به لایه قالب منتقل کنید، بدون آنکه منطق تجاری وارد قالب شود.

برای درک بهتر، تصور کنید لیستی از سفارش‌ها را دارید که همه در یک تاریخ ثبت شده‌اند. بدون ifchanged باید یا در ویو تاریخ را استخراج کنید و در حلقه با تکرار مقایسه کنید، یا در قالب متغیر وضعیت دستی نگه دارید که چنین چیزی در موتور قالب جنگو به‌طور پیش‌فرض وجود ندارد. با ifchanged، تنها یک خط کافی است. اگر با تگ های جنگو در طراحی قالب آشنایی دارید، می‌دانید که این سطح از راحتی در موتور قالب جنگو کم‌نظیر است.

نحو پایه و تفاوت کلیدی با if

نحو {% ifchanged %} شبیه {% if %} است ولی یک تفاوت بنیادی دارد: {% if %} هر بار شرط را ارزیابی می‌کند و بر اساس نتیجه تصمیم می‌گیرد، در حالی که {% ifchanged %} مقدار را با «آخرین مقدار دیده‌شده در همان نقطه از قالب» مقایسه می‌کند. این state داخلی در هر رندر قالب از نو مقداردهی می‌شود و به ارث نمی‌رسد.

{% ifchanged item.category %}
  <h3>{{ item.category }}</h3>
{% endifchanged %}
<p>{{ item.title }}</p>

تفاوت دیگر این است که ifchanged می‌تواند بدون آرگومان هم استفاده شود. در آن حالت، کل بلوک داخلی را به‌عنوان یک رشته رندر می‌کند و با رندر دفعه قبل مقایسه می‌کند. این رفتار کمتر استفاده می‌شود ولی در جاهایی که می‌خواهید خروجی کاملاً یکسان حذف شود، مفید است.

{% for day in days %}
  {% ifchanged %}
    <div class="day-block">{{ day }}</div>
  {% endifchanged %}
{% endfor %}

اگر می‌خواهید تفاوت عمیق‌تری با شرط‌های معمولی قالب ببینید، پیشنهاد می‌کنم راهنمای فیلترهای قالب جنگو را هم مرور کنید تا مطمئن شوید ابزار درست را انتخاب می‌کنید، چون خیلی از نیازها با فیلتر حل می‌شوند و نیازی به تگ ندارند.

کاربرد اصلی: حذف تکرار در حلقه‌ها

بدون شک پرکاربردترین سناریو برای ifchanged داخل یک حلقه است. فرض کنید گزارش فروش روزانه دارید که در آن هر ردیف شامل تاریخ و مبلغ است. اگر بخواهید تاریخ را فقط یک بار در ابتدای هر گروه نمایش دهید، ifchanged دقیقاً همین کار را می‌کند.

<table>
{% for order in orders %}
  {% ifchanged order.date %}
    <tr class="date-row">
      <td colspan="3">{{ order.date|date:"Y-m-d" }}</td>
    </tr>
  {% endifchanged %}
  <tr>
    <td>{{ order.customer }}</td>
    <td>{{ order.amount }}</td>
    <td>{{ order.status }}</td>
  </tr>
{% endfor %}
</table>

نکته مهم این است که لیست شما باید بر اساس همان فیلدی که در ifchanged استفاده می‌کنید مرتب باشد. در غیر این صورت، ifchanged در هر ردیف یک مقدار جدید می‌بیند و همه‌چیز را رندر می‌کند. در ویو مطمئن شوید queryset با order_by روی همان فیلد مرتب شده است. برای درک عمیق‌تر این موضوع پیشنهاد می‌کنم راهنمای ORM جنگو را مطالعه کنید.

اگر روی جداول بزرگ کار می‌کنید، این الگو می‌تواند حجم خروجی HTML را به‌طور محسوسی کاهش دهد، چون بلوک‌های تکراری به‌جای N بار، فقط یک بار رندر می‌شوند. تفاوت وقتی محسوس است که هر بلوک شامل markup سنگین یا محاسبات نمایشی باشد.

بلوک else: وقتی تکرار ارزش نمایش دارد

تگ {% ifchanged %} از یک بلوک {% else %} هم پشتیبانی می‌کند. اگر مقدار تغییر نکرده باشد، بلوک else رندر می‌شود. این قابلیت در جاهایی که می‌خواهید به‌جای «هیچ کاری نکردن» یک رفتار جایگزین داشته باشید، مفید است.

{% for log in logs %}
  {% ifchanged log.level %}
    <strong class="level-{{ log.level }}">{{ log.level }}</strong>
  {% else %}
    <span class="level-repeat">⋯</span>
  {% endifchanged %}
{% endfor %}

در مثال بالا وقتی سطح لاگ تغییر نکند، به‌جای تکرار نام سطح، یک نماد کوچک نمایش داده می‌شود. این نوع طراحی، جدول های طولانی را خواناتر می‌کند و چشم کاربر سریع‌تر گروه‌های تازه را تشخیص می‌دهد.

مقایسه چند مقدار به‌طور همزمان

یکی از امکاناتی که کمتر استفاده می‌شود، توانایی ifchanged در پذیرش چندین مقدار به‌طور همزمان است. اگر چند مقدار بدهید، تا وقتی همه با هم یکسان بمانند، بلوک رندر نمی‌شود.

{% for row in rows %}
  {% ifchanged row.city row.country %}
    <h4>{{ row.city }} / {{ row.country }}</h4>
  {% endifchanged %}
  <p>{{ row.address }}</p>
{% endfor %}

این تکنیک وقتی عالی است که بخواهید گروه‌بندی چند سطحی داشته باشید ولی نخواهید برای هر سطح یک حلقه تودرتو بنویسید. اگر با forloop.parentloop هم ترکیب شود، الگوهای جالبی می‌سازد که در ادامه می‌بینیم.

استفاده با فیلترها و عبارات پیچیده

عبارت داخل ifchanged می‌تواند فیلتر داشته باشد. مثلاً اگر بخواهید بر اساس سالِ تاریخ گروه‌بندی کنید:

{% ifchanged item.date|date:"Y" %}
  <h3>سال {{ item.date|date:"Y" }}</h3>
{% endifchanged %}

توجه کنید که ifchanged خودِ عبارت را قبل از فیلتر مقایسه نمی‌کند، بلکه خروجی نهایی را مقایسه می‌کند. پس اگر فیلتر شما مقداری تولید کند که در چند مورد یکسان است، ifchanged فقط یک بار بلوک را رندر می‌کند. اگر با فیلترهای سفارشی کار می‌کنید، پیشنهاد می‌کنم راهنمای تگ های سفارشی جنگو را ببینید تا با رفتار و ترتیب اعمال فیلترها بیشتر آشنا شوید.

forloop.parentloop در حلقه‌های تودرتو

وقتی حلقه تودرتو دارید، ifchanged به‌طور پیش‌فرض در همان سطح حلقه جاری مقایسه می‌کند. اگر بخواهید مقدار حلقه بیرونی هم در مقایسه شرکت کند، می‌توانید از forloop.parentloop استفاده کنید.

{% for category in categories %}
  <h2>{{ category.name }}</h2>
  {% for product in category.products.all %}
    {% ifchanged product.brand %}
      <h3>{{ product.brand }}</h3>
    {% endifchanged %}
    <p>{{ product.name }}</p>
  {% endfor %}
{% endfor %}

در این مثال در هر دسته برندها دوباره گروه‌بندی می‌شوند. اگر می‌خواستید برند تکراری در دسته‌های مختلف هم حذف شود، باید به‌جای product.brand از ترکیب category.name product.brand استفاده کنید که قبلاً دیدیم.

الگوهای واقعی از پروژه‌های تولیدی

چند الگوی تکرارشونده که در پروژه‌های مختلف دیده‌ام را مرور می‌کنیم. این‌ها معمولاً جایی هستند که ifchanged واقعاً ارزشش را نشان می‌دهد.

گزارش لاگ با سطح‌بندی

گزارش لاگ با سطح‌بندی (DEBUG, INFO, WARNING, ERROR) دارید و می‌خواهید سطح‌ها را یک بار در بالای هر گروه نمایش دهید. این الگو را در راهنمای تنظیمات حرفه‌ای جنگو هم استفاده کرده‌ام چون لاگ‌گیری درست بخشی از تنظیمات است.

نمایش تفاوت بین نسخه‌ها

یک ابزار diff ساده: دو لیست از نسخه‌های یک فایل دارید. می‌خواهید فقط خطوطی را نشان دهید که با خط قبلی فرق دارند. اینجا ifchanged بدون آرگومان عالی کار می‌کند.

جداکننده شرطی در لیست‌ها

در جدول های طولانی هر چند خط یک خط جداکننده اضافه کنید. با ifchanged می‌توانید بر اساس یک فیلد، این جداکننده را مدیریت کنید بدون آنکه در ویو حالت نگه دارید.

نمایش خطاهای گروه‌بندی‌شده فرم

در فرم هایی که خطاهای چند فیلد را نمایش می‌دهید، می‌توانید خطاهای هم‌نوع را یک بار به‌عنوان تیتر گروه نمایش دهید. این الگو مخصوصاً در فرم ها در جنگو کاربردی است.

ifchanged، regroup و محاسبه در ویو

سه راه برای گروه‌بندی داده‌ها در قالب جنگو وجود دارد: ifchanged، regroup، و محاسبه در ویو. هر کدام نقاط قوت و ضعف مشخصی دارند.

روشنقاط قوتنقاط ضعف
ifchangedساده، درجا، بدون نیاز به داده از پیش گروه‌بندی‌شدهحالت داخلی، وابسته به ترتیب لیست
regroupگروه‌بندی صریح، قابلیت مرتب‌سازی مجددحافظه بیشتر، نحو پیچیده‌تر
محاسبه در ویوکنترل کامل، تست‌پذیری بالامنطق نمایشی در لایه اشتباه

قاعده‌ای که در پروژه‌ها رعایت می‌کنم: اگر گروه‌بندی صرفاً یک تصمیم نمایشی است، ifchanged. اگر داده باید قبل از رندر ساختاردهی شود (مثلاً برای درخت‌سازی)، ویو. regroup جایی است که بین این دو قرار می‌گیرد. برای آشنایی با regroup و تگ های مشابه، مرور تگ های قالب جنگو کمک می‌کند.

نکات کارایی و رفتار state داخلی

ifchanged state خود را در هر رندر قالب نگه می‌دارد. این یعنی در یک رندر تازه، هیچ مقدار قبلی وجود ندارد. اگر قالب را در چند جا با یک context مشترک رندر کنید، هر رندر از صفر شروع می‌شود. این رفتار برای اکثر کاربردها درست است، ولی اگر انتظار دارید state بین رندرها حفظ شود، باید آن را در ویو مدیریت کنید.

از نظر کارایی، ifchanged تقریباً هیچ هزینه‌ای ندارد. مقایسه یک مقدار ساده در موتور قالب بسیار سریع است. تنها هزینه واقعی، رندر نشدن بلوک در دفعات بعدی است که به‌طور خالص یک بهینه‌سازی است. اگر با مسائل کارایی در قالب‌ها دست‌وپنجه نرم می‌کنید، پیشنهاد می‌کنم روی ویو و URL جنگو تمرکز کنید، چون کوئری‌های پنهان بیشتر از خود تگ ها روی کارایی اثر می‌گذارند.

نکته مهم دیگر: اگر داخل بلوک ifchanged عملیاتی انجام می‌دهید که بار جانبی دارد (مثل include کردن یک قالب سنگین)، بدانید که این عملیات فقط وقتی رندر می‌شود که مقدار تغییر کرده باشد. این ویژگی می‌تواند به‌طور غیرمنتظره‌ای کارایی را بهبود دهد، یا اگر ناخواسته باشد، رفتار را پیچیده کند.

اشتباهات رایجی که در پروژه‌ها دیده‌ام

اشتباه اول، فراموش کردن مرتب‌سازی لیست است. اگر لیست مرتب نباشد، ifchanged در هر ردیف تغییر می‌بیند و عملاً بی‌اثر می‌شود. این خطا در گزارش‌ها بسیار رایج است.

اشتباه دوم، استفاده از ifchanged برای منطق تجاری است. اگر تصمیم شما به داده‌ای خارج از قالب وابسته است، جای آن ویو است نه قالب. همین موضوع درباره تصمیمات امنیتی هم صادق است؛ برای نمونه، هرگز سطح دسترسی کاربر را با ifchanged تعیین نکنید. اصول این کار در راهنمای امنیت جنگو توضیح داده شده است.

اشتباه سوم، انتظار حفظ state بین رندرها است. ifchanged را مثل یک ابزار آماری پایدار نبینید.

اشتباه چهارم، ترکیب ifchanged با include های سنگین است. اگر include شما کوئری به دیتابیس می‌زند، هر رندر یک کوئری است. مطمئن شوید داده قبل از رندر آماده شده است.

اشتباه پنجم، استفاده از ifchanged روی مقادیر شناور است. اگر مقدار شما float است و انتظار دارید با دقت مقایسه شود، ممکن است شگفتی ببینید. برای مقادیر عددی که دقت مهم است، ابتدا با فیلتر floatformat گرد کنید و بعد به ifchanged بدهید.

اشتباه ششم، فراموش کردن اینکه ifchanged روی لیست‌های نامرتب و غیرقطعی (مثل queryset بدون order_by) رفتار غیرقابل پیش‌بینی دارد. همیشه قبل از ifchanged مطمئن شوید داده ترتیب قطعی دارد.

پرسش های پرتکرار درباره تگ ifchanged

ifchanged دقیقاً چه چیزی را مقایسه می‌کند؟

مقدار عبارت داخلش را با آخرین مقداری که در همان نقطه از قالب دیده، مقایسه می‌کند. اگر آرگومان بدهید، خروجی نهایی آن؛ اگر ندهید، کل بلوک داخلی را به‌عنوان رشته.

آیا ifchanged روی همه تکرارهای قالب اثر می‌گذارد؟

خیر. state ifchanged مخصوص همان نقطه از قالب است. اگر دو ifchanged جداگانه داشته باشید، هر کدام state مستقل خود را دارند.

تفاوت ifchanged با if معمولی چیست؟

if هر بار شرط را ارزیابی می‌کند، ولی ifchanged مقدار را با مقدار قبلی مقایسه می‌کند. if برای منطق شرطی است و ifchanged برای حذف تکرار در خروجی.

چرا ifchanged در لیست من کار نمی‌کند؟

به احتمال زیاد لیست شما مرتب نیست. باید با order_by روی همان فیلدی که در ifchanged استفاده می‌کنید، مرتب باشد.

آیا ifchanged روی کوئری دیتابیس اثر می‌گذارد؟

خیر. ifchanged کاملاً در لایه نمایش کار می‌کند و به دیتابیس دست نمی‌زند. ولی اگر داخل بلوک شما دسترسی به روابط مدل باشد که prefetch نشده، می‌تواند کوئری پنهان بسازد.

آیا می‌توانم در ifchanged از متغیرهای context processor استفاده کنم؟

بله. ifchanged با هر عبارتی کار می‌کند که در context موجود باشد، شامل متغیرهای context processor.

ifchanged با include ترکیب می‌شود؟

بله ولی با احتیاط. اگر include شما کوئری می‌زند یا سنگین است، مطمئن شوید داده قبل از رندر آماده است.

آیا ifchanged در قالب های ایمیل هم کار می‌کند؟

بله. موتور قالب جنگو یکسان است، پس ifchanged در ایمیل‌ها هم در دسترس است. فقط توجه کنید که context ایمیل ممکن است متفاوت باشد.

چطور ifchanged را تست کنم؟

با Template و Context در تست های واحد. یک لیست با مقادیر تکراری بسازید، رندر بگیرید و خروجی را با انتظار مقایسه کنید. برای تست های پیچیده‌تر، تسلط بر مبانی پایتون کمک بزرگی است چون درک ساختارهای داده در نوشتن تست‌های بهتر حیاتی است.

یک نگاه تازه به ifchanged

ifchanged یکی از آن ابزارهایی است که ارزشش را وقتی می‌فهمید که یک بار در پروژه واقعی استفاده‌اش کنید. این تگ نشان می‌دهد موتور قالب جنگو با همه محدودیت‌هایش، ابزارهایی دارد که در فریم‌ورک‌های دیگر کم پیدا می‌شود. اگر در پروژه‌هایتان گزارش یا لیست گروه‌بندی‌شده دارید، پیشنهاد می‌کنم قبل از نوشتن کد اضافی در ویو، یک بار ifchanged را امتحان کنید. احتمال زیادی وجود دارد همان یک خط، کار ده‌ها خط کد دیگر را انجام دهد. اگر تجربه‌ای از ifchanged در پروژه‌های واقعی دارید، به‌خصوص اگر با مورد غیرمنتظره‌ای روبرو شده‌اید، خوشحال می‌شوم بشنوم. برای من جالب است بدانم کدام بخش بیشترین زمان را از شما گرفت یا کدام راه‌حل جایگزین را انتخاب کردید، چون همین نوع تجربه‌ها است که به خواننده بعدی کمک می‌کند.