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
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?
