Perbaiki kode python-mu dan berhenti jadi jamet

Published

December 3, 2021

Warning: maybe heavily opinionated

Menurut saya python adalah bahasa yang relatif elegan. Kita bisa menyelesaikan banyak hal dengan sedikit baris kode saja. Sintaksnya jelas dan cenderung mirip bahasa inggris. Dia sangat ramah digunakan untuk proyek individu maupun dalam tim.

Sayang sekali banyak saya temui kode jamet jele, walaupun sudah ada panduan penulisan standar untuk kode python. Beberapa di antaranya pernah saya temukan juga di suatu proyek (yang ngakunya) profesional. No mention 🙅🏻‍♂️.

Saya coba tunjukkan kejametan kode python seperti contoh yang saya buat (dengan susah payah keringat darah) berikut ini:

def Concat(S1, S2):
    return S1+S2

class Manipulate:
    def remove_number(self, s):
        result = ""
        for i in range(len(s)):
           if not s[i].isnumeric():
               result += s[i]
        return result
    def removeWhiteSpace(self, s):
        result = ""
        for i in range(len(s)):
            if not s[i].isspace():
                result += s[i]
        return result

someText = Concat("ABC 123", "DEF 456")
manipulate = Manipulate()
print("Original input        : ", someText)
print("Input without numbers : ", manipulate.remove_number(someText))
print("Input without spaces  : ", manipulate.removeWhiteSpace(someText))

Intinya, ada satu fungsi untuk menggabung 2 string, dan satu kelas berisi method untuk memanipulasi string. Output memang seperti yang diharapkan:

$ python jamet.py
Original input        :  ABC 123DEF 456
Input without numbers :  ABC DEF
Input without spaces  :  ABC123DEF456

Sekarang mari kita coba preteli jelek-jeleknya.

Blank lines

Kode di atas membuat mata jadi sepet karena kurang memanfaatkan garis blank lines (garis kosong). PEP-8 menyarankan penggunaan blank lines, minimal sebagai berikut:

  • Gunakan dua baris blank lines untuk memisahkan:
    • Dua top-level function
    • Dua kelas
    • Top-level function dengan kelas
  • Gunakan satu baris blank lines untuk memisahkan method dalam kelas.

Penamaan

Pertama, kelas Manipulate adalah contoh buruk dari penamaan kelas. Kelas seharusnya menggunakan frasa kata benda (noun phrase), karena kelas merepresentasikan suatu entitas. Dalam hal ini, Manipulate seharusnya diubah menjadi, misalnya, Manipulator karena tujuannya adalah melakukan manipulasi string. Bahkan, lebih baik lagi menjadi StringManipulator. Aturan ini berlaku tidak hanya untuk python saja.

Untuk urusan gaya penamaan, poin-poin berikut ini bisa menjadi acuan:

  • Nama kelas menggunakan CapsWord.
  • Nama fungsi, method, dan variabel (termasuk parameter fungsi) menggunakan snake_case.
    • Fungsi Concat seharusnya concat, S1 dan S2 harusnya s1 dan s2.
    • Method removeWhiteSpace seharusnya remove_white_space.
    • Variabel someText seharusnya some_text.

Trivia: sebetulnya kita bisa saja mengganti parameter self tiap-tiap method dengan nama lain. Namun, marilah menjaga perdamaian dunia dengan tetap menggunakan nama self seperti coder python waras lainnya.

Bro… Move on dari C-style looping

Perhatikan potongan kode ini:

...
    
    def remove_number(self, s):
        result = ""
        for i in range(len(s)):
           if not s[i].isnumeric():
               result += s[i]
        return result

...

Tidak ada error. Kode melakukan apa yang diharapkan: iterasi tiap karakter di s dengan bantuan indeks i dalam range 0 hingga len(s) (eksklusif). Tambahkan s[i] ke variabel result kalau si karakter bukan numerik.

Di python, kita diberi kemudahan dengan diizinkan iterasi string, list, atau iterable lainnya, lansung (ngomong-ngomong, saya ganti nama parameter s menjadi text biar lebih jelas):

...
    
    def remove_number(self, text):
        result = ""
        for c in text:
           if not c.isnumeric():
               result += c
        return result
        
...

Bagaimana kalau kita butuh mengakses indeks-nya? Gampang. Pakai enumerate:

    for i, c in enumerate(text):
        print(f"Character {c} at index {i}")

Sebagai bonus, python memberikan kita fungsionalitas lebih, sehingga kita bisa menulis kode yang lebih ✨pythonic✨:

...
    
    def remove_number(self, text):
        result = "".join(c for c in text if not c.isnumeric())
        return result
        
...

Sampai di sini, setelah kode di atas dipermak, kita akan mendapatkan kode yang lebih bersih seperti dibawah ini. Pro-tip: letakkanlah driver code di dalam blok statement if __name__ == "__main__": agar output hanya keluar saat file ini langsung dijalankan di terminal. Jika tidak, output akan keluar juga saat file ini di-import di file python lainnya.

def concat(s1, s2):
    return s1 + s2


class StringManipulator:
    def remove_number(self, text):
        result = "".join(c for c in text if not c.isnumeric())
        return result

    def remove_white_space(self, text):
        result = "".join(c for c in text if not c.isspace())
        return result


if __name__ == "__main__":
    some_text = concat("ABC 123", "DEF 456")
    manipulator = StringManipulator()
    
    print("Original input        : ", some_text)
    print("Input without numbers : ", manipulator.remove_number(some_text))
    print("Input without spaces  : ", manipulator.remove_white_space(some_text))

Step-up the game with type annotation

Python, seperti yang kita tahu, adalah bahasa yang menganut dynamic typing. Artinya, kita tidak perlu menyertakan tipe data untuk tiap variabel yang kita buat. Namun, python mendukung penggunaan type annotation agar kita bisa menyertakan tipe data. Saya sangat menyarankan type annotation ini, walaupun pada akhirnya interpreter mengabaikan.

Type annotation membuat kode semakin eksplisit. Ini akan sangat membantu jika kita bekerja dalam tim. Rekan kita tidak perlu banyak waktu untuk mengerti tujuan suatu fungsi karena bisa mengira-ngira dari tipe data nama fungsi, tipe data parameter, dan tipe data return value dari fungsi.

Perhatikan fungsi di bawah ini:

from typing import List

def print_self_introduction(
    first_name: str, last_name: str, age: int, hobbies: List[str]
) -> None:
    print(f"Hi, there! My name is {first_name} {last_name}.")
    print(f"I am {age} years old.")
    
    # Make a copy of the list to prevent altering original list. 
    # Later, the last item of this list will be prepended with 
    # "and" to print hobbies more naturally.
    formatted_hobbies = hobbies.copy()
    if len(formatted_hobbies) > 2: 
        formatted_hobbies[-1] = f"and {formatted_hobbies[-1]}"
        joint_hobbies = ", ".join(formatted_hobbies)
    else:
        joint_hobbies = " ".join(formatted_hobbies)

    print(f"I like {joint_hobbies}.")

Dari namanya, cukup jelas kira-kira apa yang akan dilakukan: mencetak informasi perkenalan diri berdasarkan parameter first_name, last_name, age, dan hobbies. Rekan tim lain tidak akan mikir lagi harus masukkan nilai apa dengan tipe data apa untu masing-masing parameternya. Untuk first_name dan last_name? String. Untuk age? Integer. Untuk hobbies? List of string!

Kita juga bisa langsung tahu bahwa fungsi ini tidak memiliki return value, karena sudah tertera def print_self_introduction(...) -> None:. Kalau misal tertera def get_self_introduction(...) -> str:, kita bisa langsung menebak bahwa fungsi ini akan mengembalikan nilai berupa string.

Tidak akan lagi ada pertanyaan, “Eh ngab… Parameter hobbies ini perlu kumasukkan apa, ya? String? String yang pake delimiter koma? Name file teks berisi daftar hobi? Atau apa?” Error-pun akan diminimalisir.

Note: saya tidak akan membahas detail type annotation di sini

Gunakan formatter

Saya biasanya menggunakan black untuk melakukan formatting dan isort untuk merapikan bagian import. Selain agar nyaman dipandang, format kode akan menjadi konsisten di setiap bagian proyek. Fix, no debat.

Black

Black bisa membuat kode seperti ini

def very_important_function(template: str, *variables, file: os.PathLike, engine: str, header: bool = True, debug: bool = False):
    """Applies `variables` to the `template` and writes to `file`."""
    with open(file, 'w') as f:
        ...

menjadi seperti ini

def very_important_function(
    template: str,
    *variables,
    file: os.PathLike,
    engine: str,
    header: bool = True,
    debug: bool = False,
):
    """Applies `variables` to the `template` and writes to `file`."""
    with open(file, "w") as f:
        ...

Atau merapikan call chaining yang panjang menjadi seperti ini

def example(session):
    result = (
        session.query(models.Customer.id)
        .filter(
            models.Customer.account_id == account_id,
            models.Customer.email == email_address,
        )
        .order_by(models.Customer.id.asc())
        .all()
    )

Dan seterusnya. Silakan mengacu ke dokumentasinya.

Isort

Kita bisa dengan mudah merapikan bagian import. Isort akan merapikan kode ini

from my_lib import Object

import os

from my_lib import Object3

from my_lib import Object2

import sys

from third_party import lib15, lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9, lib10, lib11, lib12, lib13, lib14

import sys

from __future__ import absolute_import

from third_party import lib3

print("Hey")
print("yo")

menjadi seperti ini

from __future__ import absolute_import

import os
import sys

from third_party import (lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8,
                         lib9, lib10, lib11, lib12, lib13, lib14, lib15)

from my_lib import Object, Object2, Object3

print("Hey")
print("yo")

Demikian. Selamat mencoba.

Tentu saja tidak ada yang menghalangimu untuk tidak mengikuti standar. Bebas. Santai. Selamat menjadi coder python yang edgy dan melawan arus.