Sebenarnya, judul artikel yang luar biasa ini oleh Jeff Knapp, penulis buku "
Writing Idiomatic Python " sepenuhnya mencerminkan esensinya. Baca dengan cermat dan jangan ragu untuk berkomentar.
Karena kami benar-benar tidak ingin meninggalkan istilah penting dalam huruf Latin dalam teks, kami mengizinkan diri untuk menerjemahkan kata "docstring" sebagai "docstring", setelah menemukan
istilah ini di
beberapa sumber berbahasa Rusia .
Dalam Python, seperti dalam kebanyakan bahasa pemrograman modern, fungsi adalah metode utama abstrak dan enkapsulasi. Anda, sebagai pengembang, mungkin sudah menulis ratusan fungsi. Tetapi fungsi ke fungsi - perselisihan. Selain itu, jika Anda menulis fungsi "buruk", ini akan segera mempengaruhi keterbacaan dan dukungan kode Anda. Jadi, apa fungsi "buruk", dan yang lebih penting - bagaimana menjadikannya fungsi "baik"?
Perbarui topik
Matematika penuh dengan fungsi, namun sulit untuk mengingatnya. Jadi mari kita kembali ke disiplin favorit kita: analisis. Anda mungkin telah melihat rumus seperti
f(x) = 2x + 3
. Ini adalah fungsi yang disebut
f
yang mengambil argumen
x
dan kemudian "mengembalikan" dua kali
x + 3
. Meskipun tidak terlalu mirip dengan fungsi yang biasa kita gunakan di Python, ini benar-benar mirip dengan kode berikut:
def f(x): return 2*x + 3
Fungsinya telah lama ada dalam matematika, tetapi dalam ilmu komputer mereka sepenuhnya berubah. Namun, kekuatan ini tidak diberikan dengan sia-sia: Anda harus melewati berbagai perangkap. Mari kita bahas apa fungsi "baik" seharusnya dan apa "lonceng dan peluit" khas untuk fungsi yang mungkin memerlukan refactoring.
Rahasia Fungsi yang Baik
Apa yang membedakan fungsi Python "baik" dari yang biasa-biasa saja? Anda akan terkejut betapa banyak interpretasi kata "baik" memungkinkan. Sebagai bagian dari artikel ini, saya akan menganggap fungsi Python βbaikβ jika memenuhi
sebagian besar item dalam daftar berikut (kadang-kadang tidak mungkin untuk menyelesaikan semua item untuk fungsi tertentu):
- Itu jelas namanya
- Sesuai dengan prinsip kewajiban tunggal
- Berisi Dock
- Nilai pengembalian
- Terdiri dari tidak lebih dari 50 baris
- Dia idempoten dan, jika mungkin, murni
Bagi banyak dari Anda, persyaratan ini mungkin tampak terlalu keras. Namun, saya berjanji: jika fungsi Anda mematuhi aturan-aturan ini, mereka akan menjadi sangat indah sehingga mereka bahkan akan menusuk unicorn dengan air mata. Di bawah ini saya akan mencurahkan bagian untuk masing-masing elemen dari daftar di atas, dan kemudian saya akan menyelesaikan cerita dengan menceritakan bagaimana mereka selaras satu sama lain dan membantu menciptakan fungsi yang baik.
PenamaanBerikut adalah kutipan favorit saya tentang hal ini, sering dikaitkan secara keliru dengan Donald, tetapi sebenarnya dimiliki oleh
Phil Carleton :
Ada dua tantangan untuk ilmu komputer: pembatalan dan penamaan cache.
Tidak peduli seberapa konyol kedengarannya, memberi nama adalah hal yang sulit. Berikut adalah contoh nama fungsi "buruk":
def get_knn_from_df(df):
Sekarang, nama-nama buruk menjumpai saya hampir di mana-mana, tetapi contoh ini diambil dari bidang Ilmu Data (lebih tepatnya, pembelajaran mesin), di mana para praktisi biasanya menulis kode dalam buku catatan Jupyter, dan kemudian mencoba menyusun program yang dapat dicerna dari sel-sel ini.
Masalah pertama dengan nama fungsi ini adalah ia menggunakan singkatan.
Lebih baik menggunakan kata-kata bahasa Inggris penuh, daripada singkatan dan bukan singkatan terkenal . Satu-satunya alasan saya ingin mempersingkat kata-kata adalah tidak membuang waktu mengetik terlalu banyak teks, tetapi
setiap editor modern memiliki fungsi pelengkapan otomatis , jadi Anda harus mengetikkan nama lengkap fungsi hanya sekali. Singkatan adalah masalah, karena sering khusus untuk bidang subjek. Dalam kode di atas,
knn
berarti "tetangga terdekat K," dan
df
berarti "DataFrame," struktur data yang biasa digunakan di
panda library. Jika seorang programmer yang tidak mengetahui singkatan-singkatan ini membaca kode, maka ia hampir tidak akan mengerti apa-apa dalam nama fungsinya.
Ada dua kelemahan kecil lagi dalam nama fungsi ini. Pertama, kata
"get"
berlebihan. Dalam fungsi yang paling kompeten namanya, segera jelas bahwa fungsi ini mengembalikan sesuatu, yang secara khusus tercermin dalam namanya.
from_d
f juga tidak diperlukan. Baik di dock fungsi, atau (jika berada di pinggiran) dalam anotasi tipe, tipe parameter akan dijelaskan jika informasi
ini belum jelas dari nama parameter .
Jadi bagaimana kita mengganti nama fitur ini? Hanya:
def k_nearest_neighbors(dataframe):
Sekarang bahkan orang awam mengerti apa yang sedang dihitung dalam fungsi ini, dan nama parameter
(dataframe)
tidak menyisakan argumen mana yang harus diteruskan ke sana.
Tanggung jawab tunggal
Mengembangkan gagasan Bob Martin, saya akan mengatakan bahwa
prinsip tanggung jawab tunggal berlaku untuk fungsi tidak kurang dari kelas dan modul (tentang apa yang awalnya ditulis Mr. Martin). Menurut prinsip ini (dalam kasus kami), suatu fungsi harus memiliki satu tanggung jawab. Artinya, dia harus melakukan satu dan hanya satu hal. Salah satu alasan yang paling meyakinkan untuk ini: jika suatu fungsi hanya melakukan satu hal, maka harus ditulis ulang dalam satu-satunya kasus: jika hal ini harus dilakukan dengan cara yang baru. Ini juga menjadi jelas ketika suatu fungsi dapat dihapus; jika, membuat perubahan di tempat lain, kami memahami bahwa tugas satu-satunya fungsi tidak lagi relevan, maka kami hanya akan menyingkirkannya.
Lebih baik memberi contoh. Berikut adalah fungsi yang melakukan lebih dari satu "hal":
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)
Yaitu, dua: menghitung satu set statistik pada daftar angka dan menampilkannya dalam
STDOUT
. Suatu fungsi melanggar aturan: harus ada satu alasan spesifik mengapa itu perlu diubah. Dalam hal ini, ada dua alasan yang jelas mengapa ini diperlukan: apakah Anda perlu menghitung statistik baru atau berbeda, atau Anda perlu mengubah format output. Oleh karena itu, lebih baik menulis ulang fungsi ini dalam bentuk dua fungsi terpisah: satu akan melakukan perhitungan dan mengembalikan hasilnya, dan yang lain akan menerima hasil ini dan menampilkannya di konsol.
Suatu fungsi (atau lebih tepatnya, ia memiliki dua tanggung jawab) dengan jeroan ayam itik memberi kata dan dalam namanya .
Pemisahan ini juga sangat menyederhanakan pengujian fungsi, dan juga memungkinkan Anda untuk tidak hanya membaginya menjadi dua fungsi dalam modul yang sama, tetapi bahkan untuk memisahkan kedua fungsi ini menjadi modul yang sama sekali berbeda, jika sesuai. Ini selanjutnya berkontribusi pada pengujian yang lebih bersih dan menyederhanakan dukungan kode.
Bahkan, fungsi yang melakukan tepat dua hal jarang terjadi. Lebih sering, Anda menemukan fungsi yang melakukan lebih banyak, lebih banyak operasi. Sekali lagi, untuk alasan keterbacaan dan pengujian, fungsi "multi-stasiun" tersebut harus dibagi menjadi satu-tugas, yang masing-masing berisi satu aspek pekerjaan.
Docstrings
Tampaknya semua orang sadar bahwa ada dokumen
PEP-8 yang memberikan rekomendasi tentang gaya kode Python, tetapi ada jauh lebih sedikit orang di antara kita yang tahu
PEP-257 , di mana rekomendasi yang sama diberikan mengenai dockstrings. Agar tidak menceritakan kembali isi PEP-257, saya mengirim Anda sendiri ke dokumen ini - baca di waktu luang Anda. Namun, ide utamanya adalah sebagai berikut:
- Setiap fungsi membutuhkan string doc.
- Harus memperhatikan tata bahasa dan tanda baca; tulis kalimat lengkap
- Docstring dimulai dengan deskripsi singkat (dalam satu kalimat) tentang apa fungsi tidak.
- Docstring diformulasikan dalam gaya preskriptif dan bukan deskriptif
Semua poin ini mudah diikuti ketika menulis fitur. Hanya menulis dokumen harus menjadi kebiasaan, dan mencoba menulisnya sebelum melanjutkan dengan kode fungsi itu sendiri. Jika Anda tidak dapat menulis string dokumen yang jelas menjelaskan fungsi, ini adalah alasan yang baik untuk memikirkan mengapa Anda menulis fungsi ini.
Nilai pengembalian
Fungsi dapat (dan
harus ) diartikan sebagai program mandiri kecil. Mereka mengambil beberapa input dalam bentuk parameter dan mengembalikan hasilnya. Parameter, tentu saja, adalah opsional.
Tetapi nilai kembali diperlukan dari sudut pandang struktur internal Python . Jika Anda bahkan mencoba untuk menulis fungsi yang tidak mengembalikan nilai, Anda tidak bisa. Jika fungsi tersebut bahkan tidak mengembalikan nilai, maka juru bahasa Python akan "memaksa" untuk mengembalikan
None
. Tidak percaya Coba sendiri:
β― 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
Seperti yang Anda lihat, nilai
b
pada dasarnya
None
. Jadi, bahkan jika Anda menulis fungsi tanpa pernyataan kembali, itu masih akan mengembalikan sesuatu. Dan itu seharusnya. Bagaimanapun, ini adalah program kecil, bukan? Seberapa bermanfaatkah program yang tidak memiliki kesimpulan - dan karena itu tidak mungkin menilai apakah program ini dijalankan dengan benar? Tetapi yang paling penting, bagaimana Anda akan
menguji program semacam itu?
Saya bahkan tidak takut untuk mengatakan yang berikut: setiap fungsi harus mengembalikan nilai yang bermanfaat, setidaknya untuk kepentingan pengujian. Kode yang saya tulis harus diuji (ini tidak dibahas). Bayangkan saja bagaimana pengujian kikuk dari fungsi
add
atas dapat berubah (petunjuk: Anda harus mengarahkan input / output, setelah itu semuanya akan serba salah segera). Selain itu, dengan mengembalikan nilai, kita dapat mengaitkan metode dan karenanya menulis kode seperti ini:
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'):
berfungsi karena masing-masing metode string (
strip()
,
lower()
,
endswith()
) mengembalikan string sebagai akibat dari memanggil fungsi.
Berikut adalah beberapa alasan umum yang dapat diberikan oleh seorang programmer ketika menjelaskan mengapa fungsi yang ia tulis tidak mengembalikan nilai:
βItu hanya [semacam operasi yang terkait dengan input / output, misalnya, menyimpan nilai dalam database]. Di sini saya tidak dapat mengembalikan sesuatu yang bermanfaat. "
Saya tidak setuju. Fungsi dapat mengembalikan True jika operasi selesai dengan sukses.
"Di sini kita mengubah salah satu parameter yang tersedia, menggunakannya sebagai parameter referensi." ""
Inilah dua poin. Pertama, lakukan yang terbaik untuk tidak melakukan ini. Kedua, menyediakan fungsi dengan semacam argumen hanya untuk mengetahui bahwa itu telah berubah sangat mengejutkan, dan hanya berbahaya paling buruk. Sebagai gantinya, seperti halnya metode string, cobalah untuk mengembalikan instance baru dari parameter yang sudah mencerminkan perubahan yang diterapkan padanya. Bahkan jika ini tidak dapat dilakukan, karena membuat salinan dari beberapa parameter penuh dengan biaya yang berlebihan, Anda masih dapat memutar kembali ke opsi "Return
True
jika operasi selesai dengan sukses" yang diusulkan di atas.
βSaya perlu mengembalikan beberapa nilai. Tidak ada nilai tunggal yang dalam hal ini disarankan untuk dikembalikan. "
Argumen ini agak dibuat-buat, tetapi saya sudah mendengarnya. Jawabannya, tentu saja, persis apa yang ingin dilakukan penulis - tetapi tidak tahu caranya:
menggunakan tuple untuk mengembalikan beberapa nilai .
Akhirnya, argumen terkuat bahwa lebih baik mengembalikan nilai yang berguna dalam hal apa pun adalah bahwa pemanggil selalu dapat dibenarkan mengabaikan nilai-nilai ini. Singkatnya, mengembalikan nilai dari suatu fungsi hampir pasti merupakan ide yang bagus, dan sangat tidak mungkin bahwa kita akan merusak apa pun dengan cara ini, bahkan dalam basis kode yang ada.
Panjang fungsi
Saya mengakui lebih dari sekali bahwa saya cukup bodoh. Saya dapat menyimpan sekitar tiga hal di kepala saya secara bersamaan. Jika Anda membiarkan saya membaca fungsi 200-line dan bertanya apa fungsinya, saya mungkin akan menatapnya setidaknya selama 10 detik.
Panjang fungsi secara langsung memengaruhi keterbacaannya dan karenanya mendukungnya . Karena itu, cobalah untuk membuat fungsi Anda singkat. 50 baris - nilai yang diambil sepenuhnya dari langit-langit, tetapi tampaknya masuk akal bagi saya. (Saya harap) bahwa sebagian besar fungsi yang Anda tulis akan jauh lebih pendek.
Jika suatu fungsi mematuhi Prinsip Tanggung Jawab Tunggal, maka itu kemungkinan akan cukup singkat. Jika sedang membaca atau idempoten (kami akan membicarakan ini) di bawah - maka, mungkin, itu juga akan menjadi singkat. Semua ide ini dikombinasikan secara harmonis satu sama lain dan membantu menulis kode yang baik dan bersih.
Jadi apa yang harus dilakukan jika fungsi Anda terlalu lama?
REFACTOR! Anda mungkin harus melakukan refactoring sepanjang waktu, bahkan jika Anda tidak tahu istilahnya.
Refactoring hanya mengubah struktur program, tanpa mengubah perilakunya. Oleh karena itu, mengekstraksi beberapa baris kode dari fungsi panjang dan mengubahnya menjadi fungsi independen adalah salah satu jenis refactoring. Ternyata ini juga merupakan cara paling umum dan tercepat untuk secara singkat mempersingkat fungsi yang panjang. Karena Anda memberi fungsi baru ini nama yang sesuai, kode yang dihasilkan jauh lebih mudah dibaca. Saya menulis seluruh buku tentang refactoring (pada kenyataannya, saya melakukannya sepanjang waktu), jadi saya tidak akan memerinci di sini. Ketahuilah bahwa jika Anda memiliki fungsi yang terlalu panjang, Anda harus mengubahnya.
Idempotensi dan Kebersihan Fungsional
Judul bagian ini mungkin tampak sedikit menakutkan, tetapi secara konseptual bagian ini sederhana. Fungsi idempoten dengan set argumen yang sama selalu mengembalikan nilai yang sama, terlepas dari berapa kali dipanggil. Hasilnya tidak bergantung pada variabel non-lokal, variabilitas argumen, atau pada data apa pun yang berasal dari aliran input / output. Fungsi
add_three(number)
ini idempoten:
def add_three(number): """ ** + 3.""" return number + 3
Terlepas dari berapa kali kita memanggil
add_three(7)
, jawabannya akan selalu 10. Tapi kasus lain adalah fungsi yang tidak idempoten:
def add_three(): """ 3 + , .""" number = int(input('Enter a number: ')) return number + 3
Fungsi ini terus terang dibuat bukan idempoten, karena nilai kembali fungsi tergantung pada input / output, yaitu, pada nomor yang dimasukkan oleh pengguna. Tentu saja, dengan panggilan berbeda untuk
add_three()
kembali akan berbeda. Jika kita memanggil fungsi ini dua kali, maka pengguna dalam kasus pertama dapat memasukkan 3, dan pada yang kedua - 7, dan kemudian dua panggilan ke
add_three()
akan mengembalikan masing-masing 6 dan 10.
Di luar pemrograman, ada juga contoh idempotensi - misalnya, tombol naik lift dirancang sesuai dengan prinsip ini. Dengan menekannya untuk pertama kali, kami "memberi tahu" lift yang ingin kami naiki. Karena tombolnya idempoten, tidak peduli berapa banyak Anda menekannya nanti, tidak ada hal buruk yang akan terjadi. Hasilnya akan selalu sama.
Mengapa idempotensi begitu penting
Dukungan testability dan usability. Fungsi idempoten mudah diuji, karena dijamin akan mengembalikan hasil yang sama jika Anda memanggilnya dengan argumen yang sama. Pengujian turun untuk memverifikasi bahwa dengan berbagai panggilan, fungsi selalu mengembalikan nilai yang diharapkan. Selain itu, tes ini akan cepat: kecepatan tes adalah masalah penting yang sering diabaikan dalam pengujian unit. Dan refactoring ketika bekerja dengan fungsi idempoten umumnya mudah. Tidak masalah bagaimana Anda mengubah kode di luar fungsi - hasil memanggilnya dengan argumen yang sama akan selalu sama.
Apa fungsi "murni"?
Dalam pemrograman fungsional, fungsi dianggap murni jika,
pertama , idempoten, dan
kedua , tidak menyebabkan
efek samping yang diamati. Jangan lupa: suatu fungsi idempoten jika selalu mengembalikan hasil yang sama dengan serangkaian argumen tertentu. Namun, ini tidak berarti bahwa fungsi tersebut tidak dapat mempengaruhi komponen lain - misalnya, variabel non-lokal atau aliran input / output. Misalnya, jika versi idempoten dari fungsi
add_three(number)
atas mengeluarkan hasil ke konsol, dan hanya mengembalikannya, itu akan tetap dianggap idempoten, karena ketika mengakses aliran input / output, operasi akses ini tidak mempengaruhi nilai yang dikembalikan. dari fungsi. Panggilan
print()
hanyalah
efek samping : interaksi dengan seluruh program atau sistem, yang terjadi bersamaan dengan nilai pengembalian.
Mari kita kembangkan contoh kita sedikit dengan
add_three(number)
. Anda dapat menulis kode berikut untuk menentukan berapa kali
add_three(number)
telah dipanggil:
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
Sekarang kita menjalankan output ke konsol (ini adalah efek samping) dan mengubah variabel non-lokal (efek samping lain), tetapi karena tak satu pun dari ini mempengaruhi nilai yang dikembalikan oleh fungsi, itu tetap idempoten.
Fungsi murni tidak memiliki efek samping. Itu tidak hanya tidak menggunakan "data eksternal" ketika menghitung nilai, tetapi tidak berinteraksi dengan sisa program / sistem, hanya menghitung dan mengembalikan nilai yang ditentukan. Oleh karena itu, walaupun definisi baru kita tentang
add_three(number)
tetap idempoten, fungsi ini tidak lagi murni.
Dalam fungsi murni tidak ada instruksi logging atau
print()
panggilan. Saat bekerja, mereka tidak mengakses database dan tidak menggunakan koneksi internet. Jangan mengakses atau memodifikasi variabel non-lokal.
Dan jangan panggil fungsi non-murni lainnya .
Singkatnya, mereka tidak memiliki "tindakan jangka panjang yang mengerikan", seperti yang diungkapkan oleh kata-kata Einstein (tetapi dalam konteks ilmu komputer, bukan fisika). Mereka tidak mengubah dengan cara apa pun sisa program atau sistem. Dalam
pemrograman imperatif (yang adalah apa yang Anda lakukan saat menulis kode dengan Python), fungsi-fungsi tersebut adalah yang paling aman. Mereka dikenal karena kemampuannya untuk diuji dan kemudahan dukungan; selain itu, karena mereka idempoten, pengujian fungsi-fungsi tersebut dijamin akan secepat mengeksekusi. Tes itu sendiri juga sederhana: Anda tidak perlu terhubung ke database atau mensimulasikan sumber daya eksternal, menyiapkan konfigurasi awal kode, dan di akhir pekerjaan Anda tidak perlu membersihkan apa pun.
Jujur, idempotensi dan kebersihan sangat diinginkan, tetapi tidak diperlukan. , , , . , , , , . , , .
Kesimpulan
Itu saja. , β . . , . β ! . , , , Β« Β». .