پروفایلینگ و بهینهسازی کد Python: سریعتر کردن برنامهها
اگر برنامه Python شما کند اجرا میشود، اولین واکنش نباید تغییر زبان برنامهنویسی یا بازنویسی کامل پروژه باشد. در بیشتر موارد، فقط چند بخش کوچک از کد زمان اصلی اجرا را مصرف میکنند. هنر یک برنامهنویس حرفهای این است که قبل از تغییر کد، ابتدا اندازهگیری کند، سپس گلوگاه را پیدا کند و در نهایت همان قسمت را هدفمند بهینهسازی کند.
این مقاله یک راهنمای کامل برای پروفایلینگ و بهینهسازی کد Python است؛ از ابزارهای ساده مثل timeit تا تحلیل حرفهای با cProfile، tracemalloc، line_profiler، py-spy و Scalene. در پایان مقاله، یک پروژه کوچک را مرحلهبهمرحله کند میکنیم، پروفایل میگیریم و بهینه میکنیم.
پروفایلینگ یعنی چه؟
پروفایلینگ یعنی جمعآوری داده درباره اجرای برنامه: کدام تابع چند بار اجرا شده، چقدر زمان گرفته، چه مقدار حافظه مصرف کرده و کدام خط یا تابع گلوگاه اصلی است. در مستندات رسمی Python، cProfile و profile برای پروفایلینگ قطعی برنامهها معرفی شدهاند؛ این ابزارها آمار اجرای قسمتهای مختلف برنامه را گزارش میکنند، اما برای بنچمارک دقیق قطعهکدهای کوچک، timeit گزینه مناسبتری است.
- Benchmarking: اندازهگیری زمان اجرای یک قطعه کد کوچک و کنترلشده.
- Profiling: پیدا کردن بخشهای کند در یک برنامه واقعی و بزرگتر.
- Optimization: تغییر الگوریتم، ساختار داده، I/O یا معماری برای بهبود سرعت یا حافظه.
- Regression Check: مطمئن شدن از اینکه بهینهسازی، خروجی برنامه را خراب نکرده است.
| مفهوم | هدف | ابزارهای پیشنهادی |
| Benchmark | مقایسه زمان اجرای چند راهحل کوچک | timeit، pytest-benchmark |
| CPU Profiling | پیدا کردن توابع کند و پرتکرار | cProfile، pstats، line_profiler، py-spy، Scalene |
| Memory Profiling | پیدا کردن مصرف حافظه و Memory Leak | tracemalloc، memory_profiler، Scalene |
| Optimization | اصلاح الگوریتم، ساختار داده و I/O | set/dict، cache، generator، NumPy، async، multiprocessing |
اصل طلایی: قبل از حدس زدن، اندازهگیری کنید
بزرگترین اشتباه در بهینهسازی این است که فقط بر اساس حس شخصی تصمیم بگیریم کدام بخش کند است. ممکن است فکر کنید حلقه اصلی مشکل دارد، اما پروفایلینگ نشان دهد که مشکل اصلی خواندن فایل، درخواست شبکه، تبدیل نوع داده یا جستجوی غیرضروری در لیست است.
- ابتدا یک سناریوی واقعی از استفاده برنامه انتخاب کنید.
- با timeit یا perf_counter زمان کلی را اندازه بگیرید.
- با cProfile یا Scalene توابع کند را پیدا کنید.
- فقط همان گلوگاه را تغییر دهید، نه کل پروژه را.
- بعد از بهینهسازی دوباره اندازهگیری کنید.
ابزار اول: timeit برای اندازهگیری قطعهکدهای کوچک
timeit برای زمانی مناسب است که میخواهید دو پیادهسازی کوتاه را مقایسه کنید. مثلا آیا جستجو در list سریعتر است یا set؟ پاسخ به اندازه داده و نوع استفاده بستگی دارد، اما برای membership test معمولاً set بسیار سریعتر از list است.
<br />
# benchmark_membership.py<br />
import timeit</p>
<p>setup = "items_list = list(range(1_000_000)); items_set = set(items_list); target = 999_999"</p>
<p>list_time = timeit.timeit("target in items_list", setup=setup, number=1000)<br />
set_time = timeit.timeit("target in items_set", setup=setup, number=1000)</p>
<p>print(f"List membership: {list_time:.6f} sec")<br />
print(f"Set membership : {set_time:.6f} sec")<br />
print(f"Speedup : {list_time / set_time:.1f}x")<br />
نکته آموزشی: در این مثال، ساخت set هزینه اولیه دارد؛ اما اگر قرار است بارها membership را بررسی کنید، این هزینه اولیه معمولاً جبران میشود.
ابزار دوم: cProfile برای پیدا کردن تابع کند
cProfile از ابزارهای استاندارد Python است و برای شروع پروفایلینگ در اکثر پروژهها کافی است. با آن میتوانید بفهمید هر تابع چند بار صدا زده شده و چقدر زمان کل و زمان داخلی مصرف کرده است.
<br />
# slow_report.py<br />
import random</p>
<p>users = [<br />
{"id": i, "score": random.randint(0, 1000), "active": i % 3 != 0}<br />
for i in range(200_000)<br />
]</p>
<p>blocked_ids = list(range(50_000, 90_000))</p>
<p>def build_report(users, blocked_ids):<br />
result = []<br />
for user in users:<br />
if user["active"] and user["id"] not in blocked_ids:<br />
result.append((user["id"], user["score"] * 1.2))<br />
return sorted(result, key=lambda x: x[1], reverse=True)[:20]</p>
<p>if __name__ == "__main__":<br />
print(build_report(users, blocked_ids)[:3])<br />
برای پروفایل گرفتن از این فایل، از دستور زیر استفاده کنید:
<br />
python -m cProfile -s cumulative slow_report.py<br />
اگر خروجی را بررسی کنید، میبینید بخش زیادی از زمان در شرط `user[“id”] not in blocked_ids` مصرف میشود، چون blocked_ids یک list است و جستجو در آن خطی انجام میشود.
بهینهسازی مثال: تبدیل list به set
<br />
# optimized_report.py<br />
import random</p>
<p>users = [<br />
{"id": i, "score": random.randint(0, 1000), "active": i % 3 != 0}<br />
for i in range(200_000)<br />
]</p>
<p>blocked_ids = list(range(50_000, 90_000))</p>
<p>def build_report(users, blocked_ids):<br />
blocked_set = set(blocked_ids) # فقط یک بار تبدیل میکنیم<br />
result = []<br />
for user in users:<br />
if user["active"] and user["id"] not in blocked_set:<br />
result.append((user["id"], user["score"] * 1.2))<br />
return sorted(result, key=lambda x: x[1], reverse=True)[:20]</p>
<p>if __name__ == "__main__":<br />
print(build_report(users, blocked_ids)[:3])<br />
این تغییر ساده معمولاً بهبود قابلتوجهی ایجاد میکند، چون جستجو در set بهطور متوسط بسیار سریعتر از جستجوی خطی در list است. این نوع بهینهسازی یکی از بهترین مثالهای «انتخاب ساختار داده درست» است.
چطور خروجی cProfile را بخوانیم؟
| ستون | معنی | چطور تفسیر کنیم؟ |
| ncalls | تعداد فراخوانی تابع | اگر خیلی زیاد است، شاید تابع داخل حلقه بیش از حد صدا زده میشود. |
| tottime | زمان صرفشده داخل خود تابع | برای پیدا کردن توابع سنگین داخلی مهم است. |
| percall | میانگین زمان هر فراخوانی | برای تشخیص توابع کوچک اما پرتکرار مفید است. |
| cumtime | زمان تابع همراه با زیرتابعها | برای پیدا کردن گلوگاههای سطح بالا مناسب است. |
| filename:lineno(function) | محل تابع در فایل | برای رفتن مستقیم به خط کد مشکلدار استفاده میشود. |
ابزار سوم: pstats برای گزارش خواناتر
وقتی پروژه بزرگتر شد، بهتر است نتیجه cProfile را در یک فایل ذخیره کنید و بعد با pstats آن را مرتب و فیلتر کنید.
# profile_runner.py
import cProfile
import pstats
from pstats import SortKey
from slow_report import build_report, users, blocked_ids
profiler = cProfile.Profile()
profiler.enable()
build_report(users, blocked_ids)
profiler.disable()
profiler.dump_stats(“profile_output.prof”)
stats = pstats.Stats(“profile_output.prof”)
stats.strip_dirs().sort_stats(SortKey.CUMULATIVE).print_stats(15)
SortKey.CUMULATIVE زمانی مناسب است که میخواهید بفهمید کدام تابع در مجموع بیشترین سهم را از زمان اجرا گرفته است. SortKey.TIME زمانی کاربرد دارد که بخواهید فقط زمان داخلی خود تابع را ببینید.
ابزار چهارم: tracemalloc برای تحلیل حافظه
گاهی برنامه سریع است اما حافظه زیادی مصرف میکند یا در طول زمان Memory Leak دارد. tracemalloc یکی از ابزارهای استاندارد Python برای ردیابی allocationهای حافظه است و میتواند نشان دهد بیشترین حافظه در کدام خطها مصرف شده است.
<br />
# memory_example.py<br />
import tracemalloc</p>
<p>tracemalloc.start()</p>
<p>big_data = []<br />
for i in range(200_000):<br />
big_data.append({"id": i, "text": "academy-tech" * 10})</p>
<p>snapshot = tracemalloc.take_snapshot()<br />
top_stats = snapshot.statistics("lineno")</p>
<p>print("Top 5 memory allocations:")<br />
for stat in top_stats[:5]:<br />
print(stat)<br />
این ابزار برای پیدا کردن listهای بزرگ، dictهای غیرضروری، cacheهای کنترلنشده و DataFrameهای حجیم بسیار مفید است.
بهینهسازی حافظه با generator به جای list
اگر لازم نیست همه دادهها را یکجا در حافظه نگه دارید، generator میتواند مصرف حافظه را بهشدت کاهش دهد.
<br />
# روش پرمصرف حافظه<br />
numbers = [x * x for x in range(10_000_000)]<br />
print(sum(numbers))</p>
<p># روش کممصرفتر با generator<br />
numbers = (x * x for x in range(10_000_000))<br />
print(sum(numbers))<br />
در روش اول، یک list بزرگ ساخته میشود. در روش دوم، اعداد بهصورت lazy تولید میشوند و همه مقادیر همزمان در حافظه قرار نمیگیرند.
ابزار پنجم: line_profiler برای تحلیل خطبهخط
وقتی میدانید یک تابع کند است اما نمیدانید دقیقاً کدام خط مشکل دارد، line_profiler میتواند زمان اجرای هر خط تابع را گزارش کند. این ابزار برای توابع محاسباتی، پردازش فایل و حلقههای سنگین کاربرد زیادی دارد.
<br />
# نصب<br />
pip install line_profiler</p>
<p># اجرای پروفایل خطبهخط<br />
kernprof -l -v script.py<br />
در پروژههای آموزشی آکادمی تک، میتوان از line_profiler برای نشان دادن اثر واقعی یک تغییر کوچک در حلقهها استفاده کرد.
ابزار ششم: py-spy برای برنامههای در حال اجرا
py-spy یک sampling profiler است که میتواند بدون تغییر کد و بدون restart کردن برنامه، از فرآیند در حال اجرا پروفایل بگیرد. این ویژگی برای سرویسهای وب، باتها و برنامههای production بسیار ارزشمند است.
<br />
# نصب<br />
pip install py-spy</p>
<p># ساخت flame graph از یک برنامه<br />
py-spy record -o profile.svg -- python app.py</p>
<p># مشاهده زنده توابع پرمصرف<br />
py-spy top --pid 12345<br />
Flame Graph خروجی py-spy برای پیدا کردن مسیرهای پرمصرف در برنامههای واقعی بسیار خوانا است. بخشهای پهنتر در نمودار معمولاً سهم بیشتری از زمان اجرا دارند.
ابزار هفتم: Scalene برای CPU، حافظه و GPU
Scalene یک profiler پیشرفته برای Python است که میتواند زمان CPU، مصرف حافظه، کپی شدن دادهها و حتی برخی سناریوهای GPU را تحلیل کند. برای پروژههای Data Science و AI که ترکیبی از Python و کتابخانههای native مثل NumPy، pandas، PyTorch یا TensorFlow دارند، Scalene میتواند دید بهتری از محل واقعی هزینهها بدهد.
<br />
# نصب<br />
pip install scalene</p>
<p># اجرای پروفایل<br />
scalene script.py<br />
جدول انتخاب ابزار مناسب
| نیاز شما | ابزار پیشنهادی | سطح |
| مقایسه دو قطعه کد کوچک | timeit | مبتدی تا متوسط |
| پیدا کردن تابع کند در برنامه | cProfile + pstats | متوسط |
| تحلیل دقیق خطهای یک تابع | line_profiler | متوسط تا پیشرفته |
| تحلیل حافظه و allocationها | tracemalloc | متوسط |
| پروفایل برنامه در حال اجرا یا production | py-spy | پیشرفته |
| تحلیل ترکیبی CPU، حافظه و GPU | Scalene | پیشرفته |
تکنیکهای کاربردی بهینهسازی کد Python
۱. الگوریتم را قبل از سینتکس بهینه کنید
گاهی مشکل اصلی از نوع الگوریتم است، نه از زبان Python. اگر الگوریتم شما O(n²) باشد، تغییر چند خط کد شاید تأثیر کمی داشته باشد. ابتدا پیچیدگی زمانی را بررسی کنید.
<br />
# کند: O(n^2)<br />
def find_common_slow(a, b):<br />
result = []<br />
for x in a:<br />
if x in b:<br />
result.append(x)<br />
return result</p>
<p># سریعتر: تبدیل b به set<br />
def find_common_fast(a, b):<br />
b_set = set(b)<br />
return [x for x in a if x in b_set]<br />
۲. از ساختار داده درست استفاده کنید
| مسئله | انتخاب بهتر | دلیل |
| جستجوی سریع وجود یک عضو | set | Membership سریعتر از list در دادههای بزرگ |
| نگاشت کلید به مقدار | dict | دسترسی سریع بر اساس key |
| صف دوطرفه | collections.deque | افزودن/حذف سریع از دو سمت |
| شمارش تکرارها | collections.Counter | خواناتر و بهینهتر از پیادهسازی دستی |
| داده عددی حجیم | NumPy array | محاسبات vectorized و اجرای بخشهای سنگین در C |
۳. از caching برای محاسبات تکراری استفاده کنید
اگر یک تابع با ورودیهای مشابه بارها اجرا میشود و خروجی آن تغییر نمیکند، caching میتواند زمان اجرا را کاهش دهد. در Python، functools.lru_cache یکی از ابزارهای استاندارد برای این کار است.
<br />
from functools import lru_cache</p>
<p>@lru_cache(maxsize=128)<br />
def fibonacci(n):<br />
if n < 2:<br />
return n<br />
return fibonacci(n - 1) + fibonacci(n - 2)</p>
<p>print(fibonacci(35))<br />
print(fibonacci.cache_info())<br />
هشدار: cache برای توابعی مناسب است که خروجی آنها فقط به ورودی وابسته است. برای دادههای متغیر، دیتابیس، API و فایلها باید با دقت استفاده شود.
۴. حلقههای سنگین عددی را با NumPy جایگزین کنید
در پردازش عددی، حلقه Python معمولاً کندتر از عملیات vectorized در NumPy است؛ زیرا NumPy بسیاری از عملیات را در سطح C انجام میدهد و با broadcasting میتواند حلقههای Python را حذف کند.
<br />
# روش کندتر با حلقه Python<br />
values = list(range(1_000_000))<br />
squared = []<br />
for x in values:<br />
squared.append(x * x)</p>
<p># روش سریعتر با NumPy<br />
import numpy as np<br />
values = np.arange(1_000_000)<br />
squared = values * values<br />
البته broadcasting همیشه بهترین انتخاب نیست؛ اگر باعث تولید آرایههای بسیار بزرگ واسطه شود، مصرف حافظه میتواند بالا برود. پس باز هم اصل اول برقرار است: اندازهگیری کنید.
۵. I/O را جدا از CPU بررسی کنید
اگر برنامه زمان زیادی را صرف خواندن فایل، اتصال شبکه یا دیتابیس میکند، بهینهسازی حلقهها ممکن است تأثیر زیادی نداشته باشد. در این حالت باید batch processing، caching، async I/O یا کاهش تعداد queryها را بررسی کنید.
<br />
# بد: باز کردن فایل در هر تکرار<br />
for item in items:<br />
with open("output.txt", "a", encoding="utf-8") as f:<br />
f.write(str(item) + "\n")</p>
<p># بهتر: باز کردن فایل یک بار<br />
with open("output.txt", "a", encoding="utf-8") as f:<br />
for item in items:<br />
f.write(str(item) + "\n")<br />
۶. از list comprehension در جای درست استفاده کنید
list comprehension معمولاً خواناتر و در بسیاری از موارد سریعتر از حلقه دستی با append است، اما نباید خوانایی قربانی فشردهنویسی شود.
<br />
# قابل قبول و خوانا<br />
active_scores = [user["score"] for user in users if user["active"]]</p>
<p># نامناسب: منطق پیچیده در یک خط<br />
# بهتر است به تابع جداگانه منتقل شود.<br />
۷. از بهینهسازی زودهنگام پرهیز کنید
بهینهسازی زودهنگام باعث میشود کد پیچیده، شکننده و سختنگهداری شود. هدف شما باید این باشد که ابتدا کد درست، خوانا و تستپذیر بنویسید؛ سپس بخشهای واقعاً کند را بهینه کنید.
- کد خوانا را قربانی چند میلیثانیه نکنید، مگر اینکه واقعاً لازم باشد.
- قبل و بعد از بهینهسازی تست واحد اجرا کنید.
- برای هر تغییر مهم، عدد قبل و بعد را ثبت کنید.
- بهینهسازی باید قابل توضیح، قابل تکرار و قابل نگهداری باشد.
پروژه عملی: سریعتر کردن یک تحلیل ساده داده
در این بخش یک مثال عملی میسازیم که برای دانشجویان Data Science، اتوماسیون و توسعه نرمافزار قابل فهم باشد. فرض کنید یک فایل بزرگ از تراکنشها داریم و میخواهیم مجموع خرید کاربران فعال را محاسبه کنیم.
نسخه کند
<br />
# transactions_slow.py<br />
import random</p>
<p>transactions = [<br />
{"user_id": random.randint(1, 50_000), "amount": random.randint(10, 500)}<br />
for _ in range(500_000)<br />
]</p>
<p>active_users = list(range(1, 30_000))</p>
<p>def total_for_active_users(transactions, active_users):<br />
totals = {}<br />
for tx in transactions:<br />
if tx["user_id"] in active_users:<br />
user_id = tx["user_id"]<br />
if user_id not in totals:<br />
totals[user_id] = 0<br />
totals[user_id] += tx["amount"]<br />
return totals</p>
<p>print(len(total_for_active_users(transactions, active_users)))<br />
پروفایل گرفتن
<br />
python -m cProfile -s cumulative transactions_slow.py<br />
گلوگاه اصلی احتمالاً بررسی `tx[“user_id”] in active_users` است؛ چون active_users یک list بزرگ است و برای هر تراکنش، جستجوی خطی انجام میشود.
نسخه بهینهتر
<br />
# transactions_fast.py<br />
import random<br />
from collections import defaultdict</p>
<p>transactions = [<br />
{"user_id": random.randint(1, 50_000), "amount": random.randint(10, 500)}<br />
for _ in range(500_000)<br />
]</p>
<p>active_users = list(range(1, 30_000))</p>
<p>def total_for_active_users(transactions, active_users):<br />
active_set = set(active_users)<br />
totals = defaultdict(int)<br />
for tx in transactions:<br />
user_id = tx["user_id"]<br />
if user_id in active_set:<br />
totals[user_id] += tx["amount"]<br />
return dict(totals)</p>
<p>print(len(total_for_active_users(transactions, active_users)))<br />
در نسخه بهینهتر، سه تغییر مهم انجام شد: تبدیل list به set برای membership، ذخیره user_id در متغیر محلی، و استفاده از defaultdict برای حذف شرط اضافی.
اندازهگیری قبل و بعد
<br />
# compare_versions.py<br />
from time import perf_counter<br />
from transactions_slow import total_for_active_users as slow_fn, transactions, active_users<br />
from transactions_fast import total_for_active_users as fast_fn</p>
<p>start = perf_counter()<br />
slow_result = slow_fn(transactions, active_users)<br />
slow_time = perf_counter() - start</p>
<p>start = perf_counter()<br />
fast_result = fast_fn(transactions, active_users)<br />
fast_time = perf_counter() - start</p>
<p>assert slow_result == fast_result<br />
print(f"Slow: {slow_time:.3f} sec")<br />
print(f"Fast: {fast_time:.3f} sec")<br />
print(f"Speedup: {slow_time / fast_time:.1f}x")<br />
در مقاله سایت، بهتر است یک اسکرینشات از اجرای این کد قرار دهید تا کاربر بهصورت بصری تفاوت زمان را ببیند.
چکلیست ۷ مرحلهای پروفایلینگ و بهینهسازی Python
- سناریوی واقعی اجرا را مشخص کنید؛ نه یک مثال غیرواقعی و خیلی کوچک.
- زمان کلی اجرای برنامه را با perf_counter یا timeit اندازه بگیرید.
- با cProfile یا Scalene گلوگاه اصلی را پیدا کنید.
- اگر مشکل در یک تابع خاص بود، از line_profiler کمک بگیرید.
- اگر مشکل حافظه بود، tracemalloc یا Scalene را اجرا کنید.
- فقط گلوگاه واقعی را اصلاح کنید؛ الگوریتم، ساختار داده، I/O یا vectorization را بررسی کنید.
- بعد از تغییر، خروجی را با تست بررسی کنید و زمان قبل/بعد را ثبت کنید.
اشتباهات رایج در بهینهسازی Python
| اشتباه | چرا بد است؟ | راه درست |
| بهینهسازی بدون اندازهگیری | ممکن است روی بخش اشتباه وقت بگذارید. | اول Profiling، بعد تغییر کد |
| پیچیده کردن کد برای سرعت کم | نگهداری کد سخت میشود. | خوانایی و تست را حفظ کنید |
| استفاده افراطی از caching | حافظه مصرف میشود و داده ممکن است stale شود. | برای توابع pure و پرتکرار استفاده کنید |
| نادیده گرفتن I/O | گاهی مشکل اصلاً CPU نیست. | فایل، شبکه و دیتابیس را جدا بررسی کنید |
| اعتماد به یک بار اجرا | نتایج ممکن است نوسان داشته باشند. | چند بار اندازهگیری و میانگین بگیرید |
مسیر پیشنهادی آکادمی تک برای یادگیری Performance در Python
برای یادگیری اصولی، بهتر است کاربر مسیر زیر را در Academy-Tech.ir دنبال کند:
- مرحله ۱: Python مقدماتی – متغیرها، شرطها، حلقهها و توابع.
- مرحله ۲: Python متوسط – فایل، ماژول، exception، virtual environment و پروژههای کوچک.
- مرحله ۳: Clean Code در Python – نوشتن کد خوانا و قابل نگهداری.
- مرحله ۴: پروفایلینگ و بهینهسازی – cProfile، timeit، tracemalloc و بهینهسازی عملی.
- مرحله ۵: Python برای Data Science، وب یا اتوماسیون – استفاده از بهینهسازی در پروژه واقعی.
پیشنهاد CTA داخل مقاله: «اگر میخواهید این مثالها را مرحلهبهمرحله تمرین کنید، دوره Python آکادمی تک را دنبال کنید. بهزودی کامپایلر آنلاین Academy-Tech.ir نیز اضافه میشود تا بتوانید کدها را مستقیم داخل سایت اجرا و تست کنید.»
سوالات متداول درباره پروفایلینگ و بهینهسازی Python
آیا Python برای برنامههای سریع مناسب است؟
بله، اگر الگوریتم، ساختار داده و ابزار مناسب انتخاب شود. برای پردازش عددی هم میتوان از NumPy، Numba، Cython یا کتابخانههای native استفاده کرد.
اول cProfile یاد بگیرم یا timeit؟
برای قطعهکدهای کوچک timeit و برای برنامه واقعی cProfile مناسبتر است. هر دو را باید در مسیر یادگیری حرفهای Python بلد باشید.
آیا بهینهسازی همیشه لازم است؟
خیر. اگر کد شما سریع، خوانا و کافی است، بهینهسازی اضافی فقط پیچیدگی ایجاد میکند. بهینهسازی زمانی ارزشمند است که مشکل واقعی سرعت یا حافظه دارید.
بهترین ابزار برای Memory Leak در Python چیست؟
برای شروع tracemalloc مناسب است. برای تحلیل پیشرفتهتر میتوانید از Scalene یا memory_profiler استفاده کنید.
آیا NumPy همیشه سریعتر از حلقه Python است؟
در محاسبات عددی بزرگ معمولاً بله، اما اگر استفاده از broadcasting باعث ساخت آرایههای واسطه بسیار بزرگ شود، ممکن است حافظه زیادی مصرف کند.
آیا py-spy برای پروژههای واقعی مناسب است؟
بله، py-spy برای نمونهبرداری از برنامههای در حال اجرا کاربرد زیادی دارد و میتواند بدون تغییر کد، خروجی flame graph تولید کند.
جمعبندی
پروفایلینگ و بهینهسازی کد Python یعنی تبدیل حدس به داده. برنامهنویس حرفهای ابتدا اندازهگیری میکند، سپس گلوگاه را پیدا میکند، بعد با کمترین تغییر مؤثر کد را سریعتر میکند و در پایان دوباره خروجی و سرعت را بررسی میکند.
برای شروع، همین سه ابزار کافی است: timeit برای مقایسه قطعهکدها، cProfile برای پیدا کردن توابع کند و tracemalloc برای بررسی حافظه. بعد از آن میتوانید سراغ ابزارهای پیشرفتهتر مثل line_profiler، py-spy و Scalene بروید.
گام بعدی پیشنهادی در Academy-Tech.ir: این مقاله را همراه با مقاله «Clean Code در Python» و سپس یک پروژه عملی Data Science یا Bot تلگرام مطالعه کنید تا بهینهسازی را در پروژه واقعی ببینید.
منابع رسمی و پیشنهادی
- Python Docs – The Python Profilers: https://docs.python.org/3/library/profile.html
- Python Docs – timeit: https://docs.python.org/3/library/timeit.html
- Python Docs – tracemalloc: https://docs.python.org/3/library/tracemalloc.html
- Python Docs – functools / lru_cache: https://docs.python.org/3/library/functools.html
- NumPy Docs – Broadcasting: https://numpy.org/doc/stable/user/basics.broadcasting.html
- pandas Docs – Enhancing Performance: https://pandas.pydata.org/docs/user_guide/enhancingperf.html
- py-spy GitHub: https://github.com/benfred/py-spy
- Scalene GitHub: https://github.com/plasma-umass/scalene
line_profiler GitHub: https://github.com/pyutils/line_profiler

