Giao diện
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_cache | Sổ tay ghi đáp án — tính một lần, tra cứu mãi | Loại bỏ tính toán lặp lại, giảm độ phức tạp thời gian |
cached_property | Biển hiệu cửa hàng — treo một lần, đọc nhiều lần | Thuộc tính tốn kém chỉ tính khi được truy cập lần đầu |
partial | Công thức đã pha sẵn — cố định một số nguyên liệu | Tạo hàm chuyên biệt từ hàm tổng quát |
reduce | Dây chuyền lắp ráp — từng bước gộp lại thành một | Tích luỹ tuần tự trên iterable |
singledispatch | Tổng đài phân luồng cuộc gọi — nhận diện loại, chuyển đúng nhánh | Function overloading dựa trên kiểu đối số đầu tiên |
total_ordering | Máy photocopy — viết hai phép so sánh, nhận đủ sáu | Tự sinh các magic method __le__, __gt__, __ge__ |
wraps | Nhãn mác gốc — giữ nguyên tên và docstring qua decorator | Bả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ó.
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=128 | Mặc định. Giới hạn 128 entry, LRU eviction khi đầy |
maxsize=None | Cache không giới hạn — cẩn thận memory leak! |
typed=True | Phân biệt int(1) và 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,setkhông dùng trực tiếp — cần chuyển sangtuplehoặcfrozenset.
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:@propertychạy lại mỗi lần truy cập,@cached_propertychạ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) # 0Khi 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 "Có" 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
singledispatchmethodthay chosingledispatchkhi 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__ và 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ần3. 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 ứngUnder 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ố đã hashresult: 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ản | n=35 | Giải thích |
|---|---|---|
fib_naive(35) | ~3.5 giây | O(2ⁿ) — ~34 tỷ phép gọi đệ quy |
fib_cached(35) | ~0.00003 giây | O(n) — 35 miss + 33 hits, tăng tốc ~100,000× |
typed=Truethêm chi phí: cache key(3,)→(3, int)do phải lookuptype().
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ênChecklist ghi nhớ
✅ Checklist triển khai
Memoization & Caching
- [ ] Luôn đặt
maxsizehợp lý cholru_cachekhi đầ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
MappingProxyTypehoặc trả bản sao - [ ] Dùng
cached_propertythay vìlru_cachecho 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
partialvới keyword arguments — tránh trộn positional khi bind - [ ] Dùng
partialmethod(không phảipartial) khi cần bind method trong class - [ ] Chỉ dùng
reducekhi không có built-in tương ứng (sum,max,math.prod) - [ ] Luôn truyền
initializerchoreducekhi iterable có thể rỗng
Dispatch & Overloading
- [ ] Dùng
singledispatchthay thế chuỗiif isinstance(...)dài - [ ] Register subclass cụ thể nếu cần xử lý khác parent class
- [ ]
singledispatchmethodcho method trong class,singledispatchcho 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_orderingyê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ảiFalse) 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:
- Trả về
n!chon >= 0 - Raise
ValueErrornếun < 0 - In ra
cache_info()sau khi tínhfactorial(10)rồifactorial(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ểu | Quy tắc chuyển đổi |
|---|---|
str | Bọc trong ngoặc kép, escape dấu " thành "" |
int, float | Chuyển thành chuỗi số |
bool | "TRUE" hoặc "FALSE" |
None | Chuỗi rỗng "" |
datetime | Format YYYY-MM-DD HH:MM:SS |
list | Join 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 int vì isinstance(True, int) là 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:
- So sánh được với nhau:
CRITICAL > HIGH > MEDIUM > LOW - Tạo instance từ string:
Priority.from_str("high") - 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