मुख्य सामग्री पर जाएँ

Python कैश: दो सरल तरीके

सीखें कि Python में @functools.lru_cache या @functools.cache जैसे डेकोरेटर से फ़ंक्शन्स को कैसे कैश करें।
· 12 मि॰ पढ़ें

AI के साथ खोजें

इस लेख में, हम Python में कैशिंग के बारे में जानेंगे। हम समझेंगे कि यह क्या है और इसे प्रभावी तरीके से कैसे इस्तेमाल करें।

कैशिंग एक तकनीक है जो एप्लिकेशन के प्रदर्शन को बेहतर बनाती है—प्रोग्राम द्वारा प्राप्त नतीजों को अस्थायी रूप से सहेज कर रखती है ताकि बाद में आवश्यकता होने पर उन्हें दोबारा इस्तेमाल किया जा सके।

इस ट्यूटोरियल में, हम Python में कैशिंग की अलग-अलग तकनीकों के बारे में सीखेंगे, जिनमें @lru_cache और @cache डेकोरेटर शामिल हैं जो functools मॉड्यूल में मिलते हैं।

यदि आप जल्दी में हैं, तो आइए एक बहुत छोटे कैशिंग इम्प्लीमेंटेशन से शुरू करते हैं और फिर विस्तार से आगे बढ़ते हैं।

संक्षिप्त उत्तर: Python कैशिंग इम्प्लीमेंटेशन

Python में कैश बनाने के लिए, हम functools मॉड्यूल का @cache डेकोरेटर इस्तेमाल कर सकते हैं। नीचे दिए कोड में देखें कि print() फ़ंक्शन केवल एक बार चलता है:

import functools

@functools.cache
def square(n):
    print(f"Calculating square of {n}")
    return n * n

# Testing the cached function
print(square(4))
print(square(4))

# Calculating square of 4
# 16
# 16

Python में कैशिंग क्या है?

मान लीजिए हमें एक गणितीय समस्या हल करनी है और सही उत्तर पाने में एक घंटा लगता है। अगर अगले दिन वही समस्या दोबारा हल करनी पड़े, तो शुरू से करने के बजाय पहले का काम दोबारा इस्तेमाल करना सुविधाजनक होगा।

Python में कैशिंग भी इसी सिद्धांत का पालन करती है—यह फ़ंक्शन कॉल्स के दौरान गणना किए गए मानों को सहेजती है ताकि जरूरत पड़ने पर उन्हें फिर से इस्तेमाल किया जा सके। इस तरह की कैशिंग को मेमोइज़ेशन भी कहा जाता है।

आइए एक छोटा उदाहरण देखें जो बड़ी रेंज के संख्याओं का जोड़ दो बार निकालता है:

output = sum(range(100_000_001))
print(output)
output = sum(range(100_000_001))
print(output)

# 5000000050000000
# 5000000050000000

प्रोग्राम को हर बार जोड़ निकालना पड़ता है। हम इसे दो कॉल्स का समय नापकर पुष्टि कर सकते हैं:

import timeit

print(
    timeit.timeit(
        "sum(range(100_000_001))",
        globals=globals(),
        number=1,
    )
)

print(
    timeit.timeit(
        "sum(range(100_000_001))",
        globals=globals(),
        number=1,
    )
)

आउटपुट दिखाता है कि दोनों कॉल्स में लगभग बराबर समय लगता है (हमारे सेटअप पर निर्भर करता है, समय तेज़ या धीमा हो सकता है)।

लेकिन, हम कैश का उपयोग करके एक ही मान को एक से अधिक बार निकालने से बच सकते हैं। हम बिल्ट-इन functools मॉड्यूल की cache() फ़ंक्शन से sum नाम को फिर से परिभाषित कर सकते हैं:

import functools
import timeit

sum = functools.cache(sum)

print(
    timeit.timeit(
        "sum(range(100_000_001))",
        globals=globals(),
        number=1,
    )
)

print(
    timeit.timeit(
        "sum(range(100_000_001))",
        globals=globals(),
        number=1,
    )
)

अब दूसरी कॉल को एक सेकंड से अधिक की जगह कुछ माइक्रोसेकंड लगते हैं, क्योंकि 0 से 100,000,000 तक के संख्याओं का जोड़ पहले ही निकालकर कैश किया जा चुका है—दूसरी कॉल उसी पहले से सहेजे मान का उपयोग करती है।

ऊपर, हम functools.cache() डेकोरेटर का उपयोग करके sum() बिल्ट-इन फ़ंक्शन में कैश जोड़ते हैं। संदर्भ के लिए, Python में डेकोरेटर वह फ़ंक्शन होता है जो किसी दूसरे फ़ंक्शन के व्यवहार में बदलाव करता है बिना उसके कोड को स्थायी रूप से बदले। आप डेकोरेटर्स के बारे में अधिक जानने के लिए यह Python Decorators Tutorial देख सकते हैं।

functools.cache() डेकोरेटर Python 3.9 में जोड़ा गया था, लेकिन पुराने वर्ज़न्स के लिए हम functools.lru_cache() का उपयोग कर सकते हैं। अगला सेक्शन इन दोनों तरीकों को विस्तार से समझाएगा, जिसमें अधिक प्रचलित डेकोरेटर नोटेशन जैसे @cache का उपयोग भी शामिल है।

Python कैशिंग: अलग-अलग तरीके

Python के functools मॉड्यूल में फ़ंक्शन्स पर कैशिंग लागू करने के लिए दो डेकोरेटर्स हैं। आइए functools.lru_cache() और functools.cache() को एक उदाहरण के साथ समझें।

आइए एक फ़ंक्शन sum_digits() लिखें जो संख्याओं के अनुक्रम को लेता है और उन संख्याओं के अंकों का योग लौटाता है। उदाहरण के लिए, अगर हम इनपुट के रूप में (23, 43, 8) ट्यूपल इस्तेमाल करें, तो:

  • 23 के अंकों का योग पाँच है।
  • 43 के अंकों का योग सात है।
  • 8 के अंकों का योग आठ है।
  • अतः कुल योग 20 है।

हम अपना sum_digits() फ़ंक्शन इस तरह लिख सकते हैं:

def sum_digits(numbers):
    return sum(
        int(digit) for number in numbers for digit in str(number)
    )

numbers = 23, 43, 8

print(sum_digits(numbers))

# 20

आइए इसी फ़ंक्शन का उपयोग करके कैश बनाने के अलग-अलग तरीकों का पता लगाएँ।

Python में मैनुअल कैशिंग

पहले हम कैश मैन्युअल रूप से बनाएँ। भले ही इसे आसानी से ऑटोमेट किया जा सकता है, लेकिन मैनुअल कैश बनाना प्रक्रिया समझने में मदद करता है।

एक डिक्शनरी बनाएँ और हर बार जब फ़ंक्शन नए मान के साथ कॉल हो, तो उसमें की-वैल्यू जोड़ दें ताकि परिणाम सहेजा जा सके। अगर फ़ंक्शन ऐसे मान के साथ कॉल हो जो पहले से ही इस डिक्शनरी में मौजूद है, तो फ़ंक्शन दोबारा गणना करने के बजाय वही सहेजा मान लौटा देगा:

import random
import timeit

def sum_digits(numbers):
    if numbers not in sum_digits.my_cache:
        sum_digits.my_cache[numbers] = sum(
            int(digit) for number in numbers for digit in str(number)
        )
    return sum_digits.my_cache[numbers]
sum_digits.my_cache = {}

numbers = tuple(random.randint(1, 1000) for _ in range(1_000_000))

print(
    timeit.timeit(
        "sum_digits(numbers)",
        globals=globals(),
        number=1
    )
)

print(
    timeit.timeit(
        "sum_digits(numbers)",
        globals=globals(),
        number=1
    )
)

sum_digits(numbers) की दूसरी कॉल पहली की तुलना में काफी तेज़ है, क्योंकि यह कैश किए हुए मान का उपयोग करती है।

अब ऊपर दिए कोड को ज़्यादा विस्तार से समझते हैं। पहले ध्यान दें कि हम sum_digits.my_cache डिक्शनरी फ़ंक्शन को परिभाषित करने के बाद बनाते हैं, भले ही इसका उपयोग फ़ंक्शन डेफिनिशन के भीतर होता है।

sum_digits() फ़ंक्शन यह जाँचता है कि फ़ंक्शन को दिया गया आर्गुमेंट sum_digits.my_cache डिक्शनरी की चाबियों में पहले से मौजूद है या नहीं। सभी अंकों का जोड़ केवल तभी निकाला जाता है जब आर्गुमेंट कैश में मौजूद न हो।

क्योंकि हम कॉल के आर्गुमेंट को डिक्शनरी की कुंजी के रूप में इस्तेमाल करते हैं, उसे हैशेबल डेटा टाइप होना चाहिए। लिस्ट हैशेबल नहीं होती, इसलिए इसे डिक्शनरी की चाबी के रूप में इस्तेमाल नहीं कर सकते। उदाहरण के लिए, numbers को ट्यूपल के बजाय लिस्ट से बदलने की कोशिश करें—तो TypeError आएगा:

# ...

numbers = [random.randint(1, 1000) for _ in range(1_000_000)]

# ...
Traceback (most recent call last):
...
TypeError: unhashable type: 'list'

सीखने के उद्देश्य से मैनुअल कैशिंग अच्छी है, लेकिन अब तेज़ तरीकों को देखते हैं।

functools.lru_cache() के साथ Python कैशिंग

Python 3.2 से lru_cache() डेकोरेटर उपलब्ध है। फ़ंक्शन नाम की शुरुआत में मौजूद “lru” का अर्थ है "least recently used"। हम कैश को अक्सर इस्तेमाल होने वाली चीज़ों को रखने वाला डिब्बा मान सकते हैं—जब यह भर जाता है, तो LRU रणनीति सबसे कम हाल में इस्तेमाल हुई वस्तु को हटाकर नए के लिए जगह बनाती है।

आइए अपने sum_digits() फ़ंक्शन को @functools.lru_cache से डेकोरेट करें:

import functools
import random
import timeit

@functools.lru_cache
def sum_digits(numbers):
    return sum(
        int(digit) for number in numbers for digit in str(number)
    )

numbers = tuple(random.randint(1, 1000) for _ in range(1_000_000))

print(
    timeit.timeit(
        "sum_digits(numbers)",
        globals=globals(),
        number=1
    )
)

print(
    timeit.timeit(
        "sum_digits(numbers)",
        globals=globals(),
        number=1
    )
)

कैशिंग की बदौलत, दूसरी कॉल चलने में काफी कम समय लेती है।

डिफ़ॉल्ट रूप से, कैश पहले 128 गणना किए गए मानों को सहेजता है। जब सभी 128 जगह भर जाती हैं, तो एल्गोरिथ्म सबसे कम हाल में इस्तेमाल (LRU) हुए मान को हटाकर नए मानों के लिए जगह बनाता है।

हम maxsize पैरामीटर के माध्यम से डेकोरेट करते समय अधिकतम कैश आकार अलग सेट कर सकते हैं:

import functools
import random
import timeit

@functools.lru_cache(maxsize=5)
def sum_digits(numbers):
    return sum(
        int(digit) for number in numbers for digit in str(number)
    )

# ...

इस स्थिति में, कैश केवल पाँच मान सहेजता है। अगर हम कैश के आकार को सीमित नहीं करना चाहते, तो maxsize आर्गुमेंट को None पर सेट कर सकते हैं।

functools.cache() के साथ Python कैशिंग

Python 3.9 में एक और सरल और तेज़ कैशिंग डेकोरेटर शामिल है—functools.cache()। इस डेकोरेटर की दो मुख्य विशेषताएँ हैं:

  • इसका कोई अधिकतम आकार नहीं है—यह functools.lru_cache(maxsize=None) जैसा है।
  • यह सभी फ़ंक्शन कॉल्स और उनके परिणामों को सहेजता है (यह LRU रणनीति नहीं अपनाता)। यह उन फ़ंक्शन्स के लिए उपयुक्त है जिनका आउटपुट अपेक्षाकृत छोटा होता है या जहाँ हमें कैश आकार की पाबंदियों की चिंता नहीं करनी होती।

आइए sum_digits() फ़ंक्शन पर @functools.cache डेकोरेटर का उपयोग करें:

import functools
import random
import timeit

@functools.cache
def sum_digits(numbers):
    return sum(
        int(digit) for number in numbers for digit in str(number)
    )

numbers = tuple(random.randint(1, 1000) for _ in range(1_000_000))

print(
    timeit.timeit(
        "sum_digits(numbers)",
        globals=globals(),
        number=1
    )
)

print(
    timeit.timeit(
        "sum_digits(numbers)",
        globals=globals(),
        number=1
    )
)

@functools.cache से sum_digits() को डेकोरेट करना, sum_digits को functools.cache() असाइन करने के बराबर है:

# ...

def sum_digits(numbers):
    return sum(
        int(digit) for number in numbers for digit in str(number)
    )

sum_digits = functools.cache(sum_digits)

ध्यान दें कि हम अलग इम्पोर्ट स्टाइल भी अपना सकते हैं:

from functools import cache

इस तरह, हम सिर्फ @cache लिखकर अपने फ़ंक्शन्स को डेकोरेट कर सकते हैं।

अन्य कैशिंग रणनीतियाँ

Python के अपने टूल्स LRU कैशिंग रणनीति लागू करते हैं, जहाँ सबसे कम हाल में इस्तेमाल हुए एंट्रीज़ को नए मानों के लिए जगह बनाने के लिए हटा दिया जाता है।

आइए कुछ अन्य कैशिंग रणनीतियाँ देखें:

  • फर्स्ट-इन, फर्स्ट-आउट (FIFO): जब कैश भर जाता है, तो सबसे पहले जो आइटम जोड़ा गया था, उसे हटाकर नई जगह बनाई जाती है। LRU और FIFO में फर्क यह है कि LRU हाल में इस्तेमाल हुई चीज़ों को कैश में रखता है, जबकि FIFO इस्तेमाल की परवाह किए बिना सबसे पुरानी चीज़ हटा देता है।
  • लास्ट-इन, फर्स्ट-आउट (LIFO): जब कैश भर जाता है, तो सबसे हाल में जोड़ा गया आइटम हटाया जाता है। इसे कैंटीन में प्लेटों के स्टैक जैसा समझें—जो प्लेट सबसे ऊपर रखी (आखिरी में डाली) है, वही पहले निकलेगी।
  • मोस्ट-रिसेंटली यूज़्ड (MRU): जब जगह चाहिए, तो सबसे हाल में इस्तेमाल किया गया मान हटा दिया जाता है।
  • रैंडम रिप्लेसमेंट (RR): यह रणनीति यादृच्छिक रूप से कोई भी आइटम हटा देती है ताकि नए के लिए जगह बन सके।

इन रणनीतियों को वैध जीवनकाल के मापदंडों के साथ भी जोड़ा जा सकता है—यह बताता है कि कैश में मौजूद डेटा कितने समय तक मान्य या प्रासंगिक माना जाए। एक समाचार लेख की कल्पना करें—वह बार-बार एक्सेस हो सकता है (LRU उसे रखेगा), लेकिन एक हफ्ते बाद वह पुराना भी हो सकता है।

Python कैशिंग: सामान्य उपयोग के मामले

अभी तक, हमने सीखने के लिए सरल उदाहरणों का इस्तेमाल किया। लेकिन कैशिंग के कई वास्तविक-world उपयोग हैं।

डेटा साइंस में, हम अक्सर बड़े डेटासेट्स पर दोहराए जाने वाले ऑपरेशन्स चलाते हैं। कैश किए गए परिणामों का उपयोग करने से एक ही गणनाएँ बार-बार करने में लगने वाला समय और लागत घट जाती है।

हम बाहरी संसाधन जैसे वेब पेज या डेटाबेस को सहेजने के लिए भी कैशिंग का उपयोग कर सकते हैं। एक उदाहरण देखें और किसी DataCamp लेख को कैश करें। लेकिन पहले हमें थर्ड-पार्टी requests मॉड्यूल इंस्टॉल करना होगा—इसके लिए टर्मिनल में निम्न लाइन चलाएँ:

$ python -m pip install requests

requests इंस्टॉल होने के बाद, हम नीचे दिया कोड आज़मा सकते हैं, जो @lru_cache डेकोरेटर का उपयोग करते हुए वही DataCamp लेख दो बार फ़ेच करने की कोशिश करता है:

import requests
from functools import lru_cache

@lru_cache(maxsize=10)
def get_article(url):
    print(f"Fetching article from {url}")
    response = requests.get(url)
    return response.text

print(get_article("https://www.datacamp.com/tutorial/decorators-python"))
print(get_article("https://www.datacamp.com/tutorial/decorators-python"))

ध्यान देने योग्य बात यह है कि हमने आउटपुट छोटा कर दिया है क्योंकि यह बहुत लंबा होता है। फिर भी गौर करें कि get_article() की पहली कॉल ही Fetching article from {url} वाक्यांश प्रिंट करती है।

ऐसा इसलिए है क्योंकि वेबपेज तक पहुँचना केवल पहली बार होता है। परिणाम फ़ंक्शन के कैश में सहेजा जाता है। जब हम दूसरी बार वही वेबपेज माँगते हैं, तो कैश में सहेजा डेटा लौटा दिया जाता है।

कैशिंग सुनिश्चित करती है कि एक ही डेटा को बार-बार फ़ेच करने पर अनावश्यक देरी न हो। बाहरी APIs में अक्सर रेट लिमिट्स और डेटा फ़ेच करने की लागतें होती हैं। कैशिंग API लागतें घटाती है और रेट लिमिट से टकराने की संभावना भी कम करती है।

एक अन्य सामान्य उपयोग का मामला मशीन लर्निंग एप्लिकेशन्स में है, जहाँ कई महँगी गणनाओं को दोहराना पड़ता है। उदाहरण के लिए, अगर किसी टेक्स्ट को मशीन लर्निंग मॉडल में इस्तेमाल करने से पहले टोकनाइज़ और वेक्टराइज़ करना हो, तो हम आउटपुट को कैश में रख सकते हैं। इससे हमें बार-बार महँगे कम्प्यूटेशन्स नहीं करने पड़ेंगे।

Python में कैशिंग करते समय आम चुनौतियाँ

हमने Python में कैशिंग के फ़ायदों के बारे में सीखा। कैश लागू करते समय कुछ चुनौतियाँ और कमियाँ भी ध्यान रखने योग्य हैं:

  • कैश इनवैलिडेशन और निरंतरता: डेटा समय के साथ बदल सकता है। इसलिए, कैश में सहेजे मानों को भी अपडेट या हटाने की ज़रूरत पड़ सकती है।
  • मेमोरी प्रबंधन: कैश में बड़े पैमाने पर डेटा सहेजने के लिए मेमोरी चाहिए होती है, और अगर कैश अनियंत्रित रूप से बढ़ता जाए तो इससे प्रदर्शन समस्याएँ हो सकती हैं।
  • जटिलता: कैश जोड़ने से सिस्टम में कैश बनाने और बनाए रखने की जटिलता बढ़ती है। अक्सर, फ़ायदे इन लागतों से अधिक होते हैं, लेकिन यह बढ़ी हुई जटिलता ऐसे बग्स ला सकती है जिन्हें ढूँढना और ठीक करना मुश्किल हो।

निष्कर्ष

हम एक ही डेटा पर बार-बार चलने वाले भारी कम्प्यूटेशन्स के दौरान प्रदर्शन बेहतर करने के लिए कैशिंग का उपयोग कर सकते हैं।

Python में फ़ंक्शन कॉल्स के लिए कैश बनाने के दो डेकोरेटर्स हैं: functools मॉड्यूल में @lru_cache और @cache।

हालाँकि, यह सुनिश्चित करना ज़रूरी है कि हम कैश को अद्यतन रखें और मेमोरी का सही प्रबंधन करें।

अगर आप कैशिंग और Python के बारे में और सीखना चाहते हैं, तो यह छह-कोर्स वाला Python Programming स्किल ट्रैक देखें।

Stephen Gruppetta

लेखक

Stephen Gruppetta

मैंने यूनिवर्सिटी ऑफ माल्टा में स्नातक स्तर पर भौतिकी और गणित का अध्ययन किया। उसके बाद, मैं लंदन चला गया और इम्पीरियल कॉलेज से भौतिकी में पीएचडी प्राप्त की। मैंने मानव रेटिना की इमेजिंग के लिए नवीन ऑप्टिकल तकनीकों पर काम किया। अब, मैं Python के बारे में लिखने, Python पर संवाद करने और Python सिखाने पर ध्यान केंद्रित करता हूँ।

विषय