Skip to content

Exception and Context Manager

Sangatlah krusial untuk mempelajari caranya mendeteksi dan menangani errors. Dangat sangat bermanfaat untuk selalu meluangkan kan waktu untuk memikirkan error yang mungkin terjadi serta membuat respone atas error tersebut.

Pada bagian ini insyaAllah kita akan membahas dua hal, yaiut exception dan context manager

Exception

Ketika error terjadi saat eksekusi, error tersebut disebut dengan exception.

Umumnya, jika kita tidak membuat penanganan atas sebua exception, exception tersebut akan membuat aplikasi berhenti. Terkadang, kondisi tersebut la yang diinginkan, namun pada kasus lain, kita ingin mengehindar berhentinya aplikasi dengan menangani masalah tersebut. katakan, seperti jika terjadi error kita memberikan alert kepada user bahwa file yang ingin dibuka corrup sehingga user dapat memperbaiki atau mengupload file yang betul tanpa harus membuat aplikasi berhenti berkerja. Mari kita lihat contoh exception dibawah ini.

Code

gen = (a for a in range(1))
print(next(gen))
print(next(gen))

---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
Input In [5], in <cell line: 3>()
    1 gen = (a for a in range(1))
    2 print(next(gen))
----> 3 print(next(gen))

StopIteration: 

Code

1/0

---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Input In [6], in <cell line: 1>()
----> 1 1/0

ZeroDivisionError: division by zero

Code

_list = [1,2,3]
print (_list[3])

---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Input In [8], in <cell line: 2>()
    1 _list = [1,2,3]
----> 2 print (_list[3])

IndexError: list index out of range

Code

_dict = {'a':1,'b':2}
print(_dict['c'])

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Input In [11], in <cell line: 2>()
    1 _dict = {'a':1,'b':2}
----> 2 print(_dict['c'])

KeyError: 'c'

Code

name = "farras"
print(no_name)

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [12], in <cell line: 2>()
    1 name = "farras"
----> 2 print(no_name)

NameError: name 'no_name' is not defined

Sebagaimana yang kita lihat, python shell (saya menggunakan jupyter lab) memberikan clue yang membantu kita, dapat kita lihat pada Traceback, pada tracebackkita dapat melihat informasi mengenai error yang terjadi. Namun shellitu sendiri berjalan dengan normmal. Ini adalah special behavior yang ada pada shell python. Akan tetapi pada umumnya, sendainya menjalankan kode diatas seluruhnya pada satu file maka python akan berhenti tepat setelah exception terjadi, dengan catatan kita tidak membuat penanganan terhadap exception. Mari kita lihat contoh dibawah ini.

Code

1 + "one"
print("This line will never be reached")
Output
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [13], in <cell line: 1>()
----> 1 1 + "one"
    2 print("This line will never be reached")

TypeError: unsupported operand type(s) for +: 'int' and 'str'

Karena kita tidak membuat penanganan maka baris kode yang menampilkan kalimat "This line will nerver be reached" tidak akan pernah ditampilkan.

Raising exception

Exception yang terjadi diatas sejauh ini di keluarkan oleh Python interpreter ketika error terdeteksi. Akan tetapi, kita juga dapat mengeluarkan expcetion sendiri, dimana ketika pada kondisi tertentu pada kita mengininkan python mengeluarkan exception. Kita dapat menggunakan statement raise

Code

raise NotImplementedError("error buatan")

---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Input In [16], in <cell line: 1>()
----> 1 raise NotImplementedError("error buatan")

NotImplementedError: error buatan # (1)!
  1. Argumen pada exception class dimuat disini.

Kita dapata menggunakan tipe expcetion yang kita inginkan, namun yang terbaik adakan menggunakan tipe exception yang paling tepat menjelaskan kondisi yang menyebabkan error terjadi. Kitajuga dapat menggunakan tipe exception sendiri (yang inysaAllah akan ada dicatatan ini juga). Perhatikan pada argumen yang kita pakai pada class Exception yang kita gunakan juga di print pada bagian pesan error.

Defining your own exception

Untuk membuat class exception sendiri kita harus membuat class yang menurunkan class exception yang lain. Sebenarnya, semua exception class yang ada pada built-in bersumber pada class BaseException, namun,class ini tidak diperuntukan secara langsung diturunkan, maka dari itu baiknya menurunkan dari class Exception.

Info

Semua exception bawaan adalah turunan dari Exception. Dan jika ada exception yang tidak turunan dari Exception artinya digunakan untuk penggunaan internal oleh Python interpreter.

Code

class MyOwnException(Exception):
    """My Own exception class"""
    pass

raise MyOwnException("Ini class exception buatan")

---------------------------------------------------------------------------
MyOwnException                            Traceback (most recent call last)
Input In [25], in <cell line: 5>()
    2     """My Own exception class"""
    3     pass
----> 5 raise MyOwnException("Ini class exception buatan")

MyOwnException: Ini class exception buatan

Traceback

Traceback yang ditampilkan oleh python sangat berguna untuk memahami apa yang terjadi dan yang menjadi penyebab terjadinya exception. Mari kita lihat traceback dan apa yang dapat kita gali dari informasi yang disediakan.

Code

def avoid_nol(n):
    if n != 0:
        return n
    raise Exception("Nila dilarang nol yah")

def calcs_all(a,b,c):
    return (avoid_nol(a)*2, avoid_nol(b) / 2, avoid_nol(c) ** 2)

print(calcs_all(1,0,3))
Output
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
Input In [31], in <cell line: 9>()
    6 def calcs_all(a,b,c):
    7     return (avoid_nol(a)*2, avoid_nol(b) / 2, avoid_nol(c) ** 2)
----> 9 print(calcs_all(1,0,3))

Input In [31], in calcs_all(a, b, c)
    6 def calcs_all(a,b,c):
----> 7     return (avoid_nol(a)*2, avoid_nol(b) / 2, avoid_nol(c) ** 2)

Input In [31], in avoid_nol(n)
    2 if n != 0:
    3     return n
----> 4 raise Exception("Nila dilarang nol yah")

Exception: Nila dilarang nol yah

Dari traceback diatas kita dapat ketahui, kesalahan terjadi pertama kali pada baris 9 yang terus mendalam hingga baris ke 4 dimana kita membuat sebuah raise exception jika nilai yang diberikan adalah 0.

Handling exception

Untuk menangani exception di python kita menggunakan try statement.

try statement terdiri dari try clause adalah pembuka statment yang diikut dengan satu atau lebih except cluase dan opsional diikut dengan else clause yang akan dijalankan jika try cluase berhasil berjalan tanpa adanya expcetion yang dikelarkan.

Setelah excep dan else cluase, kita juga bisa membuat finally clause (opsional) yang mana kode yang ada pada cluase ini akan dieksekusi dengan tidak memperhatikan apa yang terjadipada try statement.

Tidak diharuskan menggunakan try diikut dengan except. Kita juga dapat hanya menggunakan cluase try dan finally. Cara ini berguna jika kita ingin exception ditangani ditempat lain namun kita ingin meng-cleanup membersihkan kode yang harus dieksekusi tidak melihat apakah exception terjadi atau tidak.

Urutan cluase sangat penting, harus berurutam

  1. try
  2. except
  3. else
  4. finally

Dan juga harus diingat, try harus diikut dengan setidaknya satu buah except atau satu buah finally. Let us see an example:

Code

Function
def try_syn(bilangan, pembagi):
    try:
        print(f"Ini di badan try: {bilangan} / {pembagi}")
        result = bilangan / pembagi
    except ZeroDivisionError as ex:
        print (ex)
    else:
        print(f"Hasilnya adalah {result}")
        return result
    finally:
        print("Ini badan final")

hasil  =try_syn(2,4)
print(hasil)
Output
Ini di badan try: 2 / 4 # try clause
Hasilnya adalah 0.5 # else cluase
Ini badan final # finally clause
0.5

hasil  =try_syn(2,0)
print(hasil)
Output
Ini di badan try: 2 / 0 # try clause
division by zero # else clause
Ini badan final # finally clause
None

Kita juga dapat membuat lebih dari satu buah exception.

Code

args = (1,"a")
try:
    hasil = divmod(*args)
    print(hasil)
except (ZeroDivisionError, TypeError) as ex:
    print (f"Class : {ex.__class__.__name__}")
    print(f"Error message : {ex}")
Output
Class : TypeError
Error message : unsupported operand type(s) for divmod(): 'int' and 'str'   

args = (1,0)
try:
    hasil = divmod(*args)
    print(hasil)
except (ZeroDivisionError, TypeError) as ex:
    print (f"Class : {ex.__class__.__name__}")
    print(f"Error message : {ex}")
Output
Class : ZeroDivisionError
Error message : integer division or modulo by zero

Dan kita juga dapat menangani exception yang berbeda dengan penanganana yang berbeda

Code

args = (1,"a")
try:
    hasil = divmod(*args)
    print(hasil)
except ZeroDivisionError as zde:
    print (f"Pembagi tidak boleh {args[1]}")
except TypeError as te:
    print (te)
Output
unsupported operand type(s) for divmod(): 'int' and 'str'

args = (1,0)
try:
    hasil = divmod(*args)
    print(hasil)
except ZeroDivisionError as zde:
    print (f"Pembagi tidak boleh {args[1]}")
except TypeError as te:
    print (te)
Output
Pembagi tidak boleh 0

Yang perlu diingat bahwa, exception akan ditangani dari block pertama yang cocok dengan exception class atau dari base classes nya. Dengan demikian, saat kita membuat multiple except clause sebagaimana contoh diatas, pastikan kita menggunakan class exception yang paling speisifk pada block yang paling atas dan semakin kebawah semakin generic. Pada pola OOP, children on top, grandparent at the bottom. Ingat, hanya satu buah expcetion yang akan dikeluarkan pada satubuah try clause.

Dibawah ini adalah hal yang kita hindari ketika membuat mutiple exception, dimana exception generic meniban exception class yang lebih spesifik

Code

args = (1,0)

try:
    hasil = divmod(*args)
    print(hasil)
except Exception as ex:
    print ("This is the generic one") # Meniban Zero Division Error
except ZeroDivisionError as zde:
    print (f"Pembagi tidak boleh {args[1]}")
except TypeError as te:
    print (te)
Output
This is the generic one

Kita juga dapat mengeluarkan exception dari dalam except cluase. Contoh, kita ingin meniban built-in exception (atau dari third-party libaray) dengan costume exception kita. Ini adalah teknik yang sering ketika menulis sebuah pustaka (library) yang membantu penggunakan untuk mendapatkan informasi yang lebih detail jika terjadi sebuah error. Contohnya

Code

class NotFoundError(Exception):
    pass

_dict = {'a':1,'b':2}

try:
    _dict['c']
except KeyError as ke:
    error = f'Dictionary {ke.args[0]} tidak tersedia' # (1)!
    raise NotFoundError(error)
  1. Mengambil argumen daru KeyError
Output
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Input In [106], in <cell line: 6>()
    6 try:
----> 7     _dict['c']
    8 except KeyError as ke:

KeyError: 'c'

During handling of the above exception, another exception occurred:

NotFoundError                             Traceback (most recent call last)
Input In [106], in <cell line: 6>()
    8 except KeyError as ke:
    9     error = f'Dictionary {ke.args[0]} tidak tersedia'
---> 10     raise NotFoundError(error)

NotFoundError: Dictionary c tidak tersedia

Asalnya, Python mengasumsikan exception yang terjadi didalam except cluase adalah error yang tidak terduga dan python membatu kita dengan mengelarkan tracebacks untuk keda exception tersebut. Kita juga dapat memberitahu interpreter bahwa kita dengan sengaja mengeluarkan exception baru menggunakan raise from statement.

Code

try:
    _dict['c']
except KeyError as ke:
    error = f'Dictionary {ke.args[0]} tidak tersedia'
    raise NotFoundError(error) from ke
Output
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Input In [107], in <cell line: 6>()
    6 try:
----> 7     _dict['c']
    8 except KeyError as ke:

KeyError: 'c'

The above exception was the direct cause of the following exception:

NotFoundError                             Traceback (most recent call last)
Input In [107], in <cell line: 6>()
    8 except KeyError as ke:
    9     error = f'Dictionary {ke.args[0]} tidak tersedia'
---> 10     raise NotFoundError(error) from ke

NotFoundError: Dictionary c tidak tersedia

Pesan errornya berubah, namun kita masih mendapatkan kedua tracebacks, yang mana sebenernya sangat dibutuhkan saat debugging. Jika kita ingin benar menekan-kan pada exception aslinya, kita harus menggunakan None dari pada menggunakan from e.

Code

try:
    _dict['c']
except KeyError as ke:
    error = f'Dictionary {ke.args[0]} tidak tersedia'
    raise NotFoundError(error) from None
Output
---------------------------------------------------------------------------
NotFoundError                             Traceback (most recent call last)
Input In [109], in <cell line: 1>()
    3 except KeyError as ke:
    4     error = f'Dictionary {ke.args[0]} tidak tersedia'
----> 5     raise NotFoundError(error) from None

NotFoundError: Dictionary c tidak tersedia

Membuat programm menggunakan exception sangat tricky. Anda bisa secara tidak sengaja menyembunyikan bug dengan cara penggunaan exception yang buruk yang mana seharunsya memberitahukita bahwa ada bugs tersebut, bukan malah menyembunyikannya. Gunakan exception dengan baik, ikut araha ini inysaAllah membuat kode program kita menjadi lebih baik.

  • Buatlah try clause se simple mungkin. Clause tersebut harus berisi hanya kode yang hanya dapat menimbulkan expcetion(s) yang ingin kita tangani.

  • Buatlah sebuah except clause sespesifik mungkin. Mungkin akan membuat menulis banyak kode saat menulis excepton, namun akan mempermudah anda nantinya jika terjadi error yang tidak terduga.

  • Gunakan tests untuk memastika error ditanganin dengan baik, error yang diduga dan tidak diduga. InsyaAllah dibagian selanjutnya ada catatan tentang Testing.

Context managers

Ketika kita menggunakan external resources atau global state, sering kali kita harus melakukan pembersihan (clean satate) yaitu membuang semua resource atau mengembalikan ke kondisi semula (restoring the original state) setelah selesai dibutuhkan. Jika proses restoring tidak dilakukan dengan baik akan menimbulkan bugs. Makadari itu, kita harus memastikan cleanup code dieksekusi walaupun terjadi sebuah exception. Kita dapat memanfaatkan try/finnaly statement, namun menggunakan statement tersebut tidak selalu tepat jika berhadapan dengan kasus ini, karena kita harus menulis statement tersebut berulang-ulang jika berhadapat dengan jenis resource yang perlu untuk di clean up. Disinilah context manager hadir sebagai solusi, yaitu dengan membuat execution context kita dapat menggunakan resource atau memodifikasi state dan secara otomatis akan melakukan cleanup ketika kita keluar dari context tersebut, bahkan jika exception muncul.

Contoh dari global state yang mungkin kita butuhkan hanya sesaat adalah presisi dari perhitungan desimal. Katakan kita ingin melakukan perhitungan untuk spesifik presisi pada beberapa perhitungan tertentu dan mengambalikan ke default setelahnya.

Code

from decimal import getcontext, setcontext, Decimal, Context

default_context = getcontext()
costume_context = Context(prec=3)

bil_1 = Decimal(10)
bil_2 = Decimal(3)

# Value of Default state
print(bil_1/bil_2)

# Set Costume state
setcontext(costume_context)

# value of costume state
print(bil_1/bil_2)

# Cleanup
setcontext(default_context) # (1)!
  1. Cleanup context disini
    Output
    3.333333333333333333333333333
    3.33
    

dari kode diatas kita menyimpan state bawaan pada variabel default_context dan kita membuat state dengan presisi 3 pada variable costume_context. Dapat kita lihat hasil dari dua kali pembagian pada kondisi state original dan costume diatas mengeluarkan hasil yang berbeda tergantung state tertentu. Dan pada akhir baris kode kita mengembalikan ke presisi bawaan dari python. Akan tetapi, jika terjadi exception sebelum sampai baris #python setcontext(default_context) maka dapat menimbulkan komputasi yang tidak sesuai yang kita harapkan. Jika demikian kita harus menggunakan try/finally statement.

Code

try:
    print(bil_1/bil_2) # Value of Default state
    setcontext(costume_context) # Set Costume state
    print(bil_1/bil_2) # value of costume state
finally:
    setcontext(default_context)

kode diatas jauh lebih aman dari bugs tak terduga. Kode diatas akan me-restore original context tidak melihat apapun yang terjadi pada try block statement. Namun ada cara yang lebih baik, yaitu menggunakan context managers. Module decimal menyediakan local context manager yang mana menagani pengaturan dan pengembalian (restoring) context.

Code

from decimal import localcontext,Context,Decimal

with localcontext(ctx=Context(prec=3)):
    print(bil_1/bil_2)
print(bil_1/bil_2)

with statement digunakan untuk masuk kedalam runtime context yang didefinisikan oleh context manager. Setelah kode keluar dari with statement, operasi cleanup yang terdefinisi oleh context manager secara otomatis akan dieksekusi.

Dan sangat memungkinkan mengkombinasi beberapa context manager dalam satu buah with statement. Cara tersebut sangat berguna dimana anda harus menggunakan dua resource pada waktu yang bersamaan.

Code

with localcontext(Context(prec=5)), open("out.txt", "w") as out_f:
    out_f.write(f"{one} / {three} = {one / three}\n")

Kode diatas, kita masuk ke local context dan membuka sebuah file (berlaku sebagai context manager) dalam sebuah with statement. Kita melakukan kalkulasi dan menulis hasinya ke sebuah file. Ketika selai melakukan operasi, file otomatis ditutup dan default decimal context dikembalikan.

Selain decimal contexts dan file, banyak objek di python (standard) dapat menggunakan context managers. Contohnya: * Socket objects, yang mana mengimplementasi low-level networking interface, dapat digunakan sebagai context manager yang secara otomatis menutup networking connections. * Lock classes digunakan untuk synkronasi pada concurrent programming menggunakan context manager protocol agar otomatis menghapus locks.

Class-based context managers

COntext managers berkerja melalui dua magic method, __enter__ dan __exit__. Fungsi __enter__() akan dipanggil tepat sebelum masuk ke bada dari with statement sedangkan __exit__() akan dipanggil tepat kode keluar dari badan with statement. Dengan begitu kita dapat membuat costume context manager dengan mengimplementasi kedua method tersebut.

Code

class MyContextManager:
    def __init__(self):
        print(f'INITIALIZE : {self.__class__.__name__} {id(self)}')

    def __enter__(self):
        print('Display before enter with body')
        return self

    def __exit__(self,exc_type,exc_value,exc_tb):
        if exc_type != None:
            print(f'Exit context')
            print(f'{exc_type=} {exc_value=} {exc_tb=}')
            print('Exiting with context')
            return False
        return True

cm = MyContextManager()

with cm as cm:
    print('Inside with')
    print(f'hasil return method __enter__ : {cm}') # Diambil dari return value pada method __start__()
    raise(Exception('Exception inside with'))
    print('This line will never reached')

print('Outside with')
Output
INITIALIZE : MyContextManager 139990031845600
Display before enter with body
Inside with
hasil return method __enter__ : <__main__.MyContextManager object at 0x7f51f81ec8e0>
Exit context
exc_type=<class 'Exception'> exc_value=Exception('Exception inside with') exc_tb=<traceback object at 0x7f51e8f64a00>
Exiting with context

Kode diatas kita membuat sebuah class MyContextManager yangmana untuk tujuan membuat context manager sendiri. Class tersebut mengimplementasi 3 sepcial method buah;

  1. __init__, method yang digunakan untuk membuat sebuah instance dari class. pada method ini menampilkan nama class dan id dari class tersebut.

  2. __start__, method harus diimplementasi untuk membuat sebuah context manager. Method ini dapat mengambilikan nilai apapun, pada kode diatas kita mengambalikan statement self. Nilai kembalian ini dapat diakses melalui instance didalam badan dari with statement.

  3. __exit__, method ini pun harus diimplementasi, pada method ini memliki 3 buah parameter, paramater pertama adalah jenis exception (exception type) jika terjadi (jika tidak mengambalikan None), parameter kedua adalah nilai dari exception (exception value) jika terjadi (jika tidak mengambalikan None), dan parameter ketiga adalah exception traceback jika terjadi (jika tidak mengambalikan None). Method ini mengambilakn tipe data boolean True atau False. Jika method ini mengembalikan nilai True maka jika terjadi exception maka tidak akan menampilkan pesan error, sedangkan False akan menampilkan pesan error, Traceback.