ترفندهایی برای افزایش سرعت برنامه‌های پایتون

python fast

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

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

زمان سنجی برنامه و تحلیل نحوه‌ی اجرای کدها

قبل از اینکه چیزی رو بهینه سازی کنیم، باید بفهمیم که کدوم قسمت از کد ما باعث کند شدن روند اجرای برنامه میشه. پس اول از همه باید در مورد Timing و Profiling صحبت کنیم. گاهی اوقات گلوگاه (bottleneck) برنامه به سادگی قابل تشخیص هست. اما زمانی که برنامه بزرگ و پیچیده بشه، بهتره تا از ابزارهایی استفاده کنیم تا به ما کمک کنن تا bottleneck رو تشخیص بدیم.

مثال زیر کدی هست که عدد ثابت e (اویلر) رو به توان عدد x می‌رسونه. توجه داشته باشید که این قطعه کد صرفا یک مثال هست که با اون قصد داریم مدت زمان اجرای یک برنامه رو تحلیل کنیم:

from decimal import *

def exp(x):
    getcontext().prec += 2
    i, lasts, s, fact, num = 0, 0, 1, 1, 1
    while s != lasts:
        lasts = s
        i += 1
        fact *= i
        num *= x
        s += num / fact
    getcontext().prec -= 2
    return +s

exp(Decimal(150))
exp(Decimal(400))
exp(Decimal(3000))

آسون ترین و ساده ترین راه برای بدست آوردن مدت زمان اجرای این برنامه، استفاده از دستور time در سیستم‌های یونیکسی هست:

$ time python3.8 slow_program.py

real 0m11,058s
user 0m11,050s
sys  0m0,008s

با کمک دستور بالا می‌تونیم مدت زمان اجرای برنامه رو بدست بیاریم. اما آیا فقط بدست آوردن مدت زمان اجرای کل برنامه کافی هست؟

تجزیه و تحلیل جزئی تر

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

$ python3.8 -m cProfile -s time slow_program.py
         ۱۲۹۷ function calls (1272 primitive calls) in 11.081 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        ۳   ۱۱.۰۷۹    ۳.۶۹۳   ۱۱.۰۷۹    ۳.۶۹۳ slow_program.py:4(exp)
        ۱    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۲    ۰.۰۰۲ {built-in method _imp.create_dynamic}
      ۴/۱    ۰.۰۰۰    ۰.۰۰۰   ۱۱.۰۸۱   ۱۱.۰۸۱ {built-in method builtins.exec}
        ۶    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰ {built-in method __new__ of type object at 0x9d12c0}
        ۶    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰ abc.py:132(__new__)
       ۲۳    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰ _weakrefset.py:36(__init__)
      ۲۴۵    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰ {built-in method builtins.getattr}
        ۲    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰ {built-in method marshal.loads}
       ۱۰    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰ <frozen importlib._bootstrap_external>:۱۲۳۳(find_spec)
      ۸/۴    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰ abc.py:196(__subclasscheck__)
       ۱۵    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰ {built-in method posix.stat}
        ۶    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰ {built-in method builtins.__build_class__}
        ۱    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰ __init__.py:357(namedtuple)
       ۴۸    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰ <frozen importlib._bootstrap_external>:۵۷(_path_join)
       ۴۸    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰    ۰.۰۰۰ <frozen importlib._bootstrap_external>:۵۹(<listcomp>)
        ۱    ۰.۰۰۰    ۰.۰۰۰   ۱۱.۰۸۱   ۱۱.۰۸۱ slow_program.py:1(<module>)
...

در دستور بالا از ماژول cProfile و آرگومان time برای این ماژول به منظور تست برنامه‌ای که داشتیم استفاده کردیم که نتایج خروجی بر اساس مدت زمان اجرای هر متد مرتب شدن. همون طور که می‌بینید تابع exp بیشترین مدت زمان اجرا رو داشت.

پس تا اینجا فهمیدیم که به چه صورت می‌تونیم مدت زمان اجرای تابع‌های مختلف رو در برناممون بدست بیاریم و بفهمیم bottleneck برنامه‌ی ما کجا هست.

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

  • در این مطلب قصد نداریم در مورد دکوریتورهای پایتون توضیح بدیم. برای آشنایی بیشتر با دکوریتورها در پایتون پیشنهاد می‌کنم حتما این مقاله رو بخونید.
import time
from functools import wraps

def timeit_wrapper(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()  # Alternatively, you can use time.process_time()
        func_return_val = func(*args, **kwargs)
        end = time.perf_counter()
        print('{0:<10}.{1:<8} : {2:<8}'.format(func.__module__, func.__name__, end - start))
        return func_return_val
    return wrapper

برای استفاده از این دکوریتور، باید اون رو برای تابعی که نوشته بودیم اعمال کنیم:

@timeit_wrapper
def exp(x):
    ...

print('{0:<10} {1:<8} {2:^8}'.format('module', 'function', 'time'))
exp(Decimal(150))
exp(Decimal(400))
exp(Decimal(3000))

حالا با اجرای برنامه‌ای که نوشتیم، خروجی زیر به ما نمایش داده میشه:

$ python3.8 slow_program.py

module     function   time
__main__  .exp      : 0.003267502994276583
__main__  .exp      : 0.038535295985639095
__main__  .exp      : 11.728486061969306

مقدار زمان نشون داده شده، در واقع چه زمانی هست؟ در اینجا باید به یک نکته توجه کنیم. دو زمان perf_counter و process_time در پکیج time وجود دارن. تفاوت اینجاست که perf_count ممکن هست تحت تاثیر بار سیستم شما قرار بگیره و مقدار مطلق زمان رو به شما برمی‌گردونه. در حالی که process_time تنها زمان اجرای پردازش رو نشون میده که شامل زمان و بار سیستم نمیشه.

افزایش سرعت برنامه

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

۱) استفاده از انواع داده‌های داخلی در پایتون (Built-in Data Types)

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

۲) استفاده از lru_cache برای کَشینگ

قبل از هر توضیحی به قطعه کد زیر توجه کنید:

import functools
import time

# caching up to 12 different results
@functools.lru_cache(maxsize=12)
def slow_func(x):
    time.sleep(2)  # Simulate long computation
    return x

slow_func(1)  # ... waiting for 2 sec before getting result
slow_func(1)  # already cached - result returned instantaneously!

slow_func(3)  # ... waiting for 2 sec before getting result

با استفاده از عبارت time.sleep(2) مدت زمان اجرای این تابع رو زیاد کردیم. وقتی برای اولین بار این تابع رو صدا می‌زنیم و پارامتر ۱ رو بهش پاس میدیم، قبل از دریافت نتیجه‌ی خروجی، برنامه‌ی ما ۲ ثانیه صبر میکنه. حالا اگه دوباره این تابع رو باز هم با مقدار ۱ فرافوانی کنیم، بلافاصله نتیجه‌ی خروجی برگشت داده میشه. چراکه این مقدار برای این تابع کش شده.

۳) استفاده از متغیرهای محلی

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

برای اینکه یک دید کلی در مورد استراتژی‌هایی بهتر در این مورد داشته باشید، به دو مثال زیر توجه کنید:

#  Example #1
class FastClass:

    def do_stuff(self):
        temp = self.value  # this speeds up lookup in loop
        for i in range(10000):
            ...  # Do something with `temp` here
#  Example #2
import random

def fast_function():
    r = random.random
    for i in range(10000):
        print(r())  # calling `r()` here, is faster than global random.random()
۴) استفاده از توابع

شاید در نگاه اول به نظرتون اینکار مفید به نظر نرسه. چرا که فکر می‌کنید تکه تکه کردن کدها و تبدیل اون‌ها به توابع کوچکتر، بار زیادی رو روی پشته‌ی برنامه‌ی در حال اجرا قرار میده تا این پشته بتونه از returnهای پیوسته برای دنباله‌ای از فراخوانی‌های تو در تو استفاده کنه. اما نکته اینجاست که اگر ما تمام کدمون رو داخل یک فایل و تابع بنویسیم، با اینکار داریم از تعداد زیادی متغیر گلوبال استفاده می‌کنیم (نکته‌ی شماره ۳)!

پس بهتر هست که کدهای برنامه رو درون تابع هایی بنویسیم که هرکدوم از اون تابع‌ها چندین متغیر محلی دارن. پس از اون قطه کد اصلی برای اجرای برنامه رو در یک تابع main نوشته و اون رو به صورت زیر فراخوانی کنیم:

def main():
    ...  # All your previously global code

main()
۵) به اتریبیوت‌ها بصورت مستقیم دسترسی نداشته باشید

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

روش جایگزین؟ به مثال زیر توجه کنید:

#  Slow:
import re

def slow_func():
    for i in range(10000):
        re.findall(regex, line)  # Slow!

#  Fast:
from re import findall

def fast_func():
    for i in range(10000):
        findall(regex, line)  # Faster!
۶) مراقب استفاده از رشته‌ها باشید

استفاده از %s یا متد format به منظور بکارگیری متغیرهای مختلف داخل یک رشته، سرعت اجرای برنامه‌ی شما رو کاهش میده. بهترین و سریع ترین روش برای اینکار استفاده از f-string هست. در قطعه کد زیر به ترتیب از خط بالا به پایین سریع ترین تا کند ترین روش برای اینکار نوشته شدن:

f'{s} {t}'  # Fast!
s + '  ' + t
' '.join((s, t))
'%s %s' % (s, t)
'{} {}'.format(s, t)
Template('$s $t').substitute(s=s, t=t)  # Slow!
۷) از مولدها (generators) استفاده کنید

همونطور که میدونید مولدها باعث صرفه جویی در مصرف حافظه (memory) میشن. خب این موضوع چه ارتباطی به سرعت اجرای برنامه داره؟

فرض کنید یک دیتاست عظیم دارید و قصد دارید تا روی اون پردازش‌هایی انجام بدین. اگه از مولدها برای انجام iteration استفاده نکنید، ممکن هست داده‌های شما از کش سطح اول CPU (L1) سر ریز بشن (overflow) که باعث میشه جستجوی مقادیر در حافظه (memory) بصورت قابل توجهی کند بشه.

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

نتیجه گیری

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

1 دیدگاه On ترفندهایی برای افزایش سرعت برنامه‌های پایتون

جوابی بنویسید:

آدرس ایمیل شما به صورت عمومی منتشر نخواهد شد.