لا تُعرف لغة بايثون بسرعتها، ولكن هناك بعض الأشياء التي يمكن أن تساعدك في تحقيق أداء أفضل. ومن المثير للدهشة أن إحدى هذه الممارسات هي تشغيل الكود في دالة بدلاً من النطاق العالمي. في هذه المقالة، سنرى لماذا يعمل كود بايثون بشكل أسرع في دالة وكيف يعمل تنفيذ كود بايثون.
تنفيذ كود بايثون
لفهم سبب تشغيل كود بايثون بشكل أسرع في دالة، نحتاج أولاً إلى فهم كيفية تنفيذ بايثون للكود. بايثون هي لغة مفسرة، مما يعني أنها تقرأ الكود وتنفذه سطرًا بسطر. عندما ينفذ بايثون نصًا برمجيًا، فإنه يقوم أولاً بتجميعه إلى رمز ثنائي، وهي لغة وسيطة أقرب إلى كود الآلة، ثم يقوم مفسّر بايثون بتنفيذ هذا الرمز الثنائي.
def hello_world():
print("Hello, World!")
import dis
dis.dis(hello_world)
2 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('Hello, World!')
4 CALL_FUNCTION 1
6 POP_TOP
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
تقوم وحدة dis في Python بتفكيك الدالة hello_world
إلى بايت كود، كما هو موضح أعلاه.
ملاحظة: مفسّر بايثون هو آلة افتراضية تنفذ الكود الثنائي. مفسّر بايثون الافتراضي هو CPython، المكتوب بلغة C. هناك مفسّرات Python أخرى مثل Jython (المكتوب بلغة Java)، وIronPython (بـ.NET)، وPyPy (المكتوب بلغة Python وC)، لكن CPython هو الأكثر استخدامًا.
لماذا يتم تشغيل كود بايثون بشكل أسرع في الدالة
فكر في مثال مبسط يحتوي على حلقة تتكرر عبر مجموعة من الأرقام:
def my_function():
for i in range(100000000):
pass
عندما يتم تجميع هذه الدالة، قد يبدو الكود الثنائي كالتالي:
SETUP_LOOP 20 (to 23)
LOAD_GLOBAL 0 (range)
LOAD_CONST 3 (100000000)
CALL_FUNCTION 1
GET_ITER
FOR_ITER 6 (to 22)
STORE_FAST 0 (i)
JUMP_ABSOLUTE 13
POP_BLOCK
LOAD_CONST 0 (None)
RETURN_VALUE
التعليمات الأساسية هنا هي STORE_FAST
، والتي تستخدم لتخزين متغير الحلقة i
.
الآن دعونا نفكر في الكود الثنائي إذا كانت الحلقة في المستوى الأعلى من البرنامج النصي Python:
SETUP_LOOP 20 (to 23)
LOAD_NAME 0 (range)
LOAD_CONST 3 (100000000)
CALL_FUNCTION 1
GET_ITER
FOR_ITER 6 (to 22)
STORE_NAME 1 (i)
JUMP_ABSOLUTE 13
POP_BLOCK
LOAD_CONST 2 (None)
RETURN_VALUE
لاحظ أنه يتم استخدام تعليمة STORE_NAME
هنا، وليس STORE_FAST
.
إن البايت كود STORE_FAST أسرع من STORE_NAME لأنه في الدالة، يتم تخزين المتغيرات المحلية في مصفوفة ذات حجم ثابت، وليس في قاموس. يمكن الوصول إلى هذه المصفوفة مباشرة عبر فهرس، مما يجعل استرداد المتغيرات سريعًا جدًا. بشكل أساسي، إنه مجرد بحث عن مؤشر في القائمة وزيادة في عدد المراجع لـ PyObject، وكلاهما عمليات عالية الكفاءة.
من ناحية أخرى، يتم تخزين المتغيرات العالمية في قاموس. عند الوصول إلى متغير عالمي، يتعين على بايثون إجراء بحث في جدول التجزئة، والذي يتضمن حساب تجزئة ثم استرداد القيمة المرتبطة بها. على الرغم من أن هذا محسّن، إلا أنه لا يزال أبطأ بطبيعته من البحث المستند إلى الفهرس.
تحليل كود بايثون
هل تريد اختبار ذلك بنفسك؟ حاول قياس مستوى التعليمات البرمجية الخاصة بك وتوصيفها.
تعد المقارنة المعيارية والتوصيف من الممارسات المهمة في تحسين الأداء. فهي تساعدك على فهم كيفية عمل الكود الخاص بك وأين توجد الاختناقات.
يُعد التحليل المعياري هو المكان الذي يمكنك فيه ضبط وقت الكود الخاص بك لمعرفة المدة التي يستغرقها التشغيل. يمكنك استخدام وحدة time
المضمنة في بايثون، كما سنوضح لاحقًا، أو استخدام أدوات أكثر تطورًا مثل timeit.
من ناحية أخرى، يوفر ملف التعريف عرضًا أكثر تفصيلاً لتنفيذ التعليمات البرمجية الخاصة بك. فهو يوضح لك المكان الذي يقضي فيه الكود معظم وقته، والدوال التي يتم استدعاؤها، ومدى تكرار ذلك. يمكن استخدام profile المدمج أو وحدات cProfile لهذا الغرض.
إليك إحدى الطرق التي يمكنك من خلالها إنشاء ملف تعريف لكود بايثون الخاص بك:
import cProfile
def loop():
for i in range(10000000):
pass
cProfile.run('loop()')
سيؤدي هذا إلى إخراج تقرير مفصل لجميع استدعاءات الدالة التي تم إجراؤها أثناء تنفيذ دالة loop
.
مقارنة الكود في دالة مقابل النطاق العالمي
في بايثون، قد تختلف سرعة تنفيذ التعليمات البرمجية وفقًا لمكان تنفيذها – في دالة أو في النطاق العالمي. دعنا نقارن بين الاثنين باستخدام مثال بسيط.
فكر في مقتطف التعليمات البرمجية التالي الذي يحسب عاملي الرقم:
def factorial(n):
result = 1
for i in range(1, n + 1):
result *= i
return result
الآن دعنا نشغل نفس الكود ولكن في النطاق العالمي:
n = 20
result = 1
for i in range(1, n + 1):
result *= i
لقياس هادين الجزأين من التعليمات البرمجية، يمكننا استخدام وحدة timeit
في بايثون، والتي توفر طريقة بسيطة لقياس توقيت أجزاء صغيرة من كود بايثون.
import timeit
# Factorial function here...
def benchmark():
start = timeit.default_timer()
factorial(20)
end = timeit.default_timer()
print(end - start)
#
# Run benchmark on function code
#
benchmark()
# Prints: 3.541994374245405e-06
#
# Run benchmark on global scope code
#
start = timeit.default_timer()
n = 20
result = 1
for i in range(1, n + 1):
result *= i
end = timeit.default_timer()
print(end - start)
# Pirnts: 5.375011824071407e-06
ستجد أن كود الدالة يتم تنفيذه بشكل أسرع من كود النطاق العالمي. وذلك لأن بايثون ينفذ كود الوظيفة بشكل أسرع بسبب الأسباب التي ناقشناها سابقًا.
ملاحظة: إذا قمت بتشغيل دالة
benchmark()
وكود النطاق العالمي في نفس البرنامج النصي، فسيتم تشغيل كود النطاق العالمي بشكل أسرع. وذلك لأن الدالةbenchmark()
تضيف بعض الحمل إلى وقت التنفيذ ويتم إعطاء الكود العام بعض التحسينات داخليًا. ومع ذلك، إذا قمت بتشغيلهما بشكل منفصل، فستجد أن كود الوظيفة يعمل بشكل أسرع.
تحديد ملف تعريف الكود في وظيفة مقابل النطاق العالمي
يوفر بايثون وحدة مدمجة تسمى cProfile
لهذا الغرض. دعنا نستخدمها لإنشاء تعريف لدالة جديدة، والتي تحسب مجموع المربعات، في نطاق محلي وعالمي.
import cProfile
def sum_of_squares():
total = 0
for i in range(1, 10000000):
total += i * i
i = None
total = 0
def sum_of_squares_g():
global i
global total
for i in range(1, 10000000):
total += i * i
def profile(func):
pr = cProfile.Profile()
pr.enable()
func()
pr.disable()
pr.print_stats()
#
# Profile function code
#
print("Function scope:")
profile(sum_of_squares)
#
# Profile global scope code
#
print("Global scope:")
profile(sum_of_squares_g)
من نتائج التحليل، ستلاحظ أن كود الدالة أكثر كفاءة من حيث وقت التنفيذ.
Function scope:
2 function calls in 0.903 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.903 0.903 0.903 0.903 profiler.py:3(sum_of_squares)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
Global scope:
2 function calls in 1.358 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 1.358 1.358 1.358 1.358 profiler.py:10(sum_of_squares_g)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
نعتبر دالة sum_of_squares_g()
دالة عالمية لأنها تستخدم متغيرين عالميين، i
وtotal
. وكما رأينا سابقًا، فإن المتغيرات العالمية هي التي تبطئ تنفيذ التعليمات البرمجية، ولهذا السبب جعلنا هذه المتغيرات عالمية في هذا الكود.
تحسين أداء دالة بايثون
نظرًا لأن دوال بايثون تميل إلى التشغيل بشكل أسرع من الكود المكافئ في المجال العام ، فمن الجدير النظر في كيفية تحسين أداء دوالنا بشكل أكبر.
بالطبع، نظرًا لما رأيناه سابقًا، فإن إحدى الاستراتيجيات هي استخدام المتغيرات المحلية بدلاً من المتغيرات العالمية. إليك مثال:
import time
# Global variable
x = 5
def calculate_power_global():
for i in range(10000000):
y = x ** 2 # Accessing global variable
def calculate_power_local(x):
for i in range(10000000):
y = x ** 2 # Accessing local variable
start = time.time()
calculate_power_global()
end = time.time()
print(f"Execution time with global variable: {end - start} seconds")
start = time.time()
calculate_power_local(x)
end = time.time()
print(f"Execution time with local variable: {end - start} seconds")
في هذا المثال، سيتم تشغيل calculate_power_local
بشكل أسرع عادةً من calculate_power_global
، لأنه يستخدم متغيرًا محليًا بدلاً من متغير عالمي.
Execution time with global variable: 1.9901456832885742 seconds
Execution time with local variable: 1.9626312255859375 seconds
تتمثل استراتيجية تحسين أخرى في استخدام الدوال والمكتبات المضمنة كلما أمكن ذلك. يتم تنفيذ الدوال المضمنة في بايثون بلغة C، وهي أسرع كثيرًا من بايثون. وبالمثل، يتم تنفيذ العديد من مكتبات بايثون، مثل NumPy وPandas، أيضًا بلغة C أو C++، مما يجعلها أسرع من كود Python المكافئ.
على سبيل المثال، ضع في اعتبارك مهمة جمع قائمة من الأرقام. يمكنك كتابة دالة للقيام بذلك:
def sum_numbers(numbers):
total = 0
for number in numbers:
total += number
return total
ومع ذلك، فإن دالة sum المدمجة في بايثون ستفعل نفس الشيء، ولكن بشكل أسرع:
numbers = [1, 2, 3, 4, 5]
total = sum(numbers)
حاول ضبط توقيت هذين المقطعين من التعليمات البرمجية بنفسك ومعرفة أيهما أسرع!
في هذه المقالة، استكشفنا عالم تنفيذ أكواد بايثون المثير للاهتمام، مع التركيز بشكل خاص على سبب ميل أكواد بايثون إلى التشغيل بشكل أسرع عند تضمينها في دالة. لقد نظرنا بإيجاز في مفاهيم المقارنة المرجعية والتوصيفات، وقدمنا أمثلة عملية لكيفية تنفيذ هذه العمليات في كل من الدالة والنطاق العالمي.
لقد ناقشنا أيضًا بعض الطرق لتحسين أداء دالة بايثون. وبينما يمكن لهذه النصائح بالتأكيد أن تجعل تشغيل الكود الخاص بك أسرع، يجب عليك استخدام بعض التحسينات بعناية حيث من المهم تحقيق التوازن بين قابلية القراءة والصيانة والأداء.
اكتشاف المزيد من بايثون العربي
اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.