Mengembangkan skrip Python yang kuat

Python adalah bahasa pemrograman yang bagus untuk mengembangkan skrip yang berdiri sendiri. Untuk mencapai hasil yang diinginkan menggunakan skrip yang serupa, Anda perlu menulis beberapa puluh atau ratusan baris kode. Dan setelah pekerjaan selesai, Anda bisa melupakan kode tertulis dan melanjutkan ke solusi dari masalah berikutnya.

Jika, katakanlah, enam bulan setelah naskah "satu kali" tertentu ditulis, seseorang bertanya kepada penulis tentang mengapa skrip ini macet, penulis skrip mungkin tidak tahu tentang ini. Ini terjadi karena skrip tidak dokumentasi tertulis, karena penggunaan parameter yang di-hardcode dalam kode, karena fakta bahwa skrip tidak mencatat apa pun selama pekerjaan, dan karena kurangnya tes yang memungkinkan untuk dengan cepat memahami penyebab masalah.



Perlu dicatat bahwa mengubah naskah yang ditulis dengan tergesa-gesa menjadi sesuatu yang jauh lebih baik tidak begitu sulit. Yaitu, skrip seperti itu cukup mudah untuk berubah menjadi kode yang andal dan mudah dimengerti yang nyaman digunakan, menjadi kode yang mudah untuk mendukung penulisnya dan juga programmer lainnya.

Penulis materi, terjemahan yang kami terbitkan hari ini, akan menunjukkan "transformasi" seperti menggunakan Fizz Buzz Test klasik sebagai contoh. Tugas ini adalah untuk menampilkan daftar angka dari 1 hingga 100, menggantikan beberapa dari mereka dengan garis khusus. Jadi, jika angkanya adalah kelipatan 3, Anda harus mencetak garis Fizz sebagai gantinya, jika angkanya adalah kelipatan 5, garis Buzz , dan jika kedua syarat ini terpenuhi, FizzBuzz .

Kode sumber


Berikut adalah kode sumber untuk skrip Python yang memecahkan masalah:

 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) 

Mari kita bicara tentang bagaimana memperbaikinya.

Dokumentasi


Saya merasa terbantu untuk menulis dokumentasi sebelum menulis kode. Ini menyederhanakan pekerjaan dan membantu untuk tidak menunda pembuatan dokumentasi tanpa batas. Dokumentasi untuk skrip dapat ditempatkan di atasnya. Misalnya, mungkin terlihat seperti ini:

 #!/usr/bin/env python3 """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". """ 

Baris pertama memberikan deskripsi singkat tentang tujuan naskah. Paragraf yang tersisa memberikan informasi tambahan tentang apa yang dilakukan skrip.

Argumen baris perintah


Tugas selanjutnya untuk memperbaiki skrip adalah mengganti nilai-nilai yang di-hardcode dalam kode dengan nilai-nilai yang terdokumentasi yang diteruskan ke skrip melalui argumen baris perintah. Ini dapat dilakukan dengan menggunakan modul argparse . Dalam contoh kami, kami menyarankan pengguna untuk menentukan rentang angka dan menentukan nilai untuk "desis" dan "buzz" yang digunakan saat memeriksa angka dari rentang yang ditentukan.

 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):    # ... 

Perubahan ini sangat bermanfaat bagi naskah. Yaitu, parameter sekarang didokumentasikan dengan benar, Anda dapat mengetahui tujuannya menggunakan flag --help . Selain itu, sesuai dengan perintah yang sesuai, dokumentasi yang kami tulis di bagian sebelumnya juga ditampilkan:

 $ ./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) 

Modul argparse adalah alat yang sangat kuat. Jika Anda tidak terbiasa dengan itu, akan berguna bagi Anda untuk melihat dokumentasi di dalamnya. Secara khusus, saya suka kemampuannya untuk mendefinisikan sub -perintah dan kelompok argumen .

Penebangan


Jika Anda melengkapi skrip dengan kemampuan untuk menampilkan beberapa informasi selama pelaksanaannya, ini akan menjadi tambahan fungsionalitas yang menyenangkan. Modul logging sangat cocok untuk tujuan ini. Pertama, kami mendeskripsikan objek yang mengimplementasikan logging:

 import logging import logging.handlers import os import sys logger = logging.getLogger(os.path.splitext(os.path.basename(sys.argv[0]))[0]) 

Kemudian kami akan memungkinkan untuk mengontrol detail informasi yang ditampilkan selama pencatatan. Jadi, perintah logger.debug() harus menampilkan sesuatu hanya jika skrip dijalankan dengan saklar --debug . Jika skrip dijalankan dengan --silent , skrip seharusnya tidak menampilkan apa pun kecuali pesan pengecualian. Untuk mengimplementasikan fitur-fitur ini, tambahkan kode berikut ke parse_args() :

 #  parse_args() g = parser.add_mutually_exclusive_group() g.add_argument("--debug", "-d", action="store_true",               default=False,               help="enable debugging") g.add_argument("--silent", "-s", action="store_true",               default=False,               help="don't log to console") 

Tambahkan fungsi berikut ke kode proyek untuk mengkonfigurasi logging:

 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) 

Kode skrip utama akan berubah sebagai berikut:

 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):            # ..    except Exception as e:        logger.exception("%s", e)        sys.exit(1)    sys.exit(0) 

Jika Anda berencana untuk menjalankan skrip tanpa partisipasi langsung dari pengguna, misalnya, menggunakan crontab , Anda dapat membuat hasilnya pergi ke 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) 

Dalam skrip kecil kami, jumlah kode yang serupa tampaknya diperlukan untuk hanya menggunakan perintah logger.debug() . Tetapi dalam skrip nyata kode ini tidak akan tampak seperti itu lagi dan manfaat darinya akan muncul ke permukaan, yaitu dengan bantuannya pengguna akan dapat mengetahui tentang kemajuan penyelesaian masalah.

 $ ./fizzbuzz.py --debug 1 3 DEBUG[fizzbuzz] compute fizzbuzz from 1 to 3 1 2 fizz 

Tes


Tes unit adalah alat yang berguna untuk memeriksa apakah aplikasi berperilaku sebagaimana mestinya. Skrip unit jarang digunakan dalam skrip, tetapi penyertaannya dalam skrip secara signifikan meningkatkan keandalan kode. Kami mengubah kode di dalam loop menjadi fungsi dan menjelaskan beberapa contoh interaktif penggunaannya dalam dokumentasinya:

 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 

Anda dapat memverifikasi operasi fungsi yang benar menggunakan 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 ========================== 

Agar semua ini berfungsi, Anda memerlukan ekstensi .py untuk muncul setelah nama skrip. Saya tidak suka menambahkan ekstensi ke nama skrip: bahasa hanyalah detail teknis yang tidak perlu ditampilkan kepada pengguna. Namun, sepertinya melengkapi nama skrip dengan ekstensi adalah cara termudah untuk membiarkan sistem untuk menjalankan tes, seperti pytest , menemukan tes yang termasuk dalam kode.

Jika kesalahan pytest akan menampilkan pesan yang menunjukkan lokasi kode yang sesuai dan sifat masalah:

 $ 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 ========================== 

Tes unit juga dapat ditulis sebagai kode biasa. Bayangkan kita perlu menguji fungsi berikut:

 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)] 

Di akhir skrip, kami menambahkan unit test berikut menggunakan pytest untuk menggunakan fungsi tes parameterized :

 #   import pytest          # noqa: E402 import shlex          # noqa: E402 @pytest.mark.parametrize("args, expected", [    ("0 0", ["fizzbuzz"]),    ("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 

Harap perhatikan bahwa, karena kode skrip diakhiri dengan panggilan ke sys.exit() , tes tidak akan dijalankan ketika dipanggil secara normal. Berkat ini, pytest tidak diperlukan untuk menjalankan skrip.

Fungsi tes akan dipanggil satu kali untuk setiap kelompok parameter. Entitas args digunakan sebagai input ke fungsi parse_args() . Berkat mekanisme ini, kami mendapatkan apa yang perlu kami sampaikan ke fungsi main() . Entitas yang expected dibandingkan dengan main() . Inilah yang akan memberitahu kami jika semuanya berjalan seperti yang diharapkan:

 $ 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 ========================== 

Jika kesalahan terjadi, pytest akan memberikan informasi berguna tentang apa yang terjadi:

 $ 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 ===================== 

Output dari perintah logger.debug() termasuk dalam output ini. Ini adalah alasan bagus lainnya untuk menggunakan mekanisme logging dalam skrip. Jika Anda ingin tahu lebih banyak tentang fitur hebat pytest , lihat materi ini .

Ringkasan


Anda dapat membuat skrip Python lebih dapat diandalkan dengan mengikuti empat langkah ini:

  • Lengkapi skrip dengan dokumentasi yang terletak di bagian atas file.
  • Gunakan modul argparse untuk mendokumentasikan parameter yang dengannya skrip dapat dipanggil.
  • Gunakan modul logging untuk menampilkan informasi tentang proses operasi skrip.
  • Tulis tes unit.

Berikut adalah kode lengkap untuk contoh yang dibahas di sini. Anda dapat menggunakannya sebagai templat untuk skrip Anda sendiri.

Diskusi menarik mulai seputar materi ini - Anda dapat menemukannya di sini dan di sini . Para hadirin, tampaknya, menerima rekomendasi tentang dokumentasi dan argumen-argumen garis perintah, tetapi bagaimana dengan logging dan tes-tes yang bagi sebagian pembaca tampaknya merupakan "tembakan dari pistol pada burung pipit." Berikut adalah bahan yang ditulis sebagai tanggapan terhadap artikel ini.

Pembaca yang budiman! Apakah Anda berencana untuk menerapkan rekomendasi untuk menulis skrip Python yang diberikan dalam publikasi ini?

Source: https://habr.com/ru/post/id462007/


All Articles