Advertisement
  1. Code
  2. Python

Penanganan Error Secara Profesional Dengan Python

Scroll to top
Read Time: 12 min

() translation by (you can also view the original English article)

Di dalam tutorial ini kamu akan mempelajari bagaimana menangani kondisi error dalam Python dari keseluruhan sudut pandang sistem. Penanganan error adalah aspek desain yang kritikal, dan itu menyebrangi dari level terendah (terkadang hardware) hingga ke pengguna akhir. Jika kamu tidak memiliki strategi yang konsisten, sistemmu akan tidak dapat diandalkan, pengalaman pengguna akan jelek, dan kamu akan memiliki banyak tantangan dalam debugging dan troubleshooting.

Kunci untuk berhasil adalah menyadari semua aspek yang saling berkaitan ini, mempertimbangkan mereka secara eksplisit, dan membentuk sebuah solusi yang mengenali tiap titik.

Status Codes vs. Exceptions

Ada dua model penanganan error utama: status code dan exception. Status code dapat digunakan oleh bahasa pemrograman apapun. Exception memerlukan dukungan bahasa/runtime.

Python mendukung exception. Python dan librari standarnya menggunakan exception secara liberal untuk melaporkan banyak situasi luar biasa seperti IO error, pembagian dengan nol, indexing di luar batas, dan juga beberapa situasi yang tidak begitu luar biasa seperti akhir iterasi (walaupun itu tersembunyi). Kebanyakan librari akan mengikuti kecocokan dan menaikkan exception.

Itu berarti code-mu akan harus menangani exception yang diangkat oleh Python dan librari, sehingga kamu mungkin juga melakukan raise pada exception dari code-mu jika perlu dan tidak bergantung pada status code.

Contoh Singkat

Sebelum menggali ke dalam tempat suci Python exception dan penerapan terbaik penanganan error, mari kita lihat beberapa penanganan error dalam aksinya:

1
def f():
2
3
    return 4 / 0
4
5
6
7
def g():
8
9
    raise Exception("Don't call us. We'll call you")
10
11
12
13
def h():
14
15
    try:
16
17
        f()
18
19
    except Exception as e:
20
21
        print(e)
22
23
    try:
24
25
        g()
26
27
    except Exception as e:
28
29
        print(e)

Berikut output ketika memanggil h():

1
h()
2
3
division by zero
4
5
Don't call us. We'll call you

Python Exceptions

Python exceptions adalah obyek diatur di dalam sebuah hirarki class:

Berikut adalah keseluruhan hirarkinya:

1
BaseException
2
3
 +-- SystemExit
4
5
 +-- KeyboardInterrupt
6
7
 +-- GeneratorExit
8
9
 +-- Exception
10
11
      +-- StopIteration
12
13
      +-- StandardError
14
15
      |    +-- BufferError
16
17
      |    +-- ArithmeticError
18
19
      |    |    +-- FloatingPointError
20
21
      |    |    +-- OverflowError
22
23
      |    |    +-- ZeroDivisionError
24
25
      |    +-- AssertionError
26
27
      |    +-- AttributeError
28
29
      |    +-- EnvironmentError
30
31
      |    |    +-- IOError
32
33
      |    |    +-- OSError
34
35
      |    |         +-- WindowsError (Windows)
36
37
      |    |         +-- VMSError (VMS)
38
39
      |    +-- EOFError
40
41
      |    +-- ImportError
42
43
      |    +-- LookupError
44
45
      |    |    +-- IndexError
46
47
      |    |    +-- KeyError
48
49
      |    +-- MemoryError
50
51
      |    +-- NameError
52
53
      |    |    +-- UnboundLocalError
54
55
      |    +-- ReferenceError
56
57
      |    +-- RuntimeError
58
59
      |    |    +-- NotImplementedError
60
61
      |    +-- SyntaxError
62
63
      |    |    +-- IndentationError
64
65
      |    |         +-- TabError
66
67
      |    +-- SystemError
68
69
      |    +-- TypeError
70
71
      |    +-- ValueError
72
73
      |         +-- UnicodeError
74
75
      |              +-- UnicodeDecodeError
76
77
      |              +-- UnicodeEncodeError
78
79
      |              +-- UnicodeTranslateError
80
81
      +-- Warning
82
83
           +-- DeprecationWarning
84
85
           +-- PendingDeprecationWarning
86
87
           +-- RuntimeWarning
88
89
           +-- SyntaxWarning
90
91
           +-- UserWarning
92
93
           +-- FutureWarning
94
95
  +-- ImportWarning
96
97
  +-- UnicodeWarning
98
99
  +-- BytesWarning
100
 

Ada beberapa exception khusus yang diturunkan secara langsung dari BaseException, seperti SystemExitKeyboardInterrupt dan GeneratorExit. Kemudian ada Exception class, yaitu class dasar untuk StopIterationStandardError dan Warning. Semua error standard diturunkan dari StandardError.

Ketika kamu menaikkan sebuah exception atau beberapa fungsi yang kamu panggil menaikkan sebuah exception, alur code normal itu mulai menyebarkan kumpulan panggilan hingga itu menghadapi exception handler yang pantas. Jika tidak ada exception handler yang tersedia untuk menanganinya, proses (atau lebih akuratnya thread terkini) akan dimatikan dengan sebuah pesan exception yang tidak dapat ditangani.

Menaikkan Exceptions

Melakukan raise pada exception sangat mudah. Kamu hanya perlu menggunakan kata kunci raise untuk menaikkan sebuah obyek yaitu sebuah sub-class dari class Exception. Itu dapat berapa sebuah contoh Exception itu sendiri, satu dari exception standar (misalnya RuntimeError), atau sebuah subclass dari Exception yang kamu turunkan sendiri. Berikut snippet kecil yang mendemonstrasikan semua kasus:

1
# Raise an instance of the Exception class itself

2
3
raise Exception('Ummm... something is wrong')
4
5
6
7
# Raise an instance of the RuntimeError class

8
9
raise RuntimeError('Ummm... something is wrong')
10
11
12
13
# Raise a custom subclass of Exception that keeps the timestamp the exception was created

14
15
from datetime import datetime
16
17
18
19
class SuperError(Exception):
20
21
    def __init__(self, message):
22
23
        Exception.__init__(message)
24
25
        self.when = datetime.now()
26
27
28
29
30
31
raise SuperError('Ummm... something is wrong')

Menangkap Exceptions

Kamu menangkap exception dengan klausul except, seperti yang kamu lihat di dalam contoh. Ketika kamu menangkap sebuah exception, kamu memiliki tiga pilihan:

  • Menelan itu secara diam (menangani itu dan tetap berjalan).
  • Melakukan sesuatu seperti logging, namun menaikkan kembali exception yang sama untuk mengijinkan penanganan tingkat yang lebih tinggi.
  • Menaikkan exception yang berbeda alih-alih original.

Menelan Exception

Kamu hendaklah menelan exception jika kamu tahu bagaimana menanganinya dan dapat menyembuhkan secara penuh.

Sebagai contoh, jika kamu menerima sebuah file input yang mungkin dalam format berbeda (JSON, YAML), kamu mungkin mencoba mengurainya menggunakan parser yang berbeda. Jika parser JSON menaikkan sebuah exception dimana file bukan merupakan file JSON yang valid, kamu menelannya dan mencobanya dengan parser YAML. Jika parser YAML gagal juga maka kamu mengijinkan exception menyebar keluar.

1
import json
2
3
import yaml
4
5
6
7
def parse_file(filename):
8
9
    try:
10
11
        return json.load(open(filename))
12
13
    except json.JSONDecodeError
14
15
        return yaml.load(open(filename))

Perhatikan bahwa exception lainnya (misalnya file tidak ditemukan atau tidak ada ijin membaca) akan menyebar dan tidak akan ditangkap oleh klausul except spesifik. Ini adalah kebijakan yang baik dalam kasus ini dimana kamu ingin mencoba YAML melakukan parsing hanya jika JSON gagal dikarenakan permasalahan encoding JSON.

Jika kamu ingin menangani semua exception maka cukup gunakan exceptException. Sebagai contoh:

1
def print_exception_type(func, *args, **kwargs):
2
3
    try:
4
5
        return func(*args, **kwargs)
6
7
    except Exception as e:
8
9
        print type(e)

Perhatikan bahwa dengan menambahkan as e, kamu mengikat obyek exception ke nama e yang tersedia di dalam klausul except.

Menaikkan Ulang Exception Yang Sama

Untuk menaikkan ulang, cukup tambahkan raise tanpa argument di dalam handlermu. Ini membiarkanmu melakukan penanganan lokal yang sama, namun tetap membiarkan level yang lebih atas menanganinya juga. Di sini, function invoke_function() mencetak jenis exception ke console dan kemudian menaikkan ulang exception.

1
def invoke_function(func, *args, **kwargs):
2
3
    try:
4
5
        return func(*args, **kwargs)
6
7
    except Exception as e:
8
9
        print type(e)
10
11
        raise

Menaikkan Exception Yang Berbeda

Ada beberapa kasus dimana kamu akan ingin menaikkan exception yang berbeda. Terkadang kamu ingin melakukan group banyak exception level rendah yang berbeda ke dalam sebuah kategori tunggal yang ditangani secara seragam oleh code level lebih tinggi. Dalam urutan kasus, kamu perlu mengubah exception ke level pengguna dan menyediakan beberapa konteks yang spesifik pada aplikasi.

Klausul Finally

Terkadang kamu ingin memastikan beberapa code pembersih berjalan bahkan jika sebuah exception dinaikkan di suatu tempat sepanjang proses. Misalnya, kamu mungkin memiliki sebuah hubungan database yang kamu ingin tutup setelah selesai. Berikut cara yang salah untuk melakukannya:

1
def fetch_some_data():
2
3
    db = open_db_connection()
4
5
    query(db)
6
7
    close_db_Connection(db)

Jika function query() menaikkan sebuah exception maka panggilan ke close_db_connection() tidak akan pernah dieksekusi dan DB connection akan tetap terbuka. Klausul finally selalu dieksekusi setelah percobaan semua exception handler dieksekusi. Berikut cara melakukannya dengan benar:

1
def fetch_some_data():
2
3
    db = None
4
5
    try:
6
7
        db = open_db_connection()
8
9
        query(db)
10
11
    finally:
12
13
        if db is not None:
14
15
            close_db_connection(db)

Panggilan ke open_db_connection() mungkin tidak mengembalikan sebuah koneksi atau menaikkan exception itu sendiri. Di dalam kasus ini tidak perlu menutup DB connection.

Ketika menggunakan finally, kamu harus berhati-hati tidak menaikkkan exception apapun di sana karena mereka akan menutupi exception original.

Context Managers

Context managers menyediakan mekanisme lainnya untuk membungkus sumber seperti file atau DB connections dalam code pembersih yang berjalan secara otomatis bahkan ketika exception telah dinaikkan. Alih-alih blok try-finally, kamu menggunakan pernyataan with. Berikut adalah sebuah contoh dengan file:

1
def process_file(filename):
2
3
     with open(filename) as f:
4
5
        process(f.read())

Sekarang, bahkan jika process() menaikkan sebuah exception, file akan ditutup dengan benar secara langsung ketika ruang lingkup blok with keluar, terlepas apakah exception ditangani atau tidak.

Logging

Logging cukup banyak merupakan persyaratan dalam sistem tidak sepele yang berjalan lama. Itu khususnya berguna di dalam aplikasi web dimana kamu dapat memperlakukan semua exception di dalam cara umum: Cukup lakukan log pada exception dan kembalikan sebuah pesan error kepada pemanggil.

Ketika melakukan logging, itu berguna untuk melakukan log jenis exception, pesan error, dan stacktrace. Semua informasi ini tersedia via obyek sys.exc_info, namun jika kamu menggunakanmetode  logger.exception() di dalam exception handler, sistem logging Python akan mengekstrak semua informasi yang relevan untukmu.

Ini adalah pelaksanaan terbaik yang saya rekomendasikan:

1
import logging
2
3
logger = logging.getLogger()
4
5
6
7
def f():
8
9
    try:
10
11
        flaky_func()
12
13
    except Exception:
14
15
        logger.exception()
16
17
        raise

Jika kamu mengikuti pola ini maka (dengan mengasumsikan kamu mengatur logging dengan benar) tidak peduli apa yang terjadi kamu akan memiliki catatan yang cukup baik di dalam log akan apa yang salah, dan kamu akan dapat memperbaiki permasalahan itu.

Jika kamu menaikkan ulang, pastikan kamu tidak melakukan log exception yang sama berulang-ulang pada tingkatan yang berbeda. Itu adalah pemborosan, dan itu mungkin membingungkanmu dan membuatmu berpikir banyak contoh permasalahan yang sama terjadi, ketika di dalam praktik sebuah contoh tunggal dicatat ke dalam log berulang kali.

Cara paling sederhana untuk melakukan itu adalah membiarkan semua exception menyebar (kecuali jika mereka dapat ditangani secara percaya diri dan ditelan sebelumnya) dan kemudian lakukan logging dekat dengan level teratas aplikasi/sistemmu.

Sentry

Logging adalah sebuah kapabilitas. Penerapan yang paling umum adalah menggunakan file log. Namun, untuk sistem terdistribusi skala besar dengan ratusan, ribuan atau lebih server, ini tidak selalu merupakan solusi terbaik.

Untuk melacak exception sepanjang keseluruhan infrastruktur, sebuah layanan seperti sentry itu super berguna. Itu memusatkan semua laporan exception, dan dalam tambahan ke stacktrace itu menambahkan keadaan tiap stack frame (nilai variabel pada waktu exception dinaikkan). Itu juga menyediakan sebuah tampilan yang sangat bagus dengan dashboard, laporan dan cara untuk memecah pesan oleh banyak project. Itu open source, sehingga kamu dapat menjalankan servermu sendiri atau berlangganan ke versi host.

Menangani Kegagalan Sementara

Beberapa kegagalan itu sementara, khususnya ketika menangani sistem terdistribusi. Sebuah sistem yang menakutkan pada tanda pertama masalah itu tidak berguna.

Jika code-mu mengakses beberapa sistem jarak jauh yang tidak merespon, solusi tradisional adalah timeout, namun terkadang tidak semua sistem didesain dengan timeout. Timeout tidak selalu mudah untuk dikalibrasi saat kondisi berubah.

Pendekatan lainnya adalah gagal dengan cepat kemudian mencoba ulang. Manfaatnya adalah jika target merespon cepat maka kamu tidak harus menghabiskan banyak waktu dalam kondisi tidur dan dapat bereaksi secara langsung. Namun jika itu gagal, kamu dapat mencoba ulang berkali-kali hingga kamu memutuskan itu benar-benar tidak dapat diraih dan menaikkan sebuah exception. Di dalam section berikutnya, saya akan memperkenalkan sebuah decorator yang dapat melakukan itu untukmu.

Decorator Yang Berguna

Dua decorator yang dapat membantu dengan penanganan error adalah @log_error, yang melakukan log sebuah exception dan kemudian menaikkan ulang itu, dan decorator @retry, yang akan mencoba ulang memanggil sebuah function beberapa kali.

Error Logger

Berikut adalah penerapan sederhananya. Decorator mengecualikan obyek logger. Ketika itu mendekorasi sebuah function dan function dipanggil, itu akan membungkus panggilan dalam sebuah klausul try-except, dan jika ada sebuah exception itu akan melakukan log lagi dan akhirnya menaikkan ulang exception.

1
def log_error(logger)
2
3
    def decorated(f):
4
5
        @functools.wraps(f)
6
7
        def wrapped(*args, **kwargs):
8
9
            try:
10
11
                return f(*args, **kwargs)
12
13
            except Exception as e:
14
15
                if logger:
16
17
                    logger.exception(e)
18
19
                raise
20
21
        return wrapped
22
23
    return decorated

Berikut bagaimana menggunakannya:

1
import logging
2
3
logger = logging.getLogger()
4
5
6
7
@log_error(logger)
8
9
def f():
10
11
    raise Exception('I am exceptional')

Retrier

Berikut adalah penerapan yang bagus dari @retry decorator.

1
import time
2
3
import math
4
5
6
7
# Retry decorator with exponential backoff

8
9
def retry(tries, delay=3, backoff=2):
10
11
  '''Retries a function or method until it returns True.

12


13


14


15
  delay sets the initial delay in seconds, and backoff sets the factor by which

16


17
  the delay should lengthen after each failure. backoff must be greater than 1,

18


19
  or else it isn't really a backoff. tries must be at least 0, and delay

20


21
  greater than 0.'''
22
23
24
25
  if backoff <= 1:
26
27
    raise ValueError("backoff must be greater than 1")
28
29
30
31
  tries = math.floor(tries)
32
33
  if tries < 0:
34
35
    raise ValueError("tries must be 0 or greater")
36
37
38
39
  if delay <= 0:
40
41
    raise ValueError("delay must be greater than 0")
42
43
44
45
  def deco_retry(f):
46
47
    def f_retry(*args, **kwargs):
48
49
      mtries, mdelay = tries, delay # make mutable

50
51
52
53
      rv = f(*args, **kwargs) # first attempt

54
55
      while mtries > 0:
56
57
        if rv is True: # Done on success

58
59
          return True
60
61
62
63
        mtries -= 1      # consume an attempt

64
65
        time.sleep(mdelay) # wait...

66
67
        mdelay *= backoff  # make future wait longer

68
69
70
71
        rv = f(*args, **kwargs) # Try again

72
73
74
75
      return False # Ran out of tries :-(

76
77
78
79
    return f_retry # true decorator -> decorated function

80
81
  return deco_retry  # @retry(arg[, ...]) -> true decorator

Kesimpulan

Penanganan error itu krusial baik bagi pengguna dan pengembang. Python menyediakan dukungan yang baik dalam bahasa dan librari standar untuk penanganan error berbasis exception. Dengan mengikuti penerapan terbaik secara pintar, kamu dapat menaklukkan aspek yang sering diabaikan ini.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.