Skip to content

Descriptors trong Python

Mỗi lần bạn viết @property, @classmethod, @staticmethod — bạn đang sử dụng descriptor mà không hề hay biết. Descriptor là cỗ máy ẩn phía sau toàn bộ cơ chế truy cập thuộc tính trong Python. Không phải decorator, không phải metaclass — chính descriptor mới là lớp nền tảng thực sự điều khiển cách Python đọc, ghi, và xóa attribute.

Trong production, descriptor xuất hiện khắp nơi: Django dùng descriptor cho model fields (CharField, IntegerField), SQLAlchemy xây dựng Column trên descriptor protocol, WTForms triển khai validators qua descriptor, và ngay cả function object cũng là descriptor (đó là lý do method binding hoạt động). Hiểu descriptor nghĩa là hiểu Python ở tầng sâu nhất.

Đây là ranh giới giữa "người dùng Python" và "người hiểu Python". Sau trang này, bạn sẽ nắm vững protocol, phân biệt rõ data vs non-data descriptor, tự tay implement validation framework, và đọc được source code của bất kỳ thư viện nào dùng descriptor pattern.


Bức tranh tư duy

Hãy hình dung descriptor như bảo vệ tòa nhà (security guard). Khi ai đó (code) muốn vào phòng (attribute), bảo vệ (descriptor) chặn lại — kiểm tra giấy tờ, ghi sổ ra vào, hoặc chuyển hướng sang phòng khác. Code không trực tiếp chạm vào attribute mà phải đi qua "bảo vệ" trước.

obj.attr (truy cập thuộc tính)


┌───────────────────────┐
│  Descriptor intercept │  ← Bảo vệ chặn lại
│  __get__ / __set__    │
│  __delete__           │
└──────────┬────────────┘


┌───────────────────────┐
│  Validate / Transform │  ← Kiểm tra, biến đổi
│  Log / Cache          │
└──────────┬────────────┘


  Trả về giá trị hoặc raise exception

Sự khác biệt then chốt nằm ở quyền hạn của bảo vệ:

  • Data descriptor = Bảo vệ toàn quyền. Dù instance __dict__ có ghi gì, bảo vệ vẫn kiểm soát. Không ai bypass được.
  • Non-data descriptor = Bảo vệ tư vấn. Nếu instance __dict__ đã có key, bảo vệ nhường quyền.

Phân biệt này quyết định toàn bộ thứ tự tra cứu thuộc tính — và là nguồn gốc của nhiều bug tinh vi trong production.


Cốt lõi kỹ thuật

Descriptor Protocol — __get__, __set__, __delete__

Một object chỉ cần implement ít nhất một trong ba method này là đã trở thành descriptor:

MethodSignatureĐược gọi khi
__get____get__(self, obj, objtype=None)obj.attr hoặc Class.attr
__set____set__(self, obj, value)obj.attr = value
__delete____delete__(self, obj)del obj.attr

Quy tắc: Descriptor chỉ hoạt động khi được gán như class attribute, không phải instance attribute.

python
class LoggedAccess:
    """Descriptor ghi log mọi truy cập attribute."""

    def __set_name__(self, owner, name):
        self.attr_name = f"_{name}"
        self.public_name = name

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        value = getattr(obj, self.attr_name, None)
        print(f"[GET] {self.public_name}{value!r}")
        return value

    def __set__(self, obj, value):
        print(f"[SET] {self.public_name} = {value!r}")
        setattr(obj, self.attr_name, value)

    def __delete__(self, obj):
        print(f"[DEL] {self.public_name}")
        delattr(obj, self.attr_name)


class Server:
    host = LoggedAccess()
    port = LoggedAccess()

    def __init__(self, host: str, port: int):
        self.host = host   # [SET] host = 'localhost'
        self.port = port   # [SET] port = 8080

srv = Server("localhost", 8080)
srv.host          # [GET] host → 'localhost'
srv.port = 3000   # [SET] port = 3000
del srv.host      # [DEL] host

Data Descriptor vs Non-data Descriptor

Tiêu chíData DescriptorNon-data Descriptor
Methods__set__ và/hoặc __delete__Chỉ có __get__
Ưu tiênCao hơn instance __dict__Thấp hơn instance __dict__
Override được?Không — luôn thắngCó — instance dict ghi đè
Ví dụproperty, member_descriptorfunction, classmethod
python
class DataDesc:
    def __get__(self, obj, objtype=None):
        return obj.__dict__.get("_val", "default") if obj else self
    def __set__(self, obj, value):
        obj.__dict__["_val"] = value

class NonDataDesc:
    def __get__(self, obj, objtype=None):
        return "từ non-data descriptor" if obj else self

class Demo:
    data_attr = DataDesc()
    nondata_attr = NonDataDesc()

obj = Demo()
obj.__dict__["data_attr"] = "instance dict"
obj.__dict__["nondata_attr"] = "instance dict"

print(obj.data_attr)     # "default"        ← Data descriptor THẮNG
print(obj.nondata_attr)  # "instance dict"  ← Instance dict THẮNG

Tại sao quan trọng? function là non-data descriptor — bạn có thể gán giá trị vào obj.method_name và nó sẽ "che" method. Nhưng property là data descriptor — không thể bypass bằng instance dict.

Chuỗi tra cứu thuộc tính (Attribute Lookup Chain)

Khi Python thực thi obj.attr, object.__getattribute__ tuân theo thứ tự:

obj.attr
  ├─ 1. Data descriptor trong type(obj).__mro__?  → gọi __get__  → DỪNG
  ├─ 2. Key trong obj.__dict__?                   → trả về value → DỪNG
  ├─ 3. Non-data descriptor trong type(obj).__mro__? → gọi __get__ → DỪNG
  ├─ 4. __getattr__ có định nghĩa?                → gọi nó       → DỪNG
  └─ 5. Raise AttributeError

Code chứng minh:

python
class DataD:
    def __get__(self, obj, objtype=None):
        return "Bước 1: Data descriptor"
    def __set__(self, obj, value):
        pass

class NonDataD:
    def __get__(self, obj, objtype=None):
        return "Bước 3: Non-data descriptor"

class MyClass:
    step1 = DataD()
    step3 = NonDataD()
    def __getattr__(self, name):
        if name == "step4":
            return "Bước 4: __getattr__"
        raise AttributeError(name)

obj = MyClass()

obj.__dict__["step1"] = "instance dict"
print(obj.step1)   # "Bước 1: Data descriptor" — data desc thắng

obj.__dict__["step3"] = "Bước 2: Instance dict"
print(obj.step3)   # "Bước 2: Instance dict" — instance dict thắng non-data

del obj.__dict__["step3"]
print(obj.step3)   # "Bước 3: Non-data descriptor"

print(obj.step4)   # "Bước 4: __getattr__" — fallback cuối cùng

@property — descriptor ẩn mình

@property là syntactic sugar cho data descriptor. Khi bạn viết @property, Python thực thi name = property(fget=name_function). Đây là cách tái tạo property từ đầu:

python
class MyProperty:
    """Tái tạo built-in property từ descriptor protocol."""

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        self.__doc__ = doc or (fget.__doc__ if fget else None)

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("thuộc tính không đọc được")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("thuộc tính không ghi được")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("thuộc tính không xóa được")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)


class Account:
    def __init__(self, balance: float):
        self._balance = balance

    @MyProperty
    def balance(self) -> float:
        return self._balance

    @balance.setter
    def balance(self, value: float) -> None:
        if value < 0:
            raise ValueError("Số dư không thể âm")
        self._balance = value

acc = Account(1000)
print(acc.balance)    # 1000
acc.balance = 500     # OK
acc.balance = -100    # ValueError: Số dư không thể âm

Chú ý setter() trả về instance mới — đây là lý do chain @balance.setter hoạt động: tạo property mới kế thừa fget cũ nhưng thêm fset mới.

__set_name__ — tự nhận biết tên (Python 3.6+)

Trước Python 3.6, descriptor không biết mình được gán cho attribute nào — phải truyền tên thủ công (viết tên 2 lần, dễ sai). __set_name__ giải quyết triệt để:

python
class AutoNamed:
    def __set_name__(self, owner, name):
        # owner = class chứa descriptor (vd: User)
        # name  = tên attribute (vd: "email")
        self.public_name = name
        self.private_name = f"_{name}"

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.private_name, None)

    def __set__(self, obj, value):
        setattr(obj, self.private_name, value)

class User:
    email = AutoNamed()    # __set_name__(User, "email") tự động
    username = AutoNamed() # __set_name__(User, "username") tự động

__set_name__ là bắt buộc cho mọi descriptor production-grade. Không có nó, bạn phải truyền tên attribute thủ công — vi phạm DRY và dễ gây bug.


Thực chiến

Tình huống: Xây dựng validation framework cho API schema — tương tự Pydantic/Django, nhưng dùng pure descriptor protocol.

Bộ validator descriptors

python
from typing import Any

class FieldDescriptor:
    """Base class cho validated fields."""

    def __init__(self, default: Any = None):
        self.default = default

    def __set_name__(self, owner, name):
        self.public_name = name
        self.private_name = f"_field_{name}"

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.private_name, self.default)

    def __set__(self, obj, value):
        value = self.validate(value)
        setattr(obj, self.private_name, value)

    def validate(self, value: Any) -> Any:
        return value


class TypeChecked(FieldDescriptor):
    def __init__(self, expected_type: type, **kwargs):
        super().__init__(**kwargs)
        self.expected_type = expected_type

    def validate(self, value: Any) -> Any:
        if not isinstance(value, self.expected_type):
            raise TypeError(
                f"{self.public_name}: cần {self.expected_type.__name__}, "
                f"nhận {type(value).__name__}"
            )
        return value


class RangeValidator(FieldDescriptor):
    def __init__(self, min_val: float = None, max_val: float = None, **kwargs):
        super().__init__(**kwargs)
        self.min_val = min_val
        self.max_val = max_val

    def validate(self, value: Any) -> Any:
        if not isinstance(value, (int, float)):
            raise TypeError(f"{self.public_name}: cần số")
        if self.min_val is not None and value < self.min_val:
            raise ValueError(f"{self.public_name}: {value} < {self.min_val}")
        if self.max_val is not None and value > self.max_val:
            raise ValueError(f"{self.public_name}: {value} > {self.max_val}")
        return value


class StringValidator(FieldDescriptor):
    def __init__(self, min_length: int = 0, max_length: int = None, **kwargs):
        super().__init__(**kwargs)
        self.min_length = min_length
        self.max_length = max_length

    def validate(self, value: Any) -> str:
        if not isinstance(value, str):
            raise TypeError(f"{self.public_name}: cần str")
        value = value.strip()
        if len(value) < self.min_length:
            raise ValueError(f"{self.public_name}: cần ít nhất {self.min_length} ký tự")
        if self.max_length and len(value) > self.max_length:
            raise ValueError(f"{self.public_name}: tối đa {self.max_length} ký tự")
        return value


class EmailValidator(StringValidator):
    def validate(self, value: Any) -> str:
        value = super().validate(value)
        if "@" not in value or "." not in value.split("@")[-1]:
            raise ValueError(f"{self.public_name}: email không hợp lệ — {value!r}")
        return value.lower()

Schema sử dụng descriptors

python
class UserSchema:
    username = StringValidator(min_length=3, max_length=30)
    email = EmailValidator(min_length=5, max_length=254)
    age = RangeValidator(min_val=0, max_val=150)
    role = StringValidator(min_length=1)

    def __init__(self, username: str, email: str, age: int, role: str = "user"):
        self.username = username
        self.email = email
        self.age = age
        self.role = role

    def __repr__(self) -> str:
        return (
            f"UserSchema(username={self.username!r}, email={self.email!r}, "
            f"age={self.age}, role={self.role!r})"
        )

# ✅ Dữ liệu hợp lệ
user = UserSchema("nguyen_van_a", "NguyenA@Company.VN", 28, "admin")
print(user)
# UserSchema(username='nguyen_van_a', email='nguyena@company.vn', age=28, role='admin')

# ❌ Validation bắt lỗi tự động
try:
    UserSchema("ab", "invalid", 200)
except ValueError as e:
    print(e)  # username: cần ít nhất 3 ký tự

try:
    user.age = -5
except ValueError as e:
    print(e)  # age: -5 < 0

So sánh: Pydantic v2 dùng Rust core cho performance, nhưng concept gốc tương tự descriptor. Descriptor approach cho nhiều quyền kiểm soát hơn và không phụ thuộc thư viện ngoài. Pydantic cho ecosystem mạnh hơn (JSON serialization, OpenAPI schema, nested models).


Sai lầm điển hình

SAI: Gán descriptor vào instance thay vì class

python
class Validator:
    def __get__(self, obj, objtype=None):
        return "validated"
    def __set__(self, obj, value):
        print(f"Validating: {value}")

# ❌ SAI
class User:
    def __init__(self):
        self.name = Validator()  # Gán vào instance!

u = User()
print(u.name)  # <Validator object> — __get__ KHÔNG được gọi!
python
# ✅ ĐÚNG — descriptor phải là class attribute
class User:
    name = Validator()

u = User()
u.name = "Alice"  # Validating: Alice

Hệ quả: Descriptor "im lặng" không hoạt động — validation bị bypass hoàn toàn. Data xấu đi thẳng vào database mà developer không hay biết.

SAI: Lưu state trong descriptor (shared state)

python
# ❌ SAI — tất cả instances chia sẻ cùng giá trị
class BadDescriptor:
    def __init__(self):
        self.value = None  # State thuộc descriptor!
    def __get__(self, obj, objtype=None):
        return self.value
    def __set__(self, obj, value):
        self.value = value

class Product:
    price = BadDescriptor()

p1, p2 = Product(), Product()
p1.price = 100
print(p2.price)  # 100 — Bug! p2 thấy giá trị của p1
python
# ✅ ĐÚNG — lưu state vào instance
class GoodDescriptor:
    def __set_name__(self, owner, name):
        self.attr_name = f"_{name}"
    def __get__(self, obj, objtype=None):
        return getattr(obj, self.attr_name, None) if obj else self
    def __set__(self, obj, value):
        setattr(obj, self.attr_name, value)

Hệ quả: Shared state gây race condition trong multi-threaded environment. User A thay đổi giá → User B thấy giá sai.

SAI: Quên if obj is None trong __get__

python
# ❌ SAI — crash khi truy cập từ class level
class Descriptor:
    def __set_name__(self, owner, name):
        self.name = f"_{name}"
    def __get__(self, obj, objtype=None):
        return getattr(obj, self.name)  # obj=None khi Class.attr!

class Config:
    debug = Descriptor()

Config.debug  # AttributeError: 'NoneType' has no attribute '_debug'
python
# ✅ ĐÚNG
def __get__(self, obj, objtype=None):
    if obj is None:
        return self  # Trả về descriptor object
    return getattr(obj, self.name, None)

Hệ quả: Django, Sphinx, pytest truy cập descriptor ở class level (introspection). Thiếu guard obj is None crash các tool này.

SAI: Non-data descriptor bị ghi đè bất ngờ

python
# ❌ SAI — non-data descriptor (chỉ __get__) bị override
class ComputedField:
    def __init__(self, func):
        self.func = func
    def __get__(self, obj, objtype=None):
        return self.func(obj) if obj else self

class Invoice:
    @ComputedField
    def total(self) -> float:
        return self.subtotal * (1 + self.tax_rate)

inv = Invoice()
inv.subtotal, inv.tax_rate = 1000, 0.1
inv.__dict__["total"] = 0      # Ghi đè!
print(inv.total)               # 0 — Descriptor bị bypass!
python
# ✅ ĐÚNG — thêm __set__ để bảo vệ
class ProtectedComputed(ComputedField):
    def __set__(self, obj, value):
        raise AttributeError("Không thể ghi đè computed field")

Hệ quả: Serialization library (pickle, JSON) ghi trực tiếp vào __dict__, bypass non-data descriptor. Computed value bị "đông cứng".


Under the Hood

CPython object.__getattribute__ — pseudocode

Khi Python thực thi obj.attr, CPython gọi type(obj).__getattribute__(obj, "attr"):

python
def object_getattribute(obj, name):
    # Tìm trong MRO
    mro_result = None
    for base in type(obj).__mro__:
        if name in base.__dict__:
            mro_result = base.__dict__[name]
            break

    # Bước 1: Data descriptor?
    if mro_result is not None:
        tp = type(mro_result)
        has_get = hasattr(tp, "__get__")
        has_set = hasattr(tp, "__set__") or hasattr(tp, "__delete__")
        if has_get and has_set:  # Data descriptor
            return tp.__get__(mro_result, obj, type(obj))

    # Bước 2: Instance __dict__?
    try:
        return obj.__dict__[name]
    except (KeyError, AttributeError):
        pass

    # Bước 3: Non-data descriptor hoặc class attribute?
    if mro_result is not None:
        tp = type(mro_result)
        if hasattr(tp, "__get__"):
            return tp.__get__(mro_result, obj, type(obj))
        return mro_result

    # Bước 4: __getattr__
    if hasattr(type(obj), "__getattr__"):
        return type(obj).__getattr__(obj, name)

    raise AttributeError(f"'{type(obj).__name__}' has no attribute '{name}'")

Function là non-data descriptor

python
def greet(self):
    return f"Xin chào, {self.name}"

print(hasattr(greet, "__get__"))   # True
print(hasattr(greet, "__set__"))   # False  → non-data descriptor

class Person:
    name = "Python"
    say_hi = greet

p = Person()
# Python gọi greet.__get__(p, Person) → bound method
print(p.say_hi())  # "Xin chào, Python"

Đây là cơ chế method binding: function.__get__ trả về bound method — callable đã "nhớ" instance self.

__slots__ = data descriptor

python
class Point:
    __slots__ = ("x", "y")

print(type(Point.x))                  # <class 'member_descriptor'>
print(hasattr(Point.x, "__get__"))     # True
print(hasattr(Point.x, "__set__"))     # True
print(hasattr(Point.x, "__delete__"))  # True

__slots__ tạo member_descriptor (data descriptor) cho mỗi slot và loại bỏ __dict__. Đây là lý do __slots__ tiết kiệm memory.

Performance benchmark

python
# Benchmark tương đối (CPython 3.12)
# Direct dict access:     ~40ns/access
# __slots__ access:       ~35ns/access  (nhanh hơn ~12%)
# property descriptor:    ~120ns/access (chậm hơn ~3x)
# Custom __get__:         ~200ns/access (chậm hơn ~5x)

Kết luận: Descriptor chậm hơn direct dict access, nhưng chênh lệch chỉ đáng kể trong tight loop hàng triệu lần. Trong production code thực tế, overhead descriptor không đáng kể so với I/O, database query, hay network latency.


✅ Checklist triển khai

Checklist ghi nhớ

Descriptor Protocol

  • [ ] Descriptor implement ít nhất một trong __get__, __set__, __delete__
  • [ ] Descriptor chỉ hoạt động khi gán như class attribute
  • [ ] __get__(self, obj, objtype)objNone khi truy cập từ class
  • [ ] __set_name__(self, owner, name) tự động gọi khi class được tạo (3.6+)

Data vs Non-data

  • [ ] Data descriptor = có __set__ và/hoặc __delete__ → ưu tiên cao hơn instance __dict__
  • [ ] Non-data descriptor = chỉ __get__ → instance __dict__ override được
  • [ ] property là data descriptor; function là non-data descriptor
  • [ ] __slots__ tạo member_descriptor — data descriptor

Attribute Lookup

  • [ ] Thứ tự: data descriptor → instance __dict__ → non-data descriptor → __getattr__
  • [ ] object.__getattribute__ cho instance; type.__getattribute__ cho class
  • [ ] __getattr__ chỉ được gọi khi tất cả bước trước thất bại

Production Patterns

  • [ ] Luôn lưu state vào instance (setattr(obj, ...)) — không lưu trong descriptor
  • [ ] Luôn guard if obj is None: return self trong __get__
  • [ ] Dùng __set_name__ thay vì truyền tên attribute thủ công
  • [ ] Thêm __set__ nếu cần bảo vệ descriptor khỏi bị override bởi instance dict

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

Bài 1: CachedProperty — Lazy evaluation (Intermediate)

Implement descriptor CachedProperty: tính toán lần đầu, cache vào instance __dict__, hỗ trợ del để reset cache.

python
class CachedProperty:
    # TODO: Implement
    pass

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

    @CachedProperty
    def mean(self) -> float:
        print("Tính mean...")
        return sum(self.data) / len(self.data)

    @CachedProperty
    def variance(self) -> float:
        print("Tính variance...")
        m = self.mean
        return sum((x - m) ** 2 for x in self.data) / len(self.data)

# Kỳ vọng:
analyzer = DataAnalyzer([1, 2, 3, 4, 5])
print(analyzer.mean)      # "Tính mean..." → 3.0
print(analyzer.mean)      # 3.0 (không tính lại)
del analyzer.mean          # Xóa cache
print(analyzer.mean)      # "Tính mean..." → 3.0
💡 Lời giải Bài 1
python
class CachedProperty:
    def __init__(self, func):
        self.func = func
        self.attr_name = None
        self.__doc__ = func.__doc__

    def __set_name__(self, owner, name):
        self.attr_name = name

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.attr_name in obj.__dict__:
            return obj.__dict__[self.attr_name]
        value = self.func(obj)
        obj.__dict__[self.attr_name] = value
        return value

    def __delete__(self, obj):
        obj.__dict__.pop(self.attr_name, None)

Giải thích: Trick ở đây — CachedProperty chủ ý để instance __dict__ "che" descriptor sau lần tính đầu. __delete__ xóa key khỏi obj.__dict__, lần truy cập sau __get__ lại được gọi.

Bài 2: TypeEnforced — Runtime type checking (Advanced)

Implement descriptor enforce type tại runtime, hỗ trợ union type và nullable:

python
class TypeEnforced:
    # TODO: Implement
    pass

class APIRequest:
    url = TypeEnforced(str)
    timeout = TypeEnforced((int, float), nullable=True)
    headers = TypeEnforced(dict, default_factory=dict)

    def __init__(self, url: str, timeout=None, headers=None):
        self.url = url
        self.timeout = timeout
        if headers is not None:
            self.headers = headers

# Kỳ vọng:
req = APIRequest("https://api.example.com")
req.timeout = 30      # OK
req.timeout = None     # OK (nullable)
req.timeout = "30"     # TypeError
req.url = 123          # TypeError
💡 Lời giải Bài 2
python
class TypeEnforced:
    def __init__(self, expected_types, nullable: bool = False,
                 default=None, default_factory=None):
        if isinstance(expected_types, type):
            expected_types = (expected_types,)
        self.expected_types = expected_types
        self.nullable = nullable
        self.default = default
        self.default_factory = default_factory

    def __set_name__(self, owner, name):
        self.public_name = name
        self.private_name = f"_typed_{name}"

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if not hasattr(obj, self.private_name):
            if self.default_factory:
                value = self.default_factory()
                setattr(obj, self.private_name, value)
                return value
            return self.default
        return getattr(obj, self.private_name)

    def __set__(self, obj, value):
        if value is None:
            if not self.nullable:
                raise TypeError(f"{self.public_name}: không chấp nhận None")
            setattr(obj, self.private_name, None)
            return
        if not isinstance(value, self.expected_types):
            names = "/".join(t.__name__ for t in self.expected_types)
            raise TypeError(
                f"{self.public_name}: cần {names}, nhận {type(value).__name__}"
            )
        setattr(obj, self.private_name, value)

    def __delete__(self, obj):
        if hasattr(obj, self.private_name):
            delattr(obj, self.private_name)

Giải thích: default_factory giải quyết mutable default problem (tương tự dataclasses.field). Mỗi instance nhận copy riêng. Pattern nullable tách biệt "không có giá trị" với "giá trị sai type" — quan trọng cho API schema.

Quiz: Attribute Lookup

🧠 Quiz

Câu hỏi: Cho đoạn code sau, print(obj.x) in ra gì?

python
class Desc:
    def __get__(self, obj, objtype=None):
        return "descriptor"

class MyClass:
    x = Desc()

obj = MyClass()
obj.__dict__["x"] = "instance"
print(obj.x)
  • A) "descriptor"
  • B) "instance"
  • C) AttributeError
  • D) None

Đáp án: BDesc chỉ có __get__non-data descriptor. Instance __dict__ ưu tiên cao hơn. Nếu Desc có thêm __set__, đáp án sẽ là A.