Memahami Abstract Syntax Tree di Python: Analisis dan Transformasi Kode dengan Modul ast

Lhuqita Fazry
Python AST Code Analysis Metaprogramming Python Programming
Memahami Abstract Syntax Tree di Python: Analisis dan Transformasi Kode dengan Modul ast

Struktur Kode Sumber Python sebagai Pohon

Sebelum Python mengeksekusi kode apapun, interpreter memproses seluruh kode sumber menjadi Abstract Syntax Tree, atau AST. Proses kompilasi Python melibatkan beberapa tahap berurutan: pertama, modul tokenize memecah teks sumber menjadi token mentah; kemudian ast.parse() menerima token tersebut dan menghasilkan objek tree yang merepresentasikan struktur sintaksis program secara hierarkis; selanjutnya AST diubah menjadi bytecode oleh kompiler internal; terakhir, bytecode dieksekusi oleh mesin virtual Python. Setiap node dalam tree merepresentasikan konstruk program, mulai dari ekspresi, penugasan, definisi fungsi, hingga perulangan.

AST bersifat abstrak karena tidak mempertahankan detail sintaksis mentah seperti spasi atau komentar. Fokusnya adalah pada makna semantik dari kode. Inilah mengapa AST menjadi fondasi bagi alat analisis statis, formatter, dan transformer kode. Tanpa AST, tools seperti flake8, pylint, dan black tidak akan bisa memahami struktur program secara akurat. Fungsi ast.dump() dengan parameter indent menghasilkan representasi teks yang terstruktur dan mudah dibaca, memungkinkan kita memverifikasi apakah tree yang dihasilkan sesuai dengan ekspektasi sebelum melakukan transformasi lebih lanjut.

python
import ast

source_code = """
def greet(name):
    print(f"Hello, {name}")

greet("World")
"""

tree = ast.parse(source_code)
print(ast.dump(tree, indent=2))

Output:

text
Module(
  body=[
    FunctionDef(
      name='greet',
      args=arguments(
        posonlyargs=[],
        args=[
          arg(arg='name')],
        kwonlyargs=[],
        kw_defaults=[],
        defaults=[]),
      body=[
        Expr(
          value=Call(
            func=Name(id='print', ctx=Load()),
            args=[
              JoinedStr(
                values=[
                  Constant(value='Hello, '),
                  FormattedValue(
                    value=Name(id='name', ctx=Load()),
                    conversion=-1)])],
            keywords=[]))],
      decorator_list=[]),
    Expr(
      value=Call(
        func=Name(id='greet', ctx=Load()),
        args=[
          Constant(value='World')],
        keywords=[]))],
  type_ignores=[])

Dari output di atas, kita bisa melihat bahwa setiap elemen program direpresentasikan sebagai node dengan hubungan parent-child. FunctionDef menjadi node induk yang berisi arguments, body, dan decorator_list. Struktur inilah yang memungkinkan kita menganalisis dan memanipulasi kode secara programatik.

Struktur Pohon AST Python

Gambar: Struktur Pohon Abstract Syntax Tree (AST) Python — Sumber: Wikipedia

Menelusuri dan Memeriksa Node AST Secara Programatik

Untuk menjelajahi pohon AST, Python menyediakan kelas ast.NodeVisitor yang mengimplementasikan pola visitor. Kita membuat subclass dari NodeVisitor dan menimpa method visit_<NodeType> untuk setiap jenis node yang ingin kita tangani. Ketika visit() dipanggil pada tree, visitor akan menelusuri setiap node secara rekursif dan memanggil method yang sesuai.

Teknik ini sangat berguna untuk mengekstrak metadata dari kode sumber, seperti daftar nama fungsi, parameter, decorator, atau variabel yang digunakan. Kita tidak perlu menulis parser sendiri; ast.parse() sudah menangani analisis sintaksis, dan NodeVisitor menangani penelusuran. Method generic_visit() memainkan peran penting karena memastikan bahwa node-node anak juga dikunjungi setelah method khusus selesai dieksekusi. Tanpa pemanggilan ini, visitor hanya akan menemukan node di level teratas dan melewatkan nested function, closure, atau node lain di dalam body.

Dalam konteks nested scope, generic_visit() memungkinkan kita mendeteksi fungsi yang bersarang di dalam fungsi lain, serta mengekstrak informasi decorator dan parameter secara menyeluruh. Misalnya, ketika kita ingin menganalisis semua argumen fungsi termasuk yang ada di dalam nested function, pemanggilan generic_visit() pada setiap visit_FunctionDef memastikan tidak ada node yang terlewat.

python
import ast

class FunctionExtractor(ast.NodeVisitor):
    def __init__(self):
        self.functions = []

    def visit_FunctionDef(self, node):
        self.functions.append({
            "name": node.name,
            "line": node.lineno,
            "args": [arg.arg for arg in node.args.args]
        })
        self.generic_visit(node)

source = """
def calculate_total(items):
    pass

def apply_discount(price, rate):
    pass
"""

tree = ast.parse(source)
extractor = FunctionExtractor()
extractor.visit(tree)

for func in extractor.functions:
    print(f"{func['name']} di baris {func['line']}, parameter: {func['args']}")

Output:

text
calculate_total di baris 2, parameter: ['items']
apply_discount di baris 5, parameter: ['price', 'rate']

Mekanisme generic_visit() memastikan bahwa node-node anak juga dikunjungi setelah method visit_FunctionDef selesai. Tanpa pemanggilan ini, visitor hanya akan menemukan fungsi di level teratas dan melewatkan nested function atau node lain di dalam body.

Pola Visitor NodeVisitor dan NodeTransformer
Data Science with Python
Data Science • Beginner

Data Science with Python

Master the art of data analysis, visualization, and predictive modeling.

Daftar

Gambar: Pola Visitor dengan NodeVisitor dan NodeTransformer — Sumber: Wikipedia

Mengubah Kode dengan Memodifikasi Node AST

Subclass ast.NodeTransformer memperluas konsep visitor dengan menambahkan kemampuan untuk mengganti node selama penelusuran. Ketika method visit_<NodeType> mengembalikan node baru, NodeTransformer akan menggantikan node asli di tree dengan nilai kembalian tersebut. Setelah semua modifikasi selesai, kita mengompilasi AST yang sudah berubah menjadi kode executable menggunakan compile().

AST transformation memodifikasi representasi struktural kode secara langsung dan menghindari kesalahan yang sering terjadi pada regex-based replacement, terutama ketika print() muncul di dalam string atau komentar. Sebagai contoh, regex yang mencari pola print( akan mencocokkan string "Jangan gunakan print() di sini" dan menggantinya secara salah, sedangkan AST hanya akan mencocokkan node Call yang sebenarnya.

Setelah transformasi selesai, ast.fix_missing_locations() harus dipanggil untuk memastikan setiap node yang baru dibuat memiliki informasi lokasi yang valid. Tanpa ini, compile() bisa gagal karena node baru tidak memiliki atribut lineno dan col_offset. Workflow lengkapnya adalah: parse sumber menjadi AST, transformasi node, perbaiki lokasi yang hilang, lalu kompilasi menjadi bytecode yang dapat dieksekusi.

python
import ast

class PrintToLoggingTransformer(ast.NodeTransformer):
    def visit_Call(self, node):
        self.generic_visit(node)
        if isinstance(node.func, ast.Name) and node.func.id == "print":
            new_func = ast.Attribute(
                value=ast.Name(id="logging", ctx=ast.Load()),
                attr="info",
                ctx=ast.Load()
            )
            return ast.Call(
                func=new_func,
                args=node.args,
                keywords=node.keywords
            )
        return node

source = """
print("Starting process")
print(f"Result: {value}")
"""

tree = ast.parse(source)
transformer = PrintToLoggingTransformer()
new_tree = transformer.visit(tree)
new_tree = ast.fix_missing_locations(new_tree)

print(ast.unparse(new_tree).strip())

Output:

text
logging.info('Starting process')
logging.info(f'Result: {value}')

Perhatikan bahwa ast.fix_missing_locations() dipanggil setelah transformasi untuk memastikan setiap node memiliki informasi lokasi yang valid. Tanpa ini, compile() bisa gagal karena node yang baru dibuat tidak memiliki atribut lineno dan col_offset.

Membangun Linter Kustom dengan Analisis Statis Berbasis AST

Tools seperti flake8, pylint, dan black mengandalkan AST untuk menegakkan aturan penulisan kode. flake8 menggabungkan pyflakes untuk analisis logika dan pycodestyle untuk pemformatan, sementara pylint melakukan analisis lebih mendalam termasuk check terhadap desain objek dan refactoring suggestions. black menggunakan AST sebagai referensi tambahan untuk memastikan transformasi formatting tidak mengubah semantik program. Pendekatan berbasis AST jauh lebih presisi dibandingkan regex-based parsing karena AST memahami konteks semantik. Sebuah Import node jelas berbeda dari ImportFrom node yang membawa nama dari modul tertentu, dan keduanya berbeda lagi dari Name node yang merujuk ke variabel.

Kita bisa membangun linter minimal yang mendeteksi unused import dengan membandingkan node Import, ImportFrom, dan Name terhadap referensi yang ditemukan di seluruh tree. Jika sebuah nama impor tidak pernah dirujuk sebagai Name node, maka impor tersebut dianggap tidak terpakai. Integrasi linter berbasis AST ke dalam pipeline CI/CD memungkinkan otomatisasi review kode pada setiap pull request, memastikan standar kualitas terjaga tanpa intervensi manual.

Pipeline Analisis Statis Berbasis AST

Gambar: Pipeline Analisis Kode dan Static Analysis — Sumber: Wikimedia Commons

python
import ast

class UnusedImportLinter(ast.NodeVisitor):
    def __init__(self):
        self.imports = {}
        self.used_names = set()

    def visit_Import(self, node):
        for alias in node.names:
            name = alias.asname if alias.asname else alias.name
            self.imports[name] = node.lineno
        self.generic_visit(node)

    def visit_ImportFrom(self, node):
        for alias in node.names:
            name = alias.asname if alias.asname else alias.name
            self.imports[name] = node.lineno
        self.generic_visit(node)

    def visit_Name(self, node):
        self.used_names.add(node.id)
        self.generic_visit(node)

    def report(self):
        for name, line in self.imports.items():
            if name not in self.used_names:
                print(f"Unused import '{name}' at line {line}")

source = """
import os
import sys
from collections import defaultdict

def process(data):
    return data.strip()
"""

tree = ast.parse(source)
linter = UnusedImportLinter()
linter.visit(tree)
linter.report()

Output:

text
Unused import 'os' at line 2
Unused import 'sys' at line 3
Unused import 'defaultdict' at line 4

Keunggulan pendekatan ini adalah ketepatan semantik. os dan sys memang tidak dirujuk dalam body fungsi, dan AST menangkap ini secara akurat. Linter berbasis regex akan kesulitan membedakan apakah os di dalam string komentar merupakan penggunaan yang valid atau tidak.

Praktik Terbaik dan Keterbatasan Manipulasi AST

Manipulasi AST memiliki keterbatasan penting yang perlu kita pahami. Pertama, AST tidak mempertahankan format asli kode sumber. Ketika kita mengompilasi AST yang sudah dimodifikasi kembali menjadi kode, formatting seperti indentasi, spasi, dan komentar akan hilang sepenuhnya. Untuk transformasi yang sensitif terhadap whitespace, modul tokenize lebih cocok karena mempertahankan informasi token mentah termasuk spasi dan komentar. Kita bisa menggunakan tokenize ketika tugas utama adalah mengubah spasi atau mempertahankan komentar, sementara AST lebih tepat ketika yang dibutuhkan adalah analisis semantik dan perubahan struktural.

Kedua, kasus-kasus edge seperti nested decorator, async function, dan list comprehension memerlukan penanganan khusus. NodeTransformer akan menelusuri node secara rekursif, tetapi kita harus memastikan bahwa setiap override method memanggil generic_visit() agar node anak juga diproses. AsyncFunctionDef memiliki atribut is_async yang harus dipertahankan selama transformasi, dan comprehension node memerlukan penanganan khusus pada nested scope-nya.

Risiko utama lainnya adalah menghasilkan kode yang tidak valid ketika memodifikasi hubungan node yang kompleks. Misalnya, mengubah tipe return sebuah function tanpa memperbarui annotation bisa menyebabkan ketidaksesuaian. Selalu gunakan ast.fix_missing_locations() setelah transformasi dan uji compiled code untuk memastikan validitasnya. Kode yang dihasilkan dari AST yang rusak bisa menyebabkan error runtime yang sulit dilacak.

python
import ast

# Contoh: menangani nested decorator dan async function
source = """
@decorator
async def handler(request):
    await process(request)
"""

tree = ast.parse(source)
# AsyncFunctionDef memiliki atribut is_async yang harus dipertahankan
for node in ast.walk(tree):
    if isinstance(node, ast.AsyncFunctionDef):
        print(f"Async function: {node.name} at line {node.lineno}")

Output:

text
Async function: handler at line 3

Ketika bekerja dengan AST, selalu verifikasi bahwa tree yang sudah dimodifikasi tetap valid dengan mengompilasinya dan menjalankan tes sederhana. Pendekatan ini mencegah error runtime yang sulit dilacak akibat transformasi yang tidak tepat.

Itulah bahasan lengkap tentang Abstract Syntax Tree di Python, mulai dari struktur pohon, traversing dengan NodeVisitor, transformasi dengan NodeTransformer, hingga pembangunan linter kustom. Konsep ini adalah fondasi penting dalam pengembangan tools analisis kode dan metaprogramming. Ingin mempraktikkan AST dalam proyek nyata? Bergabunglah dengan Python Advanced Topics di Rumah Coding, di mana kita akan membangun tool analisis kode dari awal sampai deployment.

Kursus Terkait

E-commerce Sales Dashboard
Kursus Premium Data Science

Data Science with Python

Master the art of data analysis, visualization, and predictive modeling.

Proyek Akhir

E-commerce Sales Dashboard

  • Data Cleaning Pipeline
  • Interactive Charts
  • Sales Forecasting Model
7 Weeks Beginner
Lihat Detail Kursus
GreenGuard: Intelligent Plant Disease Diagnosis Web App
Kursus Premium Machine Learning

Deep Learning Bootcamp

A beginner-friendly, highly interactive bootcamp designed to take you from foundational concepts to deploying real-world Artificial Intelligence applications. Through a completely project-based approach, you will master the core of Deep Learning, Artificial Neural Networks, and Computer Vision using Python and TensorFlow, ultimately building a professional-grade AI web application for your portfolio.

Proyek Akhir

GreenGuard: Intelligent Plant Disease Diagnosis Web App

  • Interactive Image Upload UI: A clean, user-friendly interface built with Streamlit that supports drag-and-drop image uploads directly from a computer or mobile phone.
  • Real-Time AI Inference: Utilizes a lightweight, optimized CNN model (like MobileNetV2) to process the image and return a diagnosis in seconds without heavy server load.
  • Confidence Scoring Dashboard: Visually displays the model's prediction probability (e.g., "95% confident this is Tomato Late Blight") using interactive progress bars or charts.
7 Weeks Intermediate
Lihat Detail Kursus
Domain-Specific AI Knowledge Assistant
Kursus Premium Machine Learning

LLM Bootcamp

This project-based bootcamp is designed for beginners to dive practically into the world of Large Language Models (LLMs). Through hands-on building, you will learn how to interact with top-tier AI APIs, master prompt engineering, orchestrate complex workflows using LangChain, and implement Retrieval-Augmented Generation (RAG) to query your own documents. By the end of this course, you will have the skills to build, test, and deploy a fully functional, custom AI web application.

Proyek Akhir

Domain-Specific AI Knowledge Assistant

  • Dynamic Document Processing: A sidebar interface allowing users to upload new PDF or TXT files, which the app automatically chunks, embeds, and stores in the vector database.
  • Context-Aware Chat UI: A modern chat interface built with Streamlit that maintains conversation history, allowing users to ask follow-up questions naturally.
  • Strict Guardrails (Anti-Hallucination): System instructions designed so the AI politely declines to answer questions that fall outside the context of the uploaded documents.
7 Weeks Beginner
Lihat Detail Kursus

Artikel Terkait