2Python untuk AI

Advanced Python

8 jam16 min baca
Tujuan

Decorator, generator, context manager, type hints lanjutan. Skill yang membuat kamu beda dari pemula.

09 — Advanced Python

Estimasi: 8 jam Tujuan: Decorator, generator, context manager, type hints lanjutan. Skill yang membuat kamu beda dari pemula.


Kenapa Materi Ini Penting?

Materi di file ini adalah pembeda Python "junior" dan "menengah". Decorator dipakai di mana-mana di framework AI: @torch.no_grad() di PyTorch, @app.get() di FastAPI, @tool di LangChain. Generator digunakan untuk handle dataset raksasa yang tidak muat di RAM (streaming data, lazy evaluation). Context manager memastikan resource (GPU memory, file handle, DB connection) selalu ter-release walau ada error.

Type hints lanjutan adalah investasi besar saat kamu mulai bikin library atau project tim. IDE autocomplete jadi tajam, mypy bisa catch bug sebelum runtime, code lebih self-documenting. Library modern seperti Pydantic dan FastAPI membangun seluruh API mereka di atas type hints — paham ini = paham 80% library Python modern.

Analogi besar:

  • Decorator = pembungkus kado. Function asli tetap utuh, tapi "dibungkus" dengan behavior tambahan.
  • Generator = mesin penghasil yang lazy. Dia produce nilai cuma saat diminta, tidak bikin semua sekaligus.
  • Context manager = penjaga pintu otomatis. Buka saat masuk, tutup saat keluar — pasti.
  • Type hints = label di kabel listrik. Bukan paksaan, tapi membantu kamu tahu mana yang positif/negatif sebelum konek.

Peta Konsep

flowchart TD
    A[🚀 Advanced Python] --> B[🎁 Decorator]
    A --> C[🌀 Generator]
    A --> D[🚪 Context Manager]
    A --> E[🏷️ Type Hints]
    A --> F[🔒 Closures]
    A --> G[⚡ Async]

    B --> B1[Basic decorator]
    B --> B2[functools.wraps]
    B --> B3[Decorator with args]

    C --> C1[yield]
    C --> C2[Generator expression]
    C --> C3[yield from]

    D --> D1[__enter__/__exit__]
    D --> D2[@contextmanager]

    E --> E1[Optional / Union]
    E --> E2[Generic / TypeVar]
    E --> E3[Callable / Literal]

Bagian 1 — Decorator

Diagram: Decorator = Pembungkus Kado

Cara Membaca Diagram

Layer atas: function asli + decorator + wrapper baru. Layer kanan: lifecycle saat dipanggil — input → pre-process → panggil asli → post-process → output. Decorator menambah behavior sebelum dan sesudah function asli, tanpa modifikasi body asli.

Walkthrough Step-by-Step

  1. Function aslidef add(a, b): return a + b. Logic murni.
  2. @decorator — sintaks sugar untuk add = decorator(add). Bungkus function.
  3. Wrapper — function baru yang dibikin decorator. Inilah yang dipanggil saat kamu invoke add(1, 2).
  4. Input — args & kwargs masuk ke wrapper.
  5. Pre-process — wrapper jalankan sesuatu sebelum panggil asli (start timer, log call, cek auth).
  6. Call — wrapper panggil function asli func(*args, **kwargs).
  7. Post-process — wrapper jalankan sesuatu setelah (stop timer, log result, format output).
  8. Output — wrapper return result + side effects (logging dll).

Analogi Sehari-hari

Decorator = pembungkus kado dengan label nama. Kado di dalam (function asli) tetap utuh — kamu tidak ubah isi. Tapi pembungkus tambah info (label nama pengirim = log), pita (validasi keamanan = auth), atau timer kapan dibuka. Saat orang terima kado, dia interaksi dengan pembungkus dulu (wrapper), baru ke isi. Di Python framework: @torch.no_grad() matikan gradient sementara, @app.get("/") daftarkan route, @retry(3) retry function 3x kalau gagal.

Diagram statis Mermaid sebagai fallback:

flowchart LR
    F[🎁 function asli] --> W[🎀 wrapper]
    W --> O[📤 function ter-decorate]
    
    subgraph "Saat dipanggil"
        I[📥 Input] --> PRE[Pre-process]
        PRE --> CALL[Panggil function asli]
        CALL --> POST[Post-process]
        POST --> RES[📤 Result]
    end

Analogi: decorator = pembungkus kado dengan label nama. Kado di dalam (function asli) tetap sama, tapi pembungkus tambah info, decoration, atau bahkan ubah cara kado dibuka.

Decorator = function yang membungkus function lain untuk menambah behavior.

Konsep Dasar

Function adalah object di Python:

def hello():
    print("Hi")

# Function bisa disimpan di variable
f = hello
f()    # "Hi"

# Function bisa di-pass sebagai argument
def panggil_2x(func):
    func()
    func()

panggil_2x(hello)

Decorator Sederhana

def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Done {func.__name__}")
        return result
    return wrapper

# Cara pakai (manual)
def add(a, b):
    return a + b

add = log_call(add)    # bungkus
add(2, 3)
# Calling add
# Done add
# Result: 5

# Pakai @ syntax (sugar)
@log_call
def multiply(a, b):
    return a * b

multiply(3, 4)
# Calling multiply
# Done multiply

@log_call ekuivalen dengan multiply = log_call(multiply).

Decorator dengan *args, **kwargs

def timer(func):
    import time
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"{func.__name__}: {elapsed:.4f}s")
        return result
    return wrapper

@timer
def slow_function(n):
    sum(range(n))

slow_function(1_000_000)
# slow_function: 0.0234s

functools.wraps (Wajib Pakai)

Tanpa @wraps, decorator menyembunyikan metadata function asli:

@timer
def my_func():
    """Dokumentasi function ini."""
    pass

print(my_func.__name__)    # "wrapper" — bukan "my_func"!
print(my_func.__doc__)     # None — docstring hilang!

Solusi: functools.wraps

from functools import wraps

def timer(func):
    @wraps(func)              # ← wajib
    def wrapper(*args, **kwargs):
        # ...
        return func(*args, **kwargs)
    return wrapper

Decorator dengan Argument

def repeat(times):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    print(f"Halo {name}")

greet("Budi")
# Halo Budi
# Halo Budi
# Halo Budi

3 level nested function:

  1. repeat(times) — terima decorator argument
  2. decorator(func) — terima function
  3. wrapper(*args, **kwargs) — terima function argument

Decorator Praktis

# Caching (memoization)
from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

# Logging
import logging
logger = logging.getLogger(__name__)

def log_errors(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            logger.error(f"{func.__name__} failed: {e}")
            raise
    return wrapper

# Auth check (web framework)
def require_login(func):
    @wraps(func)
    def wrapper(request, *args, **kwargs):
        if not request.user.is_authenticated:
            raise PermissionError("Login required")
        return func(request, *args, **kwargs)
    return wrapper

Konteks AI: PyTorch pakai @torch.no_grad() decorator untuk disable gradient. FastAPI pakai @app.get("/") untuk routing. Decorator ada di mana-mana.


Bagian 2 — Generator

Diagram: Generator = Mesin Lazy

Cara Membaca Diagram

Layer atas (pink) = List (Eager) — bangun semua sekaligus, simpan di RAM. Layer bawah (hijau) = Generator (Lazy) — yield satu per satu saat diminta. Sisi kanan: use case ideal untuk masing-masing.

Walkthrough Step-by-Step

List (Eager):

  1. Bikin list comprehension [i for i in range(1_000_000)].
  2. Python alokasi memori untuk 1 juta integer.
  3. Hasil: list lengkap di RAM, akses random cepat (lst[500_000]).
  4. Cost: ~50MB RAM, blocking saat construction.

Generator (Lazy):

  1. Bikin generator (i for i in range(1_000_000)).
  2. Tidak ada element dibuat dulu — cuma "resep" cara membuat.
  3. Saat next(gen) dipanggil → yield 1 element → pause function.
  4. next() lagi → resume → yield element berikut.
  5. Cost: hampir 0 RAM, tapi sekali iterate (habis setelah dikonsumsi).

Analogi Sehari-hari

List = buat 1 juta kue donat sekaligus. Cepat akses, tapi butuh oven raksasa dan tempat penyimpanan besar. Generator = mesin pencetak donat on-demand. Pesan satu, mesin produksi satu. Pesan lagi, produksi lagi. Tidak butuh stok besar. Cocok untuk file 10GB (tidak muat di RAM), streaming data sensor, atau produksi event yang infinite (itertools.count()).

Diagram statis Mermaid sebagai fallback:

flowchart LR
    subgraph Eager["📋 List (Eager)"]
        E1[Buat 1] --> E2[Buat 2] --> E3[...] --> E4[Buat 1jt]
        E4 --> M1[💾 1jt items di RAM]
    end
    subgraph Lazy["🌀 Generator (Lazy)"]
        L1[next] --> Y1[yield 1]
        Y1 --> L2[next]
        L2 --> Y2[yield 2]
        Y2 --> L3[next]
        L3 --> YN[yield 1jt]
    end

Analogi: generator = mesin pencetak uang yang produce satu lembar per kali tombol ditekan. Tidak menghasilkan 1 juta lembar sekaligus (yang akan menghabiskan kertas), tapi on-demand. Hemat memori, scalable.

Generator = function yang produce values lazy (satu per satu, on demand).

Function vs Generator

# Function biasa (eager — buat semua sekaligus)
def numbers(n):
    result = []
    for i in range(n):
        result.append(i)
    return result

# Generator (lazy — buat satu per satu)
def numbers_gen(n):
    for i in range(n):
        yield i

# Pemakaian
for n in numbers(1_000_000):
    pass    # butuh 50MB RAM untuk list

for n in numbers_gen(1_000_000):
    pass    # hampir 0 RAM (lazy)

yield bukan return — function "pause" lalu lanjut saat next() dipanggil.

Generator Expression

Mirip list comprehension tapi dengan ():

# List
sum([i ** 2 for i in range(1_000_000)])    # bikin list 1M element

# Generator
sum(i ** 2 for i in range(1_000_000))      # lazy, hemat memori

Use Case Praktis

Read large file:

def read_large_file(path):
    with open(path) as f:
        for line in f:
            yield line.strip()

for line in read_large_file("big.txt"):
    process(line)
# Tidak load semua file ke memori

Pipeline:

def gen_numbers():
    for i in range(100):
        yield i

def filter_even(nums):
    for n in nums:
        if n % 2 == 0:
            yield n

def square(nums):
    for n in nums:
        yield n ** 2

# Compose
result = square(filter_even(gen_numbers()))
print(list(result))    # [0, 4, 16, 36, ...]

yield from

def chain(iterables):
    for it in iterables:
        yield from it    # delegate to inner iterable

list(chain([[1, 2], [3, 4], [5]]))    # [1, 2, 3, 4, 5]

Iterator Protocol (Bonus)

Generator implement iterator protocol:

gen = (i for i in range(3))

next(gen)    # 0
next(gen)    # 1
next(gen)    # 2
next(gen)    # StopIteration

Bagian 3 — Context Manager

Diagram: Context Manager Lifecycle

stateDiagram-v2
    [*] --> Setup: __enter__
    Setup --> Running: yield / return
    Running --> Cleanup: normal exit
    Running --> CleanupError: exception
    Cleanup --> [*]: __exit__
    CleanupError --> [*]: __exit__

Analogi: context manager = petugas kebersihan otomatis. Pintu masuk: setup (buka resource). Pintu keluar: cleanup (tutup, walaupun ada masalah). Kamu bersih dari urusan resource management.

Context manager = object yang punya __enter__ dan __exit__ — dipakai dengan with.

Pakai Context Manager

# File handling (sudah familiar)
with open("file.txt") as f:
    data = f.read()
# Auto close saat keluar block

Bikin Context Manager (Class)

class Timer:
    def __enter__(self):
        import time
        self.start = time.time()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        elapsed = time.time() - self.start
        print(f"Elapsed: {elapsed:.4f}s")
        return False    # propagate exception

with Timer():
    # ... slow code ...
    pass

Bikin Context Manager (Decorator)

Lebih ringkas dengan @contextmanager:

from contextlib import contextmanager

@contextmanager
def timer():
    import time
    start = time.time()
    try:
        yield
    finally:
        elapsed = time.time() - start
        print(f"Elapsed: {elapsed:.4f}s")

with timer():
    # ...
    pass

Use Case

Database connection:

@contextmanager
def db_connection(url):
    conn = connect(url)
    try:
        yield conn
    finally:
        conn.close()

with db_connection("...") as conn:
    conn.execute("SELECT * FROM users")

Temporary directory:

import tempfile

with tempfile.TemporaryDirectory() as tmpdir:
    # tulis file di tmpdir
    pass
# auto-clean

Suppress exception:

from contextlib import suppress

with suppress(FileNotFoundError):
    Path("maybe_exists.txt").unlink()

Bagian 4 — Type Hints Lanjutan

Basic (Review)

def hitung(a: int, b: int) -> int:
    return a + b

nama: str = "Budi"
nilai: list[int] = [1, 2, 3]
data: dict[str, int] = {"a": 1, "b": 2}

Optional & Union

from typing import Optional, Union

# Optional = bisa None
def cari_user(id: int) -> Optional[dict]:
    # ... return dict atau None
    pass

# Union = beberapa tipe
def parse(value: Union[str, int]) -> int:
    return int(value)

# Modern Python 3.10+ syntax
def parse(value: str | int) -> int:
    return int(value)

def cari_user(id: int) -> dict | None:
    pass

Generic (Custom List/Dict Type)

from typing import TypeVar, Generic

T = TypeVar('T')

def first(items: list[T]) -> T:
    return items[0]

# Generic class
class Stack(Generic[T]):
    def __init__(self):
        self.items: list[T] = []
    
    def push(self, item: T):
        self.items.append(item)
    
    def pop(self) -> T:
        return self.items.pop()

s: Stack[int] = Stack()
s.push(1)

Callable

from typing import Callable

def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
    return func(a, b)

apply(lambda x, y: x + y, 2, 3)

Literal & TypedDict

from typing import Literal, TypedDict

# Literal — value yang spesifik
def set_mode(mode: Literal["read", "write"]) -> None:
    pass

set_mode("read")    # OK
set_mode("delete")  # type checker complain

# TypedDict — dict dengan tipe spesifik
class User(TypedDict):
    nama: str
    umur: int
    email: str

def greet(user: User) -> None:
    print(user["nama"])

Type Checker (mypy)

pip install mypy
mypy myfile.py

Mypy cek konsistensi type hints. Pakai di project profesional.


Bagian 5 — Closures

Function yang "memori"-kan variable dari outer scope.

def buat_pengali(n):
    def pengali(x):
        return x * n    # n dari outer scope
    return pengali

kali_2 = buat_pengali(2)
kali_5 = buat_pengali(5)

print(kali_2(10))    # 20
print(kali_5(10))    # 50

kali_2 "ingat" n=2. Itulah closure.

Use Case: Counter

def buat_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

c = buat_counter()
print(c(), c(), c())    # 1 2 3

nonlocal untuk modifikasi variable dari outer (non-global) scope.

Closure di Decorator

Decorator yang sudah dibahas sebenarnya menggunakan closure:

def timer(func):
    def wrapper(*args, **kwargs):
        # wrapper "ingat" func dari outer scope (closure!)
        result = func(*args, **kwargs)
        return result
    return wrapper

Bagian 6 — __slots__ (Optimisasi)

Untuk class dengan banyak instance, __slots__ lebih hemat memori:

class Point:
    __slots__ = ("x", "y")
    
    def __init__(self, x, y):
        self.x = x
        self.y = y
  • Tidak bisa tambah attribute baru di runtime
  • Memory ~50% lebih hemat
  • Cocok untuk dataclass-like dengan jutaan instance

Optimasi premature = jahat. Pakai hanya kalau memang banyak instance.


Bagian 7 — Async Basics (Sneak Peek)

Async = concurrent execution untuk I/O-bound tasks.

import asyncio

async def fetch_data():
    print("Mulai fetch")
    await asyncio.sleep(2)    # simulate I/O
    print("Done")
    return "data"

async def main():
    result = await fetch_data()
    print(result)

asyncio.run(main())
# Concurrent — fetch banyak sekaligus
async def main():
    results = await asyncio.gather(
        fetch_data(),
        fetch_data(),
        fetch_data(),
    )
    print(results)

Konteks AI: untuk panggil LLM API massal, async sangat berguna. Dibahas detail di Fase 7. Paham basic-nya saja sekarang.


Common Mistakes & FAQ

❌ Mistake 1: Lupa @functools.wraps

def my_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def hello():
    """Greet the user."""
    pass

print(hello.__name__)   # "wrapper" — kehilangan identity!
print(hello.__doc__)    # None — docstring hilang!

# Fix:
from functools import wraps

def my_decorator(func):
    @wraps(func)        # ← wajib
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

❌ Mistake 2: Generator habis setelah iterasi pertama

gen = (x ** 2 for x in range(5))
list(gen)    # [0, 1, 4, 9, 16]
list(gen)    # [] — sudah habis!

Generator hanya bisa di-iterate sekali. Kalau perlu iterate ulang, bikin generator ulang atau pakai list.

❌ Mistake 3: Lupa nonlocal di closure

def buat_counter():
    count = 0
    def increment():
        count += 1    # ❌ UnboundLocalError
        return count
    return increment

# Fix
def buat_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

❌ Mistake 4: Type hints tanpa runtime enforcement

def hitung(a: int) -> int:
    return a * 2

hitung("hello")    # tidak error! return "hellohello"

Type hints di Python tidak enforce di runtime. Untuk validasi runtime, pakai pydantic atau manual isinstance.

❌ Mistake 5: Decorator urutan terbalik

@decorator_a
@decorator_b
def func():
    pass

# Equivalent dengan: func = decorator_a(decorator_b(func))
# decorator_b dieksekusi DULU (innermost)

❌ Mistake 6: Modifikasi mutable default di decorator

# ❌ cache jadi shared antar function
def memoize(func, _cache={}):
    def wrapper(*args):
        if args not in _cache:
            _cache[args] = func(*args)
        return _cache[args]
    return wrapper

FAQ

Q: Decorator vs subclass untuk extend behavior? A:

  • Decorator = inject behavior ke function tanpa ubah body
  • Subclass = ubah behavior class lewat inheritance

Decorator lebih ringan dan composable untuk function-level changes.

Q: Generator vs list comprehension? A:

  • List comp [...] = eager, hasil di memori semua
  • Generator (...) = lazy, hemat memori, sekali iterate

Untuk pipeline data besar (e.g. baca file 10GB), generator essential.

Q: yield vs return? A:

  • return = keluar function, kasih hasil sekali
  • yield = pause function, kasih hasil, lanjut saat next() dipanggil

Function dengan yield otomatis jadi generator.

Q: Kapan pakai @contextmanager vs class? A:

  • @contextmanager = simple, satu setup-cleanup
  • Class = kompleks, butuh state, multiple methods

Q: Optional[X] vs X | None? A: Sama persis. X | None adalah sintaks modern (Python 3.10+), Optional[X] adalah bentuk lama dari typing module. Pakai X | None di project baru.

Q: Async itu sama dengan threading? A: Tidak.

  • Threading = parallel CPU
  • Async = concurrent I/O (single thread, switch saat menunggu)

Untuk panggil banyak API (LLM bulk requests), async ideal. Untuk number crunching, threading/multiprocessing.


Cek Pemahaman

  • Bisa bikin decorator dengan @wraps?
  • Tahu beda function biasa dan generator?
  • Bisa pakai context manager dengan @contextmanager?
  • Bisa pakai type hints (Optional, Union, Generic)?
  • Paham closure?

Challenge 2.9

Challenge 1 — Decorator: Timer + Logger

Bikin decorator gabungan yang:

  • Print nama function dipanggil
  • Print args & kwargs
  • Print waktu eksekusi
  • Print return value
  • Log error kalau exception

Challenge 2 — Decorator dengan Argument: Retry

@retry(max_attempts=3, delay=1)
def fetch_api():
    # ...kadang gagal
    pass

Implement decorator ini.

Challenge 3 — Generator: Read Large File

Function read_in_chunks(path, chunk_size=1024):

  • Generator yang yield chunk by chunk
  • Hemat memori untuk file besar

Challenge 4 — Generator: Fibonacci

def fibonacci():
    """Infinite fibonacci."""
    # ...

fib_gen = fibonacci()
for _ in range(10):
    print(next(fib_gen))    # 0, 1, 1, 2, 3, 5, 8, 13, 21, 34

Challenge 5 — Pipeline Generator

Bikin pipeline generator:

  1. gen_words(file) — yield kata per kata dari file
  2. filter_long(words, min_len) — yield kata dengan panjang >= min_len
  3. to_upper(words) — yield kata uppercase
  4. count_unique(words) — return set kata unik
result = count_unique(to_upper(filter_long(gen_words("file.txt"), 5)))

Hemat memori, pipeline elegan.

Challenge 6 — Custom Context Manager

Bikin context manager chdir:

with chdir("/tmp"):
    # cwd jadi /tmp
    pass
# cwd kembali ke semula

Pakai os.getcwd() dan os.chdir().

Challenge 7 — Type Hints Refactor

Ambil 1 challenge dari file 02-08 yang kamu kerjakan, tambahkan type hints lengkap. Run mypy dan fix semua warning.

Challenge 8 — Caching dengan Decorator

Bikin decorator @memoize (manual, tanpa lru_cache):

  • Cache hasil function berdasarkan argument
  • Skip kalau sudah dipanggil dengan argument sama
  • Print "(cached)" kalau ambil dari cache

Selanjutnya: 10-stdlib-tour.md — tour standard library yang akan banyak dipakai.