في الواقع ، عنوان هذا المقال الرائع لجيف ناب ، مؤلف كتاب "
كتابة لغة بايثون " يعكس جوهره بشكل كامل. اقرأ بعناية ولا تتردد في التعليق.
نظرًا لأننا لم نرغب حقًا في ترك المصطلح المهم بالأحرف اللاتينية في النص ، فقد سمحنا لأنفسنا بترجمة كلمة "docstring" على أنها "docstring" ، بعد أن اكتشفنا هذا
المصطلح في
عدة مصادر باللغة الروسية .
في Python ، كما هو الحال في معظم لغات البرمجة الحديثة ، تعد الوظيفة هي الطريقة الرئيسية للتلخيص والتغليف. أنت ، كمطور ، ربما تكون قد كتبت بالفعل مئات الوظائف. لكن وظائف لوظائف - الفتنة. علاوة على ذلك ، إذا كتبت وظائف "سيئة" ، فسيؤثر ذلك على الفور على إمكانية قراءة التعليمات البرمجية ودعمها. إذن ، ما هي الوظيفة "السيئة" ، والأهم من ذلك ، كيف تجعلها "جيدة"؟
قم بتحديث الموضوع
الرياضيات مليئة بالوظائف ، ولكن من الصعب تذكرها. لذلك دعونا نعود إلى نظامنا المفضل: التحليل. ربما تكون قد شاهدت صيغًا مثل
f(x) = 2x + 3
. هذه دالة تسمى
f
تأخذ وسيطة
x
ثم "ترجع" مرتين
x + 3
. على الرغم من أنها ليست مشابهة للوظائف التي اعتدنا عليها في Python ، إلا أنها تشبه تمامًا التعليمات البرمجية التالية:
def f(x): return 2*x + 3
توجد وظائف منذ فترة طويلة في الرياضيات ، ولكن في علوم الكمبيوتر يتم تحويلها بالكامل. ومع ذلك ، لا يتم إعطاء هذه القوة عبثا: عليك أن تمر بمزالق مختلفة. دعونا نناقش ما يجب أن تكون عليه الوظيفة "الجيدة" وما هي "الأجراس والصفارات" النموذجية للوظائف التي قد تتطلب إعادة هيكلة.
أسرار الوظيفة الجيدة
ما الذي يميز وظيفة Python "الجيدة" عن وظيفة متواضعة؟ سوف تفاجأ بعدد التفسيرات التي تسمح بها كلمة "جيد". كجزء من هذه المقالة ، سأعتبر وظيفة Python "جيدة" إذا استوفت
معظم العناصر في القائمة التالية (أحيانًا لا يمكن إكمال جميع العناصر لوظيفة معينة):
- تم تسميته بوضوح
- يتوافق مع مبدأ الواجب الوحيد
- يحتوي على قفص الاتهام
- إرجاع القيمة
- يتكون من ما لا يزيد عن 50 خط
- إنها عاطلة ، ونقية إذا أمكن
بالنسبة لكثير منكم ، قد تبدو هذه المتطلبات قاسية للغاية. ومع ذلك ، أعدك: إذا كانت وظائفك تتوافق مع هذه القواعد ، فستظهر بشكل جميل لدرجة أنها ستخترق يونيكورن بالدموع. أدناه سأكرس قسمًا لكل عنصر من عناصر القائمة أعلاه ، ثم سأكمل القصة من خلال إخبارهم كيف تنسق مع بعضها البعض وتساعد في إنشاء وظائف جيدة.
التسميةهذا هو الاقتباس المفضل لدي حول هذا الموضوع ، والذي غالبًا ما يُنسب خطأً إلى دونالد ، ولكنه مملوك بالفعل لـ
Phil Carleton :
هناك تحديان لعلوم الكمبيوتر: إبطال ذاكرة التخزين المؤقت والتسمية.
مهما كان الأمر سخيفًا ، فإن التسمية هي أمر صعب حقًا. فيما يلي مثال لاسم الوظيفة "غير صالح":
def get_knn_from_df(df):
الآن ، تصادفني أسماء سيئة في كل مكان تقريبًا ، ولكن هذا المثال مأخوذ من مجال علوم البيانات (بتعبير أدق ، التعلم الآلي) ، حيث يكتب الممارسون عادةً كودًا في دفتر ملاحظات Jupyter ، ثم يحاولون تجميع برنامج قابل للهضم من هذه الخلايا.
المشكلة الأولى في اسم هذه الوظيفة هي أنها تستخدم الاختصارات.
من الأفضل استخدام الكلمات الإنجليزية الكاملة ، بدلاً من الاختصارات وليس الاختصارات المعروفة . السبب الوحيد الذي يجعلني أرغب في تقصير الكلمات ليس إضاعة الوقت في كتابة نص كثير ، ولكن
أي محرر حديث لديه وظيفة الإكمال التلقائي ، لذلك عليك كتابة الاسم الكامل للوظيفة مرة واحدة فقط. الاختصار مشكلة ، لأنه غالبًا ما يكون محددًا لموضوع معين. في الكود أعلاه ، تعني
knn
"أقرب جيران K" ، وتعني
df
"DataFrame" ، وهي بنية بيانات شائعة الاستخدام في مكتبة
الباندا . إذا كان المبرمج الذي لا يعرف هذه الاختصارات يقرأ الشفرة ، فلن يفهم شيئًا تقريبًا في اسم الوظيفة.
هناك نوعان آخران من العيوب الطفيفة في اسم هذه الوظيفة. أولاً ، كلمة
"get"
زائدة عن الحاجة. في معظم الوظائف المسماة بكفاءة ، يتضح على الفور أن هذه الوظيفة ترجع شيئًا ما ينعكس بشكل خاص في الاسم. عنصر
from_d
f غير مطلوب أيضًا. إما في إرساء الوظيفة ، أو (إذا كان على المحيط) في التعليق التوضيحي للنوع ، سيتم وصف نوع المعلمة إذا
لم تكن هذه المعلومات
واضحة بالفعل من اسم المعلمة .
فكيف نعيد تسمية هذه الميزة؟ فقط:
def k_nearest_neighbors(dataframe):
الآن حتى الشخص العادي يفهم ما يتم حسابه في هذه الوظيفة ، ولا يترك اسم المعلمة
(dataframe)
أي شك حول أي وسيطة يجب تمريرها إليها.
المسؤولية الوحيدة
بتطوير فكرة بوب مارتن ، سأقول أن
مبدأ المسؤولية المنفردة ينطبق على الوظائف التي لا تقل عن الطبقات والوحدات (التي كتب عنها السيد مارتن في الأصل). وفقًا لهذا المبدأ (في حالتنا) ، يجب أن يكون للوظيفة مسؤولية واحدة. أي أنها يجب أن تفعل شيئًا واحدًا فقط. أحد الأسباب الأكثر إلحاحًا لذلك: إذا كانت الوظيفة تؤدي شيئًا واحدًا فقط ، فيجب إعادة كتابتها في الحالة الوحيدة: إذا كان هذا الشيء يجب أن يتم بطريقة جديدة. يصبح من الواضح أيضًا متى يمكن حذف وظيفة ؛ إذا أدركنا ، من خلال إجراء تغييرات في مكان آخر ، أن المهمة الوحيدة للوظيفة لم تعد ذات صلة ، فسوف نتخلص منها ببساطة.
من الأفضل إعطاء مثال. فيما يلي وظيفة تقوم بأكثر من "شيء" واحد:
def calculate_and print_stats(list_of_numbers): sum = sum(list_of_numbers) mean = statistics.mean(list_of_numbers) median = statistics.median(list_of_numbers) mode = statistics.mode(list_of_numbers) print('-----------------Stats-----------------') print('SUM: {}'.format(sum) print('MEAN: {}'.format(mean) print('MEDIAN: {}'.format(median) print('MODE: {}'.format(mode)
وهما اثنان: يحسب مجموعة من الإحصائيات في قائمة أرقام ويعرضها في
STDOUT
. الوظيفة تنتهك قاعدة: يجب أن يكون هناك سبب واحد محدد لضرورة تغييرها. في هذه الحالة ، هناك سببان واضحان لضرورة القيام بذلك: إما أنك تحتاج إلى حساب إحصائيات جديدة أو مختلفة ، أو تحتاج إلى تغيير تنسيق الإخراج. لذلك ، من الأفضل إعادة كتابة هذه الوظيفة في شكل وظيفتين منفصلتين: إحداهما ستقوم بإجراء العمليات الحسابية وإرجاع نتائجها ، والأخرى ستتلقى هذه النتائج وعرضها في وحدة التحكم.
دالة (أو بالأحرى ، لديها مسئولين) مع حوصلة تعطي الكلمة وباسمها .
يبسط هذا الفصل أيضًا إلى حد كبير اختبار الوظيفة ، ويسمح لك أيضًا بتقسيمها إلى وظيفتين فقط داخل نفس الوحدة ، ولكن حتى لفصل هاتين الوظيفتين إلى وحدات مختلفة تمامًا ، إذا كان ذلك مناسبًا. يساهم هذا أيضًا في إجراء اختبار أكثر نظافة ويبسط دعم التعليمات البرمجية.
في الواقع ، الوظائف التي تؤدي شيئين بالضبط نادرة. في كثير من الأحيان ، تصادف وظائف تؤدي الكثير من العمليات. مرة أخرى ، ولأسباب سهولة القراءة والاختبار ، يجب تقسيم هذه الوظائف "متعددة المحطات" إلى مهمة واحدة ، يحتوي كل منها على جانب واحد من العمل.
الوثائق
يبدو أن الجميع يدركون أن هناك وثيقة
PEP-8 تقدم توصيات حول نمط رمز Python ، ولكن هناك عدد أقل بكثير من الأشخاص بيننا الذين يعرفون
PEP-257 ، حيث يتم تقديم نفس التوصيات فيما يتعلق ب dockstrings. لكي لا أقوم بإعادة بيع محتويات PEP-257 ، أرسل لك هذا المستند بنفسك - اقرأ في وقت فراغك. ومع ذلك ، فإن أفكاره الرئيسية هي كما يلي:
- تحتاج كل وظيفة إلى سلسلة مستندات.
- يجب مراعاة قواعد اللغة وعلامات الترقيم ؛ كتابة جمل كاملة
- تبدأ الوثيقة الوثائقية بوصف موجز (في جملة واحدة) لما تقوم به الوظيفة.
- تمت صياغة الوثيقة بأسلوب وصفي بدلاً من وصفي
كل هذه النقاط سهلة المتابعة عند كتابة الميزات. مجرد كتابة الوثائق يجب أن يصبح عادة ، ومحاولة كتابتها قبل الشروع في رمز الوظيفة نفسها. إذا لم تتمكن من كتابة سلسلة مستندات واضحة تصف الوظيفة ، فهذا سبب جيد للتفكير في سبب كتابة هذه الوظيفة.
قيم الإرجاع
يمكن (
ويجب ) تفسير الوظائف على أنها برامج صغيرة قائمة بذاتها. يأخذون بعض المدخلات في شكل معلمات ويعيدون النتيجة. المعلمات ، بالطبع ، اختيارية.
لكن قيم الإرجاع مطلوبة من وجهة نظر الهيكل الداخلي لبيثون . حتى إذا حاولت كتابة دالة لا تُرجع قيمة ، لا يمكنك ذلك. إذا كانت الدالة لا تُرجع حتى القيم ، فسيقوم مترجم Python "بإجبارها" على إرجاع
None
. لا تصدق؟ جربها بنفسك:
❯ python3 Python 3.7.0 (default, Jul 23 2018, 20:22:55) [Clang 9.1.0 (clang-902.0.39.2)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> def add(a, b): ... print(a + b) ... >>> b = add(1, 2) 3 >>> b >>> b is None True
كما ترون ، فإن قيمة
b
هي في الأساس بلا. لذا ، حتى إذا قمت بكتابة دالة بدون بيان إرجاع ، فستستمر في إرجاع شيء ما. وينبغي. بعد كل شيء ، هذا برنامج صغير ، أليس كذلك؟ ما مدى فائدة البرامج التي لا يوجد استنتاج منها - وبالتالي من المستحيل الحكم على ما إذا كان هذا البرنامج قد تم تنفيذه بشكل صحيح؟ ولكن الأهم من ذلك ، كيف
ستختبر مثل هذا البرنامج؟
لا أخشى حتى أن أقول ما يلي: يجب أن ترجع كل وظيفة قيمة مفيدة ، على الأقل من أجل قابلية الاختبار. يجب اختبار الرمز الذي أكتبه (لم تتم مناقشة هذا). فقط تخيل كيف يمكن أن يتحول الاختبار الخرقاء لوظيفة
add
المذكورة أعلاه (تلميح: سيكون عليك إعادة توجيه المدخلات / المخرجات ، وبعد ذلك سوف يسير كل شيء قريبًا). بالإضافة إلى ذلك ، من خلال إرجاع قيمة ، يمكننا ربط الطرق وبالتالي كتابة رمز مثل هذا:
with open('foo.txt', 'r') as input_file: for line in input_file: if line.strip().lower().endswith('cat'):
String
if line.strip().lower().endswith('cat'):
يعمل لأن كل من أساليب السلسلة (
strip()
و
lower()
و
endswith()
) ترجع سلسلة نتيجة استدعاء الوظيفة.
فيما يلي بعض الأسباب الشائعة التي يمكن للمبرمج أن يعطيك إياها عندما تشرح سبب عدم قيام دالة يكتبها بإرجاع قيمة:
"إنها فقط [نوع من العمليات المتعلقة بالإدخال / الإخراج ، على سبيل المثال ، تخزين قيمة في قاعدة بيانات]. هنا لا يمكنني إعادة أي شيء مفيد ".
لا أوافق. يمكن أن ترجع الدالة True إذا اكتملت العملية بنجاح.
"هنا نغير إحدى المعلمات المتاحة ، ونستخدمها كمعلمة مرجعية." ""
هنا نقطتان. أولاً ، ابذل قصارى جهدك حتى لا تفعل ذلك. ثانيًا ، تزويد دالة بنوع من الجدل فقط لمعرفة أنها تغيرت أمر مثير للدهشة في أحسن الأحوال ، وخطير ببساطة في أسوأ الأحوال. بدلاً من ذلك ، كما هو الحال مع أساليب السلسلة ، حاول إرجاع مثيل جديد للمعلمة يعكس بالفعل التغييرات المطبقة عليه. حتى إذا لم ينجح ذلك ، نظرًا لأن إنشاء نسخة من بعض المعلمات محفوف بالتكاليف الزائدة ، فلا يزال بإمكانك التراجع إلى الخيار "إرجاع
True
إذا اكتملت العملية بنجاح" المقترح أعلاه.
"أحتاج إلى إعادة قيم متعددة. لا توجد قيمة واحدة في هذه الحالة من المستحسن أن تعود ".
هذه الحجة بعيدة المنال قليلاً ، لكنني سمعتها. الجواب ، بالطبع ، هو بالضبط ما أراد المؤلف القيام به - لكنه لم يعرف كيف:
استخدم مجموعة tuple لإرجاع قيم متعددة .
أخيرًا ، أقوى حجة مفادها أنه من الأفضل إرجاع قيمة مفيدة في أي حال هي أن المتصل يمكنه دائمًا تجاهل هذه القيم بشكل مبرر. وباختصار ، فإن إعادة قيمة من دالة يكاد يكون من المؤكد أنها فكرة سليمة ، ومن غير المرجح أن نتلف أي شيء بهذه الطريقة ، حتى في قواعد التعليمات البرمجية الحالية.
طول الوظيفة
اعترفت أكثر من مرة أنني غبية جدًا. يمكنني الاحتفاظ بثلاثة أشياء في رأسي في نفس الوقت. إذا سمحت لي بقراءة وظيفة 200 سطر وسألت عما تفعله ، فمن المحتمل أن أحدق بها لمدة 10 ثوانٍ على الأقل.
يؤثر طول الدالة بشكل مباشر على سهولة قراءتها وبالتالي دعمها . لذلك ، حاول أن تجعل وظائفك قصيرة. 50 خطًا - قيمة مأخوذة تمامًا من السقف ، لكنها تبدو معقولة بالنسبة لي. (آمل) أن تكون معظم الوظائف التي تصادفها في الكتابة أقصر بكثير.
إذا كانت الوظيفة تتوافق مع مبدأ المسؤولية الفردية ، فمن المحتمل أن تكون مختصرة بما فيه الكفاية. إذا كانت القراءة أو idempotent (سنتحدث عن هذا) أدناه - ثم ، على الأرجح ، ستنتهي أيضًا. يتم دمج كل هذه الأفكار بشكل متناغم مع بعضها البعض وتساعد على كتابة كود جيد ونظيف.
إذن ماذا تفعل إذا كانت وظيفتك طويلة جدًا؟
عاكس! ربما يتعين عليك إعادة بيعها طوال الوقت ، حتى لو كنت لا تعرف المصطلح.
إعادة الهيكلة هي ببساطة تغيير هيكل البرنامج ، دون تغيير سلوكه. لذلك ، يعد استخراج عدة أسطر من التعليمات البرمجية من دالة طويلة وتحويلها إلى وظيفة مستقلة أحد أنواع إعادة الهيكلة. وتبين أن هذه هي أيضًا الطريقة الأكثر شيوعًا والأسرع لتقصير الوظائف الطويلة بشكل منتج. نظرًا لأنك تعطي هذه الوظائف الجديدة أسماء مناسبة ، فإن التعليمات البرمجية الناتجة أسهل في القراءة. لقد كتبت كتابًا كاملاً حول إعادة البيع (في الواقع ، أقوم بذلك طوال الوقت) ، لذلك لن أخوض في التفاصيل هنا. فقط اعلم أنه إذا كان لديك وظيفة طويلة جدًا ، فيجب إعادة تشكيلها.
المثالية والنظافة الوظيفية
قد يبدو عنوان هذا القسم مخيفًا بعض الشيء ، ولكن القسم بسيط من الناحية النظرية. تُرجع الدالة idempotent بنفس مجموعة الوسيطات نفس القيمة دائمًا ، بغض النظر عن عدد المرات التي يتم استدعاؤها فيها. لا تعتمد النتيجة على متغيرات غير محلية ، أو تنوع الحجج ، أو على أي بيانات قادمة من تدفقات المدخلات / المخرجات.
add_three(number)
التالية هي
add_three(number)
:
def add_three(number): """ ** + 3.""" return number + 3
بغض النظر عن عدد المرات التي نسميها
add_three(7)
، فإن الإجابة ستكون دائمًا 10. ولكن حالة أخرى هي وظيفة غير معبرة:
def add_three(): """ 3 + , .""" number = int(input('Enter a number: ')) return number + 3
هذه الوظيفة المفترضة بصراحة ليست عاطلة ، لأن القيمة المرتدة للدالة تعتمد على الإدخال / الإخراج ، أي على الرقم الذي أدخله المستخدم. بالطبع ، مع مكالمات مختلفة إلى
add_three()
ستكون قيم الإرجاع مختلفة. إذا قمنا باستدعاء هذه الوظيفة مرتين ، فيمكن للمستخدم في الحالة الأولى إدخال 3 ، وفي الثانية - 7 ، ثم
add_three()
إلى
add_three()
6 و 10 ، على التوالي.
خارج البرمجة ، هناك أيضًا أمثلة على idempotency - على سبيل المثال ، تم تصميم الزر لأعلى المصعد وفقًا لهذا المبدأ. من خلال الضغط عليه لأول مرة ، "نبلغ" المصعد الذي نريد الصعود إليه. نظرًا لأن الزر idempotent ، بغض النظر عن مقدار الضغط عليه لاحقًا ، فلن يحدث شيء سيئ. ستكون النتيجة هي نفسها دائمًا.
لماذا أهمية idempotency
اختبار قابلية الاستخدام ودعمه. من السهل اختبار الوظائف Idempotent ، حيث أنها مضمونة لعرض نفس النتيجة في أي حال إذا قمت باستدعاؤها بنفس الحجج. يأتي الاختبار للتحقق من أنه مع مجموعة متنوعة من المكالمات ، ترجع الوظيفة القيمة المتوقعة دائمًا. علاوة على ذلك ، ستكون هذه الاختبارات سريعة: تعد سرعة الاختبار مشكلة مهمة غالبًا ما يتم تجاهلها في اختبار الوحدة. وإعادة الهيكلة عند العمل مع وظائف idempotent بشكل عام هو المشي السهل. لا يهم كيف تغير الشفرة خارج الوظيفة - وستكون نتيجة استدعائها بنفس الوسيطات هي نفسها دائمًا.
ما هي الوظيفة "النقية"؟
في البرمجة الوظيفية ، تعتبر الوظيفة نقية إذا كانت ،
أولاً ، عاطلة ،
وثانيًا ، لا تسبب
الآثار الجانبية الملحوظة. لا تنسى: تكون الدالة عاطفة إذا كانت تعرض دائمًا نفس النتيجة مع مجموعة محددة من الحجج. ومع ذلك ، هذا لا يعني أن الوظيفة لا يمكن أن تؤثر على المكونات الأخرى - على سبيل المثال ، المتغيرات غير المحلية أو تدفقات المدخلات / المخرجات. على سبيل المثال ، إذا كان الإصدار
add_three(number)
أعلاه ينتج النتيجة إلى وحدة التحكم ، وعندها فقط يعيدها ، فسيظل يعتبر idempotent ، لأنه عندما يصل إلى دفق الإدخال / الإخراج ، لا تؤثر عملية الوصول هذه على القيمة المرتجعة من الوظيفة. المكالمة
print()
هي مجرد
أثر جانبي : التفاعل مع بقية البرنامج أو النظام على هذا النحو ، يحدث مع القيمة المرتجعة.
دعنا نطور مثالنا قليلاً باستخدام
add_three(number)
. يمكنك كتابة الكود التالي لتحديد عدد مرات
add_three(number)
:
add_three_calls = 0 def add_three(number): """ ** + 3.""" global add_three_calls print(f'Returning {number + 3}') add_three_calls += 1 return number + 3 def num_calls(): """, *add_three*.""" return add_three_calls
الآن نقوم بتنفيذ الإخراج على وحدة التحكم (هذا تأثير جانبي) وتغيير المتغير غير المحلي (تأثير جانبي آخر) ، ولكن نظرًا لعدم تأثير أي منهما أو الآخر على القيمة التي ترجعها الوظيفة ، فهو غير قادر على أي حال.
الوظيفة النقية ليس لها آثار جانبية. فهي لا تستخدم أي "بيانات خارجية" فقط عند حساب القيمة ، ولكنها لا تتفاعل مع باقي البرنامج / النظام ، بل تقوم فقط بحساب القيمة المحددة وإرجاعها. لذلك ، على الرغم من أن تعريفنا الجديد لـ
add_three(number)
لا يزال قاصرًا ، إلا أن هذه الوظيفة لم تعد نقية.
في الوظائف الخالصة ، لا توجد تعليمات تسجيل أو
print()
مكالمات
print()
. عند العمل ، لا يمكنهم الوصول إلى قاعدة البيانات ولا يستخدمون اتصالات الإنترنت. لا تدخل أو تعدل المتغيرات غير المحلية.
ولا تدعو وظائف أخرى غير نقية .
باختصار ، ليس لديهم "عمل رهيب طويل المدى" ، كما عبرت عنه كلمات آينشتاين (ولكن في سياق علوم الكمبيوتر ، وليس الفيزياء). لا يغيرون بأي شكل من الأشكال بقية البرنامج أو النظام. في
البرمجة الحتمية (وهو ما تفعله عند كتابة التعليمات البرمجية في Python) ، فإن هذه الوظائف هي الأكثر أمانًا. وهي معروفة بإمكانية اختبارها وسهولة دعمها ؛ علاوة على ذلك ، نظرًا لأنهم متشددون ، فإن اختبار مثل هذه الوظائف مضمون ليكون سريعًا مثل التنفيذ. الاختبارات نفسها بسيطة أيضًا: ليس عليك الاتصال بقاعدة البيانات أو محاكاة أي موارد خارجية ، وإعداد التهيئة الأولية للرمز ، وفي نهاية العمل ، لا تحتاج إلى تنظيف أي شيء.
بصراحة ، التماسك والنظافة أمر مرغوب فيه للغاية ، ولكن ليس مطلوبًا. , , , . , , , , . , , .
الخلاصة
هذا كل شيء. , – . . , . – ! . , , , « ». .