Kode Buggy Python: 10 Kesalahan Paling Umum yang Dilakukan Pengembang Python
Diterbitkan: 2022-03-11Tentang Python
Python adalah bahasa pemrograman tingkat tinggi yang ditafsirkan, berorientasi objek, dengan semantik dinamis. Struktur data bawaan tingkat tinggi, dikombinasikan dengan pengetikan dinamis dan pengikatan dinamis, membuatnya sangat menarik untuk Pengembangan Aplikasi Cepat, serta untuk digunakan sebagai bahasa skrip atau lem untuk menghubungkan komponen atau layanan yang ada. Python mendukung modul dan paket, sehingga mendorong modularitas program dan penggunaan kembali kode.
Tentang artikel ini
Sintaks Python yang sederhana dan mudah dipelajari dapat menyesatkan pengembang Python – terutama mereka yang baru mengenal bahasa tersebut – untuk melewatkan beberapa kehalusannya dan meremehkan kekuatan bahasa Python yang beragam.
Dengan mengingat hal itu, artikel ini menyajikan daftar "10 teratas" dari kesalahan yang agak halus dan sulit ditangkap yang dapat menggigit bahkan beberapa pengembang Python yang lebih maju di belakang.
(Catatan: Artikel ini ditujukan untuk audiens yang lebih mahir daripada Kesalahan Umum Pemrogram Python, yang lebih ditujukan untuk mereka yang lebih baru dalam bahasa tersebut.)
Kesalahan Umum #1: Penyalahgunaan ekspresi sebagai default untuk argumen fungsi
Python memungkinkan Anda untuk menentukan bahwa argumen fungsi adalah opsional dengan memberikan nilai default untuk itu. Meskipun ini adalah fitur hebat dari bahasa ini, ini dapat menyebabkan beberapa kebingungan ketika nilai defaultnya bisa berubah . Misalnya, pertimbangkan definisi fungsi Python ini:
>>> def foo(bar=[]): # bar is optional and defaults to [] if not specified ... bar.append("baz") # but this line could be problematic, as we'll see... ... return bar
Kesalahan umum adalah berpikir bahwa argumen opsional akan disetel ke ekspresi default yang ditentukan setiap kali fungsi dipanggil tanpa memberikan nilai untuk argumen opsional. Dalam kode di atas, misalnya, orang mungkin berharap bahwa memanggil foo()
berulang kali (yaitu, tanpa menentukan argumen bar
) akan selalu mengembalikan 'baz'
, karena asumsinya adalah bahwa setiap kali foo()
dipanggil (tanpa bar
argumen yang ditentukan) bar
diatur ke []
(yaitu, daftar kosong baru).
Tapi mari kita lihat apa yang sebenarnya terjadi ketika Anda melakukan ini:
>>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"]
Hah? Mengapa itu terus menambahkan nilai default "baz"
ke daftar yang ada setiap kali foo()
dipanggil, daripada membuat daftar baru setiap kali?
Jawaban pemrograman Python yang lebih maju adalah bahwa nilai default untuk argumen fungsi hanya dievaluasi sekali, pada saat fungsi didefinisikan. Jadi, argumen bar
diinisialisasi ke defaultnya (yaitu, daftar kosong) hanya ketika foo()
pertama kali didefinisikan, tetapi kemudian panggilan ke foo()
(yaitu, tanpa argumen bar
yang ditentukan) akan terus menggunakan daftar yang sama untuk bar
mana yang awalnya diinisialisasi.
FYI, solusi umum untuk ini adalah sebagai berikut:
>>> def foo(bar=None): ... if bar is None: # or if not bar: ... bar = [] ... bar.append("baz") ... return bar ... >>> foo() ["baz"] >>> foo() ["baz"] >>> foo() ["baz"]
Kesalahan Umum #2: Menggunakan variabel kelas secara tidak benar
Perhatikan contoh berikut:
>>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print Ax, Bx, Cx 1 1 1
Masuk akal.
>>> Bx = 2 >>> print Ax, Bx, Cx 1 2 1
Ya, sekali lagi seperti yang diharapkan.
>>> Ax = 3 >>> print Ax, Bx, Cx 3 2 3
Apa $%#!& ?? Kami hanya mengubah Ax
. Mengapa Cx
berubah juga?
Dalam Python, variabel kelas ditangani secara internal sebagai kamus dan mengikuti apa yang sering disebut sebagai Method Resolution Order (MRO). Jadi dalam kode di atas, karena atribut x
tidak ditemukan di kelas C
, itu akan dicari di kelas dasarnya (hanya A
dalam contoh di atas, meskipun Python mendukung banyak pewarisan). Dengan kata lain, C
tidak memiliki properti x
sendiri, tidak bergantung pada A
. Jadi, referensi ke Cx
sebenarnya adalah referensi ke Ax
. Ini menyebabkan masalah Python kecuali jika ditangani dengan benar. Pelajari lebih lanjut tentang atribut kelas di Python.
Kesalahan Umum #3: Menentukan parameter secara tidak benar untuk blok pengecualian
Misalkan Anda memiliki kode berikut:
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError: # To catch both exceptions, right? ... pass ... Traceback (most recent call last): File "<stdin>", line 3, in <module> IndexError: list index out of range
Masalahnya di sini adalah bahwa pernyataan except
tidak mengambil daftar pengecualian yang ditentukan dengan cara ini. Sebaliknya, Dalam Python 2.x, sintaks except Exception, e
digunakan untuk mengikat pengecualian ke parameter opsional kedua yang ditentukan (dalam hal ini e
), agar tersedia untuk pemeriksaan lebih lanjut. Akibatnya, dalam kode di atas, pengecualian IndexError
tidak ditangkap oleh pernyataan except
; alih-alih, pengecualian malah berakhir dengan terikat pada parameter bernama IndexError
.
Cara yang tepat untuk menangkap beberapa pengecualian dalam pernyataan except
adalah dengan menentukan parameter pertama sebagai tupel yang berisi semua pengecualian yang akan ditangkap. Juga, untuk portabilitas maksimum, gunakan kata kunci as
, karena sintaks tersebut didukung oleh Python 2 dan Python 3:
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>
Kesalahan Umum #4: Kesalahpahaman aturan ruang lingkup Python
Resolusi lingkup Python didasarkan pada apa yang dikenal sebagai aturan LEGB, yang merupakan singkatan dari L ocal, Enclosing , G lobal, B uilt-in. Tampaknya cukup mudah, bukan? Sebenarnya, ada beberapa seluk-beluk cara kerja ini di Python, yang membawa kita ke masalah umum pemrograman Python yang lebih canggih di bawah ini. Pertimbangkan hal berikut:
>>> x = 10 >>> def foo(): ... x += 1 ... print x ... >>> foo() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'x' referenced before assignment
Apa masalahnya?
Kesalahan di atas terjadi karena, ketika Anda membuat penugasan ke variabel dalam suatu lingkup, variabel itu secara otomatis dianggap oleh Python sebagai lokal untuk lingkup itu dan membayangi variabel bernama serupa di lingkup luar mana pun.
Oleh karena itu, banyak yang terkejut mendapatkan UnboundLocalError
dalam kode yang berfungsi sebelumnya ketika kode tersebut dimodifikasi dengan menambahkan pernyataan penetapan di suatu tempat di badan suatu fungsi. (Anda dapat membaca lebih lanjut tentang ini di sini.)
Hal ini sangat umum untuk membuat pengembang tersandung saat menggunakan daftar. Perhatikan contoh berikut:
>>> lst = [1, 2, 3] >>> def foo1(): ... lst.append(5) # This works ok... ... >>> foo1() >>> lst [1, 2, 3, 5] >>> lst = [1, 2, 3] >>> def foo2(): ... lst += [5] # ... but this bombs! ... >>> foo2() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment
Hah? Mengapa foo2
mengebom sementara foo1
berjalan dengan baik?
Jawabannya sama seperti pada contoh masalah sebelumnya tetapi memang lebih halus. foo1
tidak membuat penugasan ke lst
, sedangkan foo2
adalah. Mengingat bahwa lst += [5]
sebenarnya hanyalah singkatan untuk lst = lst + [5]
, kita melihat bahwa kita mencoba untuk menetapkan nilai ke lst
(oleh karena itu dianggap oleh Python berada dalam lingkup lokal). Namun, nilai yang ingin kita tetapkan ke lst
didasarkan pada lst
itu sendiri (sekali lagi, sekarang dianggap dalam lingkup lokal), yang belum ditentukan. Ledakan.
Kesalahan Umum #5: Memodifikasi daftar saat mengulanginya
Masalah dengan kode berikut seharusnya cukup jelas:
>>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> for i in range(len(numbers)): ... if odd(numbers[i]): ... del numbers[i] # BAD: Deleting item from a list while iterating over it ... Traceback (most recent call last): File "<stdin>", line 2, in <module> IndexError: list index out of range
Menghapus item dari daftar atau larik sambil mengulanginya adalah masalah Python yang diketahui oleh pengembang perangkat lunak berpengalaman mana pun. Tapi sementara contoh di atas mungkin cukup jelas, bahkan pengembang tingkat lanjut dapat secara tidak sengaja digigit oleh kode yang jauh lebih kompleks ini.
Untungnya, Python menggabungkan sejumlah paradigma pemrograman elegan yang, bila digunakan dengan benar, dapat menghasilkan kode yang disederhanakan dan disederhanakan secara signifikan. Manfaat sampingan dari ini adalah bahwa kode yang lebih sederhana lebih kecil kemungkinannya untuk digigit oleh bug penghapusan tidak sengaja-dari-daftar-item-saat-iterasi-over-it. Salah satu paradigma tersebut adalah pemahaman daftar. Selain itu, pemahaman daftar sangat berguna untuk menghindari masalah khusus ini, seperti yang ditunjukkan oleh implementasi alternatif dari kode di atas yang bekerja dengan sempurna:
>>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> numbers[:] = [n for n in numbers if not odd(n)] # ahh, the beauty of it all >>> numbers [0, 2, 4, 6, 8]
Kesalahan Umum #6: Membingungkan bagaimana Python mengikat variabel dalam penutupan
Mengingat contoh berikut:

>>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ...
Anda mungkin mengharapkan output berikut:
0 2 4 6 8
Tetapi Anda benar-benar mendapatkan:
8 8 8 8 8
Kejutan!
Ini terjadi karena perilaku pengikatan akhir Python yang mengatakan bahwa nilai variabel yang digunakan dalam penutupan dicari pada saat fungsi bagian dalam dipanggil. Jadi dalam kode di atas, setiap kali salah satu fungsi yang dikembalikan dipanggil, nilai i
dicari di lingkup sekitarnya pada saat dipanggil (dan saat itu, loop telah selesai, jadi i
telah ditetapkan finalnya nilai 4).
Solusi untuk masalah Python umum ini adalah sedikit peretasan:
>>> def create_multipliers(): ... return [lambda x, i=i : i * x for i in range(5)] ... >>> for multiplier in create_multipliers(): ... print multiplier(2) ... 0 2 4 6 8
Voila! Kami mengambil keuntungan dari argumen default di sini untuk menghasilkan fungsi anonim untuk mencapai perilaku yang diinginkan. Beberapa orang akan menyebut ini elegan. Beberapa akan menyebutnya halus. Beberapa membencinya. Tetapi jika Anda seorang pengembang Python, penting untuk dipahami dalam hal apa pun.
Kesalahan Umum #7: Membuat dependensi modul melingkar
Katakanlah Anda memiliki dua file, a.py
dan b.py
, yang masing-masing mengimpor yang lain, sebagai berikut:
Dalam a.py
:
import b def f(): return bx print f()
Dan di b.py
:
import a x = 1 def g(): print af()
Pertama, mari kita coba mengimpor a.py
:
>>> import a 1
Bekerja dengan baik. Mungkin itu mengejutkan Anda. Lagi pula, kami memiliki impor melingkar di sini yang mungkin seharusnya menjadi masalah, bukan?
Jawabannya adalah bahwa kehadiran impor melingkar tidak dengan sendirinya menjadi masalah di Python. Jika sebuah modul telah diimpor, Python cukup pintar untuk tidak mencoba mengimpornya kembali. Namun, tergantung pada titik di mana setiap modul mencoba mengakses fungsi atau variabel yang ditentukan di modul lain, Anda mungkin memang mengalami masalah.
Jadi kembali ke contoh kami, ketika kami mengimpor a.py
, tidak ada masalah mengimpor b.py
, karena b.py
tidak memerlukan apa pun dari a.py
untuk didefinisikan pada saat itu diimpor . Satu-satunya referensi di b.py
ke a
adalah panggilan ke af()
. Tapi panggilan itu ada di g()
dan tidak ada apa pun di a.py
atau b.py
memanggil g()
. Jadi hidup itu baik.
Tetapi apa yang terjadi jika kita mencoba mengimpor b.py
(tanpa sebelumnya mengimpor a.py
, yaitu):
>>> import b Traceback (most recent call last): File "<stdin>", line 1, in <module> File "b.py", line 1, in <module> import a File "a.py", line 6, in <module> print f() File "a.py", line 4, in f return bx AttributeError: 'module' object has no attribute 'x'
Uh oh. Itu tidak baik! Masalahnya di sini adalah, dalam proses mengimpor b.py
, ia mencoba mengimpor a.py
, yang pada gilirannya memanggil f()
, yang mencoba mengakses bx
. Tetapi bx
belum ditentukan. Oleh karena itu pengecualian AttributeError
.
Setidaknya satu solusi untuk ini cukup sepele. Cukup ubah b.py
untuk mengimpor a.py
di dalam g()
:
x = 1 def g(): import a # This will be evaluated only when g() is called print af()
Tidak ketika kami mengimpornya, semuanya baik-baik saja:
>>> import b >>> bg() 1 # Printed a first time since module 'a' calls 'print f()' at the end 1 # Printed a second time, this one is our call to 'g'
Kesalahan Umum #8: Nama bentrok dengan modul Pustaka Standar Python
Salah satu keindahan Python adalah kekayaan modul perpustakaan yang datang dengan "di luar kotak". Tetapi akibatnya, jika Anda tidak secara sadar menghindarinya, tidak sulit untuk mengalami bentrokan nama antara nama salah satu modul Anda dan modul dengan nama yang sama di pustaka standar yang dikirimkan dengan Python (misalnya , Anda mungkin memiliki modul bernama email.py
dalam kode Anda, yang akan bertentangan dengan modul pustaka standar dengan nama yang sama).
Ini dapat menyebabkan masalah yang parah, seperti mengimpor pustaka lain yang pada gilirannya mencoba mengimpor versi Pustaka Standar Python dari sebuah modul tetapi, karena Anda memiliki modul dengan nama yang sama, paket lain secara keliru mengimpor versi Anda alih-alih yang ada di dalamnya Pustaka Standar Python. Di sinilah kesalahan Python yang buruk terjadi.
Oleh karena itu, kehati-hatian harus dilakukan untuk menghindari penggunaan nama yang sama seperti yang ada di modul Pustaka Standar Python. Jauh lebih mudah bagi Anda untuk mengubah nama modul di dalam paket Anda daripada mengajukan Python Enhancement Proposal (PEP) untuk meminta perubahan nama di bagian hulu dan mencoba untuk mendapatkan persetujuan itu.
Kesalahan Umum #9: Gagal mengatasi perbedaan antara Python 2 dan Python 3
Pertimbangkan file berikut foo.py
:
import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def bad(): e = None try: bar(int(sys.argv[1])) except KeyError as e: print('key error') except ValueError as e: print('value error') print(e) bad()
Pada Python 2, ini berjalan dengan baik:
$ python foo.py 1 key error 1 $ python foo.py 2 value error 2
Tapi sekarang mari kita putar di Python 3:
$ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19, in <module> bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment
Apa yang baru saja terjadi di sini? "Masalahnya" adalah, dalam Python 3, objek pengecualian tidak dapat diakses di luar cakupan blok except
. (Alasan untuk ini adalah, jika tidak, ia akan menyimpan siklus referensi dengan bingkai tumpukan di memori hingga pengumpul sampah berjalan dan menghapus referensi dari memori. Detail teknis lebih lanjut tentang ini tersedia di sini).
Salah satu cara untuk menghindari masalah ini adalah dengan mempertahankan referensi ke objek pengecualian di luar cakupan blok except
sehingga tetap dapat diakses. Berikut adalah versi dari contoh sebelumnya yang menggunakan teknik ini, sehingga menghasilkan kode yang ramah Python 2 dan Python 3:
import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def good(): exception = None try: bar(int(sys.argv[1])) except KeyError as e: exception = e print('key error') except ValueError as e: exception = e print('value error') print(exception) good()
Menjalankan ini di Py3k:
$ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2
Hura!
(Kebetulan, Panduan Perekrutan Python kami membahas sejumlah perbedaan penting lainnya yang harus diperhatikan saat memigrasikan kode dari Python 2 ke Python 3.)
Kesalahan Umum #10: Menyalahgunakan metode __del__
Katakanlah Anda memiliki ini dalam file bernama mod.py
:
import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle)
Dan Anda kemudian mencoba melakukan ini dari another_mod.py
:
import mod mybar = mod.Bar()
Anda akan mendapatkan pengecualian AttributeError
yang jelek.
Mengapa? Karena, seperti yang dilaporkan di sini, ketika interpreter dimatikan, variabel global modul semuanya disetel ke None
. Akibatnya, dalam contoh di atas, pada saat __del__
dipanggil, nama foo
telah disetel ke None
.
Solusi untuk masalah pemrograman Python yang agak lebih canggih ini adalah dengan menggunakan atexit.register()
sebagai gantinya. Dengan begitu, ketika program Anda selesai dijalankan (yaitu saat keluar secara normal), penangan terdaftar Anda akan ditendang sebelum interpreter dimatikan.
Dengan pemahaman itu, perbaikan untuk kode mod.py
di atas mungkin akan terlihat seperti ini:
import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle)
Implementasi ini menyediakan cara yang bersih dan andal untuk memanggil fungsionalitas pembersihan yang diperlukan setelah penghentian program normal. Jelas, terserah foo.cleanup
untuk memutuskan apa yang harus dilakukan dengan objek yang terikat dengan nama self.myhandle
, tetapi Anda mendapatkan idenya.
Bungkus
Python adalah bahasa yang kuat dan fleksibel dengan banyak mekanisme dan paradigma yang dapat sangat meningkatkan produktivitas. Namun, seperti halnya alat perangkat lunak atau bahasa apa pun, memiliki pemahaman atau apresiasi yang terbatas atas kemampuannya terkadang bisa lebih menjadi hambatan daripada manfaat, membuat seseorang dalam kondisi pepatah "cukup tahu untuk menjadi berbahaya".
Membiasakan diri dengan nuansa utama Python, seperti (tetapi tidak terbatas pada) masalah pemrograman tingkat lanjut yang diangkat dalam artikel ini, akan membantu mengoptimalkan penggunaan bahasa sambil menghindari beberapa kesalahan yang lebih umum.
Anda mungkin juga ingin melihat Panduan Orang Dalam untuk Wawancara Python untuk saran tentang pertanyaan wawancara yang dapat membantu mengidentifikasi pakar Python.
Kami harap Anda menemukan petunjuk dalam artikel ini bermanfaat dan menyambut umpan balik Anda.