# Praktik Baik dan Buruk saat Menulis Kode Python

Python adalah bahasa pemrograman multi-paradigma tingkat tinggi yang menekankan **_readability_** atau **keterbacaan**. Bahasa pemrograman ini dikembangkan, dipelihara, dan sering digunakan umumnya sesuai dengan _coding convention_ yang disebut "The Zen of Python" atau [PEP 20][].

Hi! Pada artikel ini, kita akan coba melihat beberapa contoh praktik baik dan buruk dalam menulis kode Python yang mungkin sering Anda temui.


## Gunakan _Unpacking_ dan _Chaining_ untuk Menulis Kode yang Ringkas
- ### Unpacking

_Packing_ dan _unpacking_ adalah salah satu fitur Python yang sangat berguna. Kita dapat menggunakan _unpacking_ untuk menetapkan nilai variabel kita:
```python
>>> a, b = 2, 'my-string'
>>> a
2
>>> b
'my-string'
```

Dengan fitur ini, kita pun bisa menukar nilai variabel dengan cara yang mungkin paling **ringkas** dan **elegan** di seluruh dunia pemrograman:
```python
>>> a, b = b, a
>>> a
'my-string'
>>> b
2
```

Luar biasa, bukan? _Unpacking_ juga dapat digunakan untuk mengisi nilai ke beberapa variabel sekaligus dalam kasus yang lebih kompleks. Misalnya, Anda mungkin menginisialisasi variabel seperti ini:
```python
>>> x = (1, 2, 4, 8, 16)
>>> a = x[0]
>>> b = x[1]
>>> c = x[2]
>>> d = x[3]
>>> e = x[4]
>>> a, b, c, d, e
(1, 2, 4, 8, 16)
```

Oke, mungkin ribet ya begitu? Kita akan menggunakan pendekatan yang lebih ringkas dan bisa dibilang lebih mudah dibaca:
```python
>>> a, b, c, d, e = x
>>> a, b, c, d, e
(1, 2, 4, 8, 16)
```

Keren, kan? Tapi masih bisa lebih keren lagi:
```python
>>> a, *y, e = x
>>> a, e, y
(1, 16, [2, 4, 8])
```
Intinya, variabel dengan prefix `*` 'mengumpulkan' nilai-nilai yang tidak diberikan kepada variabel lain.

- ### Chaining

Dengan Python, kita bisa menghubungkan (_chaining_) operasi perbandingan. Jadi, kita tidak perlu periksa apakah dua atau lebih perbandingan bernilai `True`:
```python
>>> x = 4
>>> x >= 2 and x <= 8
True
```

Sebagai gantinya, kita tulis kode di atas dalam bentuk yang lebih ringkas, persis seperti yang dilakukan ahli matematika:
```python
>>> 2 <= x <= 8
True
>>> 2 <= x <= 3
False
```

Python juga mendukung _assignment_ beruntun. Jadi, jika kita ingin mengisi nilai yang sama ke beberapa variabel, daripada kita melakukannya seperti ini:
```python
>>> x = 2
>>> y = 2
>>> z = 2
```

Kita gunakan _unpacking_ saja agar lebih elegan :
```python
>>> x, y, z = 2, 2, 2
```

Penggunaan _chained assignments_ juga bisa membuat pekerjaan kita lebih mudah:
```python
>>> x = y = z = 2
>>> x, y, z
(2, 2, 2)
```

Tapi hati-hati dengan cara ini! Karena semua variabel merujuk ke _instance_ yang sama, perubahan salah satu nilai variabel mengakibatkan nilai dua variabel lainnya pun ikut berubah. Oleh karena itu, cara _unpacking_ bisa jadi alternatif yang lebih aman.


## Membandingkan dengan Nol

Saat kita memiliki data numerik dan kita harus memeriksa apakah angkanya sama dengan 0 (nol), kita bisa tapi tidak harus memakai operator pembanding `==` dan `!=` seperti ini:
```python
>>> x = (1, 2, 0, 3, 0, 4)
>>> for item in x:
...     if item != 0:
...         print(item)
... 
1
2
3
4
```

Kita gunakan cara yang _pythonic_ dengan mengingat bahwa nol ditafsirkan sebagai `False`  ([_falsy value_](https://developer.mozilla.org/en-US/docs/Glossary/Falsy)) dalam konteks **_Boolean_**, sementara semua angka lainnya dianggap `True`:
```python
>>> bool(0)
False
>>> bool(-1), bool(1), bool(20), bool(28.4)
(True, True, True, True)
```

Dengan mengingat konsep ini, kita bisa gunakan `if item` alih-alih `if item != 0`:
```python
>>> for item in x:
...     if item:
...         print(item)
... 
1
2
3
4
```

Kita bisa mengikuti logika yang sama dan menggunakan `if not item` alih-alih `if item == 0`.


## Mengecek Nilai `None`

`None` adalah objek khusus dan unik dalam Python yang juga memiliki fungsi yang sama seperti `null` dalam bahasa yang mirip seperti C (C#, Javascript dkk.).

Kita bisa memeriksa apakah suatu variabel adalah `None` dengan operator perbandingan `==` dan `!=`:
```python
>>> x, y = 2, None
>>> x == None
False
>>> y == None
True
>>> x != None
True
>>> y != None
False
```

Namun, cara yang lebih _pythonic_ dan umum adalah menggunakan keyword `is` dan `not`:
```python
>>> x is None
False
>>> y is None
True
>>> x is not None
True
>>> y is not None
False
```

Selain itu, kita lebih baik menggunakan cara `is`  (`x is None`) daripada alternatifnya, `is not`, yang kurang mudah dibaca (`x is not None`).


## Iterasi pada _Sequences_ dan _Mappings_

Kita dapat menerapkan iterasi atau _looping_ dengan Python dalam beberapa cara. Python menyediakan beberapa _class_ bawaan untuk memfasilitasinya.

Di hampir semua kasus, kita bisa menggunakan `range()` untuk mendapatkan **_iterator_** yang menghasilkan _integer_:
```python
>>> x = [1, 2, 4, 8, 16]
>>> for i in range(len(x)):
...     print(x[i])
... 
1
2
4
8
16
```

Namun, ada cara yang lebih baik untuk mengiterasi sebuah _sequence_:
```python
>>> for item in x:
...     print(item)
... 
1
2
4
8
16
```

Bagaimana bila kita ingin mengiterasi _sequence_ dalam urutan terbalik? Tentu saja, kita bisa gunakan opsi `range()` lagi:
```python
>>> for i in range(len(x)-1, -1, -1):
...     print(x[i])
... 
16
8
4
2
1
```

_Eits!_ Kita balikkan saja urutan _sequence_-nya untuk meringkas kode:
```python
>>> for item in x[::-1]:
...     print(item)
... 
16
8
4
2
1
```

Supaya lebih _pythonic_, kita gunakan `reversed()` untuk mendapatkan _iterator_ yang menghasilkan item sebuah _sequence_ dalam urutan terbalik:
```python
>>> for item in reversed(x):
...     print(item)
... 
16
8
4
2
1
```

* * *
Acapkali, kita membutuhkan sebuah item suatu _sequence_ beserta indeksnya sekaligus:
```python
>>> for i in range(len(x)):
...     print(i, x[i])
... 
0 1
1 2
2 4
3 8
4 16
```

Kalau begini kasusnya, lebih baik gunakan `enumerate()` untuk mendapatkan _iterator_ yang menghasilkan **_tuple _** dengan indeks sekaligus dengan itemnya:
```python
>>> for i, item in enumerate(x):
...     print(i, item)
... 
0 1
1 2
2 4
3 8
4 16
```

Wah, keren ya? Tapi bagaimana jika kita ingin mengiterasi dua atau lebih _sequence_? Tentu saja, kita andalkan `range()` lagi:
```python
>>> y = 'abcde'
>>> for i in range(len(x)):
...     print(x[i], y[i])
... 
1 a
2 b
4 c
8 d
16 e
```

Untuk kasus seperti ini, Python juga menawarkan solusi yang lebih baik. Kita dapat menerapkan `zip()` dan mendapatkan _tuple_ yang berisi item sesuai dengan urutan masing-masing _sequence_ yang diiterasi:
```python
>>> for item in zip(x, y):
...     print(item)
... 
(1, 'a')
(2, 'b')
(4, 'c')
(8, 'd')
(16, 'e')
```

Bahkan, kita bisa mengombinasikannya dengan _unpacking_:
```python
>>> for x_item, y_item in zip(x, y):
...     print(x_item, y_item)
... 
1 a
2 b
4 c
8 d
16 e
```

Harap diingat bahwa `range()` bisa sangat berguna. Namun, ada kasus (seperti yang ditunjukkan di atas) di mana ada alternatif yang lebih mudah. Contohnya, iterasi terhadap **_dictionary _** menghasilkan _key_-nya:
```python
>>> z = {'a': 0, 'b': 1}
>>> for k in z:
... print(k, z[k])
... 
a 0
b 1
```

Daripada begitu, kita pakai saja _method_ `.items()` untuk mendapatkan _tuple_ dengan _key_ dan _value_ sekaligus:
```python
>>> for k, v in z.items():
...     print(k, v)
... 
a 0
b 1
```

Kita bisa juga menggunakan _method_ `.keys()` dan `.values()` untuk mengiterasi masing-masing _key_ dan _value_ pada sebuah _dictionary_.


## Hindari Penggunaan Objek _Mutable_ pada Argumen Opsional

Sistem Python sangatlah fleksibel dalam hal memberikan argumen pada fungsi dan _method_. Argumen opsional merupakan bagian dari kelebihan ini. Tapi hati-hati! Kita seharusnya tidak menggunakan argumen opsional yang bersifat _mutable_. Perhatikan contoh berikut:
```python
>>> def f(value, seq=[]):
...     seq.append(value)
...     return seq
```

Di sini kita bisa lihat, sepertinya jika kita tidak mengisi `seq`, `f()` akan menambahkan `value` ke **_list_** kosong dan mengembalikan sesuatu seperti `[value]`:
```python
>>> f(value=2)
[2]
```

Tampaknya tidak ada masalah, 'kan? Tentu saja ada! Perhatikan contoh berikut:
```python
>>> f(4)
[2, 4]
>>> f(8)
[2, 4, 8]
>>> f(16)
[2, 4, 8, 16]
```

Terkejut? Bingung? Jika iya, Anda bukan satu-satunya. Ini terjadi karena **_instance _** yang sama dari argumen opsional (_list_ `seq[]` dalam kasus ini) dirujuk setiap kali fungsi di atas dipanggil. Oke, mungkin tujuan Anda memang seperti apa yang dilakukan kode di atas. Namun, umumnya Anda harus menghindarinya. Kita dapat menghindarinya dengan beberapa logika tambahan. Salah satu caranya adalah seperti ini:
```python
>>> def f(value, seq=None):
...     if seq is None:
...         seq = []
...     seq.append(value)
...     return seq
```

Versi yang lebih pendeknya bisa dibuat dengan mengganti bagian `if seq is None` dengan `if not seq`. Sekarang, kita mendapatkan perilaku yang berbeda:
```python
>>> f(value=2)
[2]
>>> f(value=4)
[4]
>>> f(value=8)
[8]
>>> f(value=16)
[16]
```

Dalam kebanyakan kasus, beginilah yang kita diinginkan.


## Hindari _Getter_ dan _Setter_ Klasik

Python membolehkan kita mendefinisikan _method_ _getter_ dan _setter_ sama seperti C++ dan Java:
```python
>>> class C:
...     def get_x(self):
...         return self.__x
...     
...     def set_x(self, value):
...         self.__x = value
```

Begini cara kita menggunakannya untuk mendapatkan dan mengatur _state_ suatu objek:
```python
>>> c = C()
>>> c.set_x(2)
>>> c.get_x()
2
```

Pada kasus tertentu, inilah cara terbaik untuk menyelesaikan masalah. Namun, agar lebih elegan kita akan mendefinisikannya dengan menggunakan `property`, terutama dalam kasus sederhana:
```python
...     @property
...     def x(self):
...         return self.__x
... 
...     @x.setter
...     def x(self, value):
...         self.__x = value
```

Properti dianggap lebih _pythonic_ daripada _getter_ dan _setter_ klasik. Kita bisa menggunakannya dengan cara yang sama seperti di C#, yaitu seperti atribut data biasa:
```python
>>> c = C()
>>> c.x = 2
>>> c.x
2
```

Jadi pada umumnya, penggunaan properti merupakan praktik yang baik. Gunakan _getter_ dan _setter_ seperti C++ saat memang diperlukan.


## Hindari Mengakses _Protected Class Members_

Python tidak memiliki _class member_ privat. Namun, ada _coding convention_ yang menyatakan bahwa kita tidak boleh mengakses atau memodifikasi _member_ yang dimulai dengan garis bawah (_) di luar _instance_ mereka. Tidak ada jaminan bahwa mereka akan mempertahankan perilaku yang ada.

Misalnya, perhatikan kode berikut:
```python
>>> class C:
...     def __init__(self, *args):
...         self.x, self._y, self.__z = args
... 
>>> c = C(1, 2, 4)
```

_Instance_ kelas `C` ini memiliki tiga _member_ data: `.x`, `._y`, dan `._C__z`. Kenapa malah ada `._C__z`? Ke mana perginya `.__z`? Jika nama _member_ dimulai dengan _double underscore_ (_dunder_), namanya malah jadi kacau, alias dirombak otomatis oleh python. Itulah sebabnya kita punya `._C__z` alih-alih `.__z`. OK sekarang kita bisa mengakses atau merubah `.x` secara langsung:
```python
>>> c.x  # OK
1
```

Kita juga bisa mengakses atau memodifikasi `._y` dari luar _instance_-nya, tapi ini dianggap sebagai praktik yang buruk:
```python
>>> c._y  # Bisa, tapi ini praktik buruk!
2
```

Kita tidak bisa lagi mengakses `.__z` karena namanya sudah dirubah Python jadi `._C__z`. Jadi kita bisa mengakses atau memodifikasi `._C__z`:
```python
>>> c.__z  # Error!
Traceback (most recent call last):
File "", line 1, in 
AttributeError: 'C' object has no attribute '__z'
>>> c._C__z  # Bisa, tapi begini malah lebih buruk!
4
```

Anda harus benar-benar menghindari ini. Penulis _class_ ini kemungkinan besar memulai nama atribut dengan garis bawah untuk memberi tahu Anda, "**jangan digunakan!**".


## Bebaskan Sumber Daya Memori Menggunakan _Context Managers_

Kadang kala kita perlu menulis kode untuk mengelola sumber daya dengan tepat. Kasus ini sering terjadi saat bekerja dengan file, koneksi database, atau entitas lain dengan sumber daya yang tak terkelola. Misalnya, kita membuka file dan memprosesnya:
```python
>>> my_file = open('filename.csv', 'w')
>>> # do something with `my_file`
```

Untuk mengelola memori dengan semestinya, kita harus menutup file ini terlebih dahulu setelah menyelesaikan pekerjaan sebelum melanjutkan ke proses berikutnya:
```python
>>> my_file = open('filename.csv', 'w')
>>> # do something with `my_file` and..
>>> my_file.close()
```

Baiklah, cara ini lebih baik daripada tidak melakukannya sama sekali. Namun, bagaimana jika sebuah **_exception_** terjadi saat memproses file ini sehingga `my_file.close()` tidak pernah dieksekusi? Kita dapat mencegah hal ini dengan sintaks **_exception handling_** atau dengan **_context managers_**. Cara kedua berarti kita memasukkan kode ke dalam blok `with`:
```python
>>> with open('filename.csv', 'w') as my_file:
...     # do something with `my_file`
```

Dengan menggunakan blok `with`, _method_ spesial `.enter()` dan `.exit()` akan dipanggil, bahkan ketika terjadi _exception_. Metode ini dirancang untuk menjaga sumber daya memori. Kita pun dapat mengembangkan sistem yang kokoh dengan mengombinasikan _context managers_ dan _exception handling_.


## Saran Gaya Penulisan

Kode Python harus elegan, ringkas, dan mudah dibaca. Intinya harus **indah**.

Referensi utama tentang cara menulis kode Python yang indah adalah _Style Guide for Python Code_ atau [PEP 8][]. Anda tentu harus membacanya jika ingin menulis kode atau _ngoding_ Python.


## Akhir Kata

Artikel ini memberikan beberapa saran tentang cara menulis kode yang lebih efisien, lebih mudah dibaca, dan lebih ringkas. Singkatnya, menunjukkan cara menulis kode yang _Pythonic_. Sebagai referensi, [PEP 8][] menyediakan panduan gaya untuk kode Python, dan [PEP 20][] menerangkan prinsip-prinsip bahasa Python.

Nikmati menulis kode yang **_Pythonic_**, bermanfaat, dan indah!

Terima kasih telah membaca.

[PEP 8]: https://www.python.org/dev/peps/pep-0008/
[PEP 20]: https://pep20.org/
