Konteks
Misalkan saya memiliki kode Python berikut:
def example_function(numbers, n_iters):
sum_all = 0
for number in numbers:
for _ in range(n_iters):
number = halve(number)
sum_all += number
return sum_all
ns = [1, 3, 12]
print(example_function(ns, 3))
example_function
di sini hanya melalui masing-masing elemen dalam ns
daftar dan membagi dua 3 kali, sambil mengumpulkan hasilnya. Output dari menjalankan skrip ini adalah:
2.0
Sejak 1 / (2 ^ 3) * (1 + 3 + 12) = 2.
Sekarang, katakanlah (untuk alasan apa pun, mungkin debugging, atau logging), saya ingin menampilkan beberapa jenis informasi tentang langkah-langkah perantara yang example_function
sedang dilakukan. Mungkin saya akan menulis ulang fungsi ini menjadi sesuatu seperti ini:
def example_function(numbers, n_iters):
sum_all = 0
for number in numbers:
print('Processing number', number)
for i_iter in range(n_iters):
number = number/2
print(number)
sum_all += number
print('sum_all:', sum_all)
return sum_all
yang sekarang, ketika dipanggil dengan argumen yang sama seperti sebelumnya, menampilkan yang berikut:
Processing number 1
0.5
0.25
0.125
sum_all: 0.125
Processing number 3
1.5
0.75
0.375
sum_all: 0.5
Processing number 12
6.0
3.0
1.5
sum_all: 2.0
Ini mencapai apa yang saya maksudkan. Namun, ini sedikit bertentangan dengan prinsip bahwa suatu fungsi seharusnya hanya melakukan satu hal, dan sekarang kode untuk example_function
sligthly lebih panjang dan lebih kompleks. Untuk fungsi sederhana seperti ini, ini bukan masalah, tetapi dalam konteks saya, saya memiliki fungsi yang cukup rumit saling memanggil, dan pernyataan pencetakan sering melibatkan langkah-langkah yang lebih rumit daripada yang ditunjukkan di sini, menghasilkan peningkatan substansial dalam kompleksitas kode saya (untuk satu fungsi saya ada lebih banyak baris kode yang terkait dengan logging daripada ada baris yang terkait dengan tujuan sebenarnya!).
Selain itu, jika nanti saya memutuskan bahwa saya tidak ingin ada lagi pernyataan pencetakan dalam fungsi saya, saya harus melalui example_function
dan menghapus semua print
pernyataan secara manual, bersama dengan variabel apa pun yang terkait dengan fungsi ini, suatu proses yang membosankan dan juga salah. -Rentan.
Situasi semakin memburuk jika saya ingin selalu memiliki kemungkinan untuk mencetak atau tidak mencetak selama pelaksanaan fungsi, membuat saya mendeklarasikan dua fungsi yang sangat mirip (satu dengan print
pernyataan, satu tanpa), yang mengerikan untuk dijaga, atau untuk mendefinisikan sesuatu seperti:
def example_function(numbers, n_iters, debug_mode=False):
sum_all = 0
for number in numbers:
if debug_mode:
print('Processing number', number)
for i_iter in range(n_iters):
number = number/2
if debug_mode:
print(number)
sum_all += number
if debug_mode:
print('sum_all:', sum_all)
return sum_all
yang menghasilkan fungsi rumit dan mudah-mudahan tidak perlu, bahkan dalam kasus sederhana kami example_function
.
Pertanyaan
Apakah ada cara pythonic untuk "memisahkan" fungsi pencetakan dari fungsi asli example_function
?
Secara umum, adakah cara pythonic untuk memisahkan fungsi opsional dari tujuan utama suatu fungsi?
Apa yang saya coba sejauh ini:
Solusi yang saya temukan saat ini adalah menggunakan callback untuk decoupling. Misalnya, seseorang dapat menulis ulang example_function
seperti ini:
def example_function(numbers, n_iters, callback=None):
sum_all = 0
for number in numbers:
for i_iter in range(n_iters):
number = number/2
if callback is not None:
callback(locals())
sum_all += number
return sum_all
dan kemudian mendefinisikan fungsi panggilan balik yang melakukan fungsi pencetakan mana pun yang saya inginkan:
def print_callback(locals):
print(locals['number'])
dan memanggil example_function
seperti ini:
ns = [1, 3, 12]
example_function(ns, 3, callback=print_callback)
yang kemudian menghasilkan:
0.5
0.25
0.125
1.5
0.75
0.375
6.0
3.0
1.5
2.0
Ini berhasil memisahkan fungsi pencetakan dari fungsionalitas dasar example_function
. Namun, masalah utama dengan pendekatan ini adalah bahwa fungsi callback hanya dapat dijalankan pada bagian tertentu dari example_function
(dalam hal ini tepat setelah mengurangi separuh nomor saat ini), dan semua pencetakan harus terjadi tepat di sana. Ini kadang-kadang memaksa desain fungsi callback menjadi cukup rumit (dan membuat beberapa perilaku tidak mungkin untuk dicapai).
Sebagai contoh, jika seseorang ingin mencapai jenis pencetakan yang sama persis seperti yang saya lakukan di bagian sebelumnya dari pertanyaan (menunjukkan nomor mana yang sedang diproses, bersama dengan separuh yang sesuai) panggilan balik yang dihasilkan adalah:
def complicated_callback(locals):
i_iter = locals['i_iter']
number = locals['number']
if i_iter == 0:
print('Processing number', number*2)
print(number)
if i_iter == locals['n_iters']-1:
print('sum_all:', locals['sum_all']+number)
yang menghasilkan output yang sama persis seperti sebelumnya:
Processing number 1.0
0.5
0.25
0.125
sum_all: 0.125
Processing number 3.0
1.5
0.75
0.375
sum_all: 0.5
Processing number 12.0
6.0
3.0
1.5
sum_all: 2.0
tetapi sulit untuk menulis, membaca, dan men-debug.
logging
modul pythonlogging
modul akan membantu di sini. Meskipun pertanyaan saya menggunakanprint
pernyataan ketika mengatur konteks, saya sebenarnya mencari solusi bagaimana memisahkan segala jenis fungsi opsional dari tujuan utama suatu fungsi. Misalnya, mungkin saya ingin fungsi untuk merencanakan berbagai hal saat berjalan. Dalam hal ini saya percaya bahwalogging
modul itu bahkan tidak akan berlaku.logging
menunjukkan), tetapi tidak bagaimana memisahkan kode arbitrer.Jawaban:
Jika Anda memerlukan fungsionalitas di luar fungsi untuk menggunakan data dari dalam fungsi, maka perlu ada beberapa sistem pesan di dalam fungsi untuk mendukung ini. Tidak ada jalan lain untuk ini. Variabel lokal dalam fungsi sepenuhnya terisolasi dari luar.
Modul logging cukup bagus dalam mengatur sistem pesan. Tidak hanya terbatas untuk mencetak pesan log - menggunakan penangan khusus, Anda dapat melakukan apa saja.
Menambahkan sistem pesan mirip dengan contoh panggilan balik Anda, kecuali bahwa tempat-tempat di mana 'panggilan balik' (penangan logging) ditangani dapat ditentukan di mana saja di dalam
example_function
(dengan mengirim pesan ke pencatat). Variabel apa saja yang diperlukan oleh penangan logging dapat ditentukan saat Anda mengirim pesan (Anda masih bisa menggunakanlocals()
, tetapi yang terbaik adalah secara eksplisit mendeklarasikan variabel yang Anda butuhkan).Yang baru
example_function
mungkin terlihat seperti:Ini menentukan tiga lokasi di mana pesan dapat ditangani. Dengan sendirinya, ini
example_function
tidak akan melakukan apa pun selain fungsiexample_function
itu sendiri. Itu tidak akan mencetak apa pun, atau melakukan fungsi lainnya.Untuk menambahkan fungsionalitas tambahan ke
example_function
, maka Anda perlu menambahkan penangan ke logger.Misalnya, jika Anda ingin melakukan beberapa pencetakan dari variabel yang dikirim (mirip dengan
debugging
contoh Anda ), maka Anda mendefinisikan penangan kustom, dan menambahkannya keexample_function
logger:Jika Anda ingin memplot hasil pada grafik, maka cukup tentukan handler lain:
Anda dapat mendefinisikan dan menambahkan penangan apa pun yang Anda inginkan. Mereka akan benar-benar terpisah dari fungsionalitas
example_function
, dan hanya dapat menggunakan variabel yang diberikanexample_function
kepada mereka.Meskipun pencatatan dapat digunakan sebagai sistem pengiriman pesan, mungkin lebih baik untuk pindah ke sistem perpesanan yang lengkap, seperti PyPubSub , sehingga tidak mengganggu pencatatan yang sebenarnya yang mungkin Anda lakukan:
sumber
logging
modul memang lebih terorganisir dan dapat dikelola daripada apa yang saya usulkan menggunakanprint
danif
pernyataan. Namun, itu tidak memisahkan fungsi pencetakan dari fungsi utamaexample_function
fungsi. Artinya, masalah utama setelahexample_function
melakukan dua hal sekaligus masih tetap, membuat kodenya lebih rumit daripada yang saya inginkan.example_function
sekarang hanya memiliki satu fungsi, dan hal-hal pencetakan (atau fungsi apa pun yang ingin kita miliki) terjadi di luarnya.example_function
dipisahkan dari fungsi pencetakan - satu-satunya fungsi yang ditambahkan ke fungsi adalah mengirim pesan. Ini mirip dengan contoh panggilan balik Anda, kecuali hanya mengirim variabel spesifik yang Anda inginkan, bukan semualocals()
. Terserah penangan log (yang Anda lampirkan ke logger di tempat lain) untuk melakukan fungsionalitas tambahan (pencetakan, grafik, dll). Anda tidak perlu melampirkan penangan sama sekali, dalam hal ini tidak ada yang terjadi ketika pesan dikirim. Saya telah memperbarui posting saya untuk membuatnya lebih jelas.example_function
. Terima kasih telah membuatnya lebih jelas sekarang! Saya sangat suka jawaban ini, satu-satunya harga yang dibayar adalah kompleksitas tambahan dari pesan yang lewat, yang, seperti yang Anda sebutkan, tampaknya tidak dapat dihindari. Terima kasih juga untuk referensi ke PyPubSub, yang membuat saya membaca tentang pola pengamat .Jika Anda ingin tetap dengan hanya mencetak pernyataan, Anda dapat menggunakan dekorator yang menambahkan argumen yang menghidupkan / mematikan pencetakan ke konsol.
Berikut adalah dekorator yang menambahkan argumen hanya kata kunci dan nilai default
verbose=False
untuk fungsi apa pun, memperbarui dokumen dan tanda tangan. Memanggil fungsi apa adanya mengembalikan output yang diharapkan. Memanggil fungsi denganverbose=True
akan mengaktifkan pernyataan cetak dan mengembalikan hasil yang diharapkan. Ini memiliki manfaat tambahan karena tidak harus membuka setiap cetakan denganif debug:
blok.Membungkus fungsi Anda sekarang memungkinkan Anda untuk menghidupkan / mematikan fungsi cetak menggunakan
verbose
.Contoh:
Saat Anda memeriksa
example_function
, Anda akan melihat dokumentasi yang diperbarui juga. Karena fungsi Anda tidak memiliki dokumen, itu hanya apa yang ada di dekorator.Dalam hal filosofi pengkodean. Memiliki fungsi yang tidak menimbulkan efek samping adalah paradigma pemrograman fungsional. Python bisa menjadi bahasa fungsional, tetapi tidak dirancang untuk secara eksklusif seperti itu. Saya selalu mendesain kode saya dengan memikirkan pengguna.
Jika menambahkan opsi untuk mencetak langkah-langkah perhitungan adalah keuntungan bagi pengguna, maka TIDAK ADA yang salah dengan melakukan itu. Dari sudut pandang desain, Anda akan terjebak dengan menambahkan perintah print / logging di suatu tempat.
sumber
print
danif
pernyataan. Selain itu, ia benar-benar berhasil memisahkan bagian dari fungsi pencetakan dariexample_function
fungsi utama, yang sangat bagus (saya juga suka bahwa dekorator secara otomatis menambahkan ke docstring, sentuhan yang bagus). Namun, itu tidak sepenuhnya memisahkan fungsi pencetakan dari fungsi utamaexample_function
: Anda masih harus menambahkanprint
pernyataan dan logika yang menyertainya ke tubuh fungsi `s.example_function
tubuh, sehingga kompleksitasnya tetap hanya terkait dengan kompleksitas fungsi utamanya. Dalam aplikasi kehidupan nyata saya dari semua ini, saya memiliki fungsi utama yang sudah sangat kompleks. Menambahkan pernyataan mencetak / merencanakan / mencatat ke dalam tubuhnya membuatnya menjadi binatang buas yang cukup sulit untuk dirawat dan didebug.Anda dapat mendefinisikan fungsi yang merangkum
debug_mode
kondisi dan meneruskan fungsi opsional yang diinginkan dan argumennya ke fungsi tersebut (seperti yang disarankan di sini ):Catatan yang
debug_mode
jelas harus telah diberi nilai sebelum meneleponDEBUG
.Tentu saja dimungkinkan untuk menjalankan fungsi selain
print
.Anda juga dapat memperluas konsep ini ke beberapa level debug dengan menggunakan nilai numerik untuk
debug_mode
.sumber
if
pernyataan di semua tempat, dan juga membuatnya lebih mudah untuk menghidupkan dan mematikan pencetakan. Namun, itu tidak memisahkan fungsi pencetakan dari fungsi utamaexample_function
. Bandingkan ini dengan misalnya saran panggilan balik saya. Menggunakan panggilan balik, example_function sekarang hanya memiliki satu fungsi, dan hal-hal pencetakan (atau fungsi apa pun yang ingin kita miliki) terjadi di luarnya.Saya telah memperbarui jawaban saya dengan penyederhanaan: fungsi
example_function
dilewatkan satu panggilan balik atau pengait dengan nilai default sehinggaexample_function
tidak perlu lagi menguji untuk melihat apakah sudah lulus atau tidak:Di atas adalah ekspresi lambda yang mengembalikan
None
danexample_function
dapat memanggil nilai default inihook
dengan kombinasi parameter posisi dan kata kunci di berbagai tempat dalam fungsi.Dalam contoh di bawah ini, saya hanya tertarik pada acara
"end_iteration"
dan"result
".Cetakan:
Fungsi kait bisa sesederhana atau serumit yang Anda inginkan. Ini dia melakukan pengecekan jenis acara dan melakukan cetak sederhana. Tapi itu bisa mendapatkan
logger
contoh dan mencatat pesan. Anda dapat memiliki semua kekayaan penebangan jika Anda membutuhkannya tetapi kesederhanaan jika tidak.sumber
example_function
.if
pernyataan :)