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:
 
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() :
 
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):             
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 :
 
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 argparseuntuk mendokumentasikan parameter yang dengannya skrip dapat dipanggil.
- Gunakan modul logginguntuk 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?
