Skip to content

Module functools — Công cụ lập trình hàm Standard Library

Hàm đệ quy tính Fibonacci chạy O(2ⁿ) — thêm một dòng @lru_cache giảm còn O(n). Một class cần sáu phép so sánh — @total_ordering sinh tự động từ hai phép duy nhất. Một hàm xử lý ba kiểu dữ liệu khác nhau — @singledispatch phân luồng thay cho chuỗi if/elif.

functools đóng gói các pattern đã kiểm chứng trong production — từ memoization đến function overloading. Bài viết đi từ nguyên lý bên trong đến ứng dụng thực tế.


Bức tranh tư duy

Hãy hình dung functools như bộ gia vị nấu ăn của lập trình viên Python. Mỗi decorator hay tiện ích trong module này là một loại gia vị — bản thân nó không phải món ăn, nhưng khi rắc đúng chỗ thì nâng tầm hàm số của bạn lên một cấp độ hoàn toàn khác.

Công cụPhép ẩn dụGiải quyết bài toán gì
lru_cacheSổ tay ghi đáp án — tính một lần, tra cứu mãiLoại bỏ tính toán lặp lại, giảm độ phức tạp thời gian
cached_propertyBiển hiệu cửa hàng — treo một lần, đọc nhiều lầnThuộc tính tốn kém chỉ tính khi được truy cập lần đầu
partialCông thức đã pha sẵn — cố định một số nguyên liệuTạo hàm chuyên biệt từ hàm tổng quát
reduceDây chuyền lắp ráp — từng bước gộp lại thành mộtTích luỹ tuần tự trên iterable
singledispatchTổng đài phân luồng cuộc gọi — nhận diện loại, chuyển đúng nhánhFunction overloading dựa trên kiểu đối số đầu tiên
total_orderingMáy photocopy — viết hai phép so sánh, nhận đủ sáuTự sinh các magic method __le__, __gt__, __ge__
wrapsNhãn mác gốc — giữ nguyên tên và docstring qua decoratorBảo toàn metadata khi viết decorator

💡 Nguyên tắc chọn công cụ: Nếu bạn đang viết boilerplate lặp đi lặp lại (caching thủ công, if/elif theo kiểu, copy 6 phép so sánh...), hãy kiểm tra xem functools đã có giải pháp chưa. Câu trả lời thường là .


Cốt lõi kỹ thuật

lru_cache và cache — Memoization

lru_cache lưu kết quả của hàm vào bộ nhớ đệm theo chiến lược Least Recently Used. Khi cache đầy, entry ít được dùng nhất bị loại bỏ.

python
from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
    """Fibonacci O(n) thay vì O(2^n) nhờ memoization."""
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# Không cache: fibonacci(35) ≈ 5 giây
# Có cache:    fibonacci(35) ≈ 0.00001 giây

print(fibonacci(100))
# 354224848179261915075

# Kiểm tra thống kê cache
info = fibonacci.cache_info()
print(info)
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)

# Xoá toàn bộ cache khi cần giải phóng bộ nhớ
fibonacci.cache_clear()

Tham số quan trọng:

Tham sốMô tả
maxsize=128Mặc định. Giới hạn 128 entry, LRU eviction khi đầy
maxsize=NoneCache không giới hạn — cẩn thận memory leak!
typed=TruePhân biệt int(1)float(1.0) thành cache key khác nhau
python
from functools import cache

# cache (Python 3.9+): tương đương lru_cache(maxsize=None)
@cache
def factorial(n: int) -> int:
    return 1 if n <= 1 else n * factorial(n - 1)

⚠️ Yêu cầu bắt buộc: Mọi đối số truyền vào hàm được cache phải hashable. list, dict, set không dùng trực tiếp — cần chuyển sang tuple hoặc frozenset.

cached_property — Thuộc tính tính toán lười

cached_property biến một method thành thuộc tính, tính toán một lần duy nhất rồi lưu trực tiếp trên instance.

python
from functools import cached_property
import statistics

class DataAnalyzer:
    def __init__(self, raw_data: list[float]):
        self._raw = raw_data

    @cached_property
    def mean(self) -> float:
        print("Đang tính mean...")
        return statistics.mean(self._raw)

    @cached_property
    def stdev(self) -> float:
        return statistics.stdev(self._raw)

analyzer = DataAnalyzer([2.5, 3.1, 2.8, 3.5, 2.9])
print(analyzer.mean)   # "Đang tính mean..." → 2.96
print(analyzer.mean)   # Không in gì — lấy từ cache
del analyzer.mean       # Xoá cache → lần sau sẽ tính lại

💡 Khác biệt với @property: @property chạy lại mỗi lần truy cập, @cached_property chạy đúng một lần rồi lưu kết quả trên __dict__ của instance.

partial và partialmethod — Cố định đối số

partial tạo hàm mới bằng cách khoá trước một số đối số của hàm gốc.

python
from functools import partial, partialmethod
import json

# --- partial: tạo hàm chuyên biệt ---

def power(base: int, exponent: int) -> int:
    return base ** exponent

square = partial(power, exponent=2)
cube = partial(power, exponent=3)

print(square(5))  # 25
print(cube(5))    # 125

# Ứng dụng thực tế: cấu hình JSON encoder
json_vi = partial(json.dumps, indent=2, ensure_ascii=False)
data = {"tên": "Nguyễn Văn A", "tuổi": 28}
print(json_vi(data))
python
# partialmethod: dùng trong class
from functools import partialmethod

class HTTPClient:
    def request(self, method: str, url: str, **kwargs) -> str:
        return f"{method} {url}{kwargs}"

    get = partialmethod(request, "GET")
    post = partialmethod(request, "POST")
    put = partialmethod(request, "PUT")
    delete = partialmethod(request, "DELETE")

print(HTTPClient().get("/api/users"))
# GET /api/users → {}
print(HTTPClient().post("/api/users", json={"name": "Bình"}))
# POST /api/users → {'json': {'name': 'Bình'}}

reduce — Tích luỹ tuần tự

reduce áp dụng hàm hai đối số lên iterable từ trái sang phải, gộp dần thành một giá trị duy nhất.

python
from functools import reduce
from operator import add, mul

numbers = [1, 2, 3, 4, 5]

# Tổng: ((((1+2)+3)+4)+5) = 15
total = reduce(add, numbers)

# Tích: ((((1×2)×3)×4)×5) = 120
product = reduce(mul, numbers)

# Với giá trị khởi tạo
total_with_init = reduce(add, numbers, 100)  # 115

# Iterable rỗng + initializer → trả về initializer
result = reduce(add, [], 0)  # 0

Khi nào dùng reduce, khi nào không:

python
from functools import reduce

# ❌ KHÔNG dùng reduce khi đã có built-in:
# reduce(add, nums)           → sum(nums)
# reduce(mul, nums)           → math.prod(nums)
# reduce(max, nums)           → max(nums)
# reduce(lambda a,b: a+b, strs) → "".join(strs)

# ✅ Dùng reduce cho logic tích luỹ phức tạp:

# Compose pipeline
def compose(*functions):
    """compose(f, g, h)(x) = f(g(h(x)))"""
    return reduce(lambda f, g: lambda x: f(g(x)), functions)

pipeline = compose(str.upper, str.strip)
print(pipeline("  hello  "))  # "HELLO"

singledispatch và singledispatchmethod — Generic functions

singledispatch cho phép overload hàm dựa trên kiểu của đối số đầu tiên — thay thế chuỗi if isinstance(...).

python
from functools import singledispatch
from datetime import datetime, date
from decimal import Decimal

@singledispatch
def format_value(value) -> str:
    """Fallback: chuyển về chuỗi."""
    return str(value)

@format_value.register
def _(value: int) -> str:
    return f"{value:,}"

@format_value.register
def _(value: float) -> str:
    return f"{value:,.2f}"

@format_value.register
def _(value: Decimal) -> str:
    return f"{value:,.4f}"

@format_value.register
def _(value: datetime) -> str:
    return value.strftime("%d/%m/%Y %H:%M:%S")

@format_value.register
def _(value: date) -> str:
    return value.strftime("%d/%m/%Y")

@format_value.register
def _(value: bool) -> str:
    return "" if value else "Không"

# Sử dụng — dispatch tự động theo kiểu
print(format_value(1500000))                    # "1,500,000"
print(format_value(3.14159))                    # "3.14"
print(format_value(Decimal("99999.1234")))      # "99,999.1234"
print(format_value(datetime(2024, 6, 15, 10)))  # "15/06/2024 10:00:00"
print(format_value(True))                       # "Có"
print(format_value(None))                       # "None" (fallback)

💡 Dùng singledispatchmethod thay cho singledispatch khi cần dispatch trong class method. Dispatch theo MRO — subclass tự động match implementation của parent nếu không register riêng.

total_ordering — Tự sinh phép so sánh

Chỉ cần định nghĩa __eq__một trong __lt__, __le__, __gt__, __ge__ — decorator sinh nốt phần còn lại.

python
from functools import total_ordering

@total_ordering
class SemVer:
    def __init__(self, major: int, minor: int, patch: int):
        self.major, self.minor, self.patch = major, minor, patch

    def _as_tuple(self) -> tuple[int, int, int]:
        return (self.major, self.minor, self.patch)

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, SemVer):
            return NotImplemented
        return self._as_tuple() == other._as_tuple()

    def __lt__(self, other: "SemVer") -> bool:
        if not isinstance(other, SemVer):
            return NotImplemented
        return self._as_tuple() < other._as_tuple()

    def __repr__(self) -> str:
        return f"SemVer({self.major}.{self.minor}.{self.patch})"

v1, v2, v3 = SemVer(1, 2, 0), SemVer(1, 3, 0), SemVer(2, 0, 0)
print(v1 < v2)    # True  (do ta định nghĩa)
print(v1 <= v2)   # True  (total_ordering sinh ra)
print(v3 > v2)    # True  (total_ordering sinh ra)
print(sorted([v3, v1, v2]))
# [SemVer(1.2.0), SemVer(1.3.0), SemVer(2.0.0)]

wraps — Bảo toàn metadata decorator

Khi viết decorator, wraps giữ nguyên __name__, __doc__, __module__ của hàm gốc.

python
from functools import wraps
import time

def timing(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} chạy trong {elapsed:.4f}s")
        return result
    return wrapper

@timing
def process_data(records: list[dict]) -> int:
    """Xử lý danh sách bản ghi và trả về số lượng hợp lệ."""
    return sum(1 for r in records if r.get("valid"))

# Metadata được bảo toàn
print(process_data.__name__)  # "process_data" (không phải "wrapper")
print(process_data.__doc__)   # "Xử lý danh sách bản ghi..."

# Truy cập hàm gốc nếu cần (bỏ qua decorator)
original = process_data.__wrapped__

Thực chiến

Hệ thống cache API response với TTL

Bài toán: Xây dựng lớp cache cho kết quả truy vấn với TTL, kết hợp partial để cấu hình callback.

python
from functools import lru_cache, partial, wraps
from datetime import datetime
import time

# ── 1. Cache truy vấn tốn kém với TTL ──

def ttl_cache(maxsize: int = 128, ttl_seconds: int = 300):
    """Decorator cache có thời hạn sống (TTL)."""
    def decorator(func):
        @lru_cache(maxsize=maxsize)
        def cached_func(*args, _ttl_bucket, **kwargs):
            return func(*args, **kwargs)

        @wraps(func)
        def wrapper(*args, **kwargs):
            bucket = int(time.time()) // ttl_seconds
            return cached_func(*args, _ttl_bucket=bucket, **kwargs)

        wrapper.cache_info = cached_func.cache_info
        wrapper.cache_clear = cached_func.cache_clear
        return wrapper
    return decorator

@ttl_cache(maxsize=512, ttl_seconds=60)
def fetch_product(product_id: int) -> dict:
    print(f"  [DB] Truy vấn product_id={product_id}...")
    time.sleep(0.1)
    return {"id": product_id, "name": f"Sản phẩm #{product_id}",
            "price": product_id * 15000}

result = fetch_product(42)  # cache miss → truy vấn DB
result = fetch_product(42)  # cache hit
print(fetch_product.cache_info())


# ── 2. partial: cấu hình callback API client ──

def send_notification(
    channel: str,
    level: str,
    message: str,
    *,
    timestamp: str | None = None,
) -> dict:
    ts = timestamp or datetime.now().isoformat()
    return {"channel": channel, "level": level, "message": message, "ts": ts}

# Tạo các hàm gửi notification chuyên biệt
notify_slack_error = partial(send_notification, "slack", "ERROR")
notify_email_info = partial(send_notification, "email", "INFO")
notify_sms_critical = partial(send_notification, "sms", "CRITICAL")

# Sử dụng — chỉ cần truyền message
print(notify_slack_error("Database connection timeout"))
print(notify_email_info("Báo cáo ngày đã sẵn sàng"))
print(notify_sms_critical("Server #3 không phản hồi"))

Sai lầm điển hình

1. Cache không giới hạn với dữ liệu đầu vào từ người dùng

python
# ❌ SAI: maxsize=None + input không kiểm soát → rò rỉ bộ nhớ
from functools import lru_cache

@lru_cache(maxsize=None)
def get_user_profile(user_id: str) -> dict:
    return query_database(user_id)

# Hàng triệu user_id khác nhau → bộ nhớ tăng không ngừng

# ✅ ĐÚNG: Đặt maxsize hợp lý
@lru_cache(maxsize=2048)
def get_user_profile(user_id: str) -> dict:
    return query_database(user_id)

# Theo dõi cache thường xuyên
info = get_user_profile.cache_info()
if info.currsize >= 2000:
    get_user_profile.cache_clear()

2. Cache trả về object khả biến (mutable)

python
from functools import lru_cache

# ❌ SAI: dict trả về bị sửa đổi → cache bị nhiễm bẩn
@lru_cache(maxsize=64)
def get_default_settings() -> dict:
    return {"theme": "dark", "language": "vi", "font_size": 14}

settings = get_default_settings()
settings["theme"] = "light"  # Sửa trực tiếp trên cache!

print(get_default_settings())
# {'theme': 'light', ...} ← DỮ LIỆU SAI

# ✅ ĐÚNG: Trả về bản sao hoặc object bất biến
from types import MappingProxyType

@lru_cache(maxsize=64)
def _get_default_settings() -> MappingProxyType:
    return MappingProxyType({"theme": "dark", "language": "vi", "font_size": 14})

def get_default_settings() -> dict:
    return dict(_get_default_settings())  # bản sao mới mỗi lần

3. lru_cache trên instance method — cache per-instance sai cách

python
from functools import lru_cache, cached_property

class UserService:
    def __init__(self, db_url: str):
        self.db_url = db_url

    # ❌ SAI: self tham gia vào cache key → mỗi instance có
    # cache riêng, và self phải hashable (thường không phải)
    @lru_cache(maxsize=128)
    def get_user(self, user_id: int) -> dict:
        return {"id": user_id}

# ✅ ĐÚNG: Dùng cached_property cho thuộc tính tốn kém
class UserService:
    def __init__(self, db_url: str):
        self.db_url = db_url

    @cached_property
    def connection_pool(self):
        return create_pool(self.db_url)

4. partial trộn lẫn positional và keyword gây xung đột

python
from functools import partial

def create_tag(tag: str, content: str, cls: str = "") -> str:
    class_attr = f' class="{cls}"' if cls else ""
    return f"<{tag}{class_attr}>{content}</{tag}>"

# ❌ SAI: bind positional "div" + keyword "content" → dễ nhầm
make_div = partial(create_tag, "div", cls="container")
# make_div("Hello", cls="box")  # TypeError: multiple values for 'cls'

# ✅ ĐÚNG: Dùng keyword arguments rõ ràng
make_div = partial(create_tag, tag="div", cls="container")
print(make_div(content="Hello"))
# <div class="container">Hello</div>

5. Dùng reduce khi đã có built-in tối ưu hơn

python
# ❌ SAI: reduce khi đã có built-in tối ưu hơn
# reduce(add, nums)             → sum(nums)
# reduce(lambda a,b: a*b, nums) → math.prod(nums)
# reduce(max, nums)             → max(nums)

# ✅ reduce chỉ hợp lý khi logic tích luỹ KHÔNG CÓ built-in tương ứng

Under the Hood

Cấu trúc nội bộ của lru_cache

lru_cache sử dụng doubly-linked list + dictionary để đạt O(1) cho cả tra cứu lẫn cập nhật thứ tự.

┌─────────────────────────────────────────────────┐
│                  lru_cache internals             │
│                                                  │
│   Dictionary (O(1) lookup by key)                │
│   ┌──────────┬──────────┬──────────┐             │
│   │ key=f(1) │ key=f(2) │ key=f(3) │ ...         │
│   │ → node_A │ → node_B │ → node_C │             │
│   └──────────┴──────────┴──────────┘             │
│                                                  │
│   Doubly-Linked List (LRU order)                 │
│                                                  │
│   HEAD ⟷ node_C ⟷ node_A ⟷ node_B ⟷ TAIL       │
│   (mới nhất)                      (cũ nhất)     │
│                                                  │
│   Khi cache đầy:                                 │
│   1. Xoá TAIL (node_B) khỏi list và dict        │
│   2. Thêm entry mới vào HEAD                     │
│                                                  │
│   Khi cache hit:                                 │
│   1. Di chuyển node được truy cập lên HEAD       │
│   2. Trả về giá trị đã lưu                       │
└─────────────────────────────────────────────────┘

Mỗi entry trong cache lưu trữ:

  • key: tuple các đối số đã hash
  • result: giá trị trả về
  • prev/next: con trỏ đến node trước/sau trong linked list
  • Chi phí bộ nhớ mỗi entry: ~200-300 bytes overhead (ngoài kích thước của result)

So sánh hiệu năng

Phiên bảnn=35Giải thích
fib_naive(35)~3.5 giâyO(2ⁿ) — ~34 tỷ phép gọi đệ quy
fib_cached(35)~0.00003 giâyO(n) — 35 miss + 33 hits, tăng tốc ~100,000×

typed=True thêm chi phí: cache key (3,)(3, int) do phải lookup type().

Dispatch resolution của singledispatch

singledispatch tìm implementation phù hợp theo Method Resolution Order (MRO) của kiểu đối số:

python
from functools import singledispatch
from collections import OrderedDict

@singledispatch
def describe(obj):
    return f"object: {obj}"

@describe.register(dict)
def _(obj):
    return f"dict với {len(obj)} khoá"

# OrderedDict kế thừa dict → dispatch tìm theo MRO:
# OrderedDict → dict → object
# Tìm thấy dict trong registry → dùng implementation của dict

print(describe(OrderedDict(a=1)))
# "dict với 1 khoá" ← dispatch theo MRO, không phải chính xác kiểu

# Muốn xử lý riêng? Register cụ thể:
@describe.register(OrderedDict)
def _(obj):
    return f"OrderedDict: {list(obj.keys())}"

print(describe(OrderedDict(a=1)))
# "OrderedDict: ['a']" ← implementation cụ thể hơn được ưu tiên

Checklist ghi nhớ

✅ Checklist triển khai

Memoization & Caching

  • [ ] Luôn đặt maxsize hợp lý cho lru_cache khi đầu vào không kiểm soát được
  • [ ] Kiểm tra cache_info() định kỳ để đánh giá hit rate
  • [ ] Không cache hàm trả về mutable object — dùng MappingProxyType hoặc trả bản sao
  • [ ] Dùng cached_property thay vì lru_cache cho instance method không có tham số
  • [ ] Mọi đối số truyền vào hàm được cache phải hashable

Function Composition

  • [ ] Ưu tiên partial với keyword arguments — tránh trộn positional khi bind
  • [ ] Dùng partialmethod (không phải partial) khi cần bind method trong class
  • [ ] Chỉ dùng reduce khi không có built-in tương ứng (sum, max, math.prod)
  • [ ] Luôn truyền initializer cho reduce khi iterable có thể rỗng

Dispatch & Overloading

  • [ ] Dùng singledispatch thay thế chuỗi if isinstance(...) dài
  • [ ] Register subclass cụ thể nếu cần xử lý khác parent class
  • [ ] singledispatchmethod cho method trong class, singledispatch cho hàm độc lập

Decorator & Comparison

  • [ ] Mọi decorator tự viết phải dùng @wraps(func) để bảo toàn metadata
  • [ ] total_ordering yêu cầu __eq__ + ít nhất một phép so sánh (__lt__ được khuyến nghị)
  • [ ] Luôn trả về NotImplemented (không phải False) khi so sánh với kiểu không tương thích

Bài tập luyện tập

Bài 1: Memoized factorial (Nền tảng)

🧠 Quiz

Yêu cầu: Viết hàm factorial(n) sử dụng lru_cache. Hàm cần:

  1. Trả về n! cho n >= 0
  2. Raise ValueError nếu n < 0
  3. In ra cache_info() sau khi tính factorial(10) rồi factorial(7)

Câu hỏi: Khi gọi factorial(7) sau factorial(10), có bao nhiêu cache hit?

Lời giải
python
from functools import lru_cache

@lru_cache(maxsize=256)
def factorial(n: int) -> int:
    if n < 0:
        raise ValueError(f"n phải >= 0, nhận n={n}")
    return 1 if n <= 1 else n * factorial(n - 1)

print(factorial(10))  # 3628800
print(factorial.cache_info())
# CacheInfo(hits=0, misses=11, maxsize=256, currsize=11)

print(factorial(7))   # 5040
print(factorial.cache_info())
# CacheInfo(hits=1, misses=11, ...) → 1 hit vì f(7) đã được tính khi đệ quy f(10)

Bài 2: Multi-type serializer với singledispatch (Trung cấp)

🧠 Quiz

Yêu cầu: Xây dựng hàm to_csv_cell(value) dùng singledispatch để chuyển đổi các kiểu dữ liệu Python sang chuỗi phù hợp cho ô CSV:

KiểuQuy tắc chuyển đổi
strBọc trong ngoặc kép, escape dấu " thành ""
int, floatChuyển thành chuỗi số
bool"TRUE" hoặc "FALSE"
NoneChuỗi rỗng ""
datetimeFormat YYYY-MM-DD HH:MM:SS
listJoin bằng dấu `

Test: to_csv_cell('Nguyễn "Văn" A')"Nguyễn ""Văn"" A"

Lời giải
python
from functools import singledispatch
from datetime import datetime

@singledispatch
def to_csv_cell(value) -> str:
    return f'"{value}"'

# register bool TRƯỚC int vì bool là subclass của int
@to_csv_cell.register(bool)
def _(value: bool) -> str:
    return "TRUE" if value else "FALSE"

@to_csv_cell.register(int)
def _(value: int) -> str:
    return str(value)

@to_csv_cell.register(float)
def _(value: float) -> str:
    return str(value)

@to_csv_cell.register(str)
def _(value: str) -> str:
    escaped = value.replace('"', '""')
    return f'"{escaped}"'

@to_csv_cell.register(type(None))
def _(value) -> str:
    return ""

@to_csv_cell.register(datetime)
def _(value: datetime) -> str:
    return f'"{value.strftime("%Y-%m-%d %H:%M:%S")}"'

@to_csv_cell.register(list)
def _(value: list) -> str:
    joined = "|".join(str(item) for item in value)
    return f'"{joined}"'

assert to_csv_cell('Nguyễn "Văn" A') == '"Nguyễn ""Văn"" A"'
assert to_csv_cell(42) == "42"
assert to_csv_cell(True) == "TRUE"
assert to_csv_cell(None) == ""
assert to_csv_cell(datetime(2024, 1, 15, 9, 30)) == '"2024-01-15 09:30:00"'
print("Tất cả test passed!")

Điểm mấu chốt: Register bool trước intisinstance(True, int)True — singledispatch dùng MRO nên bool cần register trước để ưu tiên.

Bài 3: Comparison mixin với total_ordering (Nâng cao)

🧠 Quiz

Yêu cầu: Tạo class Priority đại diện cho mức ưu tiên task (LOW, MEDIUM, HIGH, CRITICAL) sử dụng total_ordering. Class cần:

  1. So sánh được với nhau: CRITICAL > HIGH > MEDIUM > LOW
  2. Tạo instance từ string: Priority.from_str("high")
  3. Sắp xếp danh sách task theo priority giảm dần

Gợi ý: Dùng partial hoặc cached_property nếu phù hợp.

Lời giải
python
from functools import total_ordering, lru_cache

@total_ordering
class Priority:
    _LEVELS = {"low": 0, "medium": 1, "high": 2, "critical": 3}

    def __init__(self, name: str):
        key = name.lower()
        if key not in self._LEVELS:
            raise ValueError(f"Chấp nhận: {', '.join(self._LEVELS)}")
        self._name, self._value = key, self._LEVELS[key]

    @classmethod
    @lru_cache(maxsize=4)
    def from_str(cls, name: str) -> "Priority":
        return cls(name)

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Priority):
            return NotImplemented
        return self._value == other._value

    def __lt__(self, other: "Priority") -> bool:
        if not isinstance(other, Priority):
            return NotImplemented
        return self._value < other._value

    def __repr__(self) -> str:
        return f"Priority({self._name.upper()})"

    def __hash__(self) -> int:
        return hash(self._value)

low, high = Priority.from_str("low"), Priority.from_str("high")
crit = Priority.from_str("critical")
print(crit > high, low <= high)  # True True

tasks = [("Fix typo", low), ("Server down", crit), ("Security patch", high)]
for name, prio in sorted(tasks, key=lambda t: t[1], reverse=True):
    print(f"  [{prio}] {name}")
# [Priority(CRITICAL)] Server down
# [Priority(HIGH)] Security patch
# [Priority(LOW)] Fix typo

Liên kết học tiếp