FA-TOOLS — Header Component
کدهای آماده caching با functools.lru_cache

کدهای آماده caching با functools.lru_cache در پایتون

خلاصه کاربردی مقاله:

  • استفاده از دکوراتور @lru_cache سرعت برنامه‌ها را با ذخیره نتایج تکراری در حافظه رم، تا صدها برابر افزایش می‌دهد.
  • این ابزار برای توابعی مناسب است که خالص (Pure) هستند و با ورودی یکسان، همیشه خروجی یکسانی تولید می‌کنند.
  • با ابزار درون‌ساخته پایتون می‌توانید بدون راه‌اندازی سرورهای مجزا مانند Redis، کشینگ محلی بسیار سریعی داشته باشید.

استفاده از کشینگ (Caching) یکی از سریع‌ترین و کم‌هزینه‌ترین روش‌ها برای افزایش چشمگیر سرعت اجرای برنامه‌های پایتون است. با بکارگیری دکوراتور functools.lru_cache می‌توانید بدون نیاز به راه‌اندازی ابزارهای پیچیده یا نوشتن منطق‌های طولانی، نتایج محاسبات یا درخواست‌های سنگین را در حافظه موقت (RAM) ذخیره کنید. در این مقاله جامع، علاوه‌بر درک عمیق این مکانیزم، به کدهای آماده و کاربردی دسترسی پیدا می‌کنید که به شما کمک می‌کنند عملکرد پروژه‌های خود را دگرگون کنید.

۱. مفهوم LRU Cache و لزوم استفاده از آن

کدهای آماده caching با functools.lru_cache — تصویر 1

عبارت LRU مخفف Least Recently Used (کمتر استفاده‌شده در این اواخر) است. این الگوریتم به این صورت عمل می‌کند که تعداد مشخصی از خروجی‌های تابع را در حافظه نگه می‌دارد. زمانی که ظرفیت حافظه کش پر شود، این سیستم به‌طور خودکار داده‌هایی را که برای طولانی‌ترین زمان ممکن بدون استفاده مانده‌اند، حذف می‌کند تا فضای کافی برای داده‌های جدید ایجاد شود.

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

۲. ساختار و پارامترهای ورودی lru_cache

کدهای آماده caching با functools.lru_cache — تصویر 2

چه زمانی باید از functools.lru_cache استفاده کنیم؟

باید زمانی از این دکوراتور استفاده کنید که توابعی با ورودی‌های تکراری و خروجی‌های کاملاً یکسان دارید که اجرای آن‌ها زمان‌بر یا پرهزینه است. این ابزار با ذخیره مستقیم نتایج در حافظه رم، زمان پاسخ‌دهی را به نزدیک صفر ثانیه می‌رساند.

دکوراتور lru_cache دو پارامتر کلیدی و کاربردی دارد که رفتار کش را مدیریت می‌کنند:

  • maxsize: حداکثر تعداد آیتم‌هایی که قرار است در حافظه کش ذخیره شوند را مشخص می‌کند. به طور پیش‌فرض مقدار آن برابر با 128 است. اگر مقدار آن را برابر با None قرار دهید، کش بدون محدودیت حجم رشد خواهد کرد که ممکن است به پر شدن رم منجر شود.
  • typed: اگر مقدار آن را روی True بگذارید، آرگومان‌هایی با تایپ‌های مختلف به صورت جداگانه کش می‌شوند؛ برای مثال f(3.0) و f(3) دو ورودی کاملاً متمایز در نظر گرفته خواهند شد.

۳. کدهای آماده: بهینه‌سازی توابع محاسباتی سنگین

کدهای آماده caching با functools.lru_cache — تصویر 3

یکی از ملموس‌ترین سناریوها برای نمایش تاثیر شگفت‌انگیز کش، استفاده از آن در توابع بازگشتی مانند محاسبه اعداد فیبوناچی است. بدون کش، مرتبه زمانی این تابع به شکل نمایی بالا می‌رود، اما با اضافه کردن دکوراتور به خطی (O(N)) تبدیل می‌شود.

from functools import lru_cache
import time

# تعریف دکوراتور با ظرفیت ذخیره‌سازی 256 نتیجه
@lru_cache(maxsize=256)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# تست و اندازه‌گیری زمان اجرا
start_time = time.time()
result = fibonacci(100)
end_time = time.time()

print(f"Result: {result}")
print(f"Execution Time: {end_time - start_time:.6f} seconds")

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

۴. کدهای آماده: کش کردن درخواست‌های API

ارسال درخواست‌های مکرر به یک وب‌سرویس خارجی (API) می‌تواند پروسه‌ای زمان‌بر و هزینه‌بر باشد. در کد آماده زیر، نحوه کش کردن نتایج حاصل از متد requests.get را شبیه‌سازی کرده‌ایم.

import requests
from functools import lru_cache

@lru_cache(maxsize=50)
def get_user_profile(user_id):
    url = f"https://jsonplaceholder.typicode.com/users/{user_id}"
    print(f"--- Requesting from Web: User {user_id} ---")
    response = requests.get(url)
    if response.status_code == 200:
        return response.json()
    return None

# اولین فراخوانی (درخواست واقعی از اینترنت ارسال می‌شود)
user_1 = get_user_profile(1)

# دومین فراخوانی با همان پارامتر (داده از کش محلی خوانده می‌شود)
user_1_cached = get_user_profile(1)

با این متد، دومین فراخوانی هیچ تاخیری نخواهد داشت و بار اضافی به سرورهای مقصد وارد نمی‌کند.

۵. کدهای آماده: مدیریت کش در دیتابیس

بسیاری از داده‌ها در دیتابیس مانند تنظیمات اصلی سایت یا اطلاعات دسته‌بندی‌ها به ندرت تغییر می‌کنند. کش کردن کوئری‌های این بخش‌ها سرعت پاسخ‌دهی را به شدت ارتقا می‌دهد.

import time
from functools import lru_cache

# شبیه‌سازی کلاس دیتابیس
class Database:
    @lru_cache(maxsize=100)
    def fetch_product_price(self, product_id):
        # شبیه‌سازی تاخیر اجرای کوئری سنگین دیتابیس
        time.sleep(1.5)
        return {"id": product_id, "price": 50000}

db = Database()
# اجرای اول - با ۱.۵ ثانیه تاخیر همراه است
print(db.fetch_product_price(101))

# اجرای دوم - آنی خواهد بود
print(db.fetch_product_price(101))

۶. مدیریت، پایش و پاکسازی کش

کش پایتون ابزارهایی برای ارزیابی عملکرد و همچنین پاک‌سازی دستی داده‌های منسوخ شده را در اختیار ما قرار می‌دهد. با متد cache_info() می‌توانید تعداد دفعاتی که مقدار از کش بازگردانده شده (hits) و تعداد دفعاتی که محاسبه واقعی صورت گرفته (misses) را بررسی کنید.

# مشاهده آمار کش
info = get_user_profile.cache_info()
print(f"Hits: {info.hits} | Misses: {info.misses}")

# خالی کردن کامل کش برای یک تابع خاص
get_user_profile.cache_clear()
print("Cache Cleared successfully!")

پاکسازی زمانی کاربرد دارد که داده‌های منبع اصلی تغییر کرده‌اند و لازم دارید کش‌ها مجدداً بروزرسانی شوند.

۷. مقایسه lru_cache با سایر ابزارها

در جدول زیر تفاوت‌های کلیدی بین استفاده از کش محلی پایتون و ابزارهای توزیع‌شده مثل Redis آورده شده است تا بتوانید بهترین انتخاب را متناسب با نیاز پروژه خود داشته باشید:

ویژگی functools.lru_cache Redis / کش سرور
مکان ذخیره‌سازی حافظه موقت رم فرآیند جاری حافظه مجزای سرور خارجی
پیچیدگی راه‌اندازی بسیار کم (یک خط کد) متوسط تا بالا (پیکربندی سرور)
سرعت دسترسی فوق‌العاده سریع (In-memory) سریع (بستگی به سرعت شبکه دارد)
اشتراک‌گذاری اطلاعات غیرممکن بین پروسس‌های مختلف امکان اشتراک‌گذاری در چندین نمونه سرویس

۸. عیب‌یابی سریع و رفع خطاهای رایج

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

خطای TypeError: unhashable type: ‘list’

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

راه‌حل: ورودی‌های لیست را قبل از فرستادن به تابع به نوع tuple تبدیل کنید.

مشکل نشت حافظه (Memory Leak) در متدهای شیء‌گرا

علت: اگر دکوراتور را مستقیماً روی یک متد درون کلاس استفاده کنید، این دکوراتور رفرنس شیء (self) را در حافظه نگه می‌دارد و مانع پاکسازی و جمع‌آوری زباله (Garbage Collection) می‌شود.

راه‌حل: در پایتون‌های مدرن بهتر است از دکوراتور @functools.cached_property یا متدهای جانبی برای کلاس‌ها استفاده کنید.

۹. پرسش‌های متداول

۱. آیا lru_cache زمان انقضا (TTL) برای داده‌ها دارد؟

خیر، این ماژول به صورت درونی مکانیزمی برای منقضی کردن داده‌ها بر اساس زمان (مثلاً پاک کردن پس از ۱۰ دقیقه) ندارد. برای پیاده‌سازی این ویژگی باید از کتابخانه‌های جانبی مانند cachetools استفاده کنید یا با تعریف فانکشن‌های کمکی به صورت دستی کش را خالی کنید.

۲. تفاوت اصلی بین lru_cache و cache@ در پایتون چیست؟

دکوراتور @cache که در نسخه پایتون ۳.۹ معرفی شد، یک حالت ساده‌تر است که ظرفیت بی‌نهایت دارد. یعنی کارکرد آن دقیقاً مثل فرخوانی lru_cache(maxsize=None) است و هیچ آیتمی را به علت پر شدن حافظه حذف نخواهد کرد.

۳. آیا این ابزار برای برنامه‌های ناهمگام (Asyncio) کار می‌کند؟

استفاده مستقیم از lru_cache بر روی توابع Async توصیه نمی‌شود؛ چراکه کلیدهای موقتی کوروتین‌ها (coroutine objects) به درستی کش نمی‌شوند و خروجی مناسبی دریافت نمی‌کنید. برای توابع ناهمگام استفاده از ماژول‌های توسعه‌یافته‌تر ترجیح داده می‌شود.

۴. آیا حافظه اختصاص داده شده به کش با اتمام اجرای برنامه ذخیره می‌ماند؟

خیر، داده‌های ذخیره شده توسط lru_cache کاملاً در حافظه فرار رم حضور دارند؛ به این معنی که با اتمام یافتن یا متوقف شدن فرآیند مفسر پایتون، تمامی داده‌ها پاک شده و در اجرای بعدی، برنامه از ابتدا شروع به پر کردن کش خواهد کرد.

Table of Contents

آخرین نوشته‌ها