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
- Function asli —
def add(a, b): return a + b. Logic murni. @decorator— sintaks sugar untukadd = decorator(add). Bungkus function.- Wrapper — function baru yang dibikin decorator. Inilah yang dipanggil saat kamu invoke
add(1, 2). - Input — args & kwargs masuk ke wrapper.
- Pre-process — wrapper jalankan sesuatu sebelum panggil asli (start timer, log call, cek auth).
- Call — wrapper panggil function asli
func(*args, **kwargs). - Post-process — wrapper jalankan sesuatu setelah (stop timer, log result, format output).
- 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:
repeat(times)— terima decorator argumentdecorator(func)— terima functionwrapper(*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):
- Bikin list comprehension
[i for i in range(1_000_000)]. - Python alokasi memori untuk 1 juta integer.
- Hasil: list lengkap di RAM, akses random cepat (
lst[500_000]). - Cost: ~50MB RAM, blocking saat construction.
Generator (Lazy):
- Bikin generator
(i for i in range(1_000_000)). - Tidak ada element dibuat dulu — cuma "resep" cara membuat.
- Saat
next(gen)dipanggil → yield 1 element → pause function. next()lagi → resume → yield element berikut.- 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 sekaliyield= 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:
gen_words(file)— yield kata per kata dari filefilter_long(words, min_len)— yield kata dengan panjang >= min_lento_upper(words)— yield kata uppercasecount_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.