match-case trong Python 3.10: Thay thế if-else lồng nhau bằng Structural Pattern Matching

Python tutorial - IT technology blog
Python tutorial - IT technology blog

Mình dùng Python làm automation tool cho hầu hết task hàng ngày, từ deploy script đến monitoring alert. Sau một thời gian, mình nhận ra thứ làm code khó đọc nhất không phải logic phức tạp — mà là những chuỗi if-elif-else lồng nhau để xử lý nhiều case khác nhau.

Python 3.10 giới thiệu Structural Pattern Matching với cú pháp match-case. Sau khoảng 6 tháng dùng thực tế trên production, đây là đánh giá thật sự — không phải chỉ demo theo docs.

So sánh hai cách tiếp cận

Lấy ví dụ thực tế: xử lý HTTP response từ monitoring API. Với if-elif-else truyền thống:

def handle_response(status_code, data):
    if status_code == 200:
        return process_success(data)
    elif status_code == 201:
        return process_created(data)
    elif status_code == 400:
        log_error('Bad request', data)
        return None
    elif status_code == 401:
        refresh_token()
        return None
    elif status_code == 500:
        alert_team('Server error!', data)
        return None
    else:
        log_error(f'Unknown status: {status_code}', data)
        return None

Viết lại bằng match-case:

def handle_response(status_code, data):
    match status_code:
        case 200:
            return process_success(data)
        case 201:
            return process_created(data)
        case 400:
            log_error('Bad request', data)
        case 401:
            refresh_token()
        case 500:
            alert_team('Server error!', data)
        case _:
            log_error(f'Unknown status: {status_code}', data)
    return None

Ví dụ đơn giản này, match-case trông gọn hơn một chút — nhưng chưa phải lúc nó đáng dùng nhất. Sức mạnh thật sự xuất hiện khi cần match theo cấu trúc dữ liệu.

Điểm mạnh thực sự: Match theo cấu trúc dữ liệu

Xử lý dict event từ webhook

Script monitoring của mình nhận event JSON từ webhook khá thường xuyên — mỗi ngày vài trăm event từ 3–4 service khác nhau. Đây là lúc match-case thực sự khác biệt:

def process_event(event):
    match event:
        case {'type': 'deploy', 'env': 'production', 'service': service_name}:
            notify_team(f'Production deploy: {service_name}')
        case {'type': 'deploy', 'env': env}:
            log_info(f'Deploy to {env}')
        case {'type': 'alert', 'severity': 'critical', 'message': msg}:
            page_oncall(msg)
        case {'type': 'alert', 'message': msg}:
            log_warning(msg)
        case {'type': unknown_type}:
            log_info(f'Unknown event type: {unknown_type}')
        case _:
            log_error('Malformed event', event)

So với cách viết cũ bằng if-elif:

def process_event_old(event):
    event_type = event.get('type')
    if event_type == 'deploy':
        env = event.get('env')
        if env == 'production':
            service_name = event.get('service')
            notify_team(f'Production deploy: {service_name}')
        else:
            log_info(f'Deploy to {env}')
    elif event_type == 'alert':
        severity = event.get('severity')
        msg = event.get('message')
        if severity == 'critical':
            page_oncall(msg)
        else:
            log_warning(msg)
    elif event_type:
        log_info(f'Unknown event type: {event_type}')
    else:
        log_error('Malformed event', event)

match-case làm hai việc trong một: kiểm tra cấu trúc dict và extract giá trị vào biến — không cần .get() rải rác, không cần thêm tầng lồng nhau nào.

Match với dataclass và guard condition

from dataclasses import dataclass

@dataclass
class ServerStatus:
    host: str
    cpu: float
    memory: float
    status: str

def check_server(s: ServerStatus):
    match s:
        case ServerStatus(status='down'):
            alert_critical(f'{s.host} is DOWN!')
        case ServerStatus(cpu=cpu, memory=mem) if cpu > 90 or mem > 90:
            alert_warning(f'{s.host}: CPU={cpu}%, MEM={mem}%')
        case ServerStatus(status='up'):
            log_info(f'{s.host} OK')
        case _:
            log_debug(f'Unknown state: {s.host}')

Phần if cpu > 90 or mem > 90 sau pattern gọi là guard condition. Python chỉ evaluate guard sau khi structural pattern đã khớp — sạch hơn nhiều so với nest thêm if bên trong case. Đây là combo mình dùng nhiều nhất trong thực tế.

Phân tích ưu và nhược điểm

Ưu điểm

  • Code dễ đọc hơn khi có nhiều case với cấu trúc dữ liệu khác nhau — cấu trúc bài toán hiện ra rõ ngay khi nhìn vào
  • Không cần extract thủ công — Python bind giá trị vào biến ngay trong pattern, tiết kiệm nhiều dòng boilerplate
  • Guard condition giúp kết hợp structural check + value check trong cùng một case
  • Exhaustive handlingcase _ buộc bạn xử lý mọi case còn lại, ít bỏ sót hơn

Nhược điểm

  • Python 3.10+ only: Nhiều server vẫn chạy Python 3.8 hoặc 3.9. Check python --version trước khi dùng
  • Biến trong pattern là binding, không phải comparison — đây là điểm dễ nhầm nhất (xem chi tiết ở phần dưới)
  • Không phù hợp với logic tính toán: khi cần if value > threshold, if-else vẫn tự nhiên hơn
  • Learning curve: Teammate chưa quen sẽ hỏi ngay “đây là cú pháp gì” khi thấy **rest hay guard condition lần đầu — thực tế mình mất khoảng 30 phút pair coding để onboard một junior

Khi nào nên dùng match-case, khi nào không?

Sau 6 tháng, mình đúc ra nguyên tắc khá thực dụng:

Dùng match-case khi:

  • Xử lý 4+ case dựa trên cấu trúc dict, dataclass, hoặc sequence
  • Parse CLI command, message từ queue, hoặc event từ webhook
  • Implement state machine với nhiều trạng thái
  • Xử lý API response với nhiều variant cấu trúc khác nhau

Giữ nguyên if-else khi:

  • Chỉ có 2–3 case đơn giản, logic thẳng
  • Điều kiện dựa trên phép tính hoặc range: if value > threshold
  • Cần support Python < 3.10
  • Team chưa quen cú pháp match-case và không có thời gian onboard

Hướng dẫn triển khai: Các pattern hay dùng

Pattern 1: Nhiều giá trị trong một case (OR)

match command:
    case 'quit' | 'exit' | 'q':
        sys.exit(0)
    case 'help' | 'h' | '?':
        show_help()

Pattern 2: Match sequence

Dùng nhiều khi parse argument từ CLI hoặc message queue:

def handle_command(args: list):
    match args:
        case ['deploy', env]:
            deploy_to(env)
        case ['deploy', env, '--dry-run']:
            dry_run(env)
        case ['rollback', env, version]:
            rollback(env, version)
        case ['status']:
            show_status()
        case [cmd, *rest]:
            print(f'Unknown: {cmd} {rest}')
        case []:
            print('No command provided')

Pattern 3: Dict với wildcard (**rest)

def route_message(msg: dict):
    match msg:
        case {'action': 'create', 'resource': resource, **rest}:
            create_resource(resource, **rest)  # rest chứa các key còn lại
        case {'action': 'delete', 'id': resource_id}:
            delete_resource(resource_id)
        case {'action': action}:
            raise ValueError(f'Unsupported action: {action}')

Cạm bẫy hay gặp: Binding vs Comparison

Đây là chỗ mình thấy nhiều người vấp nhất khi mới học match-case. Viết tên biến trong pattern, Python sẽ bind giá trị vào đó — không so sánh với biến đã có sẵn:

target_env = 'production'

match event:
    case {'env': target_env}:  # SAI! Đây là binding, không phải comparison
        ...                    # Mọi giá trị đều match và ghi đè target_env

Muốn so sánh với biến đang có, dùng guard:

target_env = 'production'

match event:
    case {'env': env} if env == target_env:  # Đúng
        ...

Hoặc dùng dotted name — Python không bind dotted name, nó compare:

class Env:
    PRODUCTION = 'production'

match event:
    case {'env': Env.PRODUCTION}:  # Compare, không bind
        ...

Nếu đang viết Python 3.10+ và có hàm xử lý event hoặc parse command phức tạp, thử refactor một hàm đó sang match-case. Không phải vì nó “hiện đại hơn” — mà vì cấu trúc bài toán hiện ra rõ hơn khi bạn đọc lại code sau 3 tháng. Đó là lý do thực sự mình tiếp tục dùng nó.

Share: