Apa itu pengecualian? Dari namanya jelas - mereka muncul ketika pengecualian terjadi dalam program. Anda mungkin bertanya mengapa pengecualian merupakan anti-pola, dan bagaimana kaitannya dengan mengetik? Saya mencoba
mencari tahu , dan sekarang saya ingin membicarakan ini dengan Anda, harazhiteli.
Masalah Pengecualian
Sulit menemukan kekurangan pada apa yang Anda hadapi setiap hari. Kebiasaan dan kebutaan mengubah bug menjadi fitur, tetapi mari kita coba melihat pengecualian dengan pikiran terbuka.
Pengecualian sulit dikenali
Ada dua jenis pengecualian: "eksplisit" dibuat dengan memanggil
raise
langsung dalam kode yang Anda baca; "Tersembunyi" tersembunyi dalam fungsi, kelas, metode yang digunakan.
Masalahnya adalah bahwa pengecualian “tersembunyi” sangat sulit untuk diperhatikan. Mari saya tunjukkan contoh fungsi murni:
def divide(first: float, second: float) -> float: return first / second
Fungsi ini hanya membagi satu nomor dengan yang lain, mengembalikan
float
. Jenis dicentang dan Anda dapat menjalankan sesuatu seperti ini:
result = divide(1, 0) print('x / y = ', result)
Pernahkah Anda memperhatikan? Bahkan, pelaksanaan program tidak akan pernah mencapai
print
, karena membagi 1 dengan 0 adalah operasi yang mustahil, itu akan meningkatkan
ZeroDivisionError
. Ya, kode seperti itu aman, tetapi tidak bisa digunakan.
Untuk memperhatikan masalah potensial bahkan dalam kode yang sederhana dan mudah dibaca, orang perlu pengalaman. Apa pun di Python dapat berhenti bekerja dengan berbagai jenis pengecualian: pembagian, pemanggilan fungsi,
int
,
str
, generator, iterator dalam loop, akses ke atribut atau kunci. Bahkan
raise something()
dapat menyebabkan crash. Selain itu, saya bahkan tidak menyebutkan operasi input dan output. Dan
pengecualian yang dicentang tidak akan lagi didukung dalam waktu dekat.
Memulihkan perilaku normal pada tempatnya tidak dimungkinkan
Tetapi tepatnya untuk kasus seperti itu, kami memiliki pengecualian. Mari kita menangani
ZeroDivisionError
dan kodenya akan menjadi tipe aman.
def divide(first: float, second: float) -> float: try: return first / second except ZeroDivisionError: return 0.0
Sekarang semuanya baik-baik saja. Tetapi mengapa kita mengembalikan 0? Kenapa tidak 1 atau tidak sama sekali? Tentu saja, dalam kebanyakan kasus, mendapatkan
None
sama buruknya (jika tidak lebih buruk) sebagai pengecualian, tetapi Anda masih harus mengandalkan logika bisnis dan opsi untuk menggunakan fungsi tersebut.
Apa sebenarnya yang kami bagikan? Angka sewenang-wenang, ada unit atau uang tertentu? Tidak setiap opsi mudah diramalkan dan dipulihkan. Mungkin ternyata bahwa lain kali Anda menggunakan satu fungsi, ternyata Anda memerlukan logika pemulihan yang berbeda.
Kesimpulan yang menyedihkan: solusi untuk setiap masalah adalah individual, tergantung pada konteks penggunaan tertentu.
Tidak ada peluru perak untuk berurusan dengan
ZeroDivisionError
sekali dan untuk semua. Dan kita tidak berbicara tentang kemungkinan I / O kompleks dengan permintaan dan batas waktu berulang.
Mungkin itu tidak perlu untuk menangani pengecualian di mana mereka muncul? Mungkin hanya membuangnya ke proses eksekusi kode - seseorang akan mengetahuinya nanti. Dan kemudian kita dipaksa untuk kembali ke keadaan saat ini.
Proses eksekusi tidak jelas
Yah, mari kita berharap orang lain menangkap pengecualian dan mungkin menanganinya. Misalnya, sistem dapat meminta pengguna untuk mengubah nilai yang dimasukkan, karena tidak dapat dibagi dengan 0. Dan fungsi
divide
tidak boleh secara eksplisit bertanggung jawab untuk memulihkan dari kesalahan.
Dalam hal ini, Anda perlu memeriksa di mana kami menangkap pengecualian. Ngomong-ngomong, bagaimana cara menentukan dengan tepat di mana ia akan diproses? Apakah mungkin untuk pergi ke tempat yang tepat dalam kode? Ternyata tidak,
itu tidak mungkin .
Tidak mungkin untuk menentukan baris kode mana yang akan dieksekusi setelah pengecualian dilemparkan. Berbagai jenis pengecualian dapat ditangani dengan opsi pengecualian yang berbeda, dan beberapa pengecualian dapat
diabaikan . Dan Anda bisa melempar pengecualian tambahan di modul lain yang akan dijalankan sebelumnya, dan secara umum akan mematahkan semua logika.
Misalkan ada dua utas independen dalam suatu aplikasi: utas reguler yang berjalan dari atas ke bawah, dan utas pengecualian yang berjalan sesuai keinginan. Bagaimana cara membaca dan memahami kode ini?
Hanya dengan debugger diaktifkan dalam mode "tangkap semua pengecualian".
Pengecualian, seperti
goto
terkenal, merobek struktur program.
Pengecualian tidak eksklusif
Mari kita lihat contoh lain: kode akses HTTP API jarak jauh yang biasa:
import requests def fetch_user_profile(user_id: int) -> 'UserProfile': """Fetches UserProfile dict from foreign API.""" response = requests.get('/api/users/{0}'.format(user_id)) response.raise_for_status() return response.json()
Dalam contoh ini, secara harfiah semuanya bisa salah. Berikut adalah sebagian daftar kemungkinan kesalahan:
- Jaringan mungkin tidak tersedia dan permintaan tidak akan dieksekusi sama sekali.
- Server mungkin tidak berfungsi.
- Server mungkin terlalu sibuk, batas waktu akan terjadi.
- Server mungkin memerlukan otentikasi.
- API mungkin tidak memiliki URL seperti itu.
- Pengguna yang tidak ada dapat ditransfer.
- Mungkin tidak cukup hak.
- Server mungkin macet karena kesalahan internal saat memproses permintaan Anda
- Server dapat mengembalikan respons yang tidak valid atau rusak.
- Server dapat mengembalikan JSON tidak valid yang tidak dapat diuraikan.
Daftar ini terus dan terus, begitu banyak potensi masalah terletak pada kode dari tiga baris yang disayangkan. Kita dapat mengatakan bahwa itu umumnya hanya bekerja dengan peluang keberuntungan, dan jauh lebih mungkin untuk jatuh dengan pengecualian.
Bagaimana cara melindungi diri sendiri?
Sekarang kami telah memastikan bahwa pengecualian dapat merusak kode, mari cari tahu cara menghilangkannya. Untuk menulis kode tanpa kecuali, ada pola yang berbeda.
- Di mana-mana menulis
except Exception: pass
. Jalan buntu. Jangan lakukan itu. - Kembali tidak
None
Terlalu jahat. Akibatnya, Anda harus memulai hampir setiap baris dengan if something is not None:
dan semua logika akan hilang di balik sampah pemeriksaan pembersihan, atau Anda akan menderita TypeError
sepanjang waktu. Bukan pilihan yang bagus. - Tulis kelas untuk kasus penggunaan khusus. Misalnya,
User
kelas dasar dengan subkelas untuk kesalahan seperti UserNotFound
dan MissingUser
. Pendekatan ini dapat digunakan dalam beberapa situasi tertentu, seperti AnonymousUser
di Django, tetapi membungkus semua kesalahan yang mungkin terjadi di kelas tidak realistis. Ini akan mengambil terlalu banyak pekerjaan dan model domain akan menjadi kompleks yang tak terbayangkan. - Gunakan wadah untuk membungkus variabel atau nilai kesalahan yang dihasilkan dalam pembungkus dan terus bekerja dengan nilai wadah. Inilah sebabnya kami membuat proyek
@dry-python/return
. Sehingga fungsinya mengembalikan sesuatu yang bermakna, diketik, dan aman.
Mari kita kembali ke contoh pembagian, yang mengembalikan 0 ketika kesalahan terjadi. Bisakah kita secara eksplisit menunjukkan bahwa fungsi tidak berhasil tanpa mengembalikan nilai numerik tertentu?
from returns.result import Result, Success, Failure def divide(first: float, second: float) -> Result[float, ZeroDivisionError]: try: return Success(first / second) except ZeroDivisionError as exc: return Failure(exc)
Kami menyertakan nilai dalam salah satu dari dua pembungkus:
Success
atau
Failure
. Kelas-kelas ini diwarisi dari kelas basis
Result
. Jenis nilai yang dikemas dapat ditentukan dalam anotasi dengan fungsi yang dikembalikan, misalnya,
Result[float, ZeroDivisionError]
mengembalikan
Success[float]
atau
Failure[ZeroDivisionError]
.
Apa yang ini berikan pada kita? Lebih banyak
pengecualian tidak luar biasa, tetapi merupakan masalah yang diharapkan . Juga, membungkus pengecualian dalam
Failure
memecahkan masalah kedua: kompleksitas mengidentifikasi pengecualian potensial.
1 + divide(1, 0)
Sekarang mereka mudah dikenali. Jika Anda melihat
Result
dalam kode, maka fungsi tersebut dapat mengeluarkan pengecualian. Dan Anda bahkan tahu tipenya di muka.
Selain itu, perpustakaan ini sepenuhnya diketik dan
kompatibel dengan PEP561 . Artinya, mypy akan memperingatkan Anda jika Anda mencoba mengembalikan sesuatu yang tidak cocok dengan tipe yang dinyatakan.
from returns.result import Result, Success, Failure def divide(first: float, second: float) -> Result[float, ZeroDivisionError]: try: return Success('Done')
Bagaimana cara bekerja dengan wadah?
Ada dua
metode :
map
untuk fungsi yang mengembalikan nilai normal;bind
untuk fungsi yang mengembalikan wadah lain.
Success(4).bind(lambda number: Success(number / 2))
Keindahannya adalah bahwa kode ini akan melindungi Anda dari skrip yang gagal, karena
.bind
dan
.map
tidak akan dieksekusi untuk kontainer dengan
Failure
:
Failure(4).bind(lambda number: Success(number / 2))
Sekarang Anda bisa berkonsentrasi pada proses eksekusi yang benar dan memastikan bahwa kondisi yang salah tidak akan merusak program di tempat yang tidak terduga. Dan selalu ada peluang untuk
menentukan kondisi yang salah, memperbaikinya , dan kembali ke jalur yang telah dipahami dari proses tersebut.
Failure(4).rescue(lambda number: Success(number + 1))
Dalam pendekatan kami, "semua masalah diselesaikan secara individual," dan "proses eksekusi sekarang transparan." Nikmati pemrograman yang mengendarai rel!
Tetapi bagaimana cara memperluas nilai dari wadah?
Memang, jika Anda bekerja dengan fungsi yang tidak tahu apa-apa tentang wadah, Anda perlu nilai sendiri. Kemudian Anda dapat menggunakan metode
.unwrap()
atau
.value_or()
:
Success(1).unwrap()
Tunggu, kami harus menyingkirkan pengecualian, dan sekarang ternyata semua panggilan
.unwrap()
dapat menyebabkan pengecualian lain?
Bagaimana tidak memikirkan UnwrapFailedErrors?
Oke, mari kita lihat bagaimana hidup dengan pengecualian baru. Pertimbangkan contoh ini: Anda perlu memeriksa input pengguna dan membuat dua model dalam database. Setiap langkah dapat diakhiri dengan pengecualian, itulah sebabnya semua metode dibungkus dalam
Result
:
from returns.result import Result, Success, Failure class CreateAccountAndUser(object): """Creates new Account-User pair."""
Pertama, Anda tidak perlu memperluas nilai-nilai dalam logika bisnis Anda sendiri sama sekali:
class CreateAccountAndUser(object): """Creates new Account-User pair.""" def __call__(self, username: str, email: str) -> Result['User', str]: """Can return a Success(user) or Failure(str_reason).""" return self._validate_user( username, email, ).bind( self._create_account, ).bind( self._create_user, )
Semuanya akan bekerja tanpa masalah, tidak ada pengecualian akan
.unwrap()
, karena
.unwrap()
tidak digunakan. Tetapi apakah mudah untuk membaca kode seperti itu? Tidak. Dan apa alternatifnya?
@pipeline
:
from result.functions import pipeline class CreateAccountAndUser(object): """Creates new Account-User pair.""" @pipeline def __call__(self, username: str, email: str) -> Result['User', str]: """Can return a Success(user) or Failure(str_reason).""" user_schema = self._validate_user(username, email).unwrap() account = self._create_account(user_schema).unwrap() return self._create_user(account)
Sekarang kode ini dibaca dengan baik. Begini caranya
.unwrap()
dan
@pipeline
bekerja bersama: setiap kali metode
.unwrap()
gagal dan
Failure[str]
, dekorator
@pipeline
menangkapnya dan mengembalikan
Failure[str]
sebagai nilai yang dihasilkan. Ini adalah bagaimana saya mengusulkan untuk menghapus semua pengecualian dari kode dan membuatnya benar-benar aman dan diketik.
Bungkus semuanya
Oke, sekarang kami akan menerapkan alat baru, misalnya, dengan permintaan HTTP API. Ingat bahwa setiap baris dapat memberikan pengecualian? Dan tidak ada cara untuk membuat mereka mengembalikan wadah dengan
Result
. Tapi Anda bisa menggunakan
dekorator @safe untuk membungkus fungsi yang tidak aman dan membuatnya aman. Di bawah ini adalah dua opsi kode yang melakukan hal yang sama:
from returns.functions import safe @safe def divide(first: float, second: float) -> float: return first / second
Yang pertama, dengan
@safe
, lebih mudah dan lebih baik untuk dibaca.
Hal terakhir yang harus dilakukan dalam contoh permintaan API adalah menambahkan dekorator
@safe
. Hasilnya adalah kode berikut:
import requests from returns.functions import pipeline, safe from returns.result import Result class FetchUserProfile(object): """Single responsibility callable object that fetches user profile."""
Untuk meringkas cara menyingkirkan pengecualian dan mengamankan kode :
- Gunakan pembungkus
@safe
untuk semua metode yang dapat menimbulkan pengecualian. Ini akan mengubah tipe fungsi yang Result[OldReturnType, Exception]
ke Result[OldReturnType, Exception]
. - Gunakan
Result
sebagai wadah untuk mentransfer nilai dan kesalahan ke dalam abstraksi sederhana. - Gunakan
.unwrap()
untuk memperluas nilai dari wadah. - Gunakan
@pipeline
untuk membuat .unwrap
panggilan .unwrap
lebih mudah dibaca.
Dengan mematuhi aturan-aturan ini, kita dapat melakukan hal yang persis sama - hanya aman dan mudah dibaca. Semua masalah dengan pengecualian diselesaikan:
- "Pengecualian sulit dikenali . " Sekarang mereka dibungkus dalam wadah
Result
diketik, yang membuatnya benar-benar transparan. - "Memulihkan perilaku normal di tempat adalah tidak mungkin . " Sekarang Anda dapat dengan aman mendelegasikan proses pemulihan ke pemanggil. Untuk kasus seperti itu, ada
.fix()
dan .rescue()
. - "Urutan eksekusi tidak jelas . " Sekarang mereka menyatu dengan aliran bisnis yang biasa. Dari awal hingga akhir.
- "Pengecualian tidak luar biasa . " Kami tahu! Dan kami berharap ada sesuatu yang salah dan siap untuk apa pun.
Gunakan Kasus dan Batasan
Jelas, Anda tidak dapat menggunakan pendekatan ini di semua kode Anda. Ini akan
terlalu aman untuk sebagian besar situasi sehari-hari dan tidak kompatibel dengan perpustakaan atau kerangka kerja lainnya. Tetapi Anda harus menulis bagian terpenting dari logika bisnis Anda persis seperti yang saya tunjukkan, untuk memastikan operasi sistem Anda yang benar dan memfasilitasi dukungan di masa depan.
Apakah topik tersebut membuat Anda berpikir atau bahkan tampak holivarny? Datang ke Moscow Python Conf ++ pada 5 April, kita akan membahas! Selain saya, Artyom Malyshev, pendiri proyek dry-python dan pengembang inti Django Channels, akan ada di sana. Dia akan berbicara lebih banyak tentang dry-python dan logika bisnis.