Master Python Metaprogramming: Metaclasses, Decorators, and Property Accessors Explained
Learn how Python's metaprogramming lets you dynamically create, modify, and control classes and functions at runtime, covering metaclasses for singleton patterns and class registration, decorators for caching, timing, and logging, and property accessors for validation and computed attributes, each illustrated with concrete code examples.
Python metaprogramming provides the ability to create, modify, or manipulate programs while they are running. This article explains three core mechanisms—metaclasses, decorators, and property accessors—and shows how they can be used to build flexible, reusable components.
Metaclasses
Metaclasses are classes of classes; they allow you to intervene in the class creation process. The first example demonstrates a SingletonMeta metaclass that ensures only one instance of a class exists:
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class SingletonClass(metaclass=SingletonMeta):
pass
obj1 = SingletonClass()
obj2 = SingletonClass()
print(obj1 is obj2) # TrueA second metaclass, BaseClassMeta, enforces that every subclass implements a validate method, raising NotImplementedError otherwise:
class BaseClassMeta(type):
def __init__(cls, name, bases, attrs):
if not hasattr(cls, "validate"):
raise NotImplementedError("Subclasses must implement the 'validate' method.")
super().__init__(name, bases, attrs)
class SubClass(metaclass=BaseClassMeta):
def validate(self):
pass
# If SubClass omitted validate, the metaclass would raise an error.Further examples show how a metaclass can automatically register subclasses ( PluginMeta) and inject new attributes or methods into a class ( AttributeMeta and MethodMeta).
Decorators
Decorators wrap functions or classes to extend their behavior without altering the original code. The article provides four practical decorator patterns.
Caching decorator stores results of previous calls to avoid recomputation:
def cache(func):
cached_results = {}
def wrapper(*args):
if args not in cached_results:
cached_results[args] = func(*args)
return cached_results[args]
return wrapper
@cache
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10)) # 55Timing decorator measures execution time of a function:
import time
def measure_time(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"Execution time: {end_time - start_time} seconds")
return result
return wrapper
@measure_time
def long_running_function():
time.sleep(3)
long_running_function() # Prints execution timeLogging decorator prints the function name and arguments each time the function is called:
def log_function_calls(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
print(f"Function {func.__name__} called with args: {args}, kwargs: {kwargs}")
return result
return wrapper
@log_function_calls
def add(a, b):
return a + b
print(add(2, 3)) # Function add called with args: (2, 3), kwargs: {}Property Accessors
Property accessors let you customize attribute access, validation, and computed values.
Validation example ensures a value is positive:
class PositiveNumber:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
if value < 0:
raise ValueError("Value must be positive.")
instance.__dict__[self.name] = value
class MyClass:
number = PositiveNumber()
obj = MyClass()
obj.number = 10
print(obj.number) # 10
obj.number = -5 # Raises ValueErrorComputed property example defines a diameter property for a Circle class that is derived from radius and can also be set to update the radius:
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def diameter(self):
return 2 * self.radius
@diameter.setter
def diameter(self, value):
self.radius = value / 2
circle = Circle(5)
print(circle.diameter) # 10
circle.diameter = 14
print(circle.radius) # 7Putting It All Together
The examples collectively demonstrate how Python's metaprogramming tools enable dynamic, reusable, and maintainable code. By leveraging metaclasses, you can control class creation and enforce design contracts; decorators let you add cross‑cutting concerns like caching, timing, and logging; and property accessors provide fine‑grained control over attribute behavior.
These techniques empower developers to build more flexible and customizable applications, especially in backend systems where runtime adaptability is valuable.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
