Python Specific
Descriptors
When building frameworks, ORMs, or validation systems, you need fine-grained control over attribute access. Descriptors provide the low-level mechanism that powers @property, class methods, and ORM field definitions, letting you intercept and customize get, set, and delete operations.
Descriptors are a low-level mechanism for customizing attribute access. A descriptor is any object that implements __get__(), __set__(), or __delete__(). Properties are actually implemented using the descriptor protocol.
The Descriptor Protocol
"""Basic descriptor protocol"""
# Simple descriptor
print("Simple descriptor:")
class Descriptor:
def __get__(self, obj, type=None):
print(f" __get__ called: obj={obj}, type={type}")
return 42
def __set__(self, obj, value):
print(f" __set__ called: obj={obj}, value={value}")
class MyClass:
attr = Descriptor()
obj = MyClass()
# Getting attribute
print("Getting attribute:")
value = obj.attr
print(f"value={value}")
# Setting attribute
print("\nSetting attribute:")
obj.attr = 100
# Storing values
print("\nStoring values:")
class ValueDescriptor:
def __init__(self):
self.value = None
def __get__(self, obj, type=None):
print(f" Getting value: {self.value}")
return self.value
def __set__(self, obj, value):
print(f" Setting value: {value}")
self.value = value
class Container:
data = ValueDescriptor()
c = Container()
c.data = 123
print(f"Retrieved: {c.data}")
# Multiple instances problem
print("\nMultiple instances problem:")
# Problem: shared descriptor instance
c1 = Container()
c2 = Container()
c1.data = "first"
c2.data = "second"
print(f"c1.data = {c1.data}") # Will be "second"!
print(f"c2.data = {c2.data}")
print(" Note: Both share same descriptor instance")
# Proper storage
print("\nProper storage:")
class ProperDescriptor:
def __init__(self, name):
self.name = name
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
obj.__dict__[self.name] = value
class Person:
name = ProperDescriptor('name')
age = ProperDescriptor('age')
p1 = Person()
p2 = Person()
p1.name = "Alice"
p1.age = 30
p2.name = "Bob"
p2.age = 25
print(f"p1: {p1.name}, {p1.age}")
print(f"p2: {p2.name}, {p2.age}")
# Class vs instance access
print("\nClass vs instance access:")
class SmartDescriptor:
def __get__(self, obj, type=None):
if obj is None:
return f"Descriptor accessed from class {type.__name__}"
return f"Descriptor accessed from instance of {type.__name__}"
class MyClass:
attr = SmartDescriptor()
# Access from class
print(f"MyClass.attr: {MyClass.attr}")
# Access from instance
obj = MyClass()
print(f"obj.attr: {obj.attr}")
# Read-only descriptor
print("\nRead-only descriptor:")
class ReadOnlyDescriptor:
def __init__(self, value):
self.value = value
def __get__(self, obj, type=None):
return self.value
def __set__(self, obj, value):
raise AttributeError("Cannot modify read-only attribute")
class Config:
VERSION = ReadOnlyDescriptor("1.0.0")
config = Config()
print(f"Version: {config.VERSION}")
try:
config.VERSION = "2.0.0"
except AttributeError as e:
print(f"Error: {e}")
# Descriptor with state
print("\nDescriptor with state:")
class CountedDescriptor:
def __init__(self, name):
self.name = name
self.access_count = 0
def __get__(self, obj, type=None):
self.access_count += 1
print(f" Access #{self.access_count}")
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
obj.__dict__[self.name] = value
class TrackedClass:
value = CountedDescriptor('value')
tracked = TrackedClass()
tracked.value = 100
print("Accessing value multiple times:")
print(tracked.value)
print(tracked.value)
print(tracked.value)
# Delete support
print("\nDelete support:")
class DeletableDescriptor:
def __init__(self, name):
self.name = name
def __get__(self, obj, type=None):
return obj.__dict__.get(self.name, "Not set")
def __set__(self, obj, value):
obj.__dict__[self.name] = value
def __delete__(self, obj):
print(f" Deleting {self.name}")
if self.name in obj.__dict__:
del obj.__dict__[self.name]
class MyClass:
attr = DeletableDescriptor('attr')
obj = MyClass()
obj.attr = "value"
print(f"attr = {obj.attr}")
del obj.attr
print(f"After delete: {obj.attr}")
# Practical example
print("\nPractical example:")
class TypedDescriptor:
def __init__(self, name, expected_type):
self.name = name
self.expected_type = expected_type
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if not isinstance(value, self.expected_type):
raise TypeError(f"{self.name} must be {self.expected_type.__name__}")
obj.__dict__[self.name] = value
class User:
name = TypedDescriptor('name', str)
age = TypedDescriptor('age', int)
email = TypedDescriptor('email', str)
user = User()
user.name = "Alice"
user.age = 30
user.email = "alice@example.com"
print(f"User: {user.name}, {user.age}, {user.email}")
try:
user.age = "thirty"
except TypeError as e:
print(f"Error: {e}")
attr ← (empty), obj ← ⟨MyClass A⟩
1"""Basic descriptor protocol"""23# Simple descriptor4print("Simple descriptor:")56class Descriptor:7 def __get__(self, obj, type=None):8 print(f" __get__ called: obj={obj}, type={type}")9 return 4210 11 def __set__(self, obj, value):12 print(f" __set__ called: obj={obj}, value={value}")1314class MyClass:15 attr→ (empty) = Descriptor()1617obj→ ⟨MyClass A⟩ = MyClass()1819# Getting attribute20print("Getting attribute:")21value = obj.attr4222print(f"value={value}")outputSimple descriptor: Getting attribute:def __get__(self, obj, type=None):
6class Descriptor:7 def __get__(self⟨Descriptor B⟩, obj⟨MyClass A⟩, type<class '__main__.MyClass'>=NoneNone):8 print(f" __get__ called: obj={obj⟨MyClass A⟩}, type={type}")9 return 42output __get__ called: obj=⟨MyClass A⟩, type=<class '__main__.MyClass'>value ← 42
20print("Getting attribute:")21value→ 42 = obj.attr4222print(f"value={value42}")2324# Setting attribute25print("\nSetting attribute:")26obj.attr = 100outputvalue=42 Setting attribute:def __set__(self, obj, value):
11def __set__(self⟨Descriptor B⟩, obj⟨MyClass A⟩, value100):12 print(f" __set__ called: obj={obj⟨MyClass A⟩}, value={value100}")output __set__ called: obj=⟨MyClass A⟩, value=100obj.attr ← 42
25print("\nSetting attribute:")26obj.attr→ 42 = 1002728# Storing values29print("\nStoring values:")3031class ValueDescriptor:32 def __init__(self):33 self.value = None34 35 def __get__(self, obj, type=None):36 print(f" Getting value: {self.value}")37 return self.value38 39 def __set__(self, obj, value):40 print(f" Setting value: {value}")41 self.value = value4243class Container:44 data = ValueDescriptor()output Storing values:self.value ← None
31class ValueDescriptor:32 def __init__(self⟨ValueDescriptor C⟩):33 self.value→ None = Nonedata ← (empty), c ← ⟨Container D⟩
43class Container:44 data→ (empty) = ValueDescriptor()4546c→ ⟨Container D⟩ = Container()47c.data = 12348print(f"Retrieved: {c.data}")self.value ← 123
pass 1 of 339def __set__(self⟨ValueDescriptor C⟩, obj⟨Container D⟩, value123):40 print(f" Setting value: {value123}")41 self.value→ 123 = value123output Setting value: 123All 3 passes — pass 1 is the card above pass objvalueself.value1 ⟨Container D⟩ 123 123 2 ⟨Container E⟩ first first 3 ⟨Container F⟩ second second c.data ← 123
46c = Container()47c.data→ 123 = 12348print(f"Retrieved: {c.data123}")def __get__(self, obj, type=None):
pass 1 of 335def __get__(self⟨ValueDescriptor C⟩, obj⟨Container D⟩, type<class '__main__.Container'>=NoneNone):36 print(f" Getting value: {self.value123}")37 return self.value123output Getting value: 123All 3 passes — pass 1 is the card above pass objself.value1 ⟨Container D⟩ 123 2 ⟨Container E⟩ second 3 ⟨Container F⟩ second c1 ← ⟨Container E⟩, c2 ← ⟨Container F⟩
47c.data = 12348print(f"Retrieved: {c.data123}")4950# Multiple instances problem51print("\nMultiple instances problem:")5253# Problem: shared descriptor instance54c1→ ⟨Container E⟩ = Container()55c2→ ⟨Container F⟩ = Container()5657c1.data = "first"58c2.data = "second"outputRetrieved: 123 Multiple instances problem:c1.data ← first
57c1.data→ first = "first"58c2.data = "second"c2.data ← second
57c1.data = "first"58c2.data→ second = "second"5960print(f"c1.data = {c1.datasecond}") # Will be "second"!61print(f"c2.data = {c2.data}")print(f"c1.data = {c1.data}") # Will be "second"!
60print(f"c1.data = {c1.datasecond}") # Will be "second"!61print(f"c2.data = {c2.datasecond}")62print(" Note: Both share same descriptor instance")outputc1.data = secondprint(f"c2.data = {c2.data}")
60print(f"c1.data = {c1.data}") # Will be "second"!61print(f"c2.data = {c2.datasecond}")62print(" Note: Both share same descriptor instance")6364# Proper storage65print("\nProper storage:")6667class ProperDescriptor:68 def __init__(self, name):69 self.name = name70 71 def __get__(self, obj, type=None):72 if obj is None:73 return self74 return obj.__dict__.get(self.name)75 76 def __set__(self, obj, value):77 obj.__dict__[self.name] = value7879class Person:80 name = ProperDescriptor('name')81 age = ProperDescriptor('age')outputc2.data = second Note: Both share same descriptor instance Proper storage:self.name ← name
pass 1 of 267class ProperDescriptor:68 def __init__(self⟨ProperDescriptor G⟩, namename):69 self.name→ name = namenamename ← (empty)
79class Person:80 name→ (empty) = ProperDescriptor('name')81 age = ProperDescriptor('age')self.name ← age
pass 2 of 267class ProperDescriptor:68 def __init__(self⟨ProperDescriptor H⟩, nameage):69 self.name→ age = nameageage ← (empty), p1 ← ⟨Person I⟩, p2 ← ⟨Person J⟩
80 name = ProperDescriptor('name')81 age→ (empty) = ProperDescriptor('age')8283p1→ ⟨Person I⟩ = Person()84p2→ ⟨Person J⟩ = Person()8586p1.name = "Alice"87p1.age = 30obj.__dict__[self.name] ← Alice
pass 1 of 476def __set__(self⟨ProperDescriptor G⟩, obj⟨Person I⟩, valueAlice):77 obj.__dict__[self.name]→ Alice = valueAliceAll 4 passes — pass 1 is the card above pass selfobjvalueobj.__dict__[self.name]1 ⟨ProperDescriptor G⟩ ⟨Person I⟩ Alice Alice 2 ⟨ProperDescriptor H⟩ ⟨Person I⟩ 30 30 3 ⟨ProperDescriptor G⟩ ⟨Person J⟩ Bob Bob 4 ⟨ProperDescriptor H⟩ ⟨Person J⟩ 25 25 p1.name ← Alice
86p1.name→ Alice = "Alice"87p1.age = 30p1.age ← 30
86p1.name = "Alice"87p1.age→ 30 = 308889p2.name = "Bob"90p2.age = 25p2.name ← Bob
89p2.name→ Bob = "Bob"90p2.age = 25p2.age ← 25
89p2.name = "Bob"90p2.age→ 25 = 259192print(f"p1: {p1.nameAlice}, {p1.age30}")93print(f"p2: {p2.name}, {p2.age}")def __get__(self, obj, type=None):
pass 1 of 471def __get__(self⟨ProperDescriptor G⟩, obj⟨Person I⟩, type<class '__main__.Person'>=NoneNone):72 if obj is None:73 return self74 return obj.__dict__{'name': 'Alice', 'age': 30}.get(self.namename)All 4 passes — pass 1 is the card above pass selfobjobj.__dict__self.name1 ⟨ProperDescriptor G⟩ ⟨Person I⟩ {'name': 'Alice', 'age': 30} name 2 ⟨ProperDescriptor H⟩ ⟨Person I⟩ {'name': 'Alice', 'age': 30} age 3 ⟨ProperDescriptor G⟩ ⟨Person J⟩ {'name': 'Bob', 'age': 25} name 4 ⟨ProperDescriptor H⟩ ⟨Person J⟩ {'name': 'Bob', 'age': 25} age print(f"p1: {p1.name}, {p1.age}")
92print(f"p1: {p1.nameAlice}, {p1.age30}")93print(f"p2: {p2.nameBob}, {p2.age25}")outputp1: Alice, 30attr ← (empty)
92print(f"p1: {p1.name}, {p1.age}")93print(f"p2: {p2.nameBob}, {p2.age25}")9495# Class vs instance access96print("\nClass vs instance access:")9798class SmartDescriptor:99 def __get__(self, obj, type=None):100 if obj is None:101 return f"Descriptor accessed from class {type.__name__}"102 return f"Descriptor accessed from instance of {type.__name__}"103104class MyClass:105 attr→ (empty) = SmartDescriptor()106107# Access from class108print(f"MyClass.attr: {MyClass.attrDescriptor accessed from class MyClass}")outputp2: Bob, 25 Class vs instance access:def __get__(self, obj, type=None):
pass 1 of 298class SmartDescriptor:99 def __get__(self⟨SmartDescriptor K⟩, objNone, type<class '__main__.MyClass'>=NoneNone):100 if obj is None:101 return f"Descriptor accessed from class {type.__name__}"if obj is None:
99def __get__(self, obj, type=None):100 if objNone is None:101 return f"Descriptor accessed from class {type.__name__MyClass}"102 return f"Descriptor accessed from instance of {type.__name__}"obj ← ⟨MyClass L⟩
107# Access from class108print(f"MyClass.attr: {MyClass.attrDescriptor accessed from class MyClass}")109110# Access from instance111obj→ ⟨MyClass L⟩ = MyClass()112print(f"obj.attr: {obj.attrDescriptor accessed from instance of MyClass}")outputMyClass.attr: Descriptor accessed from class MyClassdef __get__(self, obj, type=None):
pass 2 of 298class SmartDescriptor:99 def __get__(self⟨SmartDescriptor K⟩, obj⟨MyClass L⟩, type<class '__main__.MyClass'>=NoneNone):100 if obj is None:101 return f"Descriptor accessed from class {type.__name__}"102 return f"Descriptor accessed from instance of {type.__name__MyClass}"print(f"obj.attr: {obj.attr}")
111obj = MyClass()112print(f"obj.attr: {obj.attrDescriptor accessed from instance of MyClass}")113114# Read-only descriptor115print("\nRead-only descriptor:")116117class ReadOnlyDescriptor:118 def __init__(self, value):119 self.value = value120 121 def __get__(self, obj, type=None):122 return self.value123 124 def __set__(self, obj, value):125 raise AttributeError("Cannot modify read-only attribute")126127class Config:128 VERSION = ReadOnlyDescriptor("1.0.0")outputobj.attr: Descriptor accessed from instance of MyClass Read-only descriptor:self.value ← 1.0.0
117class ReadOnlyDescriptor:118 def __init__(self⟨ReadOnlyDescriptor A⟩, value1.0.0):119 self.value→ 1.0.0 = value1.0.0VERSION ← (empty), config ← ⟨Config M⟩
127class Config:128 VERSION→ (empty) = ReadOnlyDescriptor("1.0.0")129130config→ ⟨Config M⟩ = Config()131print(f"Version: {config.VERSION1.0.0}")def __get__(self, obj, type=None):
121def __get__(self⟨ReadOnlyDescriptor A⟩, obj⟨Config M⟩, type<class '__main__.Config'>=NoneNone):122 return self.value1.0.0print(f"Version: {config.VERSION}")
130config = Config()131print(f"Version: {config.VERSION1.0.0}")outputVersion: 1.0.0def __set__(self, obj, value):
124def __set__(self⟨ReadOnlyDescriptor A⟩, obj⟨Config M⟩, value2.0.0):125 raise AttributeError("Cannot modify read-only attribute")except AttributeError as e:
134 config.VERSION = "2.0.0"135except AttributeError as e:136 print(f"Error: {eCannot modify read-only attribute}")outputError: Cannot modify read-only attributeprint(" Descriptor with state:")
138# Descriptor with state139print("\nDescriptor with state:")140141class CountedDescriptor:142 def __init__(self, name):143 self.name = name144 self.access_count = 0145 146 def __get__(self, obj, type=None):147 self.access_count += 1148 print(f" Access #{self.access_count}")149 return obj.__dict__.get(self.name)150 151 def __set__(self, obj, value):152 obj.__dict__[self.name] = value153154class TrackedClass:155 value = CountedDescriptor('value')output Descriptor with state:self.name ← value, self.access_count ← 0
141class CountedDescriptor:142 def __init__(self⟨CountedDescriptor N⟩, namevalue):143 self.name→ value = namevalue144 self.access_count→ 0 = 0value ← 42, tracked ← ⟨TrackedClass O⟩
154class TrackedClass:155 value→ 42 = CountedDescriptor('value')156157tracked→ ⟨TrackedClass O⟩ = TrackedClass()158tracked.value = 100obj.__dict__[self.name] ← 100
151def __set__(self⟨CountedDescriptor N⟩, obj⟨TrackedClass O⟩, value100):152 obj.__dict__[self.name]→ 100 = value100tracked.value ← 100
157tracked = TrackedClass()158tracked.value→ 100 = 100159160print("Accessing value multiple times:")161print(tracked.value100)162print(tracked.value)outputAccessing value multiple times:self.access_count ← 3
pass 1 of 3146def __get__(self⟨CountedDescriptor N⟩, obj⟨TrackedClass O⟩, type<class '__main__.TrackedClass'>=NoneNone):147 self.access_count→ 3 += 1148 print(f" Access #{self.access_count3}")149 return obj.__dict__{'value': 100}.get(self.namevalue)output Access #3All 3 passes — pass 1 is the card above pass self.access_count1 2 → 3 2 5 → 6 3 8 → 9 print(tracked.value)
160print("Accessing value multiple times:")161print(tracked.value100)162print(tracked.value100)163print(tracked.value)output100print(tracked.value)
161print(tracked.value)162print(tracked.value100)163print(tracked.value100)output100print(tracked.value)
162print(tracked.value)163print(tracked.value100)164165# Delete support166print("\nDelete support:")167168class DeletableDescriptor:169 def __init__(self, name):170 self.name = name171 172 def __get__(self, obj, type=None):173 return obj.__dict__.get(self.name, "Not set")174 175 def __set__(self, obj, value):176 obj.__dict__[self.name] = value177 178 def __delete__(self, obj):179 print(f" Deleting {self.name}")180 if self.name in obj.__dict__:181 del obj.__dict__[self.name]182183class MyClass:184 attr = DeletableDescriptor('attr')output100 Delete support:self.name ← attr
168class DeletableDescriptor:169 def __init__(self⟨DeletableDescriptor P⟩, nameattr):170 self.name→ attr = nameattrattr ← (empty), obj ← ⟨MyClass Q⟩
183class MyClass:184 attr→ (empty) = DeletableDescriptor('attr')185186obj→ ⟨MyClass Q⟩ = MyClass()187obj.attr = "value"188print(f"attr = {obj.attr}")obj.__dict__[self.name] ← value
175def __set__(self⟨DeletableDescriptor P⟩, obj⟨MyClass Q⟩, valuevalue):176 obj.__dict__[self.name]→ value = valuevalueobj.attr ← value
186obj = MyClass()187obj.attr→ value = "value"188print(f"attr = {obj.attrvalue}")def __get__(self, obj, type=None):
pass 1 of 2172def __get__(self⟨DeletableDescriptor P⟩, obj⟨MyClass Q⟩, type<class '__main__.MyClass'>=NoneNone):173 return obj.__dict__{'attr': 'value'}.get(self.nameattr, "Not set")print(f"attr = {obj.attr}")
187obj.attr = "value"188print(f"attr = {obj.attrvalue}")outputattr = valuedef __delete__(self, obj):
178def __delete__(self⟨DeletableDescriptor P⟩, obj⟨MyClass Q⟩):179 print(f" Deleting {self.nameattr}")180 if self.name in obj.__dict__:output Deleting attrobj.__dict__[self.name] ← (empty)
179print(f" Deleting {self.name}")180if self.nameattr in obj.__dict__{'attr': 'value'}:181 del obj.__dict__[self.name]→ (empty)print(f"After delete: {obj.attr}")
190del obj.attr191print(f"After delete: {obj.attrNot set}")def __get__(self, obj, type=None):
pass 2 of 2172def __get__(self⟨DeletableDescriptor P⟩, obj⟨MyClass Q⟩, type<class '__main__.MyClass'>=NoneNone):173 return obj.__dict__{}.get(self.nameattr, "Not set")print(f"After delete: {obj.attr}")
190del obj.attr191print(f"After delete: {obj.attrNot set}")192193# Practical example194print("\nPractical example:")195196class TypedDescriptor:197 def __init__(self, name, expected_type):198 self.name = name199 self.expected_type = expected_type200 201 def __get__(self, obj, type=None):202 if obj is None:203 return self204 return obj.__dict__.get(self.name)205 206 def __set__(self, obj, value):207 if not isinstance(value, self.expected_type):208 raise TypeError(f"{self.name} must be {self.expected_type.__name__}")209 obj.__dict__[self.name] = value210211class User:212 name = TypedDescriptor('name', str)213 age = TypedDescriptor('age', int)outputAfter delete: Not set Practical example:self.name ← name, self.expected_type ← <class 'str'>
pass 1 of 3196class TypedDescriptor:197 def __init__(self⟨TypedDescriptor L⟩, namename, expected_type<class 'str'>):198 self.name→ name = namename199 self.expected_type→ <class 'str'> = expected_type<class 'str'>All 3 passes — pass 1 is the card above pass selfnameexpected_typeself.nameself.expected_type1 ⟨TypedDescriptor L⟩ name <class 'str'> name <class 'str'> 2 ⟨TypedDescriptor R⟩ age <class 'int'> age <class 'int'> 3 ⟨TypedDescriptor S⟩ email <class 'str'> email <class 'str'> name ← (empty)
211class User:212 name→ (empty) = TypedDescriptor('name', str)213 age = TypedDescriptor('age', int)214 email = TypedDescriptor('email', str)age ← (empty)
212name = TypedDescriptor('name', str)213age→ (empty) = TypedDescriptor('age', int)214email = TypedDescriptor('email', str)email ← (empty), user ← ⟨User T⟩
213 age = TypedDescriptor('age', int)214 email→ (empty) = TypedDescriptor('email', str)215216user→ ⟨User T⟩ = User()217user.name = "Alice"218user.age = 30obj.__dict__[self.name] ← Alice
pass 1 of 4206def __set__(self⟨TypedDescriptor L⟩, obj⟨User T⟩, valueAlice):207 if not isinstance(value, self.expected_type):208 raise TypeError(f"{self.name} must be {self.expected_type.__name__}")209 obj.__dict__[self.name]→ Alice = valueAliceAll 4 passes — pass 1 is the card above pass selfvalueself.expected_typeself.nameself.expected_type.__name__eobj.__dict__[self.name]1 ⟨TypedDescriptor L⟩ Alice — — — — Alice 2 ⟨TypedDescriptor R⟩ 30 — — — — 30 3 ⟨TypedDescriptor S⟩ alice@example.com — — — — alice@example.com 4 ⟨TypedDescriptor R⟩ thirty <class 'int'> age int age must be int — user.name ← Alice
216user = User()217user.name→ Alice = "Alice"218user.age = 30219user.email = "alice@example.com"user.age ← 30
217user.name = "Alice"218user.age→ 30 = 30219user.email = "alice@example.com"user.email ← alice@example.com
218user.age = 30219user.email→ alice@example.com = "alice@example.com"220221print(f"User: {user.nameAlice}, {user.age30}, {user.emailalice@example.com}")def __get__(self, obj, type=None):
pass 1 of 3201def __get__(self⟨TypedDescriptor L⟩, obj⟨User T⟩, type<class '__main__.User'>=NoneNone):202 if obj is None:203 return self204 return obj.__dict__{'name': 'Alice', 'age': 30, 'email': 'alice@example.com'}.get(self.namename)All 3 passes — pass 1 is the card above pass selfself.name1 ⟨TypedDescriptor L⟩ name 2 ⟨TypedDescriptor R⟩ age 3 ⟨TypedDescriptor S⟩ email print(f"User: {user.name}, {user.age}, {user.email}")
221print(f"User: {user.nameAlice}, {user.age30}, {user.emailalice@example.com}")outputUser: Alice, 30, alice@example.comif not isinstance(value, self.expected_type):
206def __set__(self, obj, value):207 if not isinstance(valuethirty, self.expected_type<class 'int'>):208 raise TypeError(f"{self.nameage} must be {self.expected_type.__name__int}")209 obj.__dict__[self.name] = valueexcept TypeError as e:
224 user.age = "thirty"225except TypeError as e:226 print(f"Error: {eage must be int}")outputError: age must be int
The key insight: descriptors are defined on the class but called when accessing attributes on instances. The obj parameter tells you which instance is being accessed.
Property as a Descriptor
"""Property as a descriptor"""
# Property implementation
print("Property implementation:")
# This is roughly how @property works
class PropertyDescriptor:
def __init__(self, fget=None, fset=None, fdel=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
def __get__(self, obj, type=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
def get_celsius(self):
return self._celsius
def set_celsius(self, value):
self._celsius = value
celsius = PropertyDescriptor(get_celsius, set_celsius)
temp = Temperature(25)
print(f"Celsius: {temp.celsius}")
temp.celsius = 30
print(f"Updated: {temp.celsius}")
# Standard @property
print("\nStandard @property:")
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Get radius"""
return self._radius
@radius.setter
def radius(self, value):
"""Set radius"""
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@property
def area(self):
"""Computed area"""
import math
return math.pi * self._radius ** 2
circle = Circle(5)
print(f"Radius: {circle.radius}")
print(f"Area: {circle.area:.2f}")
circle.radius = 10
print(f"New radius: {circle.radius}")
print(f"New area: {circle.area:.2f}")
# Read-only property
print("\nRead-only property:")
class Person:
def __init__(self, first_name, last_name):
self._first_name = first_name
self._last_name = last_name
@property
def full_name(self):
"""Read-only computed property"""
return f"{self._first_name} {self._last_name}"
person = Person("Alice", "Smith")
print(f"Full name: {person.full_name}")
try:
person.full_name = "Bob Jones"
except AttributeError as e:
print(f"Error: {e}")
# Cached property
print("\nCached property:")
class CachedProperty:
def __init__(self, func):
self.func = func
self.name = func.__name__
def __get__(self, obj, type=None):
if obj is None:
return self
# Check if cached
cache_name = f'_cached_{self.name}'
if cache_name not in obj.__dict__:
# Compute and cache
print(f" Computing {self.name}...")
obj.__dict__[cache_name] = self.func(obj)
return obj.__dict__[cache_name]
class DataProcessor:
def __init__(self, data):
self.data = data
@CachedProperty
def expensive_result(self):
# Expensive computation
return sum(x ** 2 for x in self.data)
processor = DataProcessor([1, 2, 3, 4, 5])
print("First access:")
result1 = processor.expensive_result
print(f"Result: {result1}")
print("\nSecond access (cached):")
result2 = processor.expensive_result
print(f"Result: {result2}")
# Validated property
print("\nValidated property:")
class ValidatedProperty:
def __init__(self, name, validator):
self.name = name
self.validator = validator
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if not self.validator(value):
raise ValueError(f"Invalid value for {self.name}: {value}")
obj.__dict__[self.name] = value
def is_positive(value):
return value > 0
def is_valid_email(value):
return "@" in value and "." in value
class Account:
balance = ValidatedProperty('balance', is_positive)
email = ValidatedProperty('email', is_valid_email)
account = Account()
account.balance = 100
account.email = "user@example.com"
print(f"Balance: {account.balance}")
print(f"Email: {account.email}")
try:
account.balance = -50
except ValueError as e:
print(f"Error: {e}")
# Lazy property
print("\nLazy property:")
class LazyProperty:
def __init__(self, func):
self.func = func
self.name = func.__name__
def __get__(self, obj, type=None):
if obj is None:
return self
# Compute once and replace descriptor with value
value = self.func(obj)
setattr(obj, self.name, value)
return value
class Resource:
def __init__(self, filename):
self.filename = filename
@LazyProperty
def content(self):
print(f" Loading {self.filename}...")
return f"Content of {self.filename}"
resource = Resource("data.txt")
print("Resource created")
print("\nFirst access:")
print(resource.content)
print("\nSecond access:")
print(resource.content)
# Practical example
print("\nPractical example:")
# Combining property with validation
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError("Width must be positive")
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value <= 0:
raise ValueError("Height must be positive")
self._height = value
@property
def area(self):
"""Computed property"""
return self._width * self._height
@property
def perimeter(self):
"""Computed property"""
return 2 * (self._width + self._height)
rect = Rectangle(10, 5)
print(f"Rectangle: {rect.width}x{rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
rect.width = 20
print(f"Updated rectangle: {rect.width}x{rect.height}")
print(f"New area: {rect.area}")
celsius = PropertyDescriptor(get_celsius, set_celsius)
1"""Property as a descriptor"""23# Property implementation4print("Property implementation:")56# This is roughly how @property works7class PropertyDescriptor:8 def __init__(self, fget=None, fset=None, fdel=None):9 self.fget = fget10 self.fset = fset11 self.fdel = fdel12 13 def __get__(self, obj, type=None):14 if obj is None:15 return self16 if self.fget is None:17 raise AttributeError("unreadable attribute")18 return self.fget(obj)19 20 def __set__(self, obj, value):21 if self.fset is None:22 raise AttributeError("can't set attribute")23 self.fset(obj, value)24 25 def __delete__(self, obj):26 if self.fdel is None:27 raise AttributeError("can't delete attribute")28 self.fdel(obj)2930class Temperature:31 def __init__(self, celsius):32 self._celsius = celsius33 34 def get_celsius(self):35 return self._celsius36 37 def set_celsius(self, value):38 self._celsius = value39 40 celsius = PropertyDescriptor(get_celsius(empty), set_celsius(empty))outputProperty implementation:self.fget ← ⟨function Temperature.get_celsius A⟩, self.fset ← ⟨function Temperature.set_celsius B⟩
7class PropertyDescriptor:8 def __init__(self⟨PropertyDescriptor C⟩, fget⟨function Temperature.get_celsius A⟩=NoneNone, fset⟨function Temperature.set_celsius B⟩=None, fdelNone=None):9 self.fget→ ⟨function Temperature.get_celsius A⟩ = fget⟨function Temperature.get_celsius A⟩10 self.fset→ ⟨function Temperature.set_celsius B⟩ = fset⟨function Temperature.set_celsius B⟩11 self.fdel→ None = fdelNonecelsius ← (empty)
40 celsius→ (empty) = PropertyDescriptor(get_celsius(empty), set_celsius(empty))4142temp = Temperature(25)43print(f"Celsius: {temp.celsius}")self._celsius ← 25
30class Temperature:31 def __init__(self⟨Temperature D⟩, celsius25):32 self._celsius→ 25 = celsius25temp ← ⟨Temperature D⟩
42temp→ ⟨Temperature D⟩ = Temperature(25)43print(f"Celsius: {temp.celsius25}")44temp.celsius = 30def __get__(self, obj, type=None):
pass 1 of 213def __get__(self⟨PropertyDescriptor C⟩, obj⟨Temperature D⟩, type<class '__main__.Temperature'>=NoneNone):14 if obj is None:15 return self16 if self.fget is None:17 raise AttributeError("unreadable attribute")18 return self.fget(obj⟨Temperature D⟩)def get_celsius(self):
pass 1 of 234def get_celsius(self⟨Temperature D⟩):35 return self._celsius25print(f"Celsius: {temp.celsius}")
42temp = Temperature(25)43print(f"Celsius: {temp.celsius25}")44temp.celsius = 3045print(f"Updated: {temp.celsius}")outputCelsius: 25def __set__(self, obj, value):
20def __set__(self⟨PropertyDescriptor C⟩, obj⟨Temperature D⟩, value30):21 if self.fset is None:22 raise AttributeError("can't set attribute")23 self.fset(obj⟨Temperature D⟩, value30)self._celsius ← 30
22 raise AttributeError("can't set attribute")23 self.fset(obj⟨Temperature D⟩, value30)24 25 def __delete__(self, obj):26 if self.fdel is None:27 raise AttributeError("can't delete attribute")28 self.fdel(obj)2930class Temperature:31 def __init__(self, celsius):32 self._celsius = celsius33 34 def get_celsius(self):35 return self._celsius36 37 def set_celsius(self⟨Temperature D⟩, value30):38 self._celsius→ 30 = value30temp.celsius ← 30
43print(f"Celsius: {temp.celsius}")44temp.celsius→ 30 = 3045print(f"Updated: {temp.celsius30}")def __get__(self, obj, type=None):
pass 2 of 213def __get__(self⟨PropertyDescriptor C⟩, obj⟨Temperature D⟩, type<class '__main__.Temperature'>=NoneNone):14 if obj is None:15 return self16 if self.fget is None:17 raise AttributeError("unreadable attribute")18 return self.fget(obj⟨Temperature D⟩)def get_celsius(self):
pass 2 of 234def get_celsius(self⟨Temperature D⟩):35 return self._celsius30print(f"Updated: {temp.celsius}")
44temp.celsius = 3045print(f"Updated: {temp.celsius30}")4647# Standard @property48print("\nStandard @property:")4950class Circle:51 def __init__(self, radius):52 self._radius = radius53 54 @property55 def radius(self):56 """Get radius"""57 return self._radius58 59 @radius.setter60 def radius(self, value):61 """Set radius"""62 if value < 0:63 raise ValueError("Radius cannot be negative")64 self._radius = value65 66 @property67 def area(self):68 """Computed area"""69 import math70 return math.pi * self._radius ** 27172circle = Circle(5)73print(f"Radius: {circle.radius}")outputUpdated: 30 Standard @property:self._radius ← 5
50class Circle:51 def __init__(self⟨Circle E⟩, radius5):52 self._radius→ 5 = radius5circle ← ⟨Circle E⟩
72circle→ ⟨Circle E⟩ = Circle(5)73print(f"Radius: {circle.radius5}")74print(f"Area: {circle.area:.2f}")def radius(self):
pass 1 of 254@property55def radius(self⟨Circle E⟩):56 """Get radius"""57 return self._radius5print(f"Radius: {circle.radius}")
72circle = Circle(5)73print(f"Radius: {circle.radius5}")74print(f"Area: {circle.area78.53981633974483:.2f}")outputRadius: 5def area(self):
pass 1 of 266@property67def area(self⟨Circle E⟩):68 """Computed area"""69 import math70 return math.pi3.141592653589793 * self._radius5 ** 2print(f"Area: {circle.area:.2f}")
73print(f"Radius: {circle.radius}")74print(f"Area: {circle.area78.53981633974483:.2f}")7576circle.radius = 1077print(f"New radius: {circle.radius}")outputArea: 78.54self._radius ← 10
59@radius.setter60def radius(self⟨Circle E⟩, value10):61 """Set radius"""62 if value < 0:63 raise ValueError("Radius cannot be negative")64 self._radius→ 10 = value10circle.radius ← 10
76circle.radius→ 10 = 1077print(f"New radius: {circle.radius10}")78print(f"New area: {circle.area:.2f}")def radius(self):
pass 2 of 254@property55def radius(self⟨Circle E⟩):56 """Get radius"""57 return self._radius10print(f"New radius: {circle.radius}")
76circle.radius = 1077print(f"New radius: {circle.radius10}")78print(f"New area: {circle.area314.1592653589793:.2f}")outputNew radius: 10def area(self):
pass 2 of 266@property67def area(self⟨Circle E⟩):68 """Computed area"""69 import math70 return math.pi3.141592653589793 * self._radius10 ** 2print(f"New area: {circle.area:.2f}")
77print(f"New radius: {circle.radius}")78print(f"New area: {circle.area314.1592653589793:.2f}")7980# Read-only property81print("\nRead-only property:")8283class Person:84 def __init__(self, first_name, last_name):85 self._first_name = first_name86 self._last_name = last_name87 88 @property89 def full_name(self):90 """Read-only computed property"""91 return f"{self._first_name} {self._last_name}"9293person = Person("Alice", "Smith")94print(f"Full name: {person.full_name}")outputNew area: 314.16 Read-only property:self._first_name ← Alice, self._last_name ← Smith
83class Person:84 def __init__(self⟨Person F⟩, first_nameAlice, last_nameSmith):85 self._first_name→ Alice = first_nameAlice86 self._last_name→ Smith = last_nameSmithperson ← ⟨Person F⟩
93person→ ⟨Person F⟩ = Person("Alice", "Smith")94print(f"Full name: {person.full_nameAlice Smith}")def full_name(self):
88@property89def full_name(self⟨Person F⟩):90 """Read-only computed property"""91 return f"{self._first_nameAlice} {self._last_nameSmith}"print(f"Full name: {person.full_name}")
93person = Person("Alice", "Smith")94print(f"Full name: {person.full_nameAlice Smith}")outputFull name: Alice Smithexcept AttributeError as e:
97 person.full_name = "Bob Jones"98except AttributeError as e:99 print(f"Error: {eproperty 'full_name' of 'Person' object has no setter}")outputError: property 'full_name' of 'Person' object has no setterprint(" Cached property:")
101# Cached property102print("\nCached property:")output Cached property:self.func ← ⟨function DataProcessor.expensive_result G⟩, self.name ← expensive_result
104class CachedProperty:105 def __init__(self⟨CachedProperty H⟩, func⟨function DataProcessor.expensive_result G⟩):106 self.func→ ⟨function DataProcessor.expensive_result G⟩ = func⟨function DataProcessor.expensive_result G⟩107 self.name→ expensive_result = func.__name__expensive_resultprocessor = DataProcessor([1, 2, 3, 4, 5])
131processor = DataProcessor([1, 2, 3, 4, 5])self.data ← [1, 2, 3, 4, 5]
122class DataProcessor:123 def __init__(self⟨DataProcessor I⟩, data[1, 2, 3, 4, 5]):124 self.data→ [1, 2, 3, 4, 5] = data[1, 2, 3, 4, 5]processor ← ⟨DataProcessor I⟩
131processor→ ⟨DataProcessor I⟩ = DataProcessor([1, 2, 3, 4, 5])132133print("First access:")134result1 = processor.expensive_result55135print(f"Result: {result1}")outputFirst access:cache_name ← _cached_expensive_result
pass 1 of 2109def __get__(self⟨CachedProperty H⟩, obj⟨DataProcessor I⟩, type<class '__main__.DataProcessor'>=NoneNone):110 if obj is None:111 return self112 113 # Check if cached114 cache_name→ _cached_expensive_result = f'_cached_{self.nameexpensive_result}'115 if cache_name not in obj.__dict__:116 # Compute and cache117 print(f" Computing {self.name}...")118 obj.__dict__[cache_name] = self.func(obj)119 120 return obj.__dict__[cache_name]55result1 ← 55
133print("First access:")134result1→ 55 = processor.expensive_result55135print(f"Result: {result155}")136137print("\nSecond access (cached):")138result2 = processor.expensive_result55139print(f"Result: {result2}")outputResult: 55 Second access (cached):cache_name ← _cached_expensive_result
pass 2 of 2109def __get__(self⟨CachedProperty H⟩, obj⟨DataProcessor I⟩, type<class '__main__.DataProcessor'>=NoneNone):110 if obj is None:111 return self112 113 # Check if cached114 cache_name→ _cached_expensive_result = f'_cached_{self.nameexpensive_result}'115 if cache_name not in obj.__dict__:116 # Compute and cache117 print(f" Computing {self.name}...")118 obj.__dict__[cache_name] = self.func(obj)119 120 return obj.__dict__[cache_name]55result2 ← 55
137print("\nSecond access (cached):")138result2→ 55 = processor.expensive_result55139print(f"Result: {result255}")140141# Validated property142print("\nValidated property:")143144class ValidatedProperty:145 def __init__(self, name, validator):146 self.name = name147 self.validator = validator148 149 def __get__(self, obj, type=None):150 if obj is None:151 return self152 return obj.__dict__.get(self.name)153 154 def __set__(self, obj, value):155 if not self.validator(value):156 raise ValueError(f"Invalid value for {self.name}: {value}")157 obj.__dict__[self.name] = value158159def is_positive(value):160 return value > 0161162def is_valid_email(value):163 return "@" in value and "." in value164165class Account:166 balance = ValidatedProperty('balance', is_positive⟨function is_positive J⟩)167 email = ValidatedProperty('email', is_valid_email)outputResult: 55 Validated property:self.name ← balance, self.validator ← ⟨function is_positive J⟩
pass 1 of 2144class ValidatedProperty:145 def __init__(self⟨ValidatedProperty K⟩, namebalance, validator⟨function is_positive J⟩):146 self.name→ balance = namebalance147 self.validator→ ⟨function is_positive J⟩ = validator⟨function is_positive J⟩balance ← (empty)
165class Account:166 balance→ (empty) = ValidatedProperty('balance', is_positive⟨function is_positive J⟩)167 email = ValidatedProperty('email', is_valid_email⟨function is_valid_email L⟩)self.name ← email, self.validator ← ⟨function is_valid_email L⟩
pass 2 of 2144class ValidatedProperty:145 def __init__(self⟨ValidatedProperty M⟩, nameemail, validator⟨function is_valid_email L⟩):146 self.name→ email = nameemail147 self.validator→ ⟨function is_valid_email L⟩ = validator⟨function is_valid_email L⟩email ← (empty), account ← ⟨Account N⟩
166 balance = ValidatedProperty('balance', is_positive)167 email→ (empty) = ValidatedProperty('email', is_valid_email⟨function is_valid_email L⟩)168169account→ ⟨Account N⟩ = Account()170account.balance = 100171account.email = "user@example.com"def __set__(self, obj, value):
pass 1 of 3154def __set__(self⟨ValidatedProperty K⟩, obj⟨Account N⟩, value100):155 if not self.validator(value):156 raise ValueError(f"Invalid value for {self.name}: {value}")All 3 passes — pass 1 is the card above pass selfvalueself.namee1 ⟨ValidatedProperty K⟩ 100 — — 2 ⟨ValidatedProperty M⟩ user@example.com — — 3 ⟨ValidatedProperty K⟩ -50 balance Invalid value for balance: -50 def is_positive(value):
pass 1 of 2159def is_positive(value100):160 return value100 > 0obj.__dict__[self.name] ← 100, account.balance ← 100
156 raise ValueError(f"Invalid value for {self.name}: {value}")157 obj.__dict__[self.name]→ 100 = value100158159def is_positive(value):160 return value > 0161162def is_valid_email(value):163 return "@" in value and "." in value164165class Account:166 balance = ValidatedProperty('balance', is_positive)167 email = ValidatedProperty('email', is_valid_email)168169account = Account()170account.balance→ 100 = 100171account.email = "user@example.com"def is_valid_email(value):
162def is_valid_email(valueuser@example.com):163 return "@" in valueuser@example.com and "." in valueobj.__dict__[self.name] ← user@example.com, account.email ← user@example.com
156 raise ValueError(f"Invalid value for {self.name}: {value}")157 obj.__dict__[self.name]→ user@example.com = valueuser@example.com158159def is_positive(value):160 return value > 0161162def is_valid_email(value):163 return "@" in value and "." in value164165class Account:166 balance = ValidatedProperty('balance', is_positive)167 email = ValidatedProperty('email', is_valid_email)168169account = Account()170account.balance = 100171account.email→ user@example.com = "user@example.com"172173print(f"Balance: {account.balance100}")174print(f"Email: {account.email}")def __get__(self, obj, type=None):
pass 1 of 2149def __get__(self⟨ValidatedProperty K⟩, obj⟨Account N⟩, type<class '__main__.Account'>=NoneNone):150 if obj is None:151 return self152 return obj.__dict__{'balance': 100, 'email': 'user@example.com'}.get(self.namebalance)print(f"Balance: {account.balance}")
173print(f"Balance: {account.balance100}")174print(f"Email: {account.emailuser@example.com}")outputBalance: 100def __get__(self, obj, type=None):
pass 2 of 2149def __get__(self⟨ValidatedProperty M⟩, obj⟨Account N⟩, type<class '__main__.Account'>=NoneNone):150 if obj is None:151 return self152 return obj.__dict__{'balance': 100, 'email': 'user@example.com'}.get(self.nameemail)print(f"Email: {account.email}")
173print(f"Balance: {account.balance}")174print(f"Email: {account.emailuser@example.com}")outputEmail: user@example.comdef is_positive(value):
pass 2 of 2159def is_positive(value-50):160 return value-50 > 0if not self.validator(value):
154def __set__(self, obj, value):155 if not self.validator(value-50):156 raise ValueError(f"Invalid value for {self.namebalance}: {value-50}")157 obj.__dict__[self.name] = valueexcept ValueError as e:
177 account.balance = -50178except ValueError as e:179 print(f"Error: {eInvalid value for balance: -50}")outputError: Invalid value for balance: -50print(" Lazy property:")
181# Lazy property182print("\nLazy property:")output Lazy property:self.func ← ⟨function Resource.content O⟩, self.name ← content
184class LazyProperty:185 def __init__(self⟨LazyProperty P⟩, func⟨function Resource.content O⟩):186 self.func→ ⟨function Resource.content O⟩ = func⟨function Resource.content O⟩187 self.name→ content = func.__name__contentresource = Resource("data.txt")
207resource = Resource("data.txt")208print("Resource created")self.filename ← data.txt
198class Resource:199 def __init__(self⟨Resource Q⟩, filenamedata.txt):200 self.filename→ data.txt = filenamedata.txtresource ← ⟨Resource Q⟩
207resource→ ⟨Resource Q⟩ = Resource("data.txt")208print("Resource created")209210print("\nFirst access:")211print(resource.contentContent of data.txt)212213print("\nSecond access:")214print(resource.contentContent of data.txt)215216# Practical example217print("\nPractical example:")218219# Combining property with validation220class Rectangle:221 def __init__(self, width, height):222 self._width = width223 self._height = height224 225 @property226 def width(self):227 return self._width228 229 @width.setter230 def width(self, value):231 if value <= 0:232 raise ValueError("Width must be positive")233 self._width = value234 235 @property236 def height(self):237 return self._height238 239 @height.setter240 def height(self, value):241 if value <= 0:242 raise ValueError("Height must be positive")243 self._height = value244 245 @property246 def area(self):247 """Computed property"""248 return self._width * self._height249 250 @property251 def perimeter(self):252 """Computed property"""253 return 2 * (self._width + self._height)254255rect = Rectangle(10, 5)256print(f"Rectangle: {rect.width}x{rect.height}")outputResource created First access: Content of data.txt Second access: Content of data.txt Practical example:self._width ← 10, self._height ← 5
220class Rectangle:221 def __init__(self⟨Rectangle R⟩, width10, height5):222 self._width→ 10 = width10223 self._height→ 5 = height5rect ← ⟨Rectangle R⟩
255rect→ ⟨Rectangle R⟩ = Rectangle(10, 5)256print(f"Rectangle: {rect.width10}x{rect.height5}")257print(f"Area: {rect.area}")def width(self):
pass 1 of 2225@property226def width(self⟨Rectangle R⟩):227 return self._width10def height(self):
pass 1 of 2235@property236def height(self⟨Rectangle R⟩):237 return self._height5print(f"Rectangle: {rect.width}x{rect.height}")
255rect = Rectangle(10, 5)256print(f"Rectangle: {rect.width10}x{rect.height5}")257print(f"Area: {rect.area50}")258print(f"Perimeter: {rect.perimeter}")outputRectangle: 10x5def area(self):
pass 1 of 2245@property246def area(self⟨Rectangle R⟩):247 """Computed property"""248 return self._width10 * self._height5print(f"Area: {rect.area}")
256print(f"Rectangle: {rect.width}x{rect.height}")257print(f"Area: {rect.area50}")258print(f"Perimeter: {rect.perimeter30}")outputArea: 50def perimeter(self):
250@property251def perimeter(self⟨Rectangle R⟩):252 """Computed property"""253 return 2 * (self._width10 + self._height5)print(f"Perimeter: {rect.perimeter}")
257print(f"Area: {rect.area}")258print(f"Perimeter: {rect.perimeter30}")259260rect.width = 20261print(f"Updated rectangle: {rect.width}x{rect.height}")outputPerimeter: 30self._width ← 20
229@width.setter230def width(self⟨Rectangle R⟩, value20):231 if value <= 0:232 raise ValueError("Width must be positive")233 self._width→ 20 = value20rect.width ← 20
260rect.width→ 20 = 20261print(f"Updated rectangle: {rect.width20}x{rect.height5}")262print(f"New area: {rect.area}")def width(self):
pass 2 of 2225@property226def width(self⟨Rectangle R⟩):227 return self._width20def height(self):
pass 2 of 2235@property236def height(self⟨Rectangle R⟩):237 return self._height5print(f"Updated rectangle: {rect.width}x{rect.height}")
260rect.width = 20261print(f"Updated rectangle: {rect.width20}x{rect.height5}")262print(f"New area: {rect.area100}")outputUpdated rectangle: 20x5def area(self):
pass 2 of 2245@property246def area(self⟨Rectangle R⟩):247 """Computed property"""248 return self._width20 * self._height5print(f"New area: {rect.area}")
261print(f"Updated rectangle: {rect.width}x{rect.height}")262print(f"New area: {rect.area100}")outputNew area: 100
Properties are the most common descriptor. Understanding that they are descriptors helps you build more advanced patterns like cached or lazy properties.
Custom Descriptors
"""Custom descriptor classes"""
# Typed descriptor
print("Typed descriptor:")
class TypedDescriptor:
def __init__(self, name, expected_type):
self.name = name
self.expected_type = expected_type
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if not isinstance(value, self.expected_type):
raise TypeError(f"{self.name} must be {self.expected_type.__name__}, got {type(value).__name__}")
obj.__dict__[self.name] = value
class Person:
name = TypedDescriptor('name', str)
age = TypedDescriptor('age', int)
height = TypedDescriptor('height', float)
person = Person()
person.name = "Alice"
person.age = 30
person.height = 1.65
print(f"Person: {person.name}, {person.age}, {person.height}m")
try:
person.age = "thirty"
except TypeError as e:
print(f"Error: {e}")
# Bounded descriptor
print("\nBounded descriptor:")
class BoundedNumber:
def __init__(self, name, min_value=None, max_value=None):
self.name = name
self.min_value = min_value
self.max_value = max_value
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if self.min_value is not None and value < self.min_value:
raise ValueError(f"{self.name} must be >= {self.min_value}")
if self.max_value is not None and value > self.max_value:
raise ValueError(f"{self.name} must be <= {self.max_value}")
obj.__dict__[self.name] = value
class Score:
value = BoundedNumber('value', min_value=0, max_value=100)
score = Score()
initial_score = 85
score.value = initial_score
print(f"Score: {score.value}")
try:
score.value = 150
except ValueError as e:
print(f"Error: {e}")
# String descriptor
print("\nString descriptor:")
class ValidatedString:
def __init__(self, name, min_length=0, max_length=None, pattern=None):
self.name = name
self.min_length = min_length
self.max_length = max_length
self.pattern = pattern
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if not isinstance(value, str):
raise TypeError(f"{self.name} must be a string")
if len(value) < self.min_length:
raise ValueError(f"{self.name} must be at least {self.min_length} characters")
if self.max_length and len(value) > self.max_length:
raise ValueError(f"{self.name} must be at most {self.max_length} characters")
if self.pattern:
import re
if not re.match(self.pattern, value):
raise ValueError(f"{self.name} doesn't match pattern")
obj.__dict__[self.name] = value
class User:
username = ValidatedString('username', min_length=3, max_length=20)
email = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')
user = User()
user.username = "alice"
user.email = "alice@example.com"
print(f"User: {user.username}, {user.email}")
try:
user.username = "ab" # Too short
except ValueError as e:
print(f"Error: {e}")
# Choice descriptor
print("\nChoice descriptor:")
class Choice:
def __init__(self, name, choices):
self.name = name
self.choices = choices
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if value not in self.choices:
raise ValueError(f"{self.name} must be one of {self.choices}")
obj.__dict__[self.name] = value
class Status:
value = Choice('value', ['pending', 'active', 'completed', 'cancelled'])
status = Status()
status.value = 'active'
print(f"Status: {status.value}")
try:
status.value = 'unknown'
except ValueError as e:
print(f"Error: {e}")
# Logged descriptor
print("\nLogged descriptor:")
class LoggedDescriptor:
def __init__(self, name):
self.name = name
def __get__(self, obj, type=None):
if obj is None:
return self
value = obj.__dict__.get(self.name)
print(f" Getting {self.name}: {value}")
return value
def __set__(self, obj, value):
print(f" Setting {self.name}: {obj.__dict__.get(self.name)} -> {value}")
obj.__dict__[self.name] = value
class TrackedObject:
x = LoggedDescriptor('x')
y = LoggedDescriptor('y')
tracked = TrackedObject()
tracked.x = 10
tracked.y = 20
print(f"Sum: {tracked.x + tracked.y}")
# Auto-converting descriptor
print("\nAuto-converting descriptor:")
class IntDescriptor:
def __init__(self, name):
self.name = name
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
# Auto-convert to int
try:
obj.__dict__[self.name] = int(value)
except (TypeError, ValueError) as e:
raise ValueError(f"Cannot convert {value} to int")
class Config:
port = IntDescriptor('port')
timeout = IntDescriptor('timeout')
config = Config()
config.port = "8080" # String converted to int
config.timeout = 30.5 # Float converted to int
print(f"Port: {config.port} (type: {type(config.port).__name__})")
print(f"Timeout: {config.timeout} (type: {type(config.timeout).__name__})")
# Practical example
print("\nPractical example:")
# Database field descriptor
class Field:
def __init__(self, name, field_type, required=False, default=None):
self.name = name
self.field_type = field_type
self.required = required
self.default = default
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name, self.default)
def __set__(self, obj, value):
if value is None:
if self.required:
raise ValueError(f"{self.name} is required")
obj.__dict__[self.name] = self.default
elif not isinstance(value, self.field_type):
raise TypeError(f"{self.name} must be {self.field_type.__name__}")
else:
obj.__dict__[self.name] = value
class DatabaseRecord:
id = Field('id', int, required=True)
name = Field('name', str, required=True)
email = Field('email', str)
age = Field('age', int, default=0)
record = DatabaseRecord()
record.id = 1
record.name = "Alice"
record.email = "alice@example.com"
print(f"Record: id={record.id}, name={record.name}, email={record.email}, age={record.age}")
try:
record2 = DatabaseRecord()
record2.name = "Bob"
# Missing required 'id'
except ValueError as e:
print(f"Error: {e}")
"""Custom descriptor classes"""
# Typed descriptor
print("Typed descriptor:")
class TypedDescriptor:
def __init__(self, name, expected_type):
self.name = name
self.expected_type = expected_type
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if not isinstance(value, self.expected_type):
raise TypeError(f"{self.name} must be {self.expected_type.__name__}, got {type(value).__name__}")
obj.__dict__[self.name] = value
class Person:
name = TypedDescriptor('name', str)
age = TypedDescriptor('age', int)
height = TypedDescriptor('height', float)
person = Person()
person.name = "Alice"
person.age = 30
person.height = 1.65
print(f"Person: {person.name}, {person.age}, {person.height}m")
try:
person.age = "thirty"
except TypeError as e:
print(f"Error: {e}")
# Bounded descriptor
print("\nBounded descriptor:")
class BoundedNumber:
def __init__(self, name, min_value=None, max_value=None):
self.name = name
self.min_value = min_value
self.max_value = max_value
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if self.min_value is not None and value < self.min_value:
raise ValueError(f"{self.name} must be >= {self.min_value}")
if self.max_value is not None and value > self.max_value:
raise ValueError(f"{self.name} must be <= {self.max_value}")
obj.__dict__[self.name] = value
class Score:
value = BoundedNumber('value', min_value=0, max_value=100)
score = Score()
initial_score = 60
score.value = initial_score
print(f"Score: {score.value}")
try:
score.value = 150
except ValueError as e:
print(f"Error: {e}")
# String descriptor
print("\nString descriptor:")
class ValidatedString:
def __init__(self, name, min_length=0, max_length=None, pattern=None):
self.name = name
self.min_length = min_length
self.max_length = max_length
self.pattern = pattern
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if not isinstance(value, str):
raise TypeError(f"{self.name} must be a string")
if len(value) < self.min_length:
raise ValueError(f"{self.name} must be at least {self.min_length} characters")
if self.max_length and len(value) > self.max_length:
raise ValueError(f"{self.name} must be at most {self.max_length} characters")
if self.pattern:
import re
if not re.match(self.pattern, value):
raise ValueError(f"{self.name} doesn't match pattern")
obj.__dict__[self.name] = value
class User:
username = ValidatedString('username', min_length=3, max_length=20)
email = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')
user = User()
user.username = "alice"
user.email = "alice@example.com"
print(f"User: {user.username}, {user.email}")
try:
user.username = "ab" # Too short
except ValueError as e:
print(f"Error: {e}")
# Choice descriptor
print("\nChoice descriptor:")
class Choice:
def __init__(self, name, choices):
self.name = name
self.choices = choices
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if value not in self.choices:
raise ValueError(f"{self.name} must be one of {self.choices}")
obj.__dict__[self.name] = value
class Status:
value = Choice('value', ['pending', 'active', 'completed', 'cancelled'])
status = Status()
status.value = 'active'
print(f"Status: {status.value}")
try:
status.value = 'unknown'
except ValueError as e:
print(f"Error: {e}")
# Logged descriptor
print("\nLogged descriptor:")
class LoggedDescriptor:
def __init__(self, name):
self.name = name
def __get__(self, obj, type=None):
if obj is None:
return self
value = obj.__dict__.get(self.name)
print(f" Getting {self.name}: {value}")
return value
def __set__(self, obj, value):
print(f" Setting {self.name}: {obj.__dict__.get(self.name)} -> {value}")
obj.__dict__[self.name] = value
class TrackedObject:
x = LoggedDescriptor('x')
y = LoggedDescriptor('y')
tracked = TrackedObject()
tracked.x = 10
tracked.y = 20
print(f"Sum: {tracked.x + tracked.y}")
# Auto-converting descriptor
print("\nAuto-converting descriptor:")
class IntDescriptor:
def __init__(self, name):
self.name = name
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
# Auto-convert to int
try:
obj.__dict__[self.name] = int(value)
except (TypeError, ValueError) as e:
raise ValueError(f"Cannot convert {value} to int")
class Config:
port = IntDescriptor('port')
timeout = IntDescriptor('timeout')
config = Config()
config.port = "8080" # String converted to int
config.timeout = 30.5 # Float converted to int
print(f"Port: {config.port} (type: {type(config.port).__name__})")
print(f"Timeout: {config.timeout} (type: {type(config.timeout).__name__})")
# Practical example
print("\nPractical example:")
# Database field descriptor
class Field:
def __init__(self, name, field_type, required=False, default=None):
self.name = name
self.field_type = field_type
self.required = required
self.default = default
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name, self.default)
def __set__(self, obj, value):
if value is None:
if self.required:
raise ValueError(f"{self.name} is required")
obj.__dict__[self.name] = self.default
elif not isinstance(value, self.field_type):
raise TypeError(f"{self.name} must be {self.field_type.__name__}")
else:
obj.__dict__[self.name] = value
class DatabaseRecord:
id = Field('id', int, required=True)
name = Field('name', str, required=True)
email = Field('email', str)
age = Field('age', int, default=0)
record = DatabaseRecord()
record.id = 1
record.name = "Alice"
record.email = "alice@example.com"
print(f"Record: id={record.id}, name={record.name}, email={record.email}, age={record.age}")
try:
record2 = DatabaseRecord()
record2.name = "Bob"
# Missing required 'id'
except ValueError as e:
print(f"Error: {e}")
"""Custom descriptor classes"""
# Typed descriptor
print("Typed descriptor:")
class TypedDescriptor:
def __init__(self, name, expected_type):
self.name = name
self.expected_type = expected_type
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if not isinstance(value, self.expected_type):
raise TypeError(f"{self.name} must be {self.expected_type.__name__}, got {type(value).__name__}")
obj.__dict__[self.name] = value
class Person:
name = TypedDescriptor('name', str)
age = TypedDescriptor('age', int)
height = TypedDescriptor('height', float)
person = Person()
person.name = "Alice"
person.age = 30
person.height = 1.65
print(f"Person: {person.name}, {person.age}, {person.height}m")
try:
person.age = "thirty"
except TypeError as e:
print(f"Error: {e}")
# Bounded descriptor
print("\nBounded descriptor:")
class BoundedNumber:
def __init__(self, name, min_value=None, max_value=None):
self.name = name
self.min_value = min_value
self.max_value = max_value
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if self.min_value is not None and value < self.min_value:
raise ValueError(f"{self.name} must be >= {self.min_value}")
if self.max_value is not None and value > self.max_value:
raise ValueError(f"{self.name} must be <= {self.max_value}")
obj.__dict__[self.name] = value
class Score:
value = BoundedNumber('value', min_value=0, max_value=100)
score = Score()
initial_score = 100
score.value = initial_score
print(f"Score: {score.value}")
try:
score.value = 150
except ValueError as e:
print(f"Error: {e}")
# String descriptor
print("\nString descriptor:")
class ValidatedString:
def __init__(self, name, min_length=0, max_length=None, pattern=None):
self.name = name
self.min_length = min_length
self.max_length = max_length
self.pattern = pattern
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if not isinstance(value, str):
raise TypeError(f"{self.name} must be a string")
if len(value) < self.min_length:
raise ValueError(f"{self.name} must be at least {self.min_length} characters")
if self.max_length and len(value) > self.max_length:
raise ValueError(f"{self.name} must be at most {self.max_length} characters")
if self.pattern:
import re
if not re.match(self.pattern, value):
raise ValueError(f"{self.name} doesn't match pattern")
obj.__dict__[self.name] = value
class User:
username = ValidatedString('username', min_length=3, max_length=20)
email = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')
user = User()
user.username = "alice"
user.email = "alice@example.com"
print(f"User: {user.username}, {user.email}")
try:
user.username = "ab" # Too short
except ValueError as e:
print(f"Error: {e}")
# Choice descriptor
print("\nChoice descriptor:")
class Choice:
def __init__(self, name, choices):
self.name = name
self.choices = choices
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if value not in self.choices:
raise ValueError(f"{self.name} must be one of {self.choices}")
obj.__dict__[self.name] = value
class Status:
value = Choice('value', ['pending', 'active', 'completed', 'cancelled'])
status = Status()
status.value = 'active'
print(f"Status: {status.value}")
try:
status.value = 'unknown'
except ValueError as e:
print(f"Error: {e}")
# Logged descriptor
print("\nLogged descriptor:")
class LoggedDescriptor:
def __init__(self, name):
self.name = name
def __get__(self, obj, type=None):
if obj is None:
return self
value = obj.__dict__.get(self.name)
print(f" Getting {self.name}: {value}")
return value
def __set__(self, obj, value):
print(f" Setting {self.name}: {obj.__dict__.get(self.name)} -> {value}")
obj.__dict__[self.name] = value
class TrackedObject:
x = LoggedDescriptor('x')
y = LoggedDescriptor('y')
tracked = TrackedObject()
tracked.x = 10
tracked.y = 20
print(f"Sum: {tracked.x + tracked.y}")
# Auto-converting descriptor
print("\nAuto-converting descriptor:")
class IntDescriptor:
def __init__(self, name):
self.name = name
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
# Auto-convert to int
try:
obj.__dict__[self.name] = int(value)
except (TypeError, ValueError) as e:
raise ValueError(f"Cannot convert {value} to int")
class Config:
port = IntDescriptor('port')
timeout = IntDescriptor('timeout')
config = Config()
config.port = "8080" # String converted to int
config.timeout = 30.5 # Float converted to int
print(f"Port: {config.port} (type: {type(config.port).__name__})")
print(f"Timeout: {config.timeout} (type: {type(config.timeout).__name__})")
# Practical example
print("\nPractical example:")
# Database field descriptor
class Field:
def __init__(self, name, field_type, required=False, default=None):
self.name = name
self.field_type = field_type
self.required = required
self.default = default
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name, self.default)
def __set__(self, obj, value):
if value is None:
if self.required:
raise ValueError(f"{self.name} is required")
obj.__dict__[self.name] = self.default
elif not isinstance(value, self.field_type):
raise TypeError(f"{self.name} must be {self.field_type.__name__}")
else:
obj.__dict__[self.name] = value
class DatabaseRecord:
id = Field('id', int, required=True)
name = Field('name', str, required=True)
email = Field('email', str)
age = Field('age', int, default=0)
record = DatabaseRecord()
record.id = 1
record.name = "Alice"
record.email = "alice@example.com"
print(f"Record: id={record.id}, name={record.name}, email={record.email}, age={record.age}")
try:
record2 = DatabaseRecord()
record2.name = "Bob"
# Missing required 'id'
except ValueError as e:
print(f"Error: {e}")
"""Custom descriptor classes"""
1"""Custom descriptor classes"""23# Typed descriptor4print("Typed descriptor:")56class TypedDescriptor:7 def __init__(self, name, expected_type):8 self.name = name9 self.expected_type = expected_type10 11 def __get__(self, obj, type=None):12 if obj is None:13 return self14 return obj.__dict__.get(self.name)15 16 def __set__(self, obj, value):17 if not isinstance(value, self.expected_type):18 raise TypeError(f"{self.name} must be {self.expected_type.__name__}, got {type(value).__name__}")19 obj.__dict__[self.name] = value2021class Person:22 name = TypedDescriptor('name', str)23 age = TypedDescriptor('age', int)outputTyped descriptor:self.name ← name, self.expected_type ← <class 'str'>
pass 1 of 36class TypedDescriptor:7 def __init__(self⟨TypedDescriptor A⟩, namename, expected_type<class 'str'>):8 self.name→ name = namename9 self.expected_type→ <class 'str'> = expected_type<class 'str'>All 3 passes — pass 1 is the card above pass selfnameexpected_typeself.nameself.expected_type1 ⟨TypedDescriptor A⟩ name <class 'str'> name <class 'str'> 2 ⟨TypedDescriptor B⟩ age <class 'int'> age <class 'int'> 3 ⟨TypedDescriptor C⟩ height <class 'float'> height <class 'float'> name ← (empty)
21class Person:22 name→ (empty) = TypedDescriptor('name', str)23 age = TypedDescriptor('age', int)24 height = TypedDescriptor('height', float)age ← (empty)
22name = TypedDescriptor('name', str)23age→ (empty) = TypedDescriptor('age', int)24height = TypedDescriptor('height', float)height ← (empty), person ← ⟨Person D⟩
23 age = TypedDescriptor('age', int)24 height→ (empty) = TypedDescriptor('height', float)2526person→ ⟨Person D⟩ = Person()27person.name = "Alice"28person.age = 30obj.__dict__[self.name] ← Alice
pass 1 of 416def __set__(self⟨TypedDescriptor A⟩, obj⟨Person D⟩, valueAlice):17 if not isinstance(value, self.expected_type):18 raise TypeError(f"{self.name} must be {self.expected_type.__name__}, got {type(value).__name__}")19 obj.__dict__[self.name]→ Alice = valueAliceAll 4 passes — pass 1 is the card above pass selfvalueself.expected_typeself.nameself.expected_type.__name__eobj.__dict__[self.name]1 ⟨TypedDescriptor A⟩ Alice — — — — Alice 2 ⟨TypedDescriptor B⟩ 30 — — — — 30 3 ⟨TypedDescriptor C⟩ 1.65 — — — — 1.65 4 ⟨TypedDescriptor B⟩ thirty <class 'int'> age int age must be int, got str — person.name ← Alice
26person = Person()27person.name→ Alice = "Alice"28person.age = 3029person.height = 1.65person.age ← 30
27person.name = "Alice"28person.age→ 30 = 3029person.height = 1.65person.height ← 1.65
28person.age = 3029person.height→ 1.65 = 1.653031print(f"Person: {person.nameAlice}, {person.age30}, {person.height1.65}m")def __get__(self, obj, type=None):
pass 1 of 311def __get__(self⟨TypedDescriptor A⟩, obj⟨Person D⟩, type<class '__main__.Person'>=NoneNone):12 if obj is None:13 return self14 return obj.__dict__{'name': 'Alice', 'age': 30, 'height': 1.65}.get(self.namename)All 3 passes — pass 1 is the card above pass selfself.name1 ⟨TypedDescriptor A⟩ name 2 ⟨TypedDescriptor B⟩ age 3 ⟨TypedDescriptor C⟩ height print(f"Person: {person.name}, {person.age}, {person.height}m")
31print(f"Person: {person.nameAlice}, {person.age30}, {person.height1.65}m")outputPerson: Alice, 30, 1.65mif not isinstance(value, self.expected_type):
16def __set__(self, obj, value):17 if not isinstance(valuethirty, self.expected_type<class 'int'>):18 raise TypeError(f"{self.nameage} must be {self.expected_type.__name__int}, got {type(valuethirty).__name__}")19 obj.__dict__[self.name] = valueexcept TypeError as e:
34 person.age = "thirty"35except TypeError as e:36 print(f"Error: {eage must be int, got str}")outputError: age must be int, got strprint(" Bounded descriptor:")
38# Bounded descriptor39print("\nBounded descriptor:")4041class BoundedNumber:42 def __init__(self, name, min_value=None, max_value=None):43 self.name = name44 self.min_value = min_value45 self.max_value = max_value46 47 def __get__(self, obj, type=None):48 if obj is None:49 return self50 return obj.__dict__.get(self.name)51 52 def __set__(self, obj, value):53 if self.min_value is not None and value < self.min_value:54 raise ValueError(f"{self.name} must be >= {self.min_value}")55 if self.max_value is not None and value > self.max_value:56 raise ValueError(f"{self.name} must be <= {self.max_value}")57 obj.__dict__[self.name] = value5859class Score:60 value = BoundedNumber('value', min_value=0, max_value=100)output Bounded descriptor:self.name ← value, self.min_value ← 0, self.max_value ← 100
41class BoundedNumber:42 def __init__(self⟨BoundedNumber E⟩, namevalue, min_value0=NoneNone, max_value100=None):43 self.name→ value = namevalue44 self.min_value→ 0 = min_value045 self.max_value→ 100 = max_value100value ← (empty), score ← ⟨Score F⟩, initial_score ← 85
59class Score:60 value→ (empty) = BoundedNumber('value', min_value=0, max_value=100)6162score→ ⟨Score F⟩ = Score()63initial_score→ 85 = 85 #@initial_score=60, 10064score.value = initial_score8565print(f"Score: {score.value}")obj.__dict__[self.name] ← 85
pass 1 of 252def __set__(self⟨BoundedNumber E⟩, obj⟨Score F⟩, value85):53 if self.min_value is not None and value < self.min_value:54 raise ValueError(f"{self.name} must be >= {self.min_value}")55 if self.max_value is not None and value > self.max_value:56 raise ValueError(f"{self.name} must be <= {self.max_value}")57 obj.__dict__[self.name]→ 85 = value85score.value ← 85
63initial_score = 85 #@initial_score=60, 10064score.value→ 85 = initial_score8565print(f"Score: {score.value85}")def __get__(self, obj, type=None):
47def __get__(self⟨BoundedNumber E⟩, obj⟨Score F⟩, type<class '__main__.Score'>=NoneNone):48 if obj is None:49 return self50 return obj.__dict__{'value': 85}.get(self.namevalue)print(f"Score: {score.value}")
64score.value = initial_score65print(f"Score: {score.value85}")outputScore: 85def __set__(self, obj, value):
pass 2 of 252def __set__(self⟨BoundedNumber E⟩, obj⟨Score F⟩, value150):53 if self.min_value is not None and value < self.min_value:54 raise ValueError(f"{self.name} must be >= {self.min_value}")if self.max_value is not None and value > self.max_value:
54 raise ValueError(f"{self.name} must be >= {self.min_value}")55if self.max_value100 is not None and value150 > self.max_value:56 raise ValueError(f"{self.namevalue} must be <= {self.max_value100}")57obj.__dict__[self.name] = valueexcept ValueError as e:
68 score.value = 15069except ValueError as e:70 print(f"Error: {evalue must be <= 100}")outputError: value must be <= 100print(" String descriptor:")
72# String descriptor73print("\nString descriptor:")7475class ValidatedString:76 def __init__(self, name, min_length=0, max_length=None, pattern=None):77 self.name = name78 self.min_length = min_length79 self.max_length = max_length80 self.pattern = pattern81 82 def __get__(self, obj, type=None):83 if obj is None:84 return self85 return obj.__dict__.get(self.name)86 87 def __set__(self, obj, value):88 if not isinstance(value, str):89 raise TypeError(f"{self.name} must be a string")90 91 if len(value) < self.min_length:92 raise ValueError(f"{self.name} must be at least {self.min_length} characters")93 94 if self.max_length and len(value) > self.max_length:95 raise ValueError(f"{self.name} must be at most {self.max_length} characters")96 97 if self.pattern:98 import re99 if not re.match(self.pattern, value):100 raise ValueError(f"{self.name} doesn't match pattern")101 102 obj.__dict__[self.name] = value103104class User:105 username = ValidatedString('username', min_length=3, max_length=20)106 email = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')output String descriptor:self.name ← username, self.min_length ← 3, self.max_length ← 20
pass 1 of 275class ValidatedString:76 def __init__(self⟨ValidatedString G⟩, nameusername, min_length3=0, max_length20=NoneNone, patternNone=None):77 self.name→ username = nameusername78 self.min_length→ 3 = min_length379 self.max_length→ 20 = max_length2080 self.pattern→ None = patternNoneusername ← (empty)
104class User:105 username→ (empty) = ValidatedString('username', min_length=3, max_length=20)106 email = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')self.name ← email, self.min_length ← 0, self.max_length ← None
pass 2 of 275class ValidatedString:76 def __init__(self⟨ValidatedString H⟩, nameemail, min_length0=0, max_lengthNone=NoneNone, pattern^[\w\.-]+@[\w\.-]+\.\w+$=None):77 self.name→ email = nameemail78 self.min_length→ 0 = min_length079 self.max_length→ None = max_lengthNone80 self.pattern→ ^[\w\.-]+@[\w\.-]+\.\w+$ = pattern^[\w\.-]+@[\w\.-]+\.\w+$email ← (empty), user ← ⟨User I⟩
105 username = ValidatedString('username', min_length=3, max_length=20)106 email→ (empty) = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')107108user→ ⟨User I⟩ = User()109user.username = "alice"110user.email = "alice@example.com"obj.__dict__[self.name] ← alice
pass 1 of 387def __set__(self⟨ValidatedString G⟩, obj⟨User I⟩, valuealice):88 if not isinstance(value, str):89 raise TypeError(f"{self.name} must be a string")90 91 if len(value) < self.min_length:92 raise ValueError(f"{self.name} must be at least {self.min_length} characters")93 94 if self.max_length and len(value) > self.max_length:95 raise ValueError(f"{self.name} must be at most {self.max_length} characters")96 97 if self.pattern:98 import re99 if not re.match(self.pattern, value):100 raise ValueError(f"{self.name} doesn't match pattern")101 102 obj.__dict__[self.name]→ alice = valuealiceAll 3 passes — pass 1 is the card above pass selfvalueself.patternself.min_lengthself.nameeobj.__dict__[self.name]1 ⟨ValidatedString G⟩ alice — — — — alice 2 ⟨ValidatedString H⟩ alice@example.com ^[\w\.-]+@[\w\.-]+\.\w+$ — — — — 3 ⟨ValidatedString G⟩ ab — 3 username username must be at least 3 characters — user.username ← alice
108user = User()109user.username→ alice = "alice"110user.email = "alice@example.com"if self.pattern:
97if self.pattern^[\w\.-]+@[\w\.-]+\.\w+$:98 import re99 if not re.match(self.pattern, value):obj.__dict__[self.name] ← alice@example.com
102obj.__dict__[self.name]→ alice@example.com = valuealice@example.comuser.email ← alice@example.com
109user.username = "alice"110user.email→ alice@example.com = "alice@example.com"111112print(f"User: {user.usernamealice}, {user.emailalice@example.com}")def __get__(self, obj, type=None):
pass 1 of 282def __get__(self⟨ValidatedString G⟩, obj⟨User I⟩, type<class '__main__.User'>=NoneNone):83 if obj is None:84 return self85 return obj.__dict__{'username': 'alice', 'email': 'alice@example.com'}.get(self.nameusername)def __get__(self, obj, type=None):
pass 2 of 282def __get__(self⟨ValidatedString H⟩, obj⟨User I⟩, type<class '__main__.User'>=NoneNone):83 if obj is None:84 return self85 return obj.__dict__{'username': 'alice', 'email': 'alice@example.com'}.get(self.nameemail)print(f"User: {user.username}, {user.email}")
112print(f"User: {user.usernamealice}, {user.emailalice@example.com}")outputUser: alice, alice@example.comif len(value) < self.min_length:
91if len(valueab) < self.min_length3:92 raise ValueError(f"{self.nameusername} must be at least {self.min_length3} characters")except ValueError as e:
115 user.username = "ab" # Too short116except ValueError as e:117 print(f"Error: {eusername must be at least 3 characters}")outputError: username must be at least 3 charactersprint(" Choice descriptor:")
119# Choice descriptor120print("\nChoice descriptor:")121122class Choice:123 def __init__(self, name, choices):124 self.name = name125 self.choices = choices126 127 def __get__(self, obj, type=None):128 if obj is None:129 return self130 return obj.__dict__.get(self.name)131 132 def __set__(self, obj, value):133 if value not in self.choices:134 raise ValueError(f"{self.name} must be one of {self.choices}")135 obj.__dict__[self.name] = value136137class Status:138 value = Choice('value', ['pending', 'active', 'completed', 'cancelled'])output Choice descriptor:self.name ← value, self.choices ← ['pending', 'active', 'completed', 'cancelled']
122class Choice:123 def __init__(self⟨Choice J⟩, namevalue, choices['pending', 'active', 'completed', 'cancelled']):124 self.name→ value = namevalue125 self.choices→ ['pending', 'active', 'completed', 'cancelled'] = choices['pending', 'active', 'completed', 'cancelled']value ← (empty), status ← ⟨Status K⟩
137class Status:138 value→ (empty) = Choice('value', ['pending', 'active', 'completed', 'cancelled'])139140status→ ⟨Status K⟩ = Status()141status.value = 'active'142print(f"Status: {status.value}")obj.__dict__[self.name] ← active
pass 1 of 2132def __set__(self⟨Choice J⟩, obj⟨Status K⟩, valueactive):133 if value not in self.choices:134 raise ValueError(f"{self.name} must be one of {self.choices}")135 obj.__dict__[self.name]→ active = valueactivestatus.value ← active
140status = Status()141status.value→ active = 'active'142print(f"Status: {status.valueactive}")def __get__(self, obj, type=None):
127def __get__(self⟨Choice J⟩, obj⟨Status K⟩, type<class '__main__.Status'>=NoneNone):128 if obj is None:129 return self130 return obj.__dict__{'value': 'active'}.get(self.namevalue)print(f"Status: {status.value}")
141status.value = 'active'142print(f"Status: {status.valueactive}")outputStatus: activedef __set__(self, obj, value):
pass 2 of 2132def __set__(self⟨Choice J⟩, obj⟨Status K⟩, valueunknown):133 if value not in self.choices:134 raise ValueError(f"{self.name} must be one of {self.choices}")if value not in self.choices:
132def __set__(self, obj, value):133 if valueunknown not in self.choices['pending', 'active', 'completed', 'cancelled']:134 raise ValueError(f"{self.namevalue} must be one of {self.choices['pending', 'active', 'completed', 'cancelled']}")135 obj.__dict__[self.name] = valueexcept ValueError as e:
145 status.value = 'unknown'146except ValueError as e:147 print(f"Error: {evalue must be one of ['pending', 'active', 'completed', 'cancelled']}")outputError: value must be one of ['pending', 'active', 'completed', 'cancelled']print(" Logged descriptor:")
149# Logged descriptor150print("\nLogged descriptor:")151152class LoggedDescriptor:153 def __init__(self, name):154 self.name = name155 156 def __get__(self, obj, type=None):157 if obj is None:158 return self159 value = obj.__dict__.get(self.name)160 print(f" Getting {self.name}: {value}")161 return value162 163 def __set__(self, obj, value):164 print(f" Setting {self.name}: {obj.__dict__.get(self.name)} -> {value}")165 obj.__dict__[self.name] = value166167class TrackedObject:168 x = LoggedDescriptor('x')169 y = LoggedDescriptor('y')output Logged descriptor:self.name ← x
pass 1 of 2152class LoggedDescriptor:153 def __init__(self⟨LoggedDescriptor L⟩, namex):154 self.name→ x = namexx ← (empty)
167class TrackedObject:168 x→ (empty) = LoggedDescriptor('x')169 y = LoggedDescriptor('y')self.name ← y
pass 2 of 2152class LoggedDescriptor:153 def __init__(self⟨LoggedDescriptor M⟩, namey):154 self.name→ y = nameyy ← (empty), tracked ← ⟨TrackedObject N⟩
168 x = LoggedDescriptor('x')169 y→ (empty) = LoggedDescriptor('y')170171tracked→ ⟨TrackedObject N⟩ = TrackedObject()172tracked.x = 10173tracked.y = 20obj.__dict__[self.name] ← 10
pass 1 of 2163def __set__(self⟨LoggedDescriptor L⟩, obj⟨TrackedObject N⟩, value10):164 print(f" Setting {self.namex}: {obj.__dict__{}.get(self.name)} -> {value10}")165 obj.__dict__[self.name]→ 10 = value10output Setting x: None -> 10tracked.x ← 10
171tracked = TrackedObject()172tracked.x→ 10 = 10173tracked.y = 20174print(f"Sum: {tracked.x + tracked.y}")obj.__dict__[self.name] ← 20
pass 2 of 2163def __set__(self⟨LoggedDescriptor M⟩, obj⟨TrackedObject N⟩, value20):164 print(f" Setting {self.namey}: {obj.__dict__{'x': 10}.get(self.name)} -> {value20}")165 obj.__dict__[self.name]→ 20 = value20output Setting y: None -> 20tracked.y ← 20
172tracked.x = 10173tracked.y→ 20 = 20174print(f"Sum: {tracked.x10 + tracked.y20}")value ← 10
pass 1 of 2156def __get__(self⟨LoggedDescriptor L⟩, obj⟨TrackedObject N⟩, type<class '__main__.TrackedObject'>=NoneNone):157 if obj is None:158 return self159 value→ 10 = obj.__dict__{'x': 10, 'y': 20}.get(self.namex)160 print(f" Getting {self.namex}: {value10}")161 return value10output Getting x: 10value ← 20
pass 2 of 2156def __get__(self⟨LoggedDescriptor M⟩, obj⟨TrackedObject N⟩, type<class '__main__.TrackedObject'>=NoneNone):157 if obj is None:158 return self159 value→ 20 = obj.__dict__{'x': 10, 'y': 20}.get(self.namey)160 print(f" Getting {self.namey}: {value20}")161 return value20output Getting y: 20print(f"Sum: {tracked.x + tracked.y}")
173tracked.y = 20174print(f"Sum: {tracked.x10 + tracked.y20}")175176# Auto-converting descriptor177print("\nAuto-converting descriptor:")178179class IntDescriptor:180 def __init__(self, name):181 self.name = name182 183 def __get__(self, obj, type=None):184 if obj is None:185 return self186 return obj.__dict__.get(self.name)187 188 def __set__(self, obj, value):189 # Auto-convert to int190 try:191 obj.__dict__[self.name] = int(value)192 except (TypeError, ValueError) as e:193 raise ValueError(f"Cannot convert {value} to int")194195class Config:196 port = IntDescriptor('port')197 timeout = IntDescriptor('timeout')outputSum: 30 Auto-converting descriptor:self.name ← port
pass 1 of 2179class IntDescriptor:180 def __init__(self⟨IntDescriptor O⟩, nameport):181 self.name→ port = nameportport ← (empty)
195class Config:196 port→ (empty) = IntDescriptor('port')197 timeout = IntDescriptor('timeout')self.name ← timeout
pass 2 of 2179class IntDescriptor:180 def __init__(self⟨IntDescriptor P⟩, nametimeout):181 self.name→ timeout = nametimeouttimeout ← (empty), config ← ⟨Config Q⟩
196 port = IntDescriptor('port')197 timeout→ (empty) = IntDescriptor('timeout')198199config→ ⟨Config Q⟩ = Config()200config.port = "8080" # String converted to int201config.timeout = 30.5 # Float converted to intdef __set__(self, obj, value): # Auto-convert to int
pass 1 of 2188def __set__(self⟨IntDescriptor O⟩, obj⟨Config Q⟩, value8080):189 # Auto-convert to int190 try:191 obj.__dict__[self.name] = int(value)obj.__dict__[self.name] ← 8080
pass 1 of 2189# Auto-convert to int190try:191 obj.__dict__[self.name]→ 8080 = int(value8080)192except (TypeError, ValueError) as e:config.port ← 8080
199config = Config()200config.port→ 8080 = "8080" # String converted to int201config.timeout = 30.5 # Float converted to intdef __set__(self, obj, value): # Auto-convert to int
pass 2 of 2188def __set__(self⟨IntDescriptor P⟩, obj⟨Config Q⟩, value30.5):189 # Auto-convert to int190 try:191 obj.__dict__[self.name] = int(value)obj.__dict__[self.name] ← 30
pass 2 of 2189# Auto-convert to int190try:191 obj.__dict__[self.name]→ 30 = int(value30.5)192except (TypeError, ValueError) as e:config.timeout ← 30
200config.port = "8080" # String converted to int201config.timeout→ 30 = 30.5 # Float converted to int202203print(f"Port: {config.port8080} (type: {type(config.port).__name__})")204print(f"Timeout: {config.timeout} (type: {type(config.timeout).__name__})")def __get__(self, obj, type=None):
pass 1 of 4183def __get__(self⟨IntDescriptor O⟩, obj⟨Config Q⟩, type<class '__main__.Config'>=NoneNone):184 if obj is None:185 return self186 return obj.__dict__{'port': 8080, 'timeout': 30}.get(self.nameport)All 4 passes — pass 1 is the card above pass selfself.name1 ⟨IntDescriptor O⟩ port 2 ⟨IntDescriptor O⟩ port 3 ⟨IntDescriptor P⟩ timeout 4 ⟨IntDescriptor P⟩ timeout print(f"Port: {config.port} (type: {type(config.port).__name__})")
203print(f"Port: {config.port8080} (type: {type(config.port).__name__})")204print(f"Timeout: {config.timeout30} (type: {type(config.timeout).__name__})")outputPort: 8080 (type: int)print(f"Timeout: {config.timeout} (type: {type(config.timeout).__name_…
203print(f"Port: {config.port} (type: {type(config.port).__name__})")204print(f"Timeout: {config.timeout30} (type: {type(config.timeout).__name__})")205206# Practical example207print("\nPractical example:")208209# Database field descriptor210class Field:211 def __init__(self, name, field_type, required=False, default=None):212 self.name = name213 self.field_type = field_type214 self.required = required215 self.default = default216 217 def __get__(self, obj, type=None):218 if obj is None:219 return self220 return obj.__dict__.get(self.name, self.default)221 222 def __set__(self, obj, value):223 if value is None:224 if self.required:225 raise ValueError(f"{self.name} is required")226 obj.__dict__[self.name] = self.default227 elif not isinstance(value, self.field_type):228 raise TypeError(f"{self.name} must be {self.field_type.__name__}")229 else:230 obj.__dict__[self.name] = value231232class DatabaseRecord:233 id = Field('id', int, required=True)234 name = Field('name', str, required=True)outputTimeout: 30 (type: int) Practical example:self.name ← id, self.field_type ← <class 'int'>, self.required ← True
pass 1 of 4210class Field:211 def __init__(self⟨Field R⟩, nameid, field_type<class 'int'>, requiredTrue=FalseFalse, defaultNone=NoneNone):212 self.name→ id = nameid213 self.field_type→ <class 'int'> = field_type<class 'int'>214 self.required→ True = requiredTrue215 self.default→ None = defaultNoneAll 4 passes — pass 1 is the card above pass selfnamefield_typerequireddefaultself.nameself.field_typeself.requiredself.default1 ⟨Field R⟩ id <class 'int'> True None id <class 'int'> True None 2 ⟨Field S⟩ name <class 'str'> True None name <class 'str'> True None 3 ⟨Field T⟩ email <class 'str'> False None email <class 'str'> False None 4 ⟨Field U⟩ age <class 'int'> False 0 age <class 'int'> False 0 id ← <built-in function id>
232class DatabaseRecord:233 id→ <built-in function id> = Field('id', int, required=True)234 name = Field('name', str, required=True)235 email = Field('email', str)name ← (empty)
233id = Field('id', int, required=True)234name→ (empty) = Field('name', str, required=True)235email = Field('email', str)236age = Field('age', int, default=0)email ← (empty)
234name = Field('name', str, required=True)235email→ (empty) = Field('email', str)236age = Field('age', int, default=0)age ← (empty)
235email = Field('email', str)236age→ (empty) = Field('age', int, default=0)def __set__(self, obj, value):
pass 1 of 4222def __set__(self⟨Field R⟩, obj⟨DatabaseRecord V⟩, value1):223 if value is None:224 if self.required:All 4 passes — pass 1 is the card above pass selfobjvalue1 ⟨Field R⟩ ⟨DatabaseRecord V⟩ 1 2 ⟨Field S⟩ ⟨DatabaseRecord V⟩ Alice 3 ⟨Field T⟩ ⟨DatabaseRecord V⟩ alice@example.com 4 ⟨Field S⟩ ⟨DatabaseRecord W⟩ Bob obj.__dict__[self.name] ← 1
pass 1 of 4227elif not isinstance(value, self.field_type):228 raise TypeError(f"{self.name} must be {self.field_type.__name__}")229else:230 obj.__dict__[self.name]→ 1 = value1All 4 passes — pass 1 is the card above pass valueobj.__dict__[self.name]1 1 1 2 Alice Alice 3 alice@example.com alice@example.com 4 Bob Bob print(f"Record: id={record.id}, name={record.name}, email={record.emai…
243print(f"Record: id={record.id1}, name={record.nameAlice}, email={record.emailalice@example.com}, age={record.age0}")def __get__(self, obj, type=None):
pass 1 of 4217def __get__(self⟨Field R⟩, obj⟨DatabaseRecord V⟩, type<class '__main__.DatabaseRecord'>=NoneNone):218 if obj is None:219 return self220 return obj.__dict__{'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}.get(self.nameid, self.defaultNone)All 4 passes — pass 1 is the card above pass selfself.nameself.default1 ⟨Field R⟩ id None 2 ⟨Field S⟩ name None 3 ⟨Field T⟩ email None 4 ⟨Field U⟩ age 0 print(f"Record: id={record.id}, name={record.name}, email={record.emai…
243print(f"Record: id={record.id1}, name={record.nameAlice}, email={record.emailalice@example.com}, age={record.age0}")outputRecord: id=1, name=Alice, email=alice@example.com, age=0record2 ← ⟨DatabaseRecord W⟩
245try:246 record2→ ⟨DatabaseRecord W⟩ = DatabaseRecord()247 record2.name = "Bob"248 # Missing required 'id'record2.name ← Bob
246record2 = DatabaseRecord()247record2.name→ Bob = "Bob"248# Missing required 'id'
"""Custom descriptor classes"""
1"""Custom descriptor classes"""23# Typed descriptor4print("Typed descriptor:")56class TypedDescriptor:7 def __init__(self, name, expected_type):8 self.name = name9 self.expected_type = expected_type10 11 def __get__(self, obj, type=None):12 if obj is None:13 return self14 return obj.__dict__.get(self.name)15 16 def __set__(self, obj, value):17 if not isinstance(value, self.expected_type):18 raise TypeError(f"{self.name} must be {self.expected_type.__name__}, got {type(value).__name__}")19 obj.__dict__[self.name] = value2021class Person:22 name = TypedDescriptor('name', str)23 age = TypedDescriptor('age', int)outputTyped descriptor:self.name ← name, self.expected_type ← <class 'str'>
pass 1 of 36class TypedDescriptor:7 def __init__(self⟨TypedDescriptor A⟩, namename, expected_type<class 'str'>):8 self.name→ name = namename9 self.expected_type→ <class 'str'> = expected_type<class 'str'>All 3 passes — pass 1 is the card above pass selfnameexpected_typeself.nameself.expected_type1 ⟨TypedDescriptor A⟩ name <class 'str'> name <class 'str'> 2 ⟨TypedDescriptor B⟩ age <class 'int'> age <class 'int'> 3 ⟨TypedDescriptor C⟩ height <class 'float'> height <class 'float'> name ← (empty)
21class Person:22 name→ (empty) = TypedDescriptor('name', str)23 age = TypedDescriptor('age', int)24 height = TypedDescriptor('height', float)age ← (empty)
22name = TypedDescriptor('name', str)23age→ (empty) = TypedDescriptor('age', int)24height = TypedDescriptor('height', float)height ← (empty), person ← ⟨Person D⟩
23 age = TypedDescriptor('age', int)24 height→ (empty) = TypedDescriptor('height', float)2526person→ ⟨Person D⟩ = Person()27person.name = "Alice"28person.age = 30obj.__dict__[self.name] ← Alice
pass 1 of 416def __set__(self⟨TypedDescriptor A⟩, obj⟨Person D⟩, valueAlice):17 if not isinstance(value, self.expected_type):18 raise TypeError(f"{self.name} must be {self.expected_type.__name__}, got {type(value).__name__}")19 obj.__dict__[self.name]→ Alice = valueAliceAll 4 passes — pass 1 is the card above pass selfvalueself.expected_typeself.nameself.expected_type.__name__eobj.__dict__[self.name]1 ⟨TypedDescriptor A⟩ Alice — — — — Alice 2 ⟨TypedDescriptor B⟩ 30 — — — — 30 3 ⟨TypedDescriptor C⟩ 1.65 — — — — 1.65 4 ⟨TypedDescriptor B⟩ thirty <class 'int'> age int age must be int, got str — person.name ← Alice
26person = Person()27person.name→ Alice = "Alice"28person.age = 3029person.height = 1.65person.age ← 30
27person.name = "Alice"28person.age→ 30 = 3029person.height = 1.65person.height ← 1.65
28person.age = 3029person.height→ 1.65 = 1.653031print(f"Person: {person.nameAlice}, {person.age30}, {person.height1.65}m")def __get__(self, obj, type=None):
pass 1 of 311def __get__(self⟨TypedDescriptor A⟩, obj⟨Person D⟩, type<class '__main__.Person'>=NoneNone):12 if obj is None:13 return self14 return obj.__dict__{'name': 'Alice', 'age': 30, 'height': 1.65}.get(self.namename)All 3 passes — pass 1 is the card above pass selfself.name1 ⟨TypedDescriptor A⟩ name 2 ⟨TypedDescriptor B⟩ age 3 ⟨TypedDescriptor C⟩ height print(f"Person: {person.name}, {person.age}, {person.height}m")
31print(f"Person: {person.nameAlice}, {person.age30}, {person.height1.65}m")outputPerson: Alice, 30, 1.65mif not isinstance(value, self.expected_type):
16def __set__(self, obj, value):17 if not isinstance(valuethirty, self.expected_type<class 'int'>):18 raise TypeError(f"{self.nameage} must be {self.expected_type.__name__int}, got {type(valuethirty).__name__}")19 obj.__dict__[self.name] = valueexcept TypeError as e:
34 person.age = "thirty"35except TypeError as e:36 print(f"Error: {eage must be int, got str}")outputError: age must be int, got strprint(" Bounded descriptor:")
38# Bounded descriptor39print("\nBounded descriptor:")4041class BoundedNumber:42 def __init__(self, name, min_value=None, max_value=None):43 self.name = name44 self.min_value = min_value45 self.max_value = max_value46 47 def __get__(self, obj, type=None):48 if obj is None:49 return self50 return obj.__dict__.get(self.name)51 52 def __set__(self, obj, value):53 if self.min_value is not None and value < self.min_value:54 raise ValueError(f"{self.name} must be >= {self.min_value}")55 if self.max_value is not None and value > self.max_value:56 raise ValueError(f"{self.name} must be <= {self.max_value}")57 obj.__dict__[self.name] = value5859class Score:60 value = BoundedNumber('value', min_value=0, max_value=100)output Bounded descriptor:self.name ← value, self.min_value ← 0, self.max_value ← 100
41class BoundedNumber:42 def __init__(self⟨BoundedNumber E⟩, namevalue, min_value0=NoneNone, max_value100=None):43 self.name→ value = namevalue44 self.min_value→ 0 = min_value045 self.max_value→ 100 = max_value100value ← (empty), score ← ⟨Score F⟩, initial_score ← 60
59class Score:60 value→ (empty) = BoundedNumber('value', min_value=0, max_value=100)6162score→ ⟨Score F⟩ = Score()63initial_score→ 60 = 6064score.value = initial_score6065print(f"Score: {score.value}")obj.__dict__[self.name] ← 60
pass 1 of 252def __set__(self⟨BoundedNumber E⟩, obj⟨Score F⟩, value60):53 if self.min_value is not None and value < self.min_value:54 raise ValueError(f"{self.name} must be >= {self.min_value}")55 if self.max_value is not None and value > self.max_value:56 raise ValueError(f"{self.name} must be <= {self.max_value}")57 obj.__dict__[self.name]→ 60 = value60score.value ← 60
63initial_score = 6064score.value→ 60 = initial_score6065print(f"Score: {score.value60}")def __get__(self, obj, type=None):
47def __get__(self⟨BoundedNumber E⟩, obj⟨Score F⟩, type<class '__main__.Score'>=NoneNone):48 if obj is None:49 return self50 return obj.__dict__{'value': 60}.get(self.namevalue)print(f"Score: {score.value}")
64score.value = initial_score65print(f"Score: {score.value60}")outputScore: 60def __set__(self, obj, value):
pass 2 of 252def __set__(self⟨BoundedNumber E⟩, obj⟨Score F⟩, value150):53 if self.min_value is not None and value < self.min_value:54 raise ValueError(f"{self.name} must be >= {self.min_value}")if self.max_value is not None and value > self.max_value:
54 raise ValueError(f"{self.name} must be >= {self.min_value}")55if self.max_value100 is not None and value150 > self.max_value:56 raise ValueError(f"{self.namevalue} must be <= {self.max_value100}")57obj.__dict__[self.name] = valueexcept ValueError as e:
68 score.value = 15069except ValueError as e:70 print(f"Error: {evalue must be <= 100}")outputError: value must be <= 100print(" String descriptor:")
72# String descriptor73print("\nString descriptor:")7475class ValidatedString:76 def __init__(self, name, min_length=0, max_length=None, pattern=None):77 self.name = name78 self.min_length = min_length79 self.max_length = max_length80 self.pattern = pattern81 82 def __get__(self, obj, type=None):83 if obj is None:84 return self85 return obj.__dict__.get(self.name)86 87 def __set__(self, obj, value):88 if not isinstance(value, str):89 raise TypeError(f"{self.name} must be a string")90 91 if len(value) < self.min_length:92 raise ValueError(f"{self.name} must be at least {self.min_length} characters")93 94 if self.max_length and len(value) > self.max_length:95 raise ValueError(f"{self.name} must be at most {self.max_length} characters")96 97 if self.pattern:98 import re99 if not re.match(self.pattern, value):100 raise ValueError(f"{self.name} doesn't match pattern")101 102 obj.__dict__[self.name] = value103104class User:105 username = ValidatedString('username', min_length=3, max_length=20)106 email = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')output String descriptor:self.name ← username, self.min_length ← 3, self.max_length ← 20
pass 1 of 275class ValidatedString:76 def __init__(self⟨ValidatedString G⟩, nameusername, min_length3=0, max_length20=NoneNone, patternNone=None):77 self.name→ username = nameusername78 self.min_length→ 3 = min_length379 self.max_length→ 20 = max_length2080 self.pattern→ None = patternNoneusername ← (empty)
104class User:105 username→ (empty) = ValidatedString('username', min_length=3, max_length=20)106 email = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')self.name ← email, self.min_length ← 0, self.max_length ← None
pass 2 of 275class ValidatedString:76 def __init__(self⟨ValidatedString H⟩, nameemail, min_length0=0, max_lengthNone=NoneNone, pattern^[\w\.-]+@[\w\.-]+\.\w+$=None):77 self.name→ email = nameemail78 self.min_length→ 0 = min_length079 self.max_length→ None = max_lengthNone80 self.pattern→ ^[\w\.-]+@[\w\.-]+\.\w+$ = pattern^[\w\.-]+@[\w\.-]+\.\w+$email ← (empty), user ← ⟨User I⟩
105 username = ValidatedString('username', min_length=3, max_length=20)106 email→ (empty) = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')107108user→ ⟨User I⟩ = User()109user.username = "alice"110user.email = "alice@example.com"obj.__dict__[self.name] ← alice
pass 1 of 387def __set__(self⟨ValidatedString G⟩, obj⟨User I⟩, valuealice):88 if not isinstance(value, str):89 raise TypeError(f"{self.name} must be a string")90 91 if len(value) < self.min_length:92 raise ValueError(f"{self.name} must be at least {self.min_length} characters")93 94 if self.max_length and len(value) > self.max_length:95 raise ValueError(f"{self.name} must be at most {self.max_length} characters")96 97 if self.pattern:98 import re99 if not re.match(self.pattern, value):100 raise ValueError(f"{self.name} doesn't match pattern")101 102 obj.__dict__[self.name]→ alice = valuealiceAll 3 passes — pass 1 is the card above pass selfvalueself.patternself.min_lengthself.nameeobj.__dict__[self.name]1 ⟨ValidatedString G⟩ alice — — — — alice 2 ⟨ValidatedString H⟩ alice@example.com ^[\w\.-]+@[\w\.-]+\.\w+$ — — — — 3 ⟨ValidatedString G⟩ ab — 3 username username must be at least 3 characters — user.username ← alice
108user = User()109user.username→ alice = "alice"110user.email = "alice@example.com"if self.pattern:
97if self.pattern^[\w\.-]+@[\w\.-]+\.\w+$:98 import re99 if not re.match(self.pattern, value):obj.__dict__[self.name] ← alice@example.com
102obj.__dict__[self.name]→ alice@example.com = valuealice@example.comuser.email ← alice@example.com
109user.username = "alice"110user.email→ alice@example.com = "alice@example.com"111112print(f"User: {user.usernamealice}, {user.emailalice@example.com}")def __get__(self, obj, type=None):
pass 1 of 282def __get__(self⟨ValidatedString G⟩, obj⟨User I⟩, type<class '__main__.User'>=NoneNone):83 if obj is None:84 return self85 return obj.__dict__{'username': 'alice', 'email': 'alice@example.com'}.get(self.nameusername)def __get__(self, obj, type=None):
pass 2 of 282def __get__(self⟨ValidatedString H⟩, obj⟨User I⟩, type<class '__main__.User'>=NoneNone):83 if obj is None:84 return self85 return obj.__dict__{'username': 'alice', 'email': 'alice@example.com'}.get(self.nameemail)print(f"User: {user.username}, {user.email}")
112print(f"User: {user.usernamealice}, {user.emailalice@example.com}")outputUser: alice, alice@example.comif len(value) < self.min_length:
91if len(valueab) < self.min_length3:92 raise ValueError(f"{self.nameusername} must be at least {self.min_length3} characters")except ValueError as e:
115 user.username = "ab" # Too short116except ValueError as e:117 print(f"Error: {eusername must be at least 3 characters}")outputError: username must be at least 3 charactersprint(" Choice descriptor:")
119# Choice descriptor120print("\nChoice descriptor:")121122class Choice:123 def __init__(self, name, choices):124 self.name = name125 self.choices = choices126 127 def __get__(self, obj, type=None):128 if obj is None:129 return self130 return obj.__dict__.get(self.name)131 132 def __set__(self, obj, value):133 if value not in self.choices:134 raise ValueError(f"{self.name} must be one of {self.choices}")135 obj.__dict__[self.name] = value136137class Status:138 value = Choice('value', ['pending', 'active', 'completed', 'cancelled'])output Choice descriptor:self.name ← value, self.choices ← ['pending', 'active', 'completed', 'cancelled']
122class Choice:123 def __init__(self⟨Choice J⟩, namevalue, choices['pending', 'active', 'completed', 'cancelled']):124 self.name→ value = namevalue125 self.choices→ ['pending', 'active', 'completed', 'cancelled'] = choices['pending', 'active', 'completed', 'cancelled']value ← (empty), status ← ⟨Status K⟩
137class Status:138 value→ (empty) = Choice('value', ['pending', 'active', 'completed', 'cancelled'])139140status→ ⟨Status K⟩ = Status()141status.value = 'active'142print(f"Status: {status.value}")obj.__dict__[self.name] ← active
pass 1 of 2132def __set__(self⟨Choice J⟩, obj⟨Status K⟩, valueactive):133 if value not in self.choices:134 raise ValueError(f"{self.name} must be one of {self.choices}")135 obj.__dict__[self.name]→ active = valueactivestatus.value ← active
140status = Status()141status.value→ active = 'active'142print(f"Status: {status.valueactive}")def __get__(self, obj, type=None):
127def __get__(self⟨Choice J⟩, obj⟨Status K⟩, type<class '__main__.Status'>=NoneNone):128 if obj is None:129 return self130 return obj.__dict__{'value': 'active'}.get(self.namevalue)print(f"Status: {status.value}")
141status.value = 'active'142print(f"Status: {status.valueactive}")outputStatus: activedef __set__(self, obj, value):
pass 2 of 2132def __set__(self⟨Choice J⟩, obj⟨Status K⟩, valueunknown):133 if value not in self.choices:134 raise ValueError(f"{self.name} must be one of {self.choices}")if value not in self.choices:
132def __set__(self, obj, value):133 if valueunknown not in self.choices['pending', 'active', 'completed', 'cancelled']:134 raise ValueError(f"{self.namevalue} must be one of {self.choices['pending', 'active', 'completed', 'cancelled']}")135 obj.__dict__[self.name] = valueexcept ValueError as e:
145 status.value = 'unknown'146except ValueError as e:147 print(f"Error: {evalue must be one of ['pending', 'active', 'completed', 'cancelled']}")outputError: value must be one of ['pending', 'active', 'completed', 'cancelled']print(" Logged descriptor:")
149# Logged descriptor150print("\nLogged descriptor:")151152class LoggedDescriptor:153 def __init__(self, name):154 self.name = name155 156 def __get__(self, obj, type=None):157 if obj is None:158 return self159 value = obj.__dict__.get(self.name)160 print(f" Getting {self.name}: {value}")161 return value162 163 def __set__(self, obj, value):164 print(f" Setting {self.name}: {obj.__dict__.get(self.name)} -> {value}")165 obj.__dict__[self.name] = value166167class TrackedObject:168 x = LoggedDescriptor('x')169 y = LoggedDescriptor('y')output Logged descriptor:self.name ← x
pass 1 of 2152class LoggedDescriptor:153 def __init__(self⟨LoggedDescriptor L⟩, namex):154 self.name→ x = namexx ← (empty)
167class TrackedObject:168 x→ (empty) = LoggedDescriptor('x')169 y = LoggedDescriptor('y')self.name ← y
pass 2 of 2152class LoggedDescriptor:153 def __init__(self⟨LoggedDescriptor M⟩, namey):154 self.name→ y = nameyy ← (empty), tracked ← ⟨TrackedObject N⟩
168 x = LoggedDescriptor('x')169 y→ (empty) = LoggedDescriptor('y')170171tracked→ ⟨TrackedObject N⟩ = TrackedObject()172tracked.x = 10173tracked.y = 20obj.__dict__[self.name] ← 10
pass 1 of 2163def __set__(self⟨LoggedDescriptor L⟩, obj⟨TrackedObject N⟩, value10):164 print(f" Setting {self.namex}: {obj.__dict__{}.get(self.name)} -> {value10}")165 obj.__dict__[self.name]→ 10 = value10output Setting x: None -> 10tracked.x ← 10
171tracked = TrackedObject()172tracked.x→ 10 = 10173tracked.y = 20174print(f"Sum: {tracked.x + tracked.y}")obj.__dict__[self.name] ← 20
pass 2 of 2163def __set__(self⟨LoggedDescriptor M⟩, obj⟨TrackedObject N⟩, value20):164 print(f" Setting {self.namey}: {obj.__dict__{'x': 10}.get(self.name)} -> {value20}")165 obj.__dict__[self.name]→ 20 = value20output Setting y: None -> 20tracked.y ← 20
172tracked.x = 10173tracked.y→ 20 = 20174print(f"Sum: {tracked.x10 + tracked.y20}")value ← 10
pass 1 of 2156def __get__(self⟨LoggedDescriptor L⟩, obj⟨TrackedObject N⟩, type<class '__main__.TrackedObject'>=NoneNone):157 if obj is None:158 return self159 value→ 10 = obj.__dict__{'x': 10, 'y': 20}.get(self.namex)160 print(f" Getting {self.namex}: {value10}")161 return value10output Getting x: 10value ← 20
pass 2 of 2156def __get__(self⟨LoggedDescriptor M⟩, obj⟨TrackedObject N⟩, type<class '__main__.TrackedObject'>=NoneNone):157 if obj is None:158 return self159 value→ 20 = obj.__dict__{'x': 10, 'y': 20}.get(self.namey)160 print(f" Getting {self.namey}: {value20}")161 return value20output Getting y: 20print(f"Sum: {tracked.x + tracked.y}")
173tracked.y = 20174print(f"Sum: {tracked.x10 + tracked.y20}")175176# Auto-converting descriptor177print("\nAuto-converting descriptor:")178179class IntDescriptor:180 def __init__(self, name):181 self.name = name182 183 def __get__(self, obj, type=None):184 if obj is None:185 return self186 return obj.__dict__.get(self.name)187 188 def __set__(self, obj, value):189 # Auto-convert to int190 try:191 obj.__dict__[self.name] = int(value)192 except (TypeError, ValueError) as e:193 raise ValueError(f"Cannot convert {value} to int")194195class Config:196 port = IntDescriptor('port')197 timeout = IntDescriptor('timeout')outputSum: 30 Auto-converting descriptor:self.name ← port
pass 1 of 2179class IntDescriptor:180 def __init__(self⟨IntDescriptor O⟩, nameport):181 self.name→ port = nameportport ← (empty)
195class Config:196 port→ (empty) = IntDescriptor('port')197 timeout = IntDescriptor('timeout')self.name ← timeout
pass 2 of 2179class IntDescriptor:180 def __init__(self⟨IntDescriptor P⟩, nametimeout):181 self.name→ timeout = nametimeouttimeout ← (empty), config ← ⟨Config Q⟩
196 port = IntDescriptor('port')197 timeout→ (empty) = IntDescriptor('timeout')198199config→ ⟨Config Q⟩ = Config()200config.port = "8080" # String converted to int201config.timeout = 30.5 # Float converted to intdef __set__(self, obj, value): # Auto-convert to int
pass 1 of 2188def __set__(self⟨IntDescriptor O⟩, obj⟨Config Q⟩, value8080):189 # Auto-convert to int190 try:191 obj.__dict__[self.name] = int(value)obj.__dict__[self.name] ← 8080
pass 1 of 2189# Auto-convert to int190try:191 obj.__dict__[self.name]→ 8080 = int(value8080)192except (TypeError, ValueError) as e:config.port ← 8080
199config = Config()200config.port→ 8080 = "8080" # String converted to int201config.timeout = 30.5 # Float converted to intdef __set__(self, obj, value): # Auto-convert to int
pass 2 of 2188def __set__(self⟨IntDescriptor P⟩, obj⟨Config Q⟩, value30.5):189 # Auto-convert to int190 try:191 obj.__dict__[self.name] = int(value)obj.__dict__[self.name] ← 30
pass 2 of 2189# Auto-convert to int190try:191 obj.__dict__[self.name]→ 30 = int(value30.5)192except (TypeError, ValueError) as e:config.timeout ← 30
200config.port = "8080" # String converted to int201config.timeout→ 30 = 30.5 # Float converted to int202203print(f"Port: {config.port8080} (type: {type(config.port).__name__})")204print(f"Timeout: {config.timeout} (type: {type(config.timeout).__name__})")def __get__(self, obj, type=None):
pass 1 of 4183def __get__(self⟨IntDescriptor O⟩, obj⟨Config Q⟩, type<class '__main__.Config'>=NoneNone):184 if obj is None:185 return self186 return obj.__dict__{'port': 8080, 'timeout': 30}.get(self.nameport)All 4 passes — pass 1 is the card above pass selfself.name1 ⟨IntDescriptor O⟩ port 2 ⟨IntDescriptor O⟩ port 3 ⟨IntDescriptor P⟩ timeout 4 ⟨IntDescriptor P⟩ timeout print(f"Port: {config.port} (type: {type(config.port).__name__})")
203print(f"Port: {config.port8080} (type: {type(config.port).__name__})")204print(f"Timeout: {config.timeout30} (type: {type(config.timeout).__name__})")outputPort: 8080 (type: int)print(f"Timeout: {config.timeout} (type: {type(config.timeout).__name_…
203print(f"Port: {config.port} (type: {type(config.port).__name__})")204print(f"Timeout: {config.timeout30} (type: {type(config.timeout).__name__})")205206# Practical example207print("\nPractical example:")208209# Database field descriptor210class Field:211 def __init__(self, name, field_type, required=False, default=None):212 self.name = name213 self.field_type = field_type214 self.required = required215 self.default = default216 217 def __get__(self, obj, type=None):218 if obj is None:219 return self220 return obj.__dict__.get(self.name, self.default)221 222 def __set__(self, obj, value):223 if value is None:224 if self.required:225 raise ValueError(f"{self.name} is required")226 obj.__dict__[self.name] = self.default227 elif not isinstance(value, self.field_type):228 raise TypeError(f"{self.name} must be {self.field_type.__name__}")229 else:230 obj.__dict__[self.name] = value231232class DatabaseRecord:233 id = Field('id', int, required=True)234 name = Field('name', str, required=True)outputTimeout: 30 (type: int) Practical example:self.name ← id, self.field_type ← <class 'int'>, self.required ← True
pass 1 of 4210class Field:211 def __init__(self⟨Field R⟩, nameid, field_type<class 'int'>, requiredTrue=FalseFalse, defaultNone=NoneNone):212 self.name→ id = nameid213 self.field_type→ <class 'int'> = field_type<class 'int'>214 self.required→ True = requiredTrue215 self.default→ None = defaultNoneAll 4 passes — pass 1 is the card above pass selfnamefield_typerequireddefaultself.nameself.field_typeself.requiredself.default1 ⟨Field R⟩ id <class 'int'> True None id <class 'int'> True None 2 ⟨Field S⟩ name <class 'str'> True None name <class 'str'> True None 3 ⟨Field T⟩ email <class 'str'> False None email <class 'str'> False None 4 ⟨Field U⟩ age <class 'int'> False 0 age <class 'int'> False 0 id ← <built-in function id>
232class DatabaseRecord:233 id→ <built-in function id> = Field('id', int, required=True)234 name = Field('name', str, required=True)235 email = Field('email', str)name ← (empty)
233id = Field('id', int, required=True)234name→ (empty) = Field('name', str, required=True)235email = Field('email', str)236age = Field('age', int, default=0)email ← (empty)
234name = Field('name', str, required=True)235email→ (empty) = Field('email', str)236age = Field('age', int, default=0)age ← (empty)
235email = Field('email', str)236age→ (empty) = Field('age', int, default=0)def __set__(self, obj, value):
pass 1 of 4222def __set__(self⟨Field R⟩, obj⟨DatabaseRecord V⟩, value1):223 if value is None:224 if self.required:All 4 passes — pass 1 is the card above pass selfobjvalue1 ⟨Field R⟩ ⟨DatabaseRecord V⟩ 1 2 ⟨Field S⟩ ⟨DatabaseRecord V⟩ Alice 3 ⟨Field T⟩ ⟨DatabaseRecord V⟩ alice@example.com 4 ⟨Field S⟩ ⟨DatabaseRecord W⟩ Bob obj.__dict__[self.name] ← 1
pass 1 of 4227elif not isinstance(value, self.field_type):228 raise TypeError(f"{self.name} must be {self.field_type.__name__}")229else:230 obj.__dict__[self.name]→ 1 = value1All 4 passes — pass 1 is the card above pass valueobj.__dict__[self.name]1 1 1 2 Alice Alice 3 alice@example.com alice@example.com 4 Bob Bob print(f"Record: id={record.id}, name={record.name}, email={record.emai…
243print(f"Record: id={record.id1}, name={record.nameAlice}, email={record.emailalice@example.com}, age={record.age0}")def __get__(self, obj, type=None):
pass 1 of 4217def __get__(self⟨Field R⟩, obj⟨DatabaseRecord V⟩, type<class '__main__.DatabaseRecord'>=NoneNone):218 if obj is None:219 return self220 return obj.__dict__{'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}.get(self.nameid, self.defaultNone)All 4 passes — pass 1 is the card above pass selfself.nameself.default1 ⟨Field R⟩ id None 2 ⟨Field S⟩ name None 3 ⟨Field T⟩ email None 4 ⟨Field U⟩ age 0 print(f"Record: id={record.id}, name={record.name}, email={record.emai…
243print(f"Record: id={record.id1}, name={record.nameAlice}, email={record.emailalice@example.com}, age={record.age0}")outputRecord: id=1, name=Alice, email=alice@example.com, age=0record2 ← ⟨DatabaseRecord W⟩
245try:246 record2→ ⟨DatabaseRecord W⟩ = DatabaseRecord()247 record2.name = "Bob"248 # Missing required 'id'record2.name ← Bob
246record2 = DatabaseRecord()247record2.name→ Bob = "Bob"248# Missing required 'id'
"""Custom descriptor classes"""
1"""Custom descriptor classes"""23# Typed descriptor4print("Typed descriptor:")56class TypedDescriptor:7 def __init__(self, name, expected_type):8 self.name = name9 self.expected_type = expected_type10 11 def __get__(self, obj, type=None):12 if obj is None:13 return self14 return obj.__dict__.get(self.name)15 16 def __set__(self, obj, value):17 if not isinstance(value, self.expected_type):18 raise TypeError(f"{self.name} must be {self.expected_type.__name__}, got {type(value).__name__}")19 obj.__dict__[self.name] = value2021class Person:22 name = TypedDescriptor('name', str)23 age = TypedDescriptor('age', int)outputTyped descriptor:self.name ← name, self.expected_type ← <class 'str'>
pass 1 of 36class TypedDescriptor:7 def __init__(self⟨TypedDescriptor A⟩, namename, expected_type<class 'str'>):8 self.name→ name = namename9 self.expected_type→ <class 'str'> = expected_type<class 'str'>All 3 passes — pass 1 is the card above pass selfnameexpected_typeself.nameself.expected_type1 ⟨TypedDescriptor A⟩ name <class 'str'> name <class 'str'> 2 ⟨TypedDescriptor B⟩ age <class 'int'> age <class 'int'> 3 ⟨TypedDescriptor C⟩ height <class 'float'> height <class 'float'> name ← (empty)
21class Person:22 name→ (empty) = TypedDescriptor('name', str)23 age = TypedDescriptor('age', int)24 height = TypedDescriptor('height', float)age ← (empty)
22name = TypedDescriptor('name', str)23age→ (empty) = TypedDescriptor('age', int)24height = TypedDescriptor('height', float)height ← (empty), person ← ⟨Person D⟩
23 age = TypedDescriptor('age', int)24 height→ (empty) = TypedDescriptor('height', float)2526person→ ⟨Person D⟩ = Person()27person.name = "Alice"28person.age = 30obj.__dict__[self.name] ← Alice
pass 1 of 416def __set__(self⟨TypedDescriptor A⟩, obj⟨Person D⟩, valueAlice):17 if not isinstance(value, self.expected_type):18 raise TypeError(f"{self.name} must be {self.expected_type.__name__}, got {type(value).__name__}")19 obj.__dict__[self.name]→ Alice = valueAliceAll 4 passes — pass 1 is the card above pass selfvalueself.expected_typeself.nameself.expected_type.__name__eobj.__dict__[self.name]1 ⟨TypedDescriptor A⟩ Alice — — — — Alice 2 ⟨TypedDescriptor B⟩ 30 — — — — 30 3 ⟨TypedDescriptor C⟩ 1.65 — — — — 1.65 4 ⟨TypedDescriptor B⟩ thirty <class 'int'> age int age must be int, got str — person.name ← Alice
26person = Person()27person.name→ Alice = "Alice"28person.age = 3029person.height = 1.65person.age ← 30
27person.name = "Alice"28person.age→ 30 = 3029person.height = 1.65person.height ← 1.65
28person.age = 3029person.height→ 1.65 = 1.653031print(f"Person: {person.nameAlice}, {person.age30}, {person.height1.65}m")def __get__(self, obj, type=None):
pass 1 of 311def __get__(self⟨TypedDescriptor A⟩, obj⟨Person D⟩, type<class '__main__.Person'>=NoneNone):12 if obj is None:13 return self14 return obj.__dict__{'name': 'Alice', 'age': 30, 'height': 1.65}.get(self.namename)All 3 passes — pass 1 is the card above pass selfself.name1 ⟨TypedDescriptor A⟩ name 2 ⟨TypedDescriptor B⟩ age 3 ⟨TypedDescriptor C⟩ height print(f"Person: {person.name}, {person.age}, {person.height}m")
31print(f"Person: {person.nameAlice}, {person.age30}, {person.height1.65}m")outputPerson: Alice, 30, 1.65mif not isinstance(value, self.expected_type):
16def __set__(self, obj, value):17 if not isinstance(valuethirty, self.expected_type<class 'int'>):18 raise TypeError(f"{self.nameage} must be {self.expected_type.__name__int}, got {type(valuethirty).__name__}")19 obj.__dict__[self.name] = valueexcept TypeError as e:
34 person.age = "thirty"35except TypeError as e:36 print(f"Error: {eage must be int, got str}")outputError: age must be int, got strprint(" Bounded descriptor:")
38# Bounded descriptor39print("\nBounded descriptor:")4041class BoundedNumber:42 def __init__(self, name, min_value=None, max_value=None):43 self.name = name44 self.min_value = min_value45 self.max_value = max_value46 47 def __get__(self, obj, type=None):48 if obj is None:49 return self50 return obj.__dict__.get(self.name)51 52 def __set__(self, obj, value):53 if self.min_value is not None and value < self.min_value:54 raise ValueError(f"{self.name} must be >= {self.min_value}")55 if self.max_value is not None and value > self.max_value:56 raise ValueError(f"{self.name} must be <= {self.max_value}")57 obj.__dict__[self.name] = value5859class Score:60 value = BoundedNumber('value', min_value=0, max_value=100)output Bounded descriptor:self.name ← value, self.min_value ← 0, self.max_value ← 100
41class BoundedNumber:42 def __init__(self⟨BoundedNumber E⟩, namevalue, min_value0=NoneNone, max_value100=None):43 self.name→ value = namevalue44 self.min_value→ 0 = min_value045 self.max_value→ 100 = max_value100value ← (empty), score ← ⟨Score F⟩, initial_score ← 100
59class Score:60 value→ (empty) = BoundedNumber('value', min_value=0, max_value=100)6162score→ ⟨Score F⟩ = Score()63initial_score→ 100 = 10064score.value = initial_score10065print(f"Score: {score.value}")obj.__dict__[self.name] ← 100
pass 1 of 252def __set__(self⟨BoundedNumber E⟩, obj⟨Score F⟩, value100):53 if self.min_value is not None and value < self.min_value:54 raise ValueError(f"{self.name} must be >= {self.min_value}")55 if self.max_value is not None and value > self.max_value:56 raise ValueError(f"{self.name} must be <= {self.max_value}")57 obj.__dict__[self.name]→ 100 = value100score.value ← 100
63initial_score = 10064score.value→ 100 = initial_score10065print(f"Score: {score.value100}")def __get__(self, obj, type=None):
47def __get__(self⟨BoundedNumber E⟩, obj⟨Score F⟩, type<class '__main__.Score'>=NoneNone):48 if obj is None:49 return self50 return obj.__dict__{'value': 100}.get(self.namevalue)print(f"Score: {score.value}")
64score.value = initial_score65print(f"Score: {score.value100}")outputScore: 100def __set__(self, obj, value):
pass 2 of 252def __set__(self⟨BoundedNumber E⟩, obj⟨Score F⟩, value150):53 if self.min_value is not None and value < self.min_value:54 raise ValueError(f"{self.name} must be >= {self.min_value}")if self.max_value is not None and value > self.max_value:
54 raise ValueError(f"{self.name} must be >= {self.min_value}")55if self.max_value100 is not None and value150 > self.max_value:56 raise ValueError(f"{self.namevalue} must be <= {self.max_value100}")57obj.__dict__[self.name] = valueexcept ValueError as e:
68 score.value = 15069except ValueError as e:70 print(f"Error: {evalue must be <= 100}")outputError: value must be <= 100print(" String descriptor:")
72# String descriptor73print("\nString descriptor:")7475class ValidatedString:76 def __init__(self, name, min_length=0, max_length=None, pattern=None):77 self.name = name78 self.min_length = min_length79 self.max_length = max_length80 self.pattern = pattern81 82 def __get__(self, obj, type=None):83 if obj is None:84 return self85 return obj.__dict__.get(self.name)86 87 def __set__(self, obj, value):88 if not isinstance(value, str):89 raise TypeError(f"{self.name} must be a string")90 91 if len(value) < self.min_length:92 raise ValueError(f"{self.name} must be at least {self.min_length} characters")93 94 if self.max_length and len(value) > self.max_length:95 raise ValueError(f"{self.name} must be at most {self.max_length} characters")96 97 if self.pattern:98 import re99 if not re.match(self.pattern, value):100 raise ValueError(f"{self.name} doesn't match pattern")101 102 obj.__dict__[self.name] = value103104class User:105 username = ValidatedString('username', min_length=3, max_length=20)106 email = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')output String descriptor:self.name ← username, self.min_length ← 3, self.max_length ← 20
pass 1 of 275class ValidatedString:76 def __init__(self⟨ValidatedString G⟩, nameusername, min_length3=0, max_length20=NoneNone, patternNone=None):77 self.name→ username = nameusername78 self.min_length→ 3 = min_length379 self.max_length→ 20 = max_length2080 self.pattern→ None = patternNoneusername ← (empty)
104class User:105 username→ (empty) = ValidatedString('username', min_length=3, max_length=20)106 email = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')self.name ← email, self.min_length ← 0, self.max_length ← None
pass 2 of 275class ValidatedString:76 def __init__(self⟨ValidatedString H⟩, nameemail, min_length0=0, max_lengthNone=NoneNone, pattern^[\w\.-]+@[\w\.-]+\.\w+$=None):77 self.name→ email = nameemail78 self.min_length→ 0 = min_length079 self.max_length→ None = max_lengthNone80 self.pattern→ ^[\w\.-]+@[\w\.-]+\.\w+$ = pattern^[\w\.-]+@[\w\.-]+\.\w+$email ← (empty), user ← ⟨User I⟩
105 username = ValidatedString('username', min_length=3, max_length=20)106 email→ (empty) = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')107108user→ ⟨User I⟩ = User()109user.username = "alice"110user.email = "alice@example.com"obj.__dict__[self.name] ← alice
pass 1 of 387def __set__(self⟨ValidatedString G⟩, obj⟨User I⟩, valuealice):88 if not isinstance(value, str):89 raise TypeError(f"{self.name} must be a string")90 91 if len(value) < self.min_length:92 raise ValueError(f"{self.name} must be at least {self.min_length} characters")93 94 if self.max_length and len(value) > self.max_length:95 raise ValueError(f"{self.name} must be at most {self.max_length} characters")96 97 if self.pattern:98 import re99 if not re.match(self.pattern, value):100 raise ValueError(f"{self.name} doesn't match pattern")101 102 obj.__dict__[self.name]→ alice = valuealiceAll 3 passes — pass 1 is the card above pass selfvalueself.patternself.min_lengthself.nameeobj.__dict__[self.name]1 ⟨ValidatedString G⟩ alice — — — — alice 2 ⟨ValidatedString H⟩ alice@example.com ^[\w\.-]+@[\w\.-]+\.\w+$ — — — — 3 ⟨ValidatedString G⟩ ab — 3 username username must be at least 3 characters — user.username ← alice
108user = User()109user.username→ alice = "alice"110user.email = "alice@example.com"if self.pattern:
97if self.pattern^[\w\.-]+@[\w\.-]+\.\w+$:98 import re99 if not re.match(self.pattern, value):obj.__dict__[self.name] ← alice@example.com
102obj.__dict__[self.name]→ alice@example.com = valuealice@example.comuser.email ← alice@example.com
109user.username = "alice"110user.email→ alice@example.com = "alice@example.com"111112print(f"User: {user.usernamealice}, {user.emailalice@example.com}")def __get__(self, obj, type=None):
pass 1 of 282def __get__(self⟨ValidatedString G⟩, obj⟨User I⟩, type<class '__main__.User'>=NoneNone):83 if obj is None:84 return self85 return obj.__dict__{'username': 'alice', 'email': 'alice@example.com'}.get(self.nameusername)def __get__(self, obj, type=None):
pass 2 of 282def __get__(self⟨ValidatedString H⟩, obj⟨User I⟩, type<class '__main__.User'>=NoneNone):83 if obj is None:84 return self85 return obj.__dict__{'username': 'alice', 'email': 'alice@example.com'}.get(self.nameemail)print(f"User: {user.username}, {user.email}")
112print(f"User: {user.usernamealice}, {user.emailalice@example.com}")outputUser: alice, alice@example.comif len(value) < self.min_length:
91if len(valueab) < self.min_length3:92 raise ValueError(f"{self.nameusername} must be at least {self.min_length3} characters")except ValueError as e:
115 user.username = "ab" # Too short116except ValueError as e:117 print(f"Error: {eusername must be at least 3 characters}")outputError: username must be at least 3 charactersprint(" Choice descriptor:")
119# Choice descriptor120print("\nChoice descriptor:")121122class Choice:123 def __init__(self, name, choices):124 self.name = name125 self.choices = choices126 127 def __get__(self, obj, type=None):128 if obj is None:129 return self130 return obj.__dict__.get(self.name)131 132 def __set__(self, obj, value):133 if value not in self.choices:134 raise ValueError(f"{self.name} must be one of {self.choices}")135 obj.__dict__[self.name] = value136137class Status:138 value = Choice('value', ['pending', 'active', 'completed', 'cancelled'])output Choice descriptor:self.name ← value, self.choices ← ['pending', 'active', 'completed', 'cancelled']
122class Choice:123 def __init__(self⟨Choice J⟩, namevalue, choices['pending', 'active', 'completed', 'cancelled']):124 self.name→ value = namevalue125 self.choices→ ['pending', 'active', 'completed', 'cancelled'] = choices['pending', 'active', 'completed', 'cancelled']value ← (empty), status ← ⟨Status K⟩
137class Status:138 value→ (empty) = Choice('value', ['pending', 'active', 'completed', 'cancelled'])139140status→ ⟨Status K⟩ = Status()141status.value = 'active'142print(f"Status: {status.value}")obj.__dict__[self.name] ← active
pass 1 of 2132def __set__(self⟨Choice J⟩, obj⟨Status K⟩, valueactive):133 if value not in self.choices:134 raise ValueError(f"{self.name} must be one of {self.choices}")135 obj.__dict__[self.name]→ active = valueactivestatus.value ← active
140status = Status()141status.value→ active = 'active'142print(f"Status: {status.valueactive}")def __get__(self, obj, type=None):
127def __get__(self⟨Choice J⟩, obj⟨Status K⟩, type<class '__main__.Status'>=NoneNone):128 if obj is None:129 return self130 return obj.__dict__{'value': 'active'}.get(self.namevalue)print(f"Status: {status.value}")
141status.value = 'active'142print(f"Status: {status.valueactive}")outputStatus: activedef __set__(self, obj, value):
pass 2 of 2132def __set__(self⟨Choice J⟩, obj⟨Status K⟩, valueunknown):133 if value not in self.choices:134 raise ValueError(f"{self.name} must be one of {self.choices}")if value not in self.choices:
132def __set__(self, obj, value):133 if valueunknown not in self.choices['pending', 'active', 'completed', 'cancelled']:134 raise ValueError(f"{self.namevalue} must be one of {self.choices['pending', 'active', 'completed', 'cancelled']}")135 obj.__dict__[self.name] = valueexcept ValueError as e:
145 status.value = 'unknown'146except ValueError as e:147 print(f"Error: {evalue must be one of ['pending', 'active', 'completed', 'cancelled']}")outputError: value must be one of ['pending', 'active', 'completed', 'cancelled']print(" Logged descriptor:")
149# Logged descriptor150print("\nLogged descriptor:")151152class LoggedDescriptor:153 def __init__(self, name):154 self.name = name155 156 def __get__(self, obj, type=None):157 if obj is None:158 return self159 value = obj.__dict__.get(self.name)160 print(f" Getting {self.name}: {value}")161 return value162 163 def __set__(self, obj, value):164 print(f" Setting {self.name}: {obj.__dict__.get(self.name)} -> {value}")165 obj.__dict__[self.name] = value166167class TrackedObject:168 x = LoggedDescriptor('x')169 y = LoggedDescriptor('y')output Logged descriptor:self.name ← x
pass 1 of 2152class LoggedDescriptor:153 def __init__(self⟨LoggedDescriptor L⟩, namex):154 self.name→ x = namexx ← (empty)
167class TrackedObject:168 x→ (empty) = LoggedDescriptor('x')169 y = LoggedDescriptor('y')self.name ← y
pass 2 of 2152class LoggedDescriptor:153 def __init__(self⟨LoggedDescriptor M⟩, namey):154 self.name→ y = nameyy ← (empty), tracked ← ⟨TrackedObject N⟩
168 x = LoggedDescriptor('x')169 y→ (empty) = LoggedDescriptor('y')170171tracked→ ⟨TrackedObject N⟩ = TrackedObject()172tracked.x = 10173tracked.y = 20obj.__dict__[self.name] ← 10
pass 1 of 2163def __set__(self⟨LoggedDescriptor L⟩, obj⟨TrackedObject N⟩, value10):164 print(f" Setting {self.namex}: {obj.__dict__{}.get(self.name)} -> {value10}")165 obj.__dict__[self.name]→ 10 = value10output Setting x: None -> 10tracked.x ← 10
171tracked = TrackedObject()172tracked.x→ 10 = 10173tracked.y = 20174print(f"Sum: {tracked.x + tracked.y}")obj.__dict__[self.name] ← 20
pass 2 of 2163def __set__(self⟨LoggedDescriptor M⟩, obj⟨TrackedObject N⟩, value20):164 print(f" Setting {self.namey}: {obj.__dict__{'x': 10}.get(self.name)} -> {value20}")165 obj.__dict__[self.name]→ 20 = value20output Setting y: None -> 20tracked.y ← 20
172tracked.x = 10173tracked.y→ 20 = 20174print(f"Sum: {tracked.x10 + tracked.y20}")value ← 10
pass 1 of 2156def __get__(self⟨LoggedDescriptor L⟩, obj⟨TrackedObject N⟩, type<class '__main__.TrackedObject'>=NoneNone):157 if obj is None:158 return self159 value→ 10 = obj.__dict__{'x': 10, 'y': 20}.get(self.namex)160 print(f" Getting {self.namex}: {value10}")161 return value10output Getting x: 10value ← 20
pass 2 of 2156def __get__(self⟨LoggedDescriptor M⟩, obj⟨TrackedObject N⟩, type<class '__main__.TrackedObject'>=NoneNone):157 if obj is None:158 return self159 value→ 20 = obj.__dict__{'x': 10, 'y': 20}.get(self.namey)160 print(f" Getting {self.namey}: {value20}")161 return value20output Getting y: 20print(f"Sum: {tracked.x + tracked.y}")
173tracked.y = 20174print(f"Sum: {tracked.x10 + tracked.y20}")175176# Auto-converting descriptor177print("\nAuto-converting descriptor:")178179class IntDescriptor:180 def __init__(self, name):181 self.name = name182 183 def __get__(self, obj, type=None):184 if obj is None:185 return self186 return obj.__dict__.get(self.name)187 188 def __set__(self, obj, value):189 # Auto-convert to int190 try:191 obj.__dict__[self.name] = int(value)192 except (TypeError, ValueError) as e:193 raise ValueError(f"Cannot convert {value} to int")194195class Config:196 port = IntDescriptor('port')197 timeout = IntDescriptor('timeout')outputSum: 30 Auto-converting descriptor:self.name ← port
pass 1 of 2179class IntDescriptor:180 def __init__(self⟨IntDescriptor O⟩, nameport):181 self.name→ port = nameportport ← (empty)
195class Config:196 port→ (empty) = IntDescriptor('port')197 timeout = IntDescriptor('timeout')self.name ← timeout
pass 2 of 2179class IntDescriptor:180 def __init__(self⟨IntDescriptor P⟩, nametimeout):181 self.name→ timeout = nametimeouttimeout ← (empty), config ← ⟨Config Q⟩
196 port = IntDescriptor('port')197 timeout→ (empty) = IntDescriptor('timeout')198199config→ ⟨Config Q⟩ = Config()200config.port = "8080" # String converted to int201config.timeout = 30.5 # Float converted to intdef __set__(self, obj, value): # Auto-convert to int
pass 1 of 2188def __set__(self⟨IntDescriptor O⟩, obj⟨Config Q⟩, value8080):189 # Auto-convert to int190 try:191 obj.__dict__[self.name] = int(value)obj.__dict__[self.name] ← 8080
pass 1 of 2189# Auto-convert to int190try:191 obj.__dict__[self.name]→ 8080 = int(value8080)192except (TypeError, ValueError) as e:config.port ← 8080
199config = Config()200config.port→ 8080 = "8080" # String converted to int201config.timeout = 30.5 # Float converted to intdef __set__(self, obj, value): # Auto-convert to int
pass 2 of 2188def __set__(self⟨IntDescriptor P⟩, obj⟨Config Q⟩, value30.5):189 # Auto-convert to int190 try:191 obj.__dict__[self.name] = int(value)obj.__dict__[self.name] ← 30
pass 2 of 2189# Auto-convert to int190try:191 obj.__dict__[self.name]→ 30 = int(value30.5)192except (TypeError, ValueError) as e:config.timeout ← 30
200config.port = "8080" # String converted to int201config.timeout→ 30 = 30.5 # Float converted to int202203print(f"Port: {config.port8080} (type: {type(config.port).__name__})")204print(f"Timeout: {config.timeout} (type: {type(config.timeout).__name__})")def __get__(self, obj, type=None):
pass 1 of 4183def __get__(self⟨IntDescriptor O⟩, obj⟨Config Q⟩, type<class '__main__.Config'>=NoneNone):184 if obj is None:185 return self186 return obj.__dict__{'port': 8080, 'timeout': 30}.get(self.nameport)All 4 passes — pass 1 is the card above pass selfself.name1 ⟨IntDescriptor O⟩ port 2 ⟨IntDescriptor O⟩ port 3 ⟨IntDescriptor P⟩ timeout 4 ⟨IntDescriptor P⟩ timeout print(f"Port: {config.port} (type: {type(config.port).__name__})")
203print(f"Port: {config.port8080} (type: {type(config.port).__name__})")204print(f"Timeout: {config.timeout30} (type: {type(config.timeout).__name__})")outputPort: 8080 (type: int)print(f"Timeout: {config.timeout} (type: {type(config.timeout).__name_…
203print(f"Port: {config.port} (type: {type(config.port).__name__})")204print(f"Timeout: {config.timeout30} (type: {type(config.timeout).__name__})")205206# Practical example207print("\nPractical example:")208209# Database field descriptor210class Field:211 def __init__(self, name, field_type, required=False, default=None):212 self.name = name213 self.field_type = field_type214 self.required = required215 self.default = default216 217 def __get__(self, obj, type=None):218 if obj is None:219 return self220 return obj.__dict__.get(self.name, self.default)221 222 def __set__(self, obj, value):223 if value is None:224 if self.required:225 raise ValueError(f"{self.name} is required")226 obj.__dict__[self.name] = self.default227 elif not isinstance(value, self.field_type):228 raise TypeError(f"{self.name} must be {self.field_type.__name__}")229 else:230 obj.__dict__[self.name] = value231232class DatabaseRecord:233 id = Field('id', int, required=True)234 name = Field('name', str, required=True)outputTimeout: 30 (type: int) Practical example:self.name ← id, self.field_type ← <class 'int'>, self.required ← True
pass 1 of 4210class Field:211 def __init__(self⟨Field R⟩, nameid, field_type<class 'int'>, requiredTrue=FalseFalse, defaultNone=NoneNone):212 self.name→ id = nameid213 self.field_type→ <class 'int'> = field_type<class 'int'>214 self.required→ True = requiredTrue215 self.default→ None = defaultNoneAll 4 passes — pass 1 is the card above pass selfnamefield_typerequireddefaultself.nameself.field_typeself.requiredself.default1 ⟨Field R⟩ id <class 'int'> True None id <class 'int'> True None 2 ⟨Field S⟩ name <class 'str'> True None name <class 'str'> True None 3 ⟨Field T⟩ email <class 'str'> False None email <class 'str'> False None 4 ⟨Field U⟩ age <class 'int'> False 0 age <class 'int'> False 0 id ← <built-in function id>
232class DatabaseRecord:233 id→ <built-in function id> = Field('id', int, required=True)234 name = Field('name', str, required=True)235 email = Field('email', str)name ← (empty)
233id = Field('id', int, required=True)234name→ (empty) = Field('name', str, required=True)235email = Field('email', str)236age = Field('age', int, default=0)email ← (empty)
234name = Field('name', str, required=True)235email→ (empty) = Field('email', str)236age = Field('age', int, default=0)age ← (empty)
235email = Field('email', str)236age→ (empty) = Field('age', int, default=0)def __set__(self, obj, value):
pass 1 of 4222def __set__(self⟨Field R⟩, obj⟨DatabaseRecord V⟩, value1):223 if value is None:224 if self.required:All 4 passes — pass 1 is the card above pass selfobjvalue1 ⟨Field R⟩ ⟨DatabaseRecord V⟩ 1 2 ⟨Field S⟩ ⟨DatabaseRecord V⟩ Alice 3 ⟨Field T⟩ ⟨DatabaseRecord V⟩ alice@example.com 4 ⟨Field S⟩ ⟨DatabaseRecord W⟩ Bob obj.__dict__[self.name] ← 1
pass 1 of 4227elif not isinstance(value, self.field_type):228 raise TypeError(f"{self.name} must be {self.field_type.__name__}")229else:230 obj.__dict__[self.name]→ 1 = value1All 4 passes — pass 1 is the card above pass valueobj.__dict__[self.name]1 1 1 2 Alice Alice 3 alice@example.com alice@example.com 4 Bob Bob print(f"Record: id={record.id}, name={record.name}, email={record.emai…
243print(f"Record: id={record.id1}, name={record.nameAlice}, email={record.emailalice@example.com}, age={record.age0}")def __get__(self, obj, type=None):
pass 1 of 4217def __get__(self⟨Field R⟩, obj⟨DatabaseRecord V⟩, type<class '__main__.DatabaseRecord'>=NoneNone):218 if obj is None:219 return self220 return obj.__dict__{'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}.get(self.nameid, self.defaultNone)All 4 passes — pass 1 is the card above pass selfself.nameself.default1 ⟨Field R⟩ id None 2 ⟨Field S⟩ name None 3 ⟨Field T⟩ email None 4 ⟨Field U⟩ age 0 print(f"Record: id={record.id}, name={record.name}, email={record.emai…
243print(f"Record: id={record.id1}, name={record.nameAlice}, email={record.emailalice@example.com}, age={record.age0}")outputRecord: id=1, name=Alice, email=alice@example.com, age=0record2 ← ⟨DatabaseRecord W⟩
245try:246 record2→ ⟨DatabaseRecord W⟩ = DatabaseRecord()247 record2.name = "Bob"248 # Missing required 'id'record2.name ← Bob
246record2 = DatabaseRecord()247record2.name→ Bob = "Bob"248# Missing required 'id'
Custom descriptors shine when you need the same attribute behavior across many classes or attributes. They are the foundation of ORM field definitions.
Data vs Non-data Descriptors
"""Data vs non-data descriptors"""
# Non-data descriptor
print("Non-data descriptor:")
class NonDataDescriptor:
def __get__(self, obj, type=None):
return "From descriptor"
class TestClass:
attr = NonDataDescriptor()
obj = TestClass()
# Get from descriptor
print(f"obj.attr: {obj.attr}")
# Set creates instance attribute
obj.attr = "From instance"
print(f"After setting: {obj.attr}") # Instance wins
# Check __dict__
print(f"obj.__dict__: {obj.__dict__}")
# Data descriptor
print("\nData descriptor:")
class DataDescriptor:
def __init__(self, name):
self.name = name
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name, "From descriptor")
def __set__(self, obj, value):
print(f" Descriptor __set__ called")
obj.__dict__[self.name] = value
class TestClass2:
attr = DataDescriptor('attr')
obj2 = TestClass2()
# Get from descriptor
print(f"obj2.attr: {obj2.attr}")
# Set through descriptor
obj2.attr = "Value"
print(f"After setting: {obj2.attr}")
# Cannot bypass descriptor
obj2.__dict__['attr'] = "Direct"
print(f"obj2.attr (still via descriptor): {obj2.attr}")
# Lookup order
print("\nLookup order:")
class SimpleNonData:
def __get__(self, obj, type=None):
return "non-data descriptor"
class SimpleData:
def __get__(self, obj, type=None):
return "data descriptor"
def __set__(self, obj, value):
pass
class Demo:
non_data = SimpleNonData()
data = SimpleData()
demo = Demo()
print("Before instance attributes:")
print(f" non_data: {demo.non_data}")
print(f" data: {demo.data}")
# Add instance attributes
demo.__dict__['non_data'] = "instance value"
demo.__dict__['data'] = "instance value"
print("\nAfter instance attributes:")
print(f" non_data: {demo.non_data}") # Instance wins
print(f" data: {demo.data}") # Descriptor wins
print(f"\nInstance __dict__: {demo.__dict__}")
# Delete behavior
print("\nDelete behavior:")
class DeletableDescriptor:
def __init__(self, name):
self.name = name
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name, "default")
def __set__(self, obj, value):
obj.__dict__[self.name] = value
def __delete__(self, obj):
print(f" Descriptor __delete__ called")
if self.name in obj.__dict__:
del obj.__dict__[self.name]
class Container:
value = DeletableDescriptor('value')
container = Container()
container.value = 100
print(f"value: {container.value}")
# Delete through descriptor
del container.value
print(f"After delete: {container.value}")
# Method as non-data descriptor
print("\nMethod as non-data descriptor:")
class MethodDescriptor:
def __init__(self, func):
self.func = func
def __get__(self, obj, type=None):
if obj is None:
return self
# Return bound method
return lambda *args, **kwargs: self.func(obj, *args, **kwargs)
class MyClass:
def __init__(self, value):
self.value = value
@MethodDescriptor
def get_value(self):
return self.value
obj = MyClass(42)
print(f"get_value(): {obj.get_value()}")
# Overriding data descriptor
print("\nOverriding data descriptor:")
class StrictDescriptor:
def __init__(self, name):
self.name = name
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if not isinstance(value, int):
raise TypeError("Must be int")
obj.__dict__[self.name] = value
class Numbers:
value = StrictDescriptor('value')
nums = Numbers()
nums.value = 100
print(f"value: {nums.value}")
# Cannot override with direct assignment
try:
nums.value = "text"
except TypeError as e:
print(f"Error: {e}")
# Priority demonstration
print("\nPriority demonstration:")
print("Lookup priority:")
print("1. Data descriptors (from type(obj).__mro__)")
print("2. Instance attributes (from obj.__dict__)")
print("3. Non-data descriptors (from type(obj).__mro__)")
print("4. Class attributes")
print("5. __getattr__() if defined")
class ExampleNonData:
def __get__(self, obj, type=None):
return "non-data"
class ExampleData:
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get('_data', 'data descriptor')
def __set__(self, obj, value):
obj.__dict__['_data'] = value
class PriorityDemo:
non_data = ExampleNonData()
data = ExampleData()
class_attr = "class attribute"
demo = PriorityDemo()
print(f"\nInitial state:")
print(f" non_data: {demo.non_data}")
print(f" data: {demo.data}")
print(f" class_attr: {demo.class_attr}")
# Add instance attributes
demo.__dict__['non_data'] = "instance non_data"
demo.__dict__['class_attr'] = "instance class_attr"
print(f"\nAfter adding instance attributes:")
print(f" non_data: {demo.non_data}") # instance wins (non-data)
print(f" data: {demo.data}") # descriptor wins (data)
print(f" class_attr: {demo.class_attr}") # instance wins (regular attr)
# Practical example
print("\nPractical example:")
# Validator that can be overridden
class Validator:
def __init__(self, name, validate_func=None):
self.name = name
self.validate_func = validate_func
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if self.validate_func and not self.validate_func(value):
raise ValueError(f"Invalid value for {self.name}")
obj.__dict__[self.name] = value
class Product:
price = Validator('price', lambda x: x >= 0)
quantity = Validator('quantity', lambda x: x >= 0)
product = Product()
product.price = 19.99
product.quantity = 10
print(f"Product: price=${product.price}, quantity={product.quantity}")
try:
product.price = -5
except ValueError as e:
print(f"Error: {e}")
attr ← (empty), obj ← ⟨TestClass A⟩
1"""Data vs non-data descriptors"""23# Non-data descriptor4print("Non-data descriptor:")56class NonDataDescriptor:7 def __get__(self, obj, type=None):8 return "From descriptor"910class TestClass:11 attr→ (empty) = NonDataDescriptor()1213obj→ ⟨TestClass A⟩ = TestClass()1415# Get from descriptor16print(f"obj.attr: {obj.attrFrom descriptor}")outputNon-data descriptor:def __get__(self, obj, type=None):
6class NonDataDescriptor:7 def __get__(self⟨NonDataDescriptor B⟩, obj⟨TestClass A⟩, type<class '__main__.TestClass'>=NoneNone):8 return "From descriptor"obj.attr ← From instance
15# Get from descriptor16print(f"obj.attr: {obj.attrFrom descriptor}")1718# Set creates instance attribute19obj.attr→ From instance = "From instance"20print(f"After setting: {obj.attrFrom instance}") # Instance wins2122# Check __dict__23print(f"obj.__dict__: {obj.__dict__{'attr': 'From instance'}}")2425# Data descriptor26print("\nData descriptor:")2728class DataDescriptor:29 def __init__(self, name):30 self.name = name31 32 def __get__(self, obj, type=None):33 if obj is None:34 return self35 return obj.__dict__.get(self.name, "From descriptor")36 37 def __set__(self, obj, value):38 print(f" Descriptor __set__ called")39 obj.__dict__[self.name] = value4041class TestClass2:42 attr = DataDescriptor('attr')outputobj.attr: From descriptor After setting: From instance obj.__dict__: {'attr': 'From instance'} Data descriptor:self.name ← attr
28class DataDescriptor:29 def __init__(self⟨DataDescriptor C⟩, nameattr):30 self.name→ attr = nameattrattr ← (empty), obj2 ← ⟨TestClass2 D⟩
41class TestClass2:42 attr→ (empty) = DataDescriptor('attr')4344obj2→ ⟨TestClass2 D⟩ = TestClass2()4546# Get from descriptor47print(f"obj2.attr: {obj2.attrFrom descriptor}")def __get__(self, obj, type=None):
pass 1 of 332def __get__(self⟨DataDescriptor C⟩, obj⟨TestClass2 D⟩, type<class '__main__.TestClass2'>=NoneNone):33 if obj is None:34 return self35 return obj.__dict__{}.get(self.nameattr, "From descriptor")All 3 passes — pass 1 is the card above pass obj.__dict__1 {} 2 {'attr': 'Value'} 3 {'attr': 'Direct'} print(f"obj2.attr: {obj2.attr}")
46# Get from descriptor47print(f"obj2.attr: {obj2.attrFrom descriptor}")4849# Set through descriptor50obj2.attr = "Value"51print(f"After setting: {obj2.attr}")outputobj2.attr: From descriptorobj.__dict__[self.name] ← Value
37def __set__(self⟨DataDescriptor C⟩, obj⟨TestClass2 D⟩, valueValue):38 print(f" Descriptor __set__ called")39 obj.__dict__[self.name]→ Value = valueValueoutput Descriptor __set__ calledobj2.attr ← Value
49# Set through descriptor50obj2.attr→ Value = "Value"51print(f"After setting: {obj2.attrValue}")obj2.__dict__[’attr’] ← Direct
50obj2.attr = "Value"51print(f"After setting: {obj2.attrValue}")5253# Cannot bypass descriptor54obj2.__dict__['attr']→ Direct = "Direct"55print(f"obj2.attr (still via descriptor): {obj2.attrDirect}")outputAfter setting: Valuenon_data ← (empty), data ← (empty), demo ← ⟨Demo E⟩
54obj2.__dict__['attr'] = "Direct"55print(f"obj2.attr (still via descriptor): {obj2.attrDirect}")5657# Lookup order58print("\nLookup order:")5960class SimpleNonData:61 def __get__(self, obj, type=None):62 return "non-data descriptor"6364class SimpleData:65 def __get__(self, obj, type=None):66 return "data descriptor"67 68 def __set__(self, obj, value):69 pass7071class Demo:72 non_data→ (empty) = SimpleNonData()73 data→ (empty) = SimpleData()7475demo→ ⟨Demo E⟩ = Demo()7677print("Before instance attributes:")78print(f" non_data: {demo.non_datanon-data descriptor}")79print(f" data: {demo.data}")outputobj2.attr (still via descriptor): Direct Lookup order: Before instance attributes:def __get__(self, obj, type=None):
60class SimpleNonData:61 def __get__(self⟨SimpleNonData F⟩, obj⟨Demo E⟩, type<class '__main__.Demo'>=NoneNone):62 return "non-data descriptor"print(f" non_data: {demo.non_data}")
77print("Before instance attributes:")78print(f" non_data: {demo.non_datanon-data descriptor}")79print(f" data: {demo.datadata descriptor}")output non_data: non-data descriptordef __get__(self, obj, type=None):
pass 1 of 264class SimpleData:65 def __get__(self⟨SimpleData G⟩, obj⟨Demo E⟩, type<class '__main__.Demo'>=NoneNone):66 return "data descriptor"demo.__dict__[’non_data’] ← instance value, demo.__dict__[’data’] ← instance value
78print(f" non_data: {demo.non_data}")79print(f" data: {demo.datadata descriptor}")8081# Add instance attributes82demo.__dict__['non_data']→ instance value = "instance value"83demo.__dict__['data']→ instance value = "instance value"8485print("\nAfter instance attributes:")86print(f" non_data: {demo.non_datainstance value}") # Instance wins87print(f" data: {demo.datadata descriptor}") # Descriptor winsoutput data: data descriptor After instance attributes: non_data: instance valuedef __get__(self, obj, type=None):
pass 2 of 264class SimpleData:65 def __get__(self⟨SimpleData G⟩, obj⟨Demo E⟩, type<class '__main__.Demo'>=NoneNone):66 return "data descriptor"print(f" data: {demo.data}") # Descriptor wins
86print(f" non_data: {demo.non_data}") # Instance wins87print(f" data: {demo.datadata descriptor}") # Descriptor wins8889print(f"\nInstance __dict__: {demo.__dict__{'non_data': 'instance value', 'data': 'instance value'}}")9091# Delete behavior92print("\nDelete behavior:")9394class DeletableDescriptor:95 def __init__(self, name):96 self.name = name97 98 def __get__(self, obj, type=None):99 if obj is None:100 return self101 return obj.__dict__.get(self.name, "default")102 103 def __set__(self, obj, value):104 obj.__dict__[self.name] = value105 106 def __delete__(self, obj):107 print(f" Descriptor __delete__ called")108 if self.name in obj.__dict__:109 del obj.__dict__[self.name]110111class Container:112 value = DeletableDescriptor('value')output data: data descriptor Instance __dict__: {'non_data': 'instance value', 'data': 'instance value'} Delete behavior:self.name ← value
94class DeletableDescriptor:95 def __init__(self⟨DeletableDescriptor H⟩, namevalue):96 self.name→ value = namevaluevalue ← (empty), container ← ⟨Container I⟩
111class Container:112 value→ (empty) = DeletableDescriptor('value')113114container→ ⟨Container I⟩ = Container()115container.value = 100116print(f"value: {container.value}")obj.__dict__[self.name] ← 100
103def __set__(self⟨DeletableDescriptor H⟩, obj⟨Container I⟩, value100):104 obj.__dict__[self.name]→ 100 = value100container.value ← 100
114container = Container()115container.value→ 100 = 100116print(f"value: {container.value100}")def __get__(self, obj, type=None):
pass 1 of 298def __get__(self⟨DeletableDescriptor H⟩, obj⟨Container I⟩, type<class '__main__.Container'>=NoneNone):99 if obj is None:100 return self101 return obj.__dict__{'value': 100}.get(self.namevalue, "default")print(f"value: {container.value}")
115container.value = 100116print(f"value: {container.value100}")outputvalue: 100def __delete__(self, obj):
106def __delete__(self⟨DeletableDescriptor H⟩, obj⟨Container I⟩):107 print(f" Descriptor __delete__ called")108 if self.name in obj.__dict__:output Descriptor __delete__ calledobj.__dict__[self.name] ← (empty)
107print(f" Descriptor __delete__ called")108if self.namevalue in obj.__dict__{'value': 100}:109 del obj.__dict__[self.name]→ (empty)print(f"After delete: {container.value}")
119del container.value120print(f"After delete: {container.valuedefault}")def __get__(self, obj, type=None):
pass 2 of 298def __get__(self⟨DeletableDescriptor H⟩, obj⟨Container I⟩, type<class '__main__.Container'>=NoneNone):99 if obj is None:100 return self101 return obj.__dict__{}.get(self.namevalue, "default")print(f"After delete: {container.value}")
119del container.value120print(f"After delete: {container.valuedefault}")121122# Method as non-data descriptor123print("\nMethod as non-data descriptor:")outputAfter delete: default Method as non-data descriptor:self.func ← ⟨function MyClass.get_value J⟩
125class MethodDescriptor:126 def __init__(self⟨MethodDescriptor K⟩, func⟨function MyClass.get_value J⟩):127 self.func→ ⟨function MyClass.get_value J⟩ = func⟨function MyClass.get_value J⟩obj = MyClass(42)
143obj = MyClass(42)144print(f"get_value(): {obj.get_value()}")self.value ← 42
135class MyClass:136 def __init__(self⟨MyClass L⟩, value42):137 self.value→ 42 = value42obj ← ⟨MyClass L⟩
143obj→ ⟨MyClass L⟩ = MyClass(42)144print(f"get_value(): {obj⟨MyClass L⟩.get_value()}")def __get__(self, obj, type=None):
129def __get__(self⟨MethodDescriptor K⟩, obj⟨MyClass L⟩, type<class '__main__.MyClass'>=NoneNone):130 if obj is None:131 return self132 # Return bound method133 return lambda *args, **kwargs: self.func(obj, *args, **kwargs)def get_value(self):
139@MethodDescriptor140def get_value(self⟨MyClass L⟩):141 return self.value42print(f"get_value(): {obj.get_value()}")
143obj = MyClass(42)144print(f"get_value(): {obj⟨MyClass L⟩.get_value()}")145146# Overriding data descriptor147print("\nOverriding data descriptor:")148149class StrictDescriptor:150 def __init__(self, name):151 self.name = name152 153 def __get__(self, obj, type=None):154 if obj is None:155 return self156 return obj.__dict__.get(self.name)157 158 def __set__(self, obj, value):159 if not isinstance(value, int):160 raise TypeError("Must be int")161 obj.__dict__[self.name] = value162163class Numbers:164 value = StrictDescriptor('value')outputget_value(): 42 Overriding data descriptor:self.name ← value
149class StrictDescriptor:150 def __init__(self⟨StrictDescriptor A⟩, namevalue):151 self.name→ value = namevaluevalue ← (empty), nums ← ⟨Numbers M⟩
163class Numbers:164 value→ (empty) = StrictDescriptor('value')165166nums→ ⟨Numbers M⟩ = Numbers()167nums.value = 100168print(f"value: {nums.value}")obj.__dict__[self.name] ← 100
pass 1 of 2158def __set__(self⟨StrictDescriptor A⟩, obj⟨Numbers M⟩, value100):159 if not isinstance(value, int):160 raise TypeError("Must be int")161 obj.__dict__[self.name]→ 100 = value100nums.value ← 100
166nums = Numbers()167nums.value→ 100 = 100168print(f"value: {nums.value100}")def __get__(self, obj, type=None):
153def __get__(self⟨StrictDescriptor A⟩, obj⟨Numbers M⟩, type<class '__main__.Numbers'>=NoneNone):154 if obj is None:155 return self156 return obj.__dict__{'value': 100}.get(self.namevalue)print(f"value: {nums.value}")
167nums.value = 100168print(f"value: {nums.value100}")outputvalue: 100def __set__(self, obj, value):
pass 2 of 2158def __set__(self⟨StrictDescriptor A⟩, obj⟨Numbers M⟩, valuetext):159 if not isinstance(value, int):160 raise TypeError("Must be int")if not isinstance(value, int):
158def __set__(self, obj, value):159 if not isinstance(valuetext, int):160 raise TypeError("Must be int")161 obj.__dict__[self.name] = valueexcept TypeError as e:
172 nums.value = "text"173except TypeError as e:174 print(f"Error: {eMust be int}")outputError: Must be intnon_data ← (empty), data ← (empty), class_attr ← (empty), demo ← ⟨PriorityDemo N⟩
176# Priority demonstration177print("\nPriority demonstration:")178179print("Lookup priority:")180print("1. Data descriptors (from type(obj).__mro__)")181print("2. Instance attributes (from obj.__dict__)")182print("3. Non-data descriptors (from type(obj).__mro__)")183print("4. Class attributes")184print("5. __getattr__() if defined")185186class ExampleNonData:187 def __get__(self, obj, type=None):188 return "non-data"189190class ExampleData:191 def __get__(self, obj, type=None):192 if obj is None:193 return self194 return obj.__dict__.get('_data', 'data descriptor')195 196 def __set__(self, obj, value):197 obj.__dict__['_data'] = value198199class PriorityDemo:200 non_data→ (empty) = ExampleNonData()201 data→ (empty) = ExampleData()202 class_attr→ (empty) = "class attribute"203204demo→ ⟨PriorityDemo N⟩ = PriorityDemo()205print(f"\nInitial state:")206print(f" non_data: {demo.non_datanon-data}")207print(f" data: {demo.data}")output Priority demonstration: Lookup priority: 1. Data descriptors (from type(obj).__mro__) 2. Instance attributes (from obj.__dict__) 3. Non-data descriptors (from type(obj).__mro__) 4. Class attributes 5. __getattr__() if defined Initial state:def __get__(self, obj, type=None):
186class ExampleNonData:187 def __get__(self⟨ExampleNonData O⟩, obj⟨PriorityDemo N⟩, type<class '__main__.PriorityDemo'>=NoneNone):188 return "non-data"print(f" non_data: {demo.non_data}")
205print(f"\nInitial state:")206print(f" non_data: {demo.non_datanon-data}")207print(f" data: {demo.datadata descriptor}")208print(f" class_attr: {demo.class_attr}")output non_data: non-datadef __get__(self, obj, type=None):
pass 1 of 2190class ExampleData:191 def __get__(self⟨ExampleData P⟩, obj⟨PriorityDemo N⟩, type<class '__main__.PriorityDemo'>=NoneNone):192 if obj is None:193 return self194 return obj.__dict__{}.get('_data', 'data descriptor')demo.__dict__[’non_data’] ← instance non_data, demo.__dict__[’class_attr’] ← instance class_attr
206print(f" non_data: {demo.non_data}")207print(f" data: {demo.datadata descriptor}")208print(f" class_attr: {demo.class_attrclass attribute}")209210# Add instance attributes211demo.__dict__['non_data']→ instance non_data = "instance non_data"212demo.__dict__['class_attr']→ instance class_attr = "instance class_attr"213214print(f"\nAfter adding instance attributes:")215print(f" non_data: {demo.non_datainstance non_data}") # instance wins (non-data)216print(f" data: {demo.datadata descriptor}") # descriptor wins (data)217print(f" class_attr: {demo.class_attr}") # instance wins (regular attr)output data: data descriptor class_attr: class attribute After adding instance attributes: non_data: instance non_datadef __get__(self, obj, type=None):
pass 2 of 2190class ExampleData:191 def __get__(self⟨ExampleData P⟩, obj⟨PriorityDemo N⟩, type<class '__main__.PriorityDemo'>=NoneNone):192 if obj is None:193 return self194 return obj.__dict__{'non_data': 'instance non_data', 'class_attr': 'instance class_attr'}.get('_data', 'data descriptor')print(f" data: {demo.data}") # descriptor wins (data)
215print(f" non_data: {demo.non_data}") # instance wins (non-data)216print(f" data: {demo.datadata descriptor}") # descriptor wins (data)217print(f" class_attr: {demo.class_attrinstance class_attr}") # instance wins (regular attr)218219# Practical example220print("\nPractical example:")221222# Validator that can be overridden223class Validator:224 def __init__(self, name, validate_func=None):225 self.name = name226 self.validate_func = validate_func227 228 def __get__(self, obj, type=None):229 if obj is None:230 return self231 return obj.__dict__.get(self.name)232 233 def __set__(self, obj, value):234 if self.validate_func and not self.validate_func(value):235 raise ValueError(f"Invalid value for {self.name}")236 obj.__dict__[self.name] = value237238class Product:239 price = Validator('price', lambda x: x >= 0)240 quantity = Validator('quantity', lambda x: x >= 0)output data: data descriptor class_attr: instance class_attr Practical example:self.name ← price, self.validate_func ← <function Product.<lambda> at ⟨addr Q⟩>
pass 1 of 2223class Validator:224 def __init__(self⟨Validator E⟩, nameprice, validate_func<function Product.<lambda> at ⟨addr Q⟩>=NoneNone):225 self.name→ price = nameprice226 self.validate_func→ <function Product.<lambda> at ⟨addr Q⟩> = validate_func<function Product.<lambda> at ⟨addr Q⟩>price ← (empty)
238class Product:239 price→ (empty) = Validator('price', lambda x: x >= 0)240 quantity = Validator('quantity', lambda x: x >= 0)self.name ← quantity, self.validate_func ← <function Product.<lambda> at ⟨addr R⟩>
pass 2 of 2223class Validator:224 def __init__(self⟨Validator S⟩, namequantity, validate_func<function Product.<lambda> at ⟨addr R⟩>=NoneNone):225 self.name→ quantity = namequantity226 self.validate_func→ <function Product.<lambda> at ⟨addr R⟩> = validate_func<function Product.<lambda> at ⟨addr R⟩>quantity ← (empty), product ← ⟨Product T⟩
239 price = Validator('price', lambda x: x >= 0)240 quantity→ (empty) = Validator('quantity', lambda x: x >= 0)241242product→ ⟨Product T⟩ = Product()243product.price = 19.99244product.quantity = 10obj.__dict__[self.name] ← 19.99
pass 1 of 3233def __set__(self⟨Validator E⟩, obj⟨Product T⟩, value19.99):234 if self.validate_func and not self.validate_func(value):235 raise ValueError(f"Invalid value for {self.name}")236 obj.__dict__[self.name]→ 19.99 = value19.99All 3 passes — pass 1 is the card above pass selfvalueself.validate_funcself.nameeobj.__dict__[self.name]1 ⟨Validator E⟩ 19.99 — — — 19.99 2 ⟨Validator S⟩ 10 — — — 10 3 ⟨Validator E⟩ -5 <function Product.<lambda> at ⟨addr Q⟩> price Invalid value for price — product.price ← 19.99
242product = Product()243product.price→ 19.99 = 19.99244product.quantity = 10product.quantity ← 10
243product.price = 19.99244product.quantity→ 10 = 10245246print(f"Product: price=${product.price19.99}, quantity={product.quantity10}")def __get__(self, obj, type=None):
pass 1 of 2228def __get__(self⟨Validator E⟩, obj⟨Product T⟩, type<class '__main__.Product'>=NoneNone):229 if obj is None:230 return self231 return obj.__dict__{'price': 19.99, 'quantity': 10}.get(self.nameprice)def __get__(self, obj, type=None):
pass 2 of 2228def __get__(self⟨Validator S⟩, obj⟨Product T⟩, type<class '__main__.Product'>=NoneNone):229 if obj is None:230 return self231 return obj.__dict__{'price': 19.99, 'quantity': 10}.get(self.namequantity)print(f"Product: price=${product.price}, quantity={product.quantity}")
246print(f"Product: price=${product.price19.99}, quantity={product.quantity10}")outputProduct: price=$19.99, quantity=10if self.validate_func and not self.validate_func(value):
233def __set__(self, obj, value):234 if self.validate_func<function Product.<lambda> at ⟨addr Q⟩> and not self.validate_func(value-5):235 raise ValueError(f"Invalid value for {self.nameprice}")236 obj.__dict__[self.name] = valueexcept ValueError as e:
249 product.price = -5250except ValueError as e:251 print(f"Error: {eInvalid value for price}")outputError: Invalid value for price
This distinction is crucial: data descriptors cannot be shadowed by instance attributes, making them ideal for validation. Non-data descriptors can be overridden, which is how method caching works.
Practical Applications
"""Practical descriptor applications"""
# ORM-style field
print("ORM-style field:")
class Field:
def __init__(self, field_type, default=None):
self.field_type = field_type
self.default = default
self.name = None
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name, self.default)
def __set__(self, obj, value):
if value is not None and not isinstance(value, self.field_type):
raise TypeError(f"{self.name} must be {self.field_type.__name__}")
obj.__dict__[self.name] = value
class User:
id = Field(int)
name = Field(str)
email = Field(str)
age = Field(int, default=0)
user = User()
user.id = 1
user.name = "Alice"
user.email = "alice@example.com"
print(f"User: id={user.id}, name={user.name}, email={user.email}, age={user.age}")
# Lazy loading
print("\nLazy loading:")
class LazyLoad:
def __init__(self, loader_func):
self.loader_func = loader_func
self.name = None
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, type=None):
if obj is None:
return self
# Check if already loaded
cache_name = f'_lazy_{self.name}'
if cache_name not in obj.__dict__:
print(f" Loading {self.name}...")
obj.__dict__[cache_name] = self.loader_func(obj)
return obj.__dict__[cache_name]
class Report:
def __init__(self, report_id):
self.report_id = report_id
@LazyLoad
def data(self):
# Expensive operation
return f"Report data for {self.report_id}"
@LazyLoad
def statistics(self):
# Another expensive operation
return {"count": 100, "avg": 50}
report = Report(123)
print("Report created (data not loaded)")
print("\nFirst access to data:")
print(report.data)
print("\nSecond access (cached):")
print(report.data)
print("\nFirst access to statistics:")
print(report.statistics)
# Validation with __set_name__
print("\nValidation with __set_name__:")
class Validated:
def __init__(self, validator):
self.validator = validator
self.name = None
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if not self.validator(value):
raise ValueError(f"Invalid value for {self.name}: {value}")
obj.__dict__[self.name] = value
class Account:
balance = Validated(lambda x: x >= 0)
interest_rate = Validated(lambda x: 0 <= x <= 1)
account = Account()
account.balance = 1000
account.interest_rate = 0.05
print(f"Account: balance={account.balance}, interest_rate={account.interest_rate}")
try:
account.balance = -100
except ValueError as e:
print(f"Error: {e}")
# Tracked attributes
print("\nTracked attributes:")
class Tracked:
def __init__(self):
self.name = None
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
old_value = obj.__dict__.get(self.name)
obj.__dict__[self.name] = value
# Track changes
if not hasattr(obj, '_changes'):
obj._changes = []
obj._changes.append({
'field': self.name,
'old': old_value,
'new': value
})
class TrackedModel:
name = Tracked()
value = Tracked()
model = TrackedModel()
model.name = "Initial"
model.value = 100
model.name = "Updated"
model.value = 200
print("Changes:")
for change in model._changes:
print(f" {change['field']}: {change['old']} -> {change['new']}")
# Type conversion
print("\nType conversion:")
class Converted:
def __init__(self, converter):
self.converter = converter
self.name = None
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
converted = self.converter(value)
obj.__dict__[self.name] = converted
class Config:
port = Converted(int)
debug = Converted(bool)
timeout = Converted(float)
config = Config()
config.port = "8080"
config.debug = "yes"
config.timeout = "30.5"
print(f"Config: port={config.port} ({type(config.port).__name__})")
print(f" debug={config.debug} ({type(config.debug).__name__})")
print(f" timeout={config.timeout} ({type(config.timeout).__name__})")
# Units descriptor
print("\nUnits descriptor:")
class Quantity:
def __init__(self, unit):
self.unit = unit
self.name = None
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, type=None):
if obj is None:
return self
value = obj.__dict__.get(self.name, 0)
return f"{value} {self.unit}"
def __set__(self, obj, value):
if not isinstance(value, (int, float)):
raise TypeError(f"{self.name} must be numeric")
obj.__dict__[self.name] = value
class Product:
weight = Quantity("kg")
length = Quantity("cm")
price = Quantity("USD")
product = Product()
product.weight = 2.5
product.length = 30
product.price = 19.99
print(f"Product: weight={product.weight}, length={product.length}, price={product.price}")
# Practical example
print("\nPractical example:")
# Complete model with multiple descriptor types
class IntField:
def __init__(self, min_value=None, max_value=None):
self.min_value = min_value
self.max_value = max_value
self.name = None
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if not isinstance(value, int):
raise TypeError(f"{self.name} must be int")
if self.min_value is not None and value < self.min_value:
raise ValueError(f"{self.name} must be >= {self.min_value}")
if self.max_value is not None and value > self.max_value:
raise ValueError(f"{self.name} must be <= {self.max_value}")
obj.__dict__[self.name] = value
class StringField:
def __init__(self, max_length=None):
self.max_length = max_length
self.name = None
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name, "")
def __set__(self, obj, value):
if not isinstance(value, str):
raise TypeError(f"{self.name} must be str")
if self.max_length and len(value) > self.max_length:
raise ValueError(f"{self.name} too long (max {self.max_length})")
obj.__dict__[self.name] = value
class Student:
id = IntField(min_value=1)
name = StringField(max_length=50)
age = IntField(min_value=0, max_value=150)
grade = IntField(min_value=0, max_value=100)
student = Student()
student.id = 1
student.name = "Alice"
student.age = 20
student.grade = 85
print(f"Student: id={student.id}, name={student.name}, age={student.age}, grade={student.grade}")
# Validation works
try:
student.grade = 105
except ValueError as e:
print(f"Error: {e}")
"""Practical descriptor applications"""
1"""Practical descriptor applications"""23# ORM-style field4print("ORM-style field:")56class Field:7 def __init__(self, field_type, default=None):8 self.field_type = field_type9 self.default = default10 self.name = None11 12 def __set_name__(self, owner, name):13 self.name = name14 15 def __get__(self, obj, type=None):16 if obj is None:17 return self18 return obj.__dict__.get(self.name, self.default)19 20 def __set__(self, obj, value):21 if value is not None and not isinstance(value, self.field_type):22 raise TypeError(f"{self.name} must be {self.field_type.__name__}")23 obj.__dict__[self.name] = value2425class User:26 id = Field(int)27 name = Field(str)outputORM-style field:self.field_type ← <class 'int'>, self.default ← None, self.name ← None
pass 1 of 46class Field:7 def __init__(self⟨Field A⟩, field_type<class 'int'>, defaultNone=NoneNone):8 self.field_type→ <class 'int'> = field_type<class 'int'>9 self.default→ None = defaultNone10 self.name→ None = NoneAll 4 passes — pass 1 is the card above pass selffield_typedefaultself.field_typeself.defaultself.name1 ⟨Field A⟩ <class 'int'> None <class 'int'> None None 2 ⟨Field B⟩ <class 'str'> None <class 'str'> None None 3 ⟨Field C⟩ <class 'str'> None <class 'str'> None None 4 ⟨Field D⟩ <class 'int'> 0 <class 'int'> 0 None id ← <built-in function id>
25class User:26 id→ <built-in function id> = Field(int)27 name = Field(str)28 email = Field(str)name ← (empty)
26id = Field(int)27name→ (empty) = Field(str)28email = Field(str)29age = Field(int, default=0)email ← (empty)
27name = Field(str)28email→ (empty) = Field(str)29age = Field(int, default=0)age ← (empty)
28email = Field(str)29age→ (empty) = Field(int, default=0)self.name ← id
pass 1 of 412def __set_name__(self⟨Field A⟩, owner<class '__main__.User'>, nameid):13 self.name→ id = nameidAll 4 passes — pass 1 is the card above pass selfnameself.name1 ⟨Field A⟩ id id 2 ⟨Field B⟩ name name 3 ⟨Field C⟩ email email 4 ⟨Field D⟩ age age user ← ⟨User E⟩
31user→ ⟨User E⟩ = User()32user.id = 133user.name = "Alice"obj.__dict__[self.name] ← 1
pass 1 of 320def __set__(self⟨Field A⟩, obj⟨User E⟩, value1):21 if value is not None and not isinstance(value, self.field_type):22 raise TypeError(f"{self.name} must be {self.field_type.__name__}")23 obj.__dict__[self.name]→ 1 = value1All 3 passes — pass 1 is the card above pass selfvalueobj.__dict__[self.name]1 ⟨Field A⟩ 1 1 2 ⟨Field B⟩ Alice Alice 3 ⟨Field C⟩ alice@example.com alice@example.com user.id ← 1
31user = User()32user.id→ 1 = 133user.name = "Alice"34user.email = "alice@example.com"user.name ← Alice
32user.id = 133user.name→ Alice = "Alice"34user.email = "alice@example.com"user.email ← alice@example.com
33user.name = "Alice"34user.email→ alice@example.com = "alice@example.com"3536print(f"User: id={user.id1}, name={user.nameAlice}, email={user.emailalice@example.com}, age={user.age0}")def __get__(self, obj, type=None):
pass 1 of 415def __get__(self⟨Field A⟩, obj⟨User E⟩, type<class '__main__.User'>=NoneNone):16 if obj is None:17 return self18 return obj.__dict__{'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}.get(self.nameid, self.defaultNone)All 4 passes — pass 1 is the card above pass selfself.nameself.default1 ⟨Field A⟩ id None 2 ⟨Field B⟩ name None 3 ⟨Field C⟩ email None 4 ⟨Field D⟩ age 0 print(f"User: id={user.id}, name={user.name}, email={user.email}, age=…
36print(f"User: id={user.id1}, name={user.nameAlice}, email={user.emailalice@example.com}, age={user.age0}")3738# Lazy loading39print("\nLazy loading:")outputUser: id=1, name=Alice, email=alice@example.com, age=0 Lazy loading:self.loader_func ← ⟨function Report.data F⟩, self.name ← None
pass 1 of 241class LazyLoad:42 def __init__(self⟨LazyLoad G⟩, loader_func⟨function Report.data F⟩):43 self.loader_func→ ⟨function Report.data F⟩ = loader_func⟨function Report.data F⟩44 self.name→ None = Noneself.loader_func ← ⟨function Report.statistics H⟩, self.name ← None
pass 2 of 241class LazyLoad:42 def __init__(self⟨LazyLoad I⟩, loader_func⟨function Report.statistics H⟩):43 self.loader_func→ ⟨function Report.statistics H⟩ = loader_func⟨function Report.statistics H⟩44 self.name→ None = Noneself.name ← data
pass 1 of 246def __set_name__(self⟨LazyLoad G⟩, owner<class '__main__.Report'>, namedata):47 self.name→ data = namedataself.name ← statistics
pass 2 of 246def __set_name__(self⟨LazyLoad I⟩, owner<class '__main__.Report'>, namestatistics):47 self.name→ statistics = namestatisticsreport = Report(123)
75report = Report(123)76print("Report created (data not loaded)")self.report_id ← 123
61class Report:62 def __init__(self⟨Report J⟩, report_id123):63 self.report_id→ 123 = report_id123report ← ⟨Report J⟩
75report→ ⟨Report J⟩ = Report(123)76print("Report created (data not loaded)")7778print("\nFirst access to data:")79print(report.dataReport data for 123)outputReport created (data not loaded) First access to data:cache_name ← _lazy_data
pass 1 of 349def __get__(self⟨LazyLoad G⟩, obj⟨Report J⟩, type<class '__main__.Report'>=NoneNone):50 if obj is None:51 return self52 53 # Check if already loaded54 cache_name→ _lazy_data = f'_lazy_{self.namedata}'55 if cache_name not in obj.__dict__:56 print(f" Loading {self.name}...")57 obj.__dict__[cache_name] = self.loader_func(obj)58 59 return obj.__dict__[cache_name]Report data for 123All 3 passes — pass 1 is the card above pass selfself.nameobj.__dict__[cache_name]cache_name1 ⟨LazyLoad G⟩ data Report data for 123 _lazy_data 2 ⟨LazyLoad G⟩ data Report data for 123 _lazy_data 3 ⟨LazyLoad I⟩ statistics {'count': 100, 'avg': 50} _lazy_statistics print(report.data)
78print("\nFirst access to data:")79print(report.dataReport data for 123)8081print("\nSecond access (cached):")82print(report.dataReport data for 123)outputReport data for 123 Second access (cached):print(report.data)
81print("\nSecond access (cached):")82print(report.dataReport data for 123)8384print("\nFirst access to statistics:")85print(report.statistics{'count': 100, 'avg': 50})outputReport data for 123 First access to statistics:print(report.statistics)
84print("\nFirst access to statistics:")85print(report.statistics{'count': 100, 'avg': 50})8687# Validation with __set_name__88print("\nValidation with __set_name__:")8990class Validated:91 def __init__(self, validator):92 self.validator = validator93 self.name = None94 95 def __set_name__(self, owner, name):96 self.name = name97 98 def __get__(self, obj, type=None):99 if obj is None:100 return self101 return obj.__dict__.get(self.name)102 103 def __set__(self, obj, value):104 if not self.validator(value):105 raise ValueError(f"Invalid value for {self.name}: {value}")106 obj.__dict__[self.name] = value107108class Account:109 balance = Validated(lambda x: x >= 0)110 interest_rate = Validated(lambda x: 0 <= x <= 1)output{'count': 100, 'avg': 50} Validation with __set_name__:self.validator ← <function Account.<lambda> at ⟨addr K⟩>, self.name ← None
pass 1 of 290class Validated:91 def __init__(self⟨Validated L⟩, validator<function Account.<lambda> at ⟨addr K⟩>):92 self.validator→ <function Account.<lambda> at ⟨addr K⟩> = validator<function Account.<lambda> at ⟨addr K⟩>93 self.name→ None = Nonebalance ← (empty)
108class Account:109 balance→ (empty) = Validated(lambda x: x >= 0)110 interest_rate = Validated(lambda x: 0 <= x <= 1)self.validator ← <function Account.<lambda> at ⟨addr M⟩>, self.name ← None
pass 2 of 290class Validated:91 def __init__(self⟨Validated N⟩, validator<function Account.<lambda> at ⟨addr M⟩>):92 self.validator→ <function Account.<lambda> at ⟨addr M⟩> = validator<function Account.<lambda> at ⟨addr M⟩>93 self.name→ None = Noneinterest_rate ← (empty)
109balance = Validated(lambda x: x >= 0)110interest_rate→ (empty) = Validated(lambda x: 0 <= x <= 1)self.name ← balance
pass 1 of 295def __set_name__(self⟨Validated L⟩, owner<class '__main__.Account'>, namebalance):96 self.name→ balance = namebalanceself.name ← interest_rate
pass 2 of 295def __set_name__(self⟨Validated N⟩, owner<class '__main__.Account'>, nameinterest_rate):96 self.name→ interest_rate = nameinterest_rateaccount ← ⟨Account O⟩
112account→ ⟨Account O⟩ = Account()113account.balance = 1000114account.interest_rate = 0.05obj.__dict__[self.name] ← 1000
pass 1 of 3103def __set__(self⟨Validated L⟩, obj⟨Account O⟩, value1000):104 if not self.validator(value):105 raise ValueError(f"Invalid value for {self.name}: {value}")106 obj.__dict__[self.name]→ 1000 = value1000All 3 passes — pass 1 is the card above pass selfvalueself.nameeobj.__dict__[self.name]1 ⟨Validated L⟩ 1000 — — 1000 2 ⟨Validated N⟩ 0.05 — — 0.05 3 ⟨Validated L⟩ -100 balance Invalid value for balance: -100 — account.balance ← 1000
112account = Account()113account.balance→ 1000 = 1000114account.interest_rate = 0.05account.interest_rate ← 0.05
113account.balance = 1000114account.interest_rate→ 0.05 = 0.05115116print(f"Account: balance={account.balance1000}, interest_rate={account.interest_rate0.05}")def __get__(self, obj, type=None):
pass 1 of 298def __get__(self⟨Validated L⟩, obj⟨Account O⟩, type<class '__main__.Account'>=NoneNone):99 if obj is None:100 return self101 return obj.__dict__{'balance': 1000, 'interest_rate': 0.05}.get(self.namebalance)def __get__(self, obj, type=None):
pass 2 of 298def __get__(self⟨Validated N⟩, obj⟨Account O⟩, type<class '__main__.Account'>=NoneNone):99 if obj is None:100 return self101 return obj.__dict__{'balance': 1000, 'interest_rate': 0.05}.get(self.nameinterest_rate)print(f"Account: balance={account.balance}, interest_rate={account.int…
116print(f"Account: balance={account.balance1000}, interest_rate={account.interest_rate0.05}")outputAccount: balance=1000, interest_rate=0.05if not self.validator(value):
103def __set__(self, obj, value):104 if not self.validator(value-100):105 raise ValueError(f"Invalid value for {self.namebalance}: {value-100}")106 obj.__dict__[self.name] = valueexcept ValueError as e:
119 account.balance = -100120except ValueError as e:121 print(f"Error: {eInvalid value for balance: -100}")outputError: Invalid value for balance: -100print(" Tracked attributes:")
123# Tracked attributes124print("\nTracked attributes:")125126class Tracked:127 def __init__(self):128 self.name = None129 130 def __set_name__(self, owner, name):131 self.name = name132 133 def __get__(self, obj, type=None):134 if obj is None:135 return self136 return obj.__dict__.get(self.name)137 138 def __set__(self, obj, value):139 old_value = obj.__dict__.get(self.name)140 obj.__dict__[self.name] = value141 142 # Track changes143 if not hasattr(obj, '_changes'):144 obj._changes = []145 146 obj._changes.append({147 'field': self.name,148 'old': old_value,149 'new': value150 })151152class TrackedModel:153 name = Tracked()154 value = Tracked()output Tracked attributes:self.name ← None
pass 1 of 2126class Tracked:127 def __init__(self⟨Tracked P⟩):128 self.name→ None = Nonename ← (empty)
152class TrackedModel:153 name→ (empty) = Tracked()154 value = Tracked()self.name ← None
pass 2 of 2126class Tracked:127 def __init__(self⟨Tracked Q⟩):128 self.name→ None = Nonevalue ← (empty)
153name = Tracked()154value→ (empty) = Tracked()self.name ← name
pass 1 of 2130def __set_name__(self⟨Tracked P⟩, owner<class '__main__.TrackedModel'>, namename):131 self.name→ name = namenameself.name ← value
pass 2 of 2130def __set_name__(self⟨Tracked Q⟩, owner<class '__main__.TrackedModel'>, namevalue):131 self.name→ value = namevaluemodel ← ⟨TrackedModel R⟩
156model→ ⟨TrackedModel R⟩ = TrackedModel()157model.name = "Initial"158model.value = 100old_value ← None, obj.__dict__[self.name] ← Initial
pass 1 of 4138def __set__(self⟨Tracked P⟩, obj⟨TrackedModel R⟩, valueInitial):139 old_value→ None = obj.__dict__{}.get(self.namename)140 obj.__dict__[self.name]→ Initial = valueInitialAll 4 passes — pass 1 is the card above pass selfvalueobj.__dict__self.nameold_valueobj.__dict__[self.name]obj._changes1 ⟨Tracked P⟩ Initial {} name None Initial [] 2 ⟨Tracked Q⟩ 100 {'name': 'Initial', '_changes': [{'field': 'name', 'old': None, 'new': 'Initial'}]} value None 100 [{'field': 'name', 'old': None, 'new': 'Initial'}] → [{'field': 'name', 'old': None, 'new': 'Initial'}, {'field': 'value', 'old': None, 'new': 100}] 3 ⟨Tracked P⟩ Updated {'name': 'Initial', '_changes': [{'field': 'name', 'old': None, 'new': 'Initial'}, {'field': 'value', 'old': None, 'new': 100}], 'value': 100} name Initial Updated [{'field': 'name', 'old': None, 'new': 'Initial'}, {'field': 'value', 'old': None, 'new': 100}] → [{'field': 'name', 'old': None, 'new': 'Initial'}, {'field': 'value', 'old': None, 'new': 100}, {'field': 'name', 'old': 'Initial', 'new': 'Updated'}] 4 ⟨Tracked Q⟩ 200 {'name': 'Updated', '_changes': [{'field': 'name', 'old': None, 'new': 'Initial'}, {'field': 'value', 'old': None, 'new': 100}, {'field': 'name', 'old': 'Initial', 'new': 'Updated'}], 'value': 100} value 100 200 [{'field': 'name', 'old': None, 'new': 'Initial'}, {'field': 'value', 'old': None, 'new': 100}, {'field': 'name', 'old': 'Initial', 'new': 'Updated'}] → [{'field': 'name', 'old': None, 'new': 'Initial'}, {'field': 'value', 'old': None, 'new': 100}, {'field': 'name', 'old': 'Initial', 'new': 'Updated'}, {'field': 'value', 'old': 100, 'new': 200}] obj._changes ← []
142# Track changes143if not hasattr(obj⟨TrackedModel R⟩, '_changes'):144 obj._changes→ [] = []obj._changes ← [{'field': 'name', 'old': None, 'new': 'Initial'}]
146obj._changes→ [{'field': 'name', 'old': None, 'new': 'Initial'}].append({147 'field': self.namename,148 'old': old_valueNone,149 'new': valueInitial150})model.name ← Initial
156model = TrackedModel()157model.name→ Initial = "Initial"158model.value = 100159model.name = "Updated"model.value ← 100
157model.name = "Initial"158model.value→ 100 = 100159model.name = "Updated"160model.value = 200model.name ← Updated
158model.value = 100159model.name→ Updated = "Updated"160model.value = 200model.value ← 200
159model.name = "Updated"160model.value→ 200 = 200161162print("Changes:")163for change in model._changes:outputChanges:for change in model._changes:
pass 1 of 4162print("Changes:")163for change{'field': 'name', 'old': None, 'new': 'Initial'} in model._changes[{'field': 'name', 'old': None, 'new': 'Initial'}, {'field': 'value', 'old': None, 'new': 100}, {'field': 'name', 'old': 'Initial', 'new': 'Updated'}, {'field': 'value', 'old': 100, 'new': 200}]:164 print(f" {change['field']name}: {change['old']None} -> {change['new']Initial}")output name: None -> InitialAll 4 passes — pass 1 is the card above pass changechange[’field’]change[’old’]change[’new’]1 {'field': 'name', 'old': None, 'new': 'Initial'} name None Initial 2 {'field': 'value', 'old': None, 'new': 100} value None 100 3 {'field': 'name', 'old': 'Initial', 'new': 'Updated'} name Initial Updated 4 {'field': 'value', 'old': 100, 'new': 200} value 100 200 print(" Type conversion:")
166# Type conversion167print("\nType conversion:")168169class Converted:170 def __init__(self, converter):171 self.converter = converter172 self.name = None173 174 def __set_name__(self, owner, name):175 self.name = name176 177 def __get__(self, obj, type=None):178 if obj is None:179 return self180 return obj.__dict__.get(self.name)181 182 def __set__(self, obj, value):183 converted = self.converter(value)184 obj.__dict__[self.name] = converted185186class Config:187 port = Converted(int)188 debug = Converted(bool)output Type conversion:self.converter ← <class 'int'>, self.name ← None
pass 1 of 3169class Converted:170 def __init__(self⟨Converted S⟩, converter<class 'int'>):171 self.converter→ <class 'int'> = converter<class 'int'>172 self.name→ None = NoneAll 3 passes — pass 1 is the card above pass selfconverterself.converterself.name1 ⟨Converted S⟩ <class 'int'> <class 'int'> None 2 ⟨Converted T⟩ <class 'bool'> <class 'bool'> None 3 ⟨Converted U⟩ <class 'float'> <class 'float'> None port ← (empty)
186class Config:187 port→ (empty) = Converted(int)188 debug = Converted(bool)189 timeout = Converted(float)debug ← (empty)
187port = Converted(int)188debug→ (empty) = Converted(bool)189timeout = Converted(float)timeout ← (empty)
188debug = Converted(bool)189timeout→ (empty) = Converted(float)self.name ← port
pass 1 of 3174def __set_name__(self⟨Converted S⟩, owner<class '__main__.Config'>, nameport):175 self.name→ port = nameportAll 3 passes — pass 1 is the card above pass selfnameself.name1 ⟨Converted S⟩ port port 2 ⟨Converted T⟩ debug debug 3 ⟨Converted U⟩ timeout timeout config ← ⟨Config V⟩
191config→ ⟨Config V⟩ = Config()192config.port = "8080"193config.debug = "yes"converted ← 8080, obj.__dict__[self.name] ← 8080
pass 1 of 3182def __set__(self⟨Converted S⟩, obj⟨Config V⟩, value8080):183 converted→ 8080 = self.converter(value8080)184 obj.__dict__[self.name]→ 8080 = converted8080All 3 passes — pass 1 is the card above pass selfvalueconvertedobj.__dict__[self.name]1 ⟨Converted S⟩ 8080 8080 8080 2 ⟨Converted T⟩ yes True True 3 ⟨Converted U⟩ 30.5 30.5 30.5 config.port ← 8080
191config = Config()192config.port→ 8080 = "8080"193config.debug = "yes"194config.timeout = "30.5"config.debug ← True
192config.port = "8080"193config.debug→ True = "yes"194config.timeout = "30.5"config.timeout ← 30.5
193config.debug = "yes"194config.timeout→ 30.5 = "30.5"195196print(f"Config: port={config.port8080} ({type(config.port).__name__})")197print(f" debug={config.debug} ({type(config.debug).__name__})")def __get__(self, obj, type=None):
pass 1 of 6177def __get__(self⟨Converted S⟩, obj⟨Config V⟩, type<class '__main__.Config'>=NoneNone):178 if obj is None:179 return self180 return obj.__dict__{'port': 8080, 'debug': True, 'timeout': 30.5}.get(self.nameport)All 6 passes — pass 1 is the card above pass selfself.name1 ⟨Converted S⟩ port 2 ⟨Converted S⟩ port 3 ⟨Converted T⟩ debug 4 ⟨Converted T⟩ debug 5 ⟨Converted U⟩ timeout 6 ⟨Converted U⟩ timeout print(f"Config: port={config.port} ({type(config.port).__name__})")
196print(f"Config: port={config.port8080} ({type(config.port).__name__})")197print(f" debug={config.debugTrue} ({type(config.debug).__name__})")198print(f" timeout={config.timeout} ({type(config.timeout).__name__})")outputConfig: port=8080 (int)print(f" debug={config.debug} ({type(config.debug).__name__})")
196print(f"Config: port={config.port} ({type(config.port).__name__})")197print(f" debug={config.debugTrue} ({type(config.debug).__name__})")198print(f" timeout={config.timeout30.5} ({type(config.timeout).__name__})")output debug=True (bool)print(f" timeout={config.timeout} ({type(config.timeout).__name…
197print(f" debug={config.debug} ({type(config.debug).__name__})")198print(f" timeout={config.timeout30.5} ({type(config.timeout).__name__})")199200# Units descriptor201print("\nUnits descriptor:")202203class Quantity:204 def __init__(self, unit):205 self.unit = unit206 self.name = None207 208 def __set_name__(self, owner, name):209 self.name = name210 211 def __get__(self, obj, type=None):212 if obj is None:213 return self214 value = obj.__dict__.get(self.name, 0)215 return f"{value} {self.unit}"216 217 def __set__(self, obj, value):218 if not isinstance(value, (int, float)):219 raise TypeError(f"{self.name} must be numeric")220 obj.__dict__[self.name] = value221222class Product:223 weight = Quantity("kg")224 length = Quantity("cm")output timeout=30.5 (float) Units descriptor:self.unit ← kg, self.name ← None
pass 1 of 3203class Quantity:204 def __init__(self⟨Quantity W⟩, unitkg):205 self.unit→ kg = unitkg206 self.name→ None = NoneAll 3 passes — pass 1 is the card above pass selfunitself.unitself.name1 ⟨Quantity W⟩ kg kg None 2 ⟨Quantity X⟩ cm cm None 3 ⟨Quantity Y⟩ USD USD None weight ← (empty)
222class Product:223 weight→ (empty) = Quantity("kg")224 length = Quantity("cm")225 price = Quantity("USD")length ← (empty)
223weight = Quantity("kg")224length→ (empty) = Quantity("cm")225price = Quantity("USD")price ← (empty)
224length = Quantity("cm")225price→ (empty) = Quantity("USD")self.name ← weight
pass 1 of 3208def __set_name__(self⟨Quantity W⟩, owner<class '__main__.Product'>, nameweight):209 self.name→ weight = nameweightAll 3 passes — pass 1 is the card above pass selfnameself.name1 ⟨Quantity W⟩ weight weight 2 ⟨Quantity X⟩ length length 3 ⟨Quantity Y⟩ price price product ← ⟨Product Z⟩
227product→ ⟨Product Z⟩ = Product()228product.weight = 2.5229product.length = 30obj.__dict__[self.name] ← 2.5
pass 1 of 3217def __set__(self⟨Quantity W⟩, obj⟨Product Z⟩, value2.5):218 if not isinstance(value, (int, float)):219 raise TypeError(f"{self.name} must be numeric")220 obj.__dict__[self.name]→ 2.5 = value2.5All 3 passes — pass 1 is the card above pass selfvalueobj.__dict__[self.name]1 ⟨Quantity W⟩ 2.5 2.5 2 ⟨Quantity X⟩ 30 30 3 ⟨Quantity Y⟩ 19.99 19.99 product.weight ← 2.5 kg
227product = Product()228product.weight→ 2.5 kg = 2.5229product.length = 30230product.price = 19.99product.length ← 30 cm
228product.weight = 2.5229product.length→ 30 cm = 30230product.price = 19.99product.price ← 19.99 USD
229product.length = 30230product.price→ 19.99 USD = 19.99231232print(f"Product: weight={product.weight2.5 kg}, length={product.length30 cm}, price={product.price19.99 USD}")value ← 2.5
pass 1 of 3211def __get__(self⟨Quantity W⟩, obj⟨Product Z⟩, type<class '__main__.Product'>=NoneNone):212 if obj is None:213 return self214 value→ 2.5 = obj.__dict__{'weight': 2.5, 'length': 30, 'price': 19.99}.get(self.nameweight, 0)215 return f"{value2.5} {self.unitkg}"All 3 passes — pass 1 is the card above pass selfself.nameself.unitvalue1 ⟨Quantity W⟩ weight kg 2.5 2 ⟨Quantity X⟩ length cm 30 3 ⟨Quantity Y⟩ price USD 19.99 print(f"Product: weight={product.weight}, length={product.length}, pri…
232print(f"Product: weight={product.weight2.5 kg}, length={product.length30 cm}, price={product.price19.99 USD}")233234# Practical example235print("\nPractical example:")236237# Complete model with multiple descriptor types238class IntField:239 def __init__(self, min_value=None, max_value=None):240 self.min_value = min_value241 self.max_value = max_value242 self.name = None243 244 def __set_name__(self, owner, name):245 self.name = name246 247 def __get__(self, obj, type=None):248 if obj is None:249 return self250 return obj.__dict__.get(self.name)251 252 def __set__(self, obj, value):253 if not isinstance(value, int):254 raise TypeError(f"{self.name} must be int")255 if self.min_value is not None and value < self.min_value:256 raise ValueError(f"{self.name} must be >= {self.min_value}")257 if self.max_value is not None and value > self.max_value:258 raise ValueError(f"{self.name} must be <= {self.max_value}")259 obj.__dict__[self.name] = value260261class StringField:262 def __init__(self, max_length=None):263 self.max_length = max_length264 self.name = None265 266 def __set_name__(self, owner, name):267 self.name = name268 269 def __get__(self, obj, type=None):270 if obj is None:271 return self272 return obj.__dict__.get(self.name, "")273 274 def __set__(self, obj, value):275 if not isinstance(value, str):276 raise TypeError(f"{self.name} must be str")277 if self.max_length and len(value) > self.max_length:278 raise ValueError(f"{self.name} too long (max {self.max_length})")279 obj.__dict__[self.name] = value280281class Student:282 id = IntField(min_value=1)283 name = StringField(max_length=50)outputProduct: weight=2.5 kg, length=30 cm, price=19.99 USD Practical example:self.min_value ← 1, self.max_value ← None, self.name ← None
pass 1 of 3238class IntField:239 def __init__(self⟨IntField AA⟩, min_value1=NoneNone, max_valueNone=None):240 self.min_value→ 1 = min_value1241 self.max_value→ None = max_valueNone242 self.name→ None = NoneAll 3 passes — pass 1 is the card above pass selfmin_valuemax_valueself.min_valueself.max_valueself.name1 ⟨IntField AA⟩ 1 None 1 None None 2 ⟨IntField AB⟩ 0 150 0 150 None 3 ⟨IntField AC⟩ 0 100 0 100 None id ← <built-in function id>
281class Student:282 id→ <built-in function id> = IntField(min_value=1)283 name = StringField(max_length=50)284 age = IntField(min_value=0, max_value=150)self.max_length ← 50, self.name ← None
261class StringField:262 def __init__(self⟨StringField AD⟩, max_length50=NoneNone):263 self.max_length→ 50 = max_length50264 self.name→ None = Nonename ← (empty)
282id = IntField(min_value=1)283name→ (empty) = StringField(max_length=50)284age = IntField(min_value=0, max_value=150)285grade = IntField(min_value=0, max_value=100)age ← (empty)
283name = StringField(max_length=50)284age→ (empty) = IntField(min_value=0, max_value=150)285grade = IntField(min_value=0, max_value=100)grade ← (empty)
284age = IntField(min_value=0, max_value=150)285grade→ (empty) = IntField(min_value=0, max_value=100)self.name ← id
pass 1 of 3244def __set_name__(self⟨IntField AA⟩, owner<class '__main__.Student'>, nameid):245 self.name→ id = nameidAll 3 passes — pass 1 is the card above pass selfnameself.name1 ⟨IntField AA⟩ id id 2 ⟨IntField AB⟩ age age 3 ⟨IntField AC⟩ grade grade self.name ← name
266def __set_name__(self⟨StringField AD⟩, owner<class '__main__.Student'>, namename):267 self.name→ name = namenamestudent ← ⟨Student AE⟩
287student→ ⟨Student AE⟩ = Student()288student.id = 1289student.name = "Alice"obj.__dict__[self.name] ← 1
pass 1 of 4252def __set__(self⟨IntField AA⟩, obj⟨Student AE⟩, value1):253 if not isinstance(value, int):254 raise TypeError(f"{self.name} must be int")255 if self.min_value is not None and value < self.min_value:256 raise ValueError(f"{self.name} must be >= {self.min_value}")257 if self.max_value is not None and value > self.max_value:258 raise ValueError(f"{self.name} must be <= {self.max_value}")259 obj.__dict__[self.name]→ 1 = value1All 4 passes — pass 1 is the card above pass selfvalueself.max_valueself.nameeobj.__dict__[self.name]1 ⟨IntField AA⟩ 1 — — — 1 2 ⟨IntField AB⟩ 20 — — — 20 3 ⟨IntField AC⟩ 85 — — — 85 4 ⟨IntField AC⟩ 105 100 grade grade must be <= 100 — student.id ← 1
287student = Student()288student.id→ 1 = 1289student.name = "Alice"290student.age = 20obj.__dict__[self.name] ← Alice
274def __set__(self⟨StringField AD⟩, obj⟨Student AE⟩, valueAlice):275 if not isinstance(value, str):276 raise TypeError(f"{self.name} must be str")277 if self.max_length and len(value) > self.max_length:278 raise ValueError(f"{self.name} too long (max {self.max_length})")279 obj.__dict__[self.name]→ Alice = valueAlicestudent.name ← Alice
288student.id = 1289student.name→ Alice = "Alice"290student.age = 20291student.grade = 85student.age ← 20
289student.name = "Alice"290student.age→ 20 = 20291student.grade = 85student.grade ← 85
290student.age = 20291student.grade→ 85 = 85292293print(f"Student: id={student.id1}, name={student.nameAlice}, age={student.age20}, grade={student.grade85}")def __get__(self, obj, type=None):
pass 1 of 3247def __get__(self⟨IntField AA⟩, obj⟨Student AE⟩, type<class '__main__.Student'>=NoneNone):248 if obj is None:249 return self250 return obj.__dict__{'id': 1, 'name': 'Alice', 'age': 20, 'grade': 85}.get(self.nameid)All 3 passes — pass 1 is the card above pass selfself.name1 ⟨IntField AA⟩ id 2 ⟨IntField AB⟩ age 3 ⟨IntField AC⟩ grade def __get__(self, obj, type=None):
269def __get__(self⟨StringField AD⟩, obj⟨Student AE⟩, type<class '__main__.Student'>=NoneNone):270 if obj is None:271 return self272 return obj.__dict__{'id': 1, 'name': 'Alice', 'age': 20, 'grade': 85}.get(self.namename, "")print(f"Student: id={student.id}, name={student.name}, age={student.ag…
293print(f"Student: id={student.id1}, name={student.nameAlice}, age={student.age20}, grade={student.grade85}")outputStudent: id=1, name=Alice, age=20, grade=85if self.max_value is not None and value > self.max_value:
256 raise ValueError(f"{self.name} must be >= {self.min_value}")257if self.max_value100 is not None and value105 > self.max_value:258 raise ValueError(f"{self.namegrade} must be <= {self.max_value100}")259obj.__dict__[self.name] = valueexcept ValueError as e:
297 student.grade = 105298except ValueError as e:299 print(f"Error: {egrade must be <= 100}")outputError: grade must be <= 100
Modern Python frameworks like Django and SQLAlchemy use descriptors extensively for their ORM field definitions.
Types
- Data descriptors: Implement
__get__()and__set__()(and optionally__delete__()) - Non-data descriptors: Only implement
__get__()
Use Cases
- Validation and type checking
- Lazy loading of expensive resources
- Computed and cached attributes
- ORM field definitions
- Logging and debugging attribute access
Exercise: descriptor_practice.py
Create a RangeChecked descriptor that validates numeric attributes are within a specified min/max range