Dynamic Properties with @property
The @property decorator alllows methods to behave like attributes. A getter retrieves a computed value, while a setter enables cnotrolled assignment.
from datetime import datetime, date
class Person:
def __init__(self, full_name, birth_date):
self.full_name = full_name
self.birth_date = birth_date
self._cached_age = None
@property
def age(self):
return datetime.now().year - self.birth_date.year
@age.setter
def age(self, years):
self._cached_age = years
if __name__ == "__main__":
individual = Person("Alice", date(1990, 5, 15))
individual.age = 33
print(individual._cached_age) # 33
print(individual.age) # Current computed age
Attribute Access with __getattr__ and __getattribute__
__getattr__ is invoked only when an attribute is not found via normal lookup. In contrast, __getattribute__ intercepts all attribute access and has higher precedence.
class Profile:
def __init__(self, metadata):
self.metadata = metadata
def __getattr__(self, key):
return self.metadata.get(key)
# Example usage
person = Profile({"job": "Engineer", "city": "Berlin"})
print(person.job) # 'Engineer'
class LockedProfile:
def __getattribute__(self, name):
return "restricted"
obj = LockedProfile()
print(obj.any_attr) # 'restricted'
Descriptors and Attribute Lookup Order
A descriptor is any object defining __get__, __set__, or __delete__. Data descriptors (with __set__) take precedence over instance dictionaries.
import numbers
class PositiveInteger:
def __set_name__(self, owner, name):
self.private_name = f'_{name}'
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.private_name)
def __set__(self, instance, value):
if not isinstance(value, numbers.Integral):
raise TypeError("Must be an integer")
if value <= 0:
raise ValueError("Must be positive")
setattr(instance, self.private_name, value)
class Student:
score = PositiveInteger()
s = Student()
s.score = 95
print(s.score) # 95
Attribute resolution follows this order:
- Data descriptor in class or base class → call
__get__ - Instance dictionary (
obj.__dict__) - Non-data descriptor or regular class attribute
__getattr__if defined- Raise
AttributeError
Difference Between __new__ and __init__
__new__ controls object creation and must return an instance. __init__ initializes the returned isntance. __init__ is only called if __new__ returns an instance of the class.
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, identifier):
self.identifier = identifier
a = Singleton("first")
b = Singleton("second")
print(a is b) # True
Dynamic Class Creation with type
The built-in type can create classes dynamically at runtime.
def greet(self):
return f"Hello, I'm {self.name}"
Base = type('Base', (), {'respond': lambda self: "Base response"})
DynamicUser = type('DynamicUser', (Base,), {
'name': 'Dyna',
'greet': greet
})
user = DynamicUser()
print(user.greet()) # Hello, I'm Dyna
print(user.respond()) # Base response
Custom Metaclasses
A metaclass defines how a class is created. By inheriting from type, you can customize class construction.
class VerboseMeta(type):
def __new__(mcs, name, bases, namespace, **kwds):
print(f"Creating class {name}")
return super().__new__(mcs, name, bases, namespace)
class Widget(metaclass=VerboseMeta):
pass
# Output: Creating class Widget
Implementing a Simple ORM Using Metaclasses
Metaclasses enable framework-level features like Django’s ORM by inspecting class definitions at creation time.
import numbers
class Field:
pass
class IntegerField(Field):
def __init__(self, column=None, min_val=0, max_val=2**31-1):
self.column = column
self.min_val = min_val
self.max_val = max_val
def __set_name__(self, owner, name):
self.attr_name = name
if self.column is None:
self.column = name
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.attr_name)
def __set__(self, instance, value):
if not isinstance(value, numbers.Integral):
raise TypeError("Integer required")
if not (self.min_val <= value <= self.max_val):
raise ValueError(f"Value must be between {self.min_val} and {self.max_val}")
instance.__dict__[self.attr_name] = value
class StringField(Field):
def __init__(self, column=None, max_len=255):
if max_len <= 0:
raise ValueError("max_len must be positive")
self.column = column
self.max_len = max_len
def __set_name__(self, owner, name):
self.attr_name = name
if self.column is None:
self.column = name
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.attr_name)
def __set__(self, instance, value):
if not isinstance(value, str):
raise TypeError("String required")
if len(value) > self.max_len:
raise ValueError(f"Exceeds max length of {self.max_len}")
instance.__dict__[self.attr_name] = value
class ModelMeta(type):
def __new__(mcs, name, bases, attrs):
if name == "Model":
return super().__new__(mcs, name, bases, attrs)
fields = {}
for key, val in list(attrs.items()):
if isinstance(val, Field):
fields[key] = val
del attrs[key]
table_name = attrs.get('__tablename__', name.lower())
attrs['_fields'] = fields
attrs['_table'] = table_name
return super().__new__(mcs, name, bases, attrs)
class Model(metaclass=ModelMeta):
def __init__(self, **kwargs):
for field_name in self._fields:
if field_name in kwargs:
setattr(self, field_name, kwargs[field_name])
def save(self):
cols = []
vals = []
for name, field in self._fields.items():
cols.append(field.column)
val = getattr(self, name, None)
vals.append(repr(val) if isinstance(val, str) else str(val))
sql = f"INSERT INTO {self._table} ({', '.join(cols)}) VALUES ({', '.join(vals)})"
print(sql)
class Employee(Model):
__tablename__ = "employees"
name = StringField(max_len=50)
salary = IntegerField(min_val=0, max_val=1000000)
emp = Employee(name="John Doe", salary=75000)
emp.save()
# Output: INSERT INTO employees (name, salary) VALUES ('John Doe', 75000)