Memastikan Kode Bersih: Melihat Python, Berparameter
Diterbitkan: 2022-03-11Dalam posting ini, saya akan berbicara tentang apa yang saya anggap sebagai teknik atau pola terpenting dalam menghasilkan kode Pythonic yang bersih—yaitu, parameterisasi. Postingan ini untuk Anda jika:
- Anda relatif baru dalam hal pola desain keseluruhan dan mungkin sedikit bingung dengan daftar panjang nama pola dan diagram kelas. Kabar baiknya adalah hanya ada satu pola desain yang harus Anda ketahui untuk Python. Bahkan lebih baik, Anda mungkin sudah mengetahuinya, tetapi mungkin tidak semua cara itu dapat diterapkan.
- Anda datang ke Python dari bahasa OOP lain seperti Java atau C# dan ingin tahu bagaimana menerjemahkan pengetahuan Anda tentang pola desain dari bahasa itu ke Python. Dalam Python dan bahasa yang diketik secara dinamis lainnya, banyak pola yang umum dalam bahasa OOP yang diketik secara statis adalah "tidak terlihat atau lebih sederhana", seperti yang dikatakan oleh penulis Peter Norvig.
Dalam artikel ini, kita akan mengeksplorasi penerapan "parameterisasi" dan bagaimana hal itu dapat berhubungan dengan pola desain utama yang dikenal sebagai injeksi ketergantungan , strategi , metode templat , pabrik abstrak , metode pabrik , dan dekorator . Dalam Python, banyak di antaranya menjadi sederhana atau dibuat tidak perlu karena fakta bahwa parameter dalam Python dapat berupa objek atau kelas yang dapat dipanggil.
Parameterisasi adalah proses mengambil nilai atau objek yang didefinisikan dalam suatu fungsi atau metode, dan menjadikannya parameter untuk fungsi atau metode itu, untuk menggeneralisasi kode. Proses ini juga dikenal sebagai refactoring "parameter ekstrak". Di satu sisi, artikel ini adalah tentang pola desain dan refactoring.
Kasus Paling Sederhana dari Python yang Diparameterisasi
Untuk sebagian besar contoh kami, kami akan menggunakan modul kura-kura perpustakaan standar instruksional untuk melakukan beberapa grafik.
Berikut adalah beberapa kode yang akan menggambar persegi 100x100 menggunakan turtle
:
from turtle import Turtle turtle = Turtle() for i in range(0, 4): turtle.forward(100) turtle.left(90)
Misalkan kita sekarang ingin menggambar persegi dengan ukuran berbeda. Seorang programmer yang sangat junior pada saat ini akan tergoda untuk menyalin dan menempelkan blok ini dan memodifikasinya. Jelas, metode yang jauh lebih baik adalah dengan terlebih dahulu mengekstrak kode gambar persegi menjadi suatu fungsi, dan kemudian menjadikan ukuran persegi sebagai parameter untuk fungsi ini:
def draw_square(size): for i in range(0, 4): turtle.forward(size) turtle.left(90) draw_square(100)
Jadi sekarang kita dapat menggambar kotak dengan ukuran berapa pun menggunakan draw_square
. Itu saja yang ada pada teknik penting parameterisasi, dan kita baru saja melihat penggunaan utama pertama—menghilangkan pemrograman copy-paste.
Masalah langsung dengan kode di atas adalah bahwa draw_square
bergantung pada variabel global. Ini memiliki banyak konsekuensi buruk, dan ada dua cara mudah untuk memperbaikinya. Yang pertama adalah draw_square
untuk membuat instance Turtle
itu sendiri (yang akan saya bahas nanti). Ini mungkin tidak diinginkan jika kita ingin menggunakan satu Turtle
untuk semua gambar kita. Jadi untuk saat ini, kita hanya akan menggunakan parameterisasi lagi untuk membuat turtle
menjadi parameter draw_square
:
from turtle import Turtle def draw_square(turtle, size): for i in range(0, 4): turtle.forward(size) turtle.left(90) turtle = Turtle() draw_square(turtle, 100)
Ini memiliki nama yang bagus—injeksi ketergantungan. Itu hanya berarti bahwa jika suatu fungsi memerlukan beberapa jenis objek untuk melakukan pekerjaannya, seperti draw_square
membutuhkan Turtle
, pemanggil bertanggung jawab untuk meneruskan objek itu sebagai parameter. Tidak, sungguh, jika Anda penasaran dengan injeksi ketergantungan Python, ini dia.
Sejauh ini, kita telah membahas dua penggunaan yang sangat mendasar. Pengamatan utama untuk sisa artikel ini adalah bahwa, dalam Python, ada banyak hal yang dapat menjadi parameter—lebih dari beberapa bahasa lain—dan ini menjadikannya teknik yang sangat kuat.
Apapun Itu Obyek
Dalam Python, Anda dapat menggunakan teknik ini untuk membuat parameter apa pun yang merupakan objek, dan dalam Python, sebagian besar hal yang Anda temui sebenarnya adalah objek. Ini termasuk:
- Contoh tipe bawaan, seperti string
"I'm a string"
dan bilangan bulat42
atau kamus - Contoh tipe dan kelas lain, misalnya, objek
datetime.datetime
- Fungsi dan metode
- Jenis bawaan dan kelas khusus
Dua yang terakhir adalah yang paling mengejutkan, terutama jika Anda berasal dari bahasa lain, dan mereka membutuhkan lebih banyak diskusi.
Fungsi sebagai Parameter
Pernyataan fungsi dalam Python melakukan dua hal:
- Ini menciptakan objek fungsi.
- Itu menciptakan nama dalam lingkup lokal yang menunjuk ke objek itu.
Kita bisa bermain dengan objek-objek ini dalam REPL:
> >> def foo(): ... return "Hello from foo" > >> > >> foo() 'Hello from foo' > >> print(foo) <function foo at 0x7fc233d706a8> > >> type(foo) <class 'function'> > >> foo.name 'foo'
Dan seperti semua objek, kita dapat menetapkan fungsi ke variabel lain:
> >> bar = foo > >> bar() 'Hello from foo'
Perhatikan bahwa bar
adalah nama lain untuk objek yang sama, sehingga memiliki properti __name__
internal yang sama seperti sebelumnya:
> >> bar.name 'foo' > >> bar <function foo at 0x7fc233d706a8>
Tetapi poin pentingnya adalah karena fungsi hanyalah objek, di mana pun Anda melihat fungsi sedang digunakan, itu bisa menjadi parameter.
Jadi, misalkan kita memperluas fungsi menggambar persegi kita di atas, dan sekarang terkadang ketika kita menggambar persegi kita ingin berhenti sejenak di setiap sudut—panggilan ke time.sleep()
.
Tapi misalkan terkadang kita tidak ingin berhenti sejenak. Cara paling sederhana untuk mencapai ini adalah dengan menambahkan parameter pause
, mungkin dengan default nol sehingga secara default kita tidak berhenti.
Namun, kami kemudian menemukan bahwa terkadang kami benar-benar ingin melakukan sesuatu yang sama sekali berbeda di tikungan. Mungkin kita ingin menggambar bentuk lain di setiap sudut, mengubah warna pena, dll. Kita mungkin tergoda untuk menambahkan lebih banyak parameter, satu untuk setiap hal yang perlu kita lakukan. Namun, solusi yang jauh lebih baik adalah mengizinkan fungsi apa pun untuk diteruskan sebagai tindakan yang harus diambil. Untuk default, kami akan membuat fungsi yang tidak melakukan apa-apa. Kami juga akan membuat fungsi ini menerima kura- turtle
lokal dan parameter size
, jika diperlukan:
def do_nothing(turtle, size): pass def draw_square(turtle, size, at_corner=do_nothing): for i in range(0, 4): turtle.forward(size) at_corner(turtle, size) turtle.left(90) def pause(turtle, size): time.sleep(5) turtle = Turtle() draw_square(turtle, 100, at_corner=pause)
Atau, kita bisa melakukan sesuatu yang sedikit lebih keren seperti menggambar kotak yang lebih kecil secara rekursif di setiap sudut:
def smaller_square(turtle, size): if size < 10: return draw_square(turtle, size / 2, at_corner=smaller_square) draw_square(turtle, 128, at_corner=smaller_square)
Tentu saja ada variasi dalam hal ini. Dalam banyak contoh, nilai kembalian fungsi akan digunakan. Di sini, kita memiliki gaya pemrograman yang lebih imperatif, dan fungsi dipanggil hanya untuk efek sampingnya.
Dalam Bahasa Lain…
Memiliki fungsi kelas satu di Python membuat ini sangat mudah. Dalam bahasa yang tidak memilikinya, atau beberapa bahasa yang diketik secara statis yang memerlukan tanda tangan tipe untuk parameter, ini bisa lebih sulit. Bagaimana kita melakukan ini jika kita tidak memiliki fungsi kelas satu?
Salah satu solusinya adalah mengubah draw_square
menjadi kelas, SquareDrawer
:
class SquareDrawer: def __init__(self, size): self.size = size def draw(self, t): for i in range(0, 4): t.forward(self.size) self.at_corner(t, size) t.left(90) def at_corner(self, t, size): pass
Sekarang kita dapat SquareDrawer
dan menambahkan metode at_corner
yang melakukan apa yang kita butuhkan. Pola python ini dikenal sebagai pola metode templat—kelas dasar mendefinisikan bentuk keseluruhan operasi atau algoritme dan bagian varian dari operasi dimasukkan ke dalam metode yang perlu diimplementasikan oleh subkelas.
Meskipun ini terkadang membantu dalam Python, mengeluarkan kode varian ke dalam fungsi yang hanya dilewatkan sebagai parameter seringkali akan jauh lebih sederhana.
Cara kedua kita mungkin mendekati masalah ini dalam bahasa tanpa fungsi kelas pertama adalah dengan membungkus fungsi kita sebagai metode di dalam kelas, seperti ini:
class DoNothing: def run(self, turtle, size): pass def draw_square(turtle, size, at_corner=DoNothing()): for i in range(0, 4): turtle.forward(size) at_corner.run(turtle, size) t.left(90) class Pauser: def run(self, turtle, size): time.sleep(5) draw_square(turtle, 100, at_corner=Pauser())
Ini dikenal sebagai pola strategi. Sekali lagi, ini tentu saja merupakan pola yang valid untuk digunakan dalam Python, terutama jika kelas strategi sebenarnya berisi sekumpulan fungsi terkait, bukan hanya satu. Namun, seringkali yang benar-benar kita butuhkan hanyalah sebuah fungsi dan kita dapat berhenti menulis kelas.
Callable lainnya
Dalam contoh di atas, saya telah berbicara tentang meneruskan fungsi ke fungsi lain sebagai parameter. Namun, semua yang saya tulis, pada kenyataannya, berlaku untuk objek apa pun yang dapat dipanggil. Fungsi adalah contoh paling sederhana, tetapi kita juga dapat mempertimbangkan metode.
Misalkan kita memiliki daftar foo
:
foo = [1, 2, 3]
foo
sekarang memiliki banyak metode yang melekat padanya, seperti .append()
dan .count()
. “Metode terikat” ini dapat disebarkan dan digunakan seperti fungsi:
> >> appendtofoo = foo.append > >> appendtofoo(4) > >> foo [1, 2, 3, 4]
Selain metode instan ini, ada tipe lain dari objek yang dapat dipanggil— staticmethods
dan classmethods
, instance dari kelas yang mengimplementasikan __call__
, dan kelas/tipe itu sendiri.

Kelas sebagai Parameter
Dalam Python, kelas adalah "kelas pertama"—mereka adalah objek run-time seperti dict, string, dll. Ini mungkin tampak lebih aneh daripada fungsi menjadi objek, tapi untungnya, sebenarnya lebih mudah untuk mendemonstrasikan fakta ini daripada fungsi.
Pernyataan kelas yang Anda kenal adalah cara yang bagus untuk membuat kelas, tetapi itu bukan satu-satunya cara—kita juga bisa menggunakan tipe versi tiga argumen. Dua pernyataan berikut melakukan hal yang persis sama:
class Foo: pass Foo = type('Foo', (), {})
Di versi kedua, perhatikan dua hal yang baru saja kita lakukan (yang dilakukan dengan lebih mudah menggunakan pernyataan kelas):
- Di sisi kanan tanda sama dengan, kami membuat kelas baru, dengan nama internal
Foo
. Ini adalah nama yang akan Anda dapatkan kembali jika Anda melakukanFoo.__name__
. - Dengan tugas, kami kemudian membuat nama dalam cakupan saat ini, Foo, yang merujuk ke objek kelas yang baru saja kami buat.
Kami melakukan pengamatan yang sama untuk apa yang dilakukan oleh pernyataan fungsi.
Wawasan kunci di sini adalah bahwa kelas adalah objek yang dapat diberi nama (yaitu, dapat dimasukkan ke dalam variabel). Di mana pun Anda melihat kelas sedang digunakan, Anda sebenarnya hanya melihat variabel yang digunakan. Dan jika itu variabel, itu bisa menjadi parameter.
Kita dapat membaginya menjadi beberapa penggunaan:
Kelas sebagai Pabrik
Kelas adalah objek yang dapat dipanggil yang membuat instance dari dirinya sendiri:
> >> class Foo: ... pass > >> Foo() <__main__.Foo at 0x7f73e0c96780>
Dan sebagai objek, itu dapat ditugaskan ke variabel lain:
> >> myclass = Foo > >> myclass() <__main__.Foo at 0x7f73e0ca93c8>
Kembali ke contoh kura-kura kami di atas, satu masalah dengan menggunakan kura-kura untuk menggambar adalah bahwa posisi dan orientasi gambar bergantung pada posisi dan orientasi kura-kura saat ini, dan itu juga dapat membiarkannya dalam keadaan berbeda yang mungkin tidak membantu untuk penelepon. Untuk mengatasi ini, fungsi draw_square
kami dapat membuat kura-kuranya sendiri, memindahkannya ke posisi yang diinginkan, dan kemudian menggambar persegi:
def draw_square(x, y, size): turtle = Turtle() turtle.penup() # Don't draw while moving to the start position turtle.goto(x, y) turtle.pendown() for i in range(0, 4): turtle.forward(size) turtle.left(90)
Namun, kami sekarang memiliki masalah penyesuaian. Misalkan penelepon ingin mengatur beberapa atribut kura-kura atau menggunakan jenis kura-kura yang berbeda yang memiliki antarmuka yang sama tetapi memiliki beberapa perilaku khusus?
Kita bisa menyelesaikan ini dengan injeksi ketergantungan, seperti yang kita lakukan sebelumnya—pemanggil akan bertanggung jawab untuk menyiapkan objek Turtle
. Tetapi bagaimana jika fungsi kita terkadang perlu membuat banyak kura-kura untuk tujuan menggambar yang berbeda, atau jika mungkin ingin membuat empat utas, masing-masing dengan kura-kuranya sendiri untuk menggambar satu sisi persegi? Jawabannya adalah dengan menjadikan kelas Turtle sebagai parameter untuk fungsi tersebut. Kita dapat menggunakan argumen kata kunci dengan nilai default, untuk mempermudah penelepon yang tidak peduli:
def draw_square(x, y, size, make_turtle=Turtle): turtle = make_turtle() turtle.penup() turtle.goto(x, y) turtle.pendown() for i in range(0, 4): turtle.forward(size) turtle.left(90)
Untuk menggunakan ini, kita bisa menulis fungsi make_turtle
yang membuat kura-kura dan memodifikasinya. Misalkan kita ingin menyembunyikan kura-kura saat menggambar kotak:
def make_hidden_turtle(): turtle = Turtle() turtle.hideturtle() return turtle draw_square(5, 10, 20, make_turtle=make_hidden_turtle)
Atau kita bisa membuat subkelas Turtle
untuk membuat perilaku tersebut menjadi bawaan dan meneruskan subkelas sebagai parameter:
class HiddenTurtle(Turtle): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.hideturtle() draw_square(5, 10, 20, make_turtle=HiddenTurtle)
Dalam Bahasa Lain…
Beberapa bahasa OOP lainnya, seperti Java dan C#, tidak memiliki kelas kelas satu. Untuk membuat instance kelas, Anda harus menggunakan kata kunci new
diikuti dengan nama kelas yang sebenarnya.
Keterbatasan ini adalah alasan untuk pola seperti abstract factory (yang membutuhkan pembuatan satu set kelas yang tugasnya hanya untuk membuat instance kelas lain) dan pola Metode Pabrik. Seperti yang Anda lihat, dengan Python, ini hanya masalah mengeluarkan kelas sebagai parameter karena kelas adalah pabriknya sendiri.
Kelas sebagai Kelas Dasar
Misalkan kita menemukan diri kita membuat sub-kelas untuk menambahkan fitur yang sama ke kelas yang berbeda. Misalnya, kami menginginkan subkelas Turtle
yang akan menulis ke log saat dibuat:
import logging logger = logging.getLogger() class LoggingTurtle(Turtle): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logger.debug("Turtle got created")
Tapi kemudian, kami menemukan diri kami melakukan hal yang persis sama dengan kelas lain:
class LoggingHippo(Hippo): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logger.debug("Hippo got created")
Satu-satunya hal yang berbeda di antara keduanya adalah:
- Kelas dasar
- Nama sub-kelas—tetapi kami tidak terlalu peduli tentang itu dan dapat membuatnya secara otomatis dari atribut
__name__
kelas dasar. - Nama yang digunakan di dalam panggilan
debug
—tetapi sekali lagi, kita bisa menghasilkan ini dari nama kelas dasar.
Dihadapkan dengan dua bit kode yang sangat mirip dengan hanya satu varian, apa yang bisa kita lakukan? Sama seperti pada contoh pertama kami, kami membuat fungsi dan mengeluarkan bagian varian sebagai parameter:
def make_logging_class(cls): class LoggingThing(cls): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logger.debug("{0} got created".format(cls.__name__)) LoggingThing.__name__ = "Logging{0}".format(cls.__name__) return LoggingThing LoggingTurtle = make_logging_class(Turtle) LoggingHippo = make_logging_class(Hippo)
Di sini, kami memiliki demonstrasi kelas kelas satu:
- Kami meneruskan kelas ke dalam fungsi, memberikan parameter nama konvensional
cls
untuk menghindari bentrokan dengan kata kunciclass
(Anda juga akan melihatclass_
danklass
digunakan untuk tujuan ini). - Di dalam fungsi, kami membuat kelas—perhatikan bahwa setiap panggilan ke fungsi ini membuat kelas baru .
- Kami mengembalikan kelas itu sebagai nilai pengembalian fungsi.
Kami juga mengatur LoggingThing.__name__
yang sepenuhnya opsional tetapi dapat membantu dengan debugging.
Aplikasi lain dari teknik ini adalah ketika kita memiliki banyak fitur yang terkadang ingin kita tambahkan ke kelas, dan kita mungkin ingin menambahkan berbagai kombinasi fitur ini. Membuat semua kombinasi berbeda yang kita butuhkan secara manual bisa menjadi sangat berat.
Dalam bahasa di mana kelas dibuat pada waktu kompilasi daripada waktu proses, ini tidak mungkin. Sebagai gantinya, Anda harus menggunakan pola dekorator. Pola itu terkadang berguna dalam Python, tetapi kebanyakan Anda bisa menggunakan teknik di atas.
Biasanya, saya sebenarnya menghindari membuat banyak subclass untuk penyesuaian. Biasanya, ada metode Pythonic yang lebih sederhana dan tidak melibatkan kelas sama sekali. Tetapi teknik ini tersedia jika Anda membutuhkannya. Lihat juga perawatan penuh Brandon Rhodes terhadap pola dekorator dengan Python.
Kelas sebagai Pengecualian
Tempat lain Anda melihat kelas yang digunakan adalah di klausa except
dari pernyataan try/except/finally. Tidak ada kejutan untuk menebak bahwa kita juga dapat membuat parameter dari kelas-kelas itu.
Misalnya, kode berikut menerapkan strategi yang sangat umum untuk mencoba tindakan yang dapat gagal dan mencoba lagi dengan backoff eksponensial hingga jumlah upaya maksimum tercapai:
import time def retry_with_backoff(action, exceptions_to_catch, max_attempts=10, attempts_so_far=0): try: return action() except exceptions_to_catch: attempts_so_far += 1 if attempts_so_far >= max_attempts: raise else: time.sleep(attempts_so_far ** 2) return retry_with_backoff(action, exceptions_to_catch, attempts_so_far=attempts_so_far, max_attempts=max_attempts)
Kami telah mengeluarkan tindakan yang harus diambil dan pengecualian untuk ditangkap sebagai parameter. Parameter exceptions_to_catch
dapat berupa kelas tunggal, seperti IOError
atau httplib.client.HTTPConnectionError
, atau tuple kelas tersebut. (Kami ingin menghindari klausa "telanjang kecuali" atau bahkan except Exception
karena ini diketahui menyembunyikan kesalahan pemrograman lainnya).
Peringatan dan Kesimpulan
Parameterisasi adalah teknik yang ampuh untuk menggunakan kembali kode dan mengurangi duplikasi kode. Hal ini bukannya tanpa beberapa kekurangan. Dalam mengejar penggunaan kembali kode, beberapa masalah sering muncul:
- Kode yang terlalu umum atau abstrak yang menjadi sangat sulit untuk dipahami.
- Kode dengan proliferasi parameter yang mengaburkan gambaran besar atau memperkenalkan bug karena, pada kenyataannya, hanya kombinasi parameter tertentu yang diuji dengan benar.
- Penggabungan yang tidak membantu dari berbagai bagian basis kode karena "kode umum" mereka telah diperhitungkan dalam satu tempat. Kadang-kadang kode di dua tempat serupa hanya secara tidak sengaja, dan kedua tempat harus independen satu sama lain karena mereka mungkin perlu diubah secara independen.
Terkadang sedikit kode "duplikat" jauh lebih baik daripada masalah ini, jadi gunakan teknik ini dengan hati-hati.
Dalam posting ini, kami telah membahas pola desain yang dikenal sebagai injeksi ketergantungan , strategi , metode templat , pabrik abstrak , metode pabrik , dan dekorator . Dalam Python, banyak dari ini benar-benar berubah menjadi aplikasi parameterisasi sederhana atau pasti dibuat tidak perlu oleh fakta bahwa parameter dalam Python dapat berupa objek atau kelas yang dapat dipanggil. Mudah-mudahan, ini membantu meringankan beban konseptual "hal-hal yang seharusnya Anda ketahui sebagai pengembang Python nyata" dan memungkinkan Anda untuk menulis kode Pythonic yang ringkas!
Bacaan lebih lanjut:
- Pola Desain Python: Untuk Kode yang Ramping dan Modis
- Pola Python: Untuk Pola Desain Python
- Python Logging: Tutorial Mendalam