Python هي لغة برمجة رائعة لتطوير البرامج النصية المستقلة. من أجل تحقيق النتيجة المرجوة باستخدام برنامج نصي مماثل ، تحتاج إلى كتابة عدة عشرات أو مئات من أسطر التعليمات البرمجية. وبعد الانتهاء من العمل ، يمكنك ببساطة نسيان الكود المكتوب والمضي قدماً في حل المشكلة التالية.
إذا ، على سبيل المثال ، بعد ستة أشهر من كتابة نص معين لمرة واحدة ، يسأل أحدهم المؤلف عن سبب فشل هذا البرنامج النصي ، فقد لا يكون مؤلف البرنامج على علم بذلك. يحدث هذا لأن البرنامج النصي لم يكن وثائق مكتوبة ، بسبب استخدام المعلمات التي تم ترميزها في التعليمات البرمجية ، نظرًا لحقيقة أن البرنامج النصي لا يسجل أي شيء أثناء العمل ، وبسبب عدم وجود اختبارات سمحت لفهم بسرعة سبب المشكلة.

تجدر الإشارة إلى أن تحويل البرنامج النصي المكتوب على عجل إلى شيء أفضل ليس بالأمر الصعب. وهي ، مثل هذا البرنامج النصي ، من السهل جدًا أن يتحول إلى رمز موثوق ومفهوم مناسب للاستخدام ، إلى رمز بسيط لدعم كل من مؤلفيه والمبرمجين الآخرين.
سيوضح مؤلف المادة ، التي ننشر ترجمتها اليوم ، هذا "التحول" باستخدام
اختبار Fizz Buzz الكلاسيكي كمثال. هذه المهمة هي عرض قائمة بالأرقام من 1 إلى 100 ، واستبدال بعضها بخطوط خاصة. لذلك ، إذا كان الرقم مضاعفًا لـ 3 ، فأنت بحاجة إلى طباعة سطر
Fizz
بدلاً من ذلك ، وإذا كان الرقم هو مضاعف 5 ، سطر
Buzz
، وإذا تم استيفاء
FizzBuzz
،
FizzBuzz
.
شفرة المصدر
فيما يلي الكود المصدري لبرنامج نصي Python يحل المشكلة:
import sys for n in range(int(sys.argv[1]), int(sys.argv[2])): if n % 3 == 0 and n % 5 == 0: print("fizzbuzz") elif n % 3 == 0: print("fizz") elif n % 5 == 0: print("buzz") else: print(n)
دعونا نتحدث عن كيفية تحسينه.
الوثائق
أجد أنه من المفيد كتابة الوثائق قبل كتابة الكود. هذا يبسط العمل ويساعد على عدم تأخير إنشاء الوثائق إلى أجل غير مسمى. يمكن وضع وثائق البرنامج النصي في الأعلى. على سبيل المثال ، قد يبدو كالتالي:
يعطي السطر الأول وصفًا موجزًا للغرض من البرنامج النصي. توفر الفقرات المتبقية معلومات إضافية حول ما يفعله البرنامج النصي.
وسيطات سطر الأوامر
ستكون المهمة التالية لتحسين البرنامج النصي هي استبدال القيم التي تم ترميزها في التعليمات البرمجية بالقيم الموثقة التي تم تمريرها إلى البرنامج النصي من خلال وسيطات سطر الأوامر. يمكن القيام بذلك باستخدام وحدة
argparse . في مثالنا ، نقترح على المستخدم تحديد مجموعة من الأرقام وتحديد قيم "fizz" و "buzz" المستخدمة عند التحقق من الأرقام من النطاق المحدد.
import argparse import sys class CustomFormatter(argparse.RawDescriptionHelpFormatter, argparse.ArgumentDefaultsHelpFormatter): pass def parse_args(args=sys.argv[1:]): """Parse arguments.""" parser = argparse.ArgumentParser( description=sys.modules[__name__].__doc__, formatter_class=CustomFormatter) g = parser.add_argument_group("fizzbuzz settings") g.add_argument("--fizz", metavar="N", default=3, type=int, help="Modulo value for fizz") g.add_argument("--buzz", metavar="N", default=5, type=int, help="Modulo value for buzz") parser.add_argument("start", type=int, help="Start value") parser.add_argument("end", type=int, help="End value") return parser.parse_args(args) options = parse_args() for n in range(options.start, options.end + 1):
هذه التغييرات ذات فائدة كبيرة للبرنامج النصي. وهي المعلمات الآن موثقة بشكل صحيح ، يمكنك معرفة الغرض منها باستخدام
--help
العلم. علاوة على ذلك ، وفقًا للأمر المقابل ، يتم أيضًا عرض الوثائق التي كتبناها في القسم السابق:
$ ./fizzbuzz.py --help usage: fizzbuzz.py [-h] [--fizz N] [--buzz N] start end Simple fizzbuzz generator. This script prints out a sequence of numbers from a provided range with the following restrictions: - if the number is divisible by 3, then print out "fizz", - if the number is divisible by 5, then print out "buzz", - if the number is divisible by 3 and 5, then print out "fizzbuzz". positional arguments: start Start value end End value optional arguments: -h, --help show this help message and exit fizzbuzz settings: --fizz N Modulo value for fizz (default: 3) --buzz N Modulo value for buzz (default: 5)
وحدة
argparse
هي أداة قوية جدا. إذا لم تكن على دراية به ، فسيكون من المفيد لك عرض
الوثائق الموجودة عليه. على وجه الخصوص ، أحب قدرته على تحديد
الأوامر الفرعية ومجموعات الحجج .
تسجيل
إذا قمت بتجهيز البرنامج النصي بالقدرة على عرض بعض المعلومات أثناء تنفيذه ، فسوف يتحول ذلك إلى إضافة ممتعة لوظائفه. وحدة
تسجيل مناسبة تماما لهذا الغرض. أولاً ، نصف كائنًا يقوم بتنفيذ التسجيل:
import logging import logging.handlers import os import sys logger = logging.getLogger(os.path.splitext(os.path.basename(sys.argv[0]))[0])
بعد ذلك ، سنعمل على التحكم في تفاصيل المعلومات المعروضة أثناء التسجيل. لذلك ، يجب أن يقوم الأمر
logger.debug()
بإخراج شيء فقط إذا تم تشغيل البرنامج النصي مع مفتاح التبديل -
--debug
. إذا تم تشغيل البرنامج النصي باستخدام
--silent
، فلن يعرض البرنامج النصي أي شيء باستثناء رسائل الاستثناء. لتطبيق هذه الميزات ، أضف التعليمات البرمجية التالية إلى
parse_args()
:
أضف الوظيفة التالية إلى رمز المشروع لتكوين التسجيل:
def setup_logging(options): """Configure logging.""" root = logging.getLogger("") root.setLevel(logging.WARNING) logger.setLevel(options.debug and logging.DEBUG or logging.INFO) if not options.silent: ch = logging.StreamHandler() ch.setFormatter(logging.Formatter( "%(levelname)s[%(name)s] %(message)s")) root.addHandler(ch)
سيتغير رمز البرنامج النصي الرئيسي كما يلي:
if __name__ == "__main__": options = parse_args() setup_logging(options) try: logger.debug("compute fizzbuzz from {} to {}".format(options.start, options.end)) for n in range(options.start, options.end + 1):
إذا كنت تخطط لتشغيل البرنامج النصي دون المشاركة المباشرة للمستخدم ، على سبيل المثال ، باستخدام
crontab
، يمكنك جعل مخرجاته تذهب إلى
syslog
:
def setup_logging(options): """Configure logging.""" root = logging.getLogger("") root.setLevel(logging.WARNING) logger.setLevel(options.debug and logging.DEBUG or logging.INFO) if not options.silent: if not sys.stderr.isatty(): facility = logging.handlers.SysLogHandler.LOG_DAEMON sh = logging.handlers.SysLogHandler(address='/dev/log', facility=facility) sh.setFormatter(logging.Formatter( "{0}[{1}]: %(message)s".format( logger.name, os.getpid()))) root.addHandler(sh) else: ch = logging.StreamHandler() ch.setFormatter(logging.Formatter( "%(levelname)s[%(name)s] %(message)s")) root.addHandler(ch)
في البرنامج النصي الصغير الخاص بنا ، يبدو أنه من الضروري استخدام قدر مماثل من الشفرة لاستخدام الأمر
logger.debug()
. لكن في البرامج النصية الحقيقية ، لن يبدو هذا الرمز بعد الآن وسيأتي الاستفادة منه في المقدمة ، أي أنه مع مساعدة المستخدمين سيكون بإمكانهم معرفة التقدم المحرز في حل المشكلة.
$ ./fizzbuzz.py --debug 1 3 DEBUG[fizzbuzz] compute fizzbuzz from 1 to 3 1 2 fizz
اختبارات
تعتبر اختبارات الوحدة أداة مفيدة للتحقق مما إذا كانت التطبيقات تتصرف كما ينبغي. يتم استخدام البرامج النصية للوحدة بشكل غير منتظم في البرامج النصية ، ولكن إدراجها في البرامج النصية يحسن بشكل كبير من موثوقية الكود. نقوم بتحويل الكود الموجود داخل الحلقة إلى وظيفة ووصف العديد من الأمثلة التفاعلية لاستخدامها في وثائقها:
def fizzbuzz(n, fizz, buzz): """Compute fizzbuzz nth item given modulo values for fizz and buzz. >>> fizzbuzz(5, fizz=3, buzz=5) 'buzz' >>> fizzbuzz(3, fizz=3, buzz=5) 'fizz' >>> fizzbuzz(15, fizz=3, buzz=5) 'fizzbuzz' >>> fizzbuzz(4, fizz=3, buzz=5) 4 >>> fizzbuzz(4, fizz=4, buzz=6) 'fizz' """ if n % fizz == 0 and n % buzz == 0: return "fizzbuzz" if n % fizz == 0: return "fizz" if n % buzz == 0: return "buzz" return n
يمكنك التحقق من التشغيل الصحيح للوظيفة باستخدام
pytest
:
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py ============================ test session starts ============================= platform linux -- Python 3.7.4, pytest-3.10.1, py-1.8.0, pluggy-0.8.0 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/bernat/code/perso/python-script, inifile: plugins: xdist-1.26.1, timeout-1.3.3, forked-1.0.2, cov-2.6.0 collected 1 item fizzbuzz.py::fizzbuzz.fizzbuzz PASSED [100%] ========================== 1 passed in 0.05 seconds ==========================
لكي يعمل كل هذا ، تحتاج إلى ملحق
.py
ليأتي بعد اسم البرنامج النصي. لا أحب إضافة إضافات إلى أسماء البرامج النصية: اللغة هي مجرد تفاصيل فنية لا يلزم عرضها للمستخدم. ومع ذلك ، يبدو أن تجهيز اسم البرنامج النصي بامتداد هو أسهل طريقة للسماح لأنظمة تشغيل الاختبارات ، مثل
pytest
، بالعثور على الاختبارات المضمنة في الكود.
في حالة حدوث خطأ
pytest
رسالة تشير إلى موقع الكود المقابل وطبيعة المشكلة:
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py -k fizzbuzz.fizzbuzz ============================ test session starts ============================= platform linux -- Python 3.7.4, pytest-3.10.1, py-1.8.0, pluggy-0.8.0 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/bernat/code/perso/python-script, inifile: plugins: xdist-1.26.1, timeout-1.3.3, forked-1.0.2, cov-2.6.0 collected 1 item fizzbuzz.py::fizzbuzz.fizzbuzz FAILED [100%] ================================== FAILURES ================================== ________________________ [doctest] fizzbuzz.fizzbuzz _________________________ 100 101 >>> fizzbuzz(5, fizz=3, buzz=5) 102 'buzz' 103 >>> fizzbuzz(3, fizz=3, buzz=5) 104 'fizz' 105 >>> fizzbuzz(15, fizz=3, buzz=5) 106 'fizzbuzz' 107 >>> fizzbuzz(4, fizz=3, buzz=5) 108 4 109 >>> fizzbuzz(4, fizz=4, buzz=6) Expected: fizz Got: 4 /home/bernat/code/perso/python-script/fizzbuzz.py:109: DocTestFailure ========================== 1 failed in 0.02 seconds ==========================
يمكن أيضًا كتابة اختبارات الوحدة برمز منتظم. تخيل أننا بحاجة إلى اختبار الوظيفة التالية:
def main(options): """Compute a fizzbuzz set of strings and return them as an array.""" logger.debug("compute fizzbuzz from {} to {}".format(options.start, options.end)) return [str(fizzbuzz(i, options.fizz, options.buzz)) for i in range(options.start, options.end+1)]
في نهاية البرنامج النصي ، نضيف اختبارات الوحدة التالية باستخدام
pytest
لاستخدام
وظائف الاختبار المعلمة :
يرجى ملاحظة أنه بما أن رمز البرنامج النصي ينتهي باستدعاء
sys.exit()
، فلن يتم تنفيذ الاختبارات عندما يتم الاتصال به بشكل طبيعي. بفضل هذا ، ليست هناك حاجة
pytest
لتشغيل البرنامج النصي.
سيتم استدعاء وظيفة الاختبار مرة واحدة لكل مجموعة من المعلمات. يتم استخدام الكيان
args
كمدخل
parse_args()
. بفضل هذه الآلية ، نحصل على ما نحتاج إلى نقله إلى الوظيفة
main()
. تتم مقارنة الكيان
expected
main()
. إليك ما
pytest
لنا
pytest
إذا كان كل شيء يعمل بالشكل المتوقع:
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py ============================ test session starts ============================= platform linux -- Python 3.7.4, pytest-3.10.1, py-1.8.0, pluggy-0.8.0 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/bernat/code/perso/python-script, inifile: plugins: xdist-1.26.1, timeout-1.3.3, forked-1.0.2, cov-2.6.0 collected 7 items fizzbuzz.py::fizzbuzz.fizzbuzz PASSED [ 14%] fizzbuzz.py::test_main[0 0-expected0] PASSED [ 28%] fizzbuzz.py::test_main[3 5-expected1] PASSED [ 42%] fizzbuzz.py::test_main[9 12-expected2] PASSED [ 57%] fizzbuzz.py::test_main[14 17-expected3] PASSED [ 71%] fizzbuzz.py::test_main[14 17 --fizz=2-expected4] PASSED [ 85%] fizzbuzz.py::test_main[17 20 --buzz=10-expected5] PASSED [100%] ========================== 7 passed in 0.03 seconds ==========================
في حالة حدوث خطأ ، ستقدم
pytest
معلومات مفيدة حول ما حدث:
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py [...] ================================== FAILURES ================================== __________________________ test_main[0 0-expected0] __________________________ args = '0 0', expected = ['0'] @pytest.mark.parametrize("args, expected", [ ("0 0", ["0"]), ("3 5", ["fizz", "4", "buzz"]), ("9 12", ["fizz", "buzz", "11", "fizz"]), ("14 17", ["14", "fizzbuzz", "16", "17"]), ("14 17 --fizz=2", ["fizz", "buzz", "fizz", "17"]), ("17 20 --buzz=10", ["17", "fizz", "19", "buzz"]), ]) def test_main(args, expected): options = parse_args(shlex.split(args)) options.debug = True options.silent = True setup_logging(options) assert main(options) == expected E AssertionError: assert ['fizzbuzz'] == ['0'] E At index 0 diff: 'fizzbuzz' != '0' E Full diff: E - ['fizzbuzz'] E + ['0'] fizzbuzz.py:160: AssertionError ----------------------------- Captured log call ------------------------------ fizzbuzz.py 125 DEBUG compute fizzbuzz from 0 to 0 ===================== 1 failed, 6 passed in 0.05 seconds =====================
يتم
logger.debug()
تضمين الإخراج من الأمر
logger.debug()
في هذا الإخراج. هذا سبب وجيه آخر لاستخدام آليات التسجيل في البرامج النصية. إذا كنت تريد معرفة المزيد عن الميزات الرائعة لـ
pytest
،
pytest
نظرة على
هذه المواد.
النتائج
يمكنك جعل البرامج النصية Python أكثر موثوقية باتباع الخطوات الأربع التالية:
- تجهيز البرنامج النصي بالوثائق الموجودة في أعلى الملف.
- استخدم الوحدة النمطية
argparse
لتوثيق المعاملات التي يمكن استدعاء البرنامج النصي لها. - استخدم وحدة
logging
لعرض معلومات حول عملية تشغيل البرنامج النصي. - اكتب اختبارات الوحدة.
إليك الكود الكامل للمثال الذي تمت مناقشته هنا. يمكنك استخدامه كقالب للبرامج النصية الخاصة بك.
بدأت مناقشات مثيرة للاهتمام حول هذه المادة - يمكنك العثور عليها
هنا وهنا . يبدو أن الجمهور تلقى توصيات جيدة بشأن الوثائق وحجج سطر الأوامر ، لكن ماذا عن التسجيل والاختبارات بدت لبعض القراء وكأنها "لقطة من مدفع على العصافير".
إليكم المواد التي كُتبت استجابةً لهذه المقالة.
أعزائي القراء! هل تخطط لتطبيق توصيات لكتابة النصوص بيثون الواردة في هذا المنشور؟
