Praktik Baik dan Buruk saat Menulis kode Python
Tags: python, beginners, tips, programming
Published: 2022-02-17 03:05:29

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

Hi, Selamat datang di siRafeq. Pada artikel ini, kita akan melihat beberapa contoh kebiasaan 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 juga 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! _Unpacking_ juga dapat digunakan untuk mengisi nilai ke beberapa variabel dalam kasus yang lebih kompleks. Misalnya, Anda bisa menginisialisasi 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 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 gak perlu meriksa 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, 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 dapat melakukannya dengan cara seperti ini:
```python
>>> x = 2
>>> y = 2
>>> z = 2
```

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

Namun, menggunakan _chained assignments_ membuat pekerjaan kita lebih mudah:
```python
>>> x = y = z = 2
>>> x, y, z
(2, 2, 2)
```

Hati-hati dengan cara ini karena semua variabel merujuk ke _instance_ yang sama! Ketika salah satu nilai variabel berubah, nilai dua variabel juga ikut berubah. Oleh karena itu, cara _unpacking_ bisa jadi alternatif yang lebih aman.

## 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 `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 not`  (`x is not None`) daripada alternatifnya, `not`, yang kurang mudah dibaca (`x is 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 jika kita ingin mengiterasi _sequence_ dalam urutan terbalik? Tentu saja, kita 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
```

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

* * *
Acapkali, kita membutuhkan sebuah item dari _sequence_ dan 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, lebih baik menggunakan `enumerate()` untuk mendapatkan _iterator_ lain yang menghasilkan **_tuple _** dengan indeks dan 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 lebih dari dua atau lebih _sequence_? Tentu saja, kita dapat menggunakan `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_ dari item yang sesuai:
```python
>>> for item in zip(x, y):
...     print(item)
... 
(1, 'a')
(2, 'b')
(4, 'c')
(8, 'd')
(16, 'e')
```

Dan kita juga dapat 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, mengiterasi **_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_.


## 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` 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 hal 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`.


## Hindari Penggunaan Objek _Mutable_ pada Argumen Opsional

Sistem Python sangatlah fleksibel dalam 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]
```

Kayaknya gak ada masalah kan? Pasti ada! Coba lihat contoh berikut:
```python
>>> f(4)
[2, 4]
>>> f(8)
[2, 4, 8]
>>> f(16)
[2, 4, 8, 16]
```

Terkejut? Bingung? Jika ya, Anda bukan satu-satunya. Karena **_instance _** yang sama dari argumen opsional (_list_ `seq[]` dalam kasus ini) dirujuk setiap kali fungsi dipanggil. Oke, mungkin terkadang Anda maunya seperti apa yang dilakukan kode di atas. Namun, umumnya Anda harus menghindarinya. Kita dapat menghindarinya dengan beberapa logika tambahan. Salah satu caranya adalah 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 mendefinisikan dan 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, umumnya, menggunakan 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 aturan 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 ada `._C__z`, dan ke mana perginya `.__z`? Jika nama _member_ dimulai dengan _double underscore_ (_dunder_), namanya malah jadi kacau, alias termodifikasi otomatis oleh python. Itu 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 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 dapat 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, menggunakan 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 menangani 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`
```

Menggunakan blok `with` berarti _method_ spesial `.enter()` dan `.exit()` akan dipanggil, bahkan dalam jika terjadi _exception_. Metode ini seharusnya pasti menjaga sumber daya memori. Kita dapat mengolah desain yang sangat kuat 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 membuat kode atau _ngoding_ dengan Python.


## Kesimpulan

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_. Selain itu, [PEP 8][] menyediakan panduan gaya untuk kode Python, dan [PEP 20][] menerangkan prinsip-prinsip bahasa Python.

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

Terima kasih telah membaca.

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