Metaclasses are the 'classes of classes'. They allow you to intercept the creation of a class object itself, enabling advanced customization like automatically registering classes, validating class attributes upon definition, or modifying the class structure dynamically.
# Example: A simple metaclass to enforce uppercase attribute names
class UpperAttrMetaclass(type):
def __new__(cls, name, bases, dct):
print(f"Creating class: {name}")
uppercase_attrs = {}
for name, val in dct.items():
if not name.startswith('__'):
uppercase_attrs[name.upper()] = val
else:
uppercase_attrs[name] = val
# Call the parent type.__new__ with modified dictionary
return super().__new__(cls, name, bases, uppercase_attrs)
class MyClass(metaclass=UpperAttrMetaclass):
my_attribute = "value"
another_one = 123
def my_method(self):
pass # Methods are usually kept as is
# Accessing attributes (note they are now uppercase)
instance = MyClass()
print(hasattr(instance, 'MY_ATTRIBUTE')) # Output: True
print(hasattr(instance, 'ANOTHER_ONE')) # Output: True
print(hasattr(instance, 'my_attribute')) # Output: False
Decorators are a powerful way to modify or enhance functions or methods. Advanced decorators can accept arguments, allowing for more flexible and reusable wrappers, such as conditional execution, logging with specific tags, or enforcing specific types based on decorator parameters.
import functools
# Decorator factory that accepts arguments
def repeat(num_times):
def decorator_repeat(func):
@functools.wraps(func) # Preserves original function metadata
def wrapper(*args, **kwargs):
print(f"Running {func.__name__} {num_times} times:")
results = []
for _ in range(num_times):
value = func(*args, **kwargs)
results.append(value)
return results
return wrapper
return decorator_repeat
@repeat(num_times=3)
def greet(name):
"""Greets the person."""
print(f"Hello {name}!")
return f"Greeting {name}"
# Calling the decorated function
results = greet("Alice")
print(f"Results: {results}")
# Output:
# Running greet 3 times:
# Hello Alice!
# Hello Alice!
# Hello Alice!
# Results: ['Greeting Alice', 'Greeting Alice', 'Greeting Alice']
print(greet.__name__) # Output: greet (thanks to functools.wraps)
print(greet.__doc__) # Output: Greets the person. (thanks to functools.wraps)
Asyncio provides infrastructure for writing single-threaded concurrent code using coroutines, multiplexing I/O access over sockets and other resources. It's ideal for I/O-bound tasks like network requests or file operations, allowing the program to perform other work while waiting for operations to complete.
import asyncio
import time
async def fetch_data(task_id, delay):
"""Simulates an asynchronous I/O operation."""
print(f"Task {task_id}: Starting fetch, will take {delay}s...")
await asyncio.sleep(delay) # Non-blocking sleep
print(f"Task {task_id}: Finished fetching data.")
return f"Data from task {task_id}"
async def main():
"""Runs multiple asynchronous tasks concurrently."""
start_time = time.time()
print("Starting main coroutine...")
# Create multiple tasks
task1 = asyncio.create_task(fetch_data(1, 2))
task2 = asyncio.create_task(fetch_data(2, 1))
task3 = asyncio.create_task(fetch_data(3, 3))
# Wait for all tasks to complete and gather results
results = await asyncio.gather(task1, task2, task3)
print("\nAll tasks completed.")
for result in results:
print(f"Received: {result}")
end_time = time.time()
print(f"\nTotal execution time: {end_time - start_time:.2f} seconds") # Should be around 3s
# Run the main asynchronous function
if __name__ == "__main__":
asyncio.run(main())
Generators provide a memory-efficient way to create iterators using the `yield` keyword. Coroutines, often built on generators, allow functions to pause and resume execution, enabling cooperative multitasking and forming the basis for frameworks like asyncio.
# Example 1: Simple Generator for Fibonacci sequence
def fibonacci_generator(limit):
a, b = 0, 1
while a < limit:
yield a # Pauses execution and returns 'a'
a, b = b, a + b # Resumes here on next iteration
print("Fibonacci sequence:")
for number in fibonacci_generator(100):
print(number, end=" ")
print("\n")
# Example 2: Basic Coroutine using yield for receiving data
def simple_coroutine():
print("Coroutine started")
received_value = None
try:
while True:
received_value = yield received_value # Yields result, receives next input
print(f"Coroutine received: {received_value}")
if received_value == "stop":
print("Coroutine stopping.")
break
finally:
print("Coroutine finished.")
co = simple_coroutine()
print("Priming coroutine...")
next(co) # Start the coroutine up to the first yield
print("Sending values to coroutine:")
co.send(10)
co.send("hello")
try:
co.send("stop")
except StopIteration:
print("Coroutine naturally stopped after breaking loop.")
Context managers, typically used with the `with` statement, ensure resources are properly managed (e.g., files closed, locks released). You can create custom context managers by implementing the `__enter__` and `__exit__` methods in a class, providing robust setup and teardown logic.
import time
class TimerContextManager:
def __init__(self, name="Operation"):
self.name = name
self.start_time = None
def __enter__(self):
"""Called when entering the 'with' block. Returns the resource."""
print(f"Starting timer for '{self.name}'...")
self.start_time = time.perf_counter()
return self # Can return self or another object
def __exit__(self, exc_type, exc_val, exc_tb):
"""Called when exiting the 'with' block. Cleans up resources."""
end_time = time.perf_counter()
elapsed = end_time - self.start_time
print(f"'{self.name}' finished in {elapsed:.4f} seconds.")
# Handle exceptions if needed
if exc_type:
print(f"An exception occurred: {exc_type.__name__}: {exc_val}")
# Return True to suppress the exception, False (or None) to re-raise it
return False
# Indicate successful exit without exceptions
return True
# Using the custom context manager
with TimerContextManager("Long Task") as timer:
print("Inside the 'with' block, doing work...")
time.sleep(1.5) # Simulate work
print(f"Context manager object received: {timer.name}")
print("\nTrying with an exception:")
try:
with TimerContextManager("Task with Error"):
print("Doing work that will raise an error...")
time.sleep(0.5)
result = 1 / 0 # This will raise ZeroDivisionError
except ZeroDivisionError:
print("Caught the expected ZeroDivisionError outside the 'with' block.")
Descriptors are objects defining `__get__`, `__set__`, or `__delete__` methods. When assigned to a class attribute, they allow fine-grained control over how that attribute is accessed, set, or deleted on instances of the class, enabling features like type validation, lazy loading, or computed properties.
# Example: A descriptor for type-validated attributes
class TypedAttribute:
def __init__(self, name, expected_type):
self.name = name
self.expected_type = expected_type
self._private_name = '_' + name # Name for storing value in instance dict
def __get__(self, instance, owner):
"""Called when accessing the attribute."""
if instance is None:
return self # Access via class
print(f"Getting '{self.name}'")
# Retrieve value from instance's dictionary
return getattr(instance, self._private_name, None)
def __set__(self, instance, value):
"""Called when setting the attribute."""
print(f"Setting '{self.name}' to {value!r}")
if not isinstance(value, self.expected_type):
raise TypeError(f"Expected {self.expected_type.__name__}, got {type(value).__name__}")
# Store value in instance's dictionary
setattr(instance, self._private_name, value)
# __delete__ could also be implemented
class Person:
# Use the descriptor for name (string) and age (int)
name = TypedAttribute('name', str)
age = TypedAttribute('age', int)
def __init__(self, name, age):
self.name = name # Calls TypedAttribute.__set__
self.age = age # Calls TypedAttribute.__set__
# Usage
p = Person("Alice", 30)
# Setting 'name' to 'Alice'
# Setting 'age' to 30
print(f"Name: {p.name}") # Calls TypedAttribute.__get__
# Getting 'name'
# Name: Alice
print(f"Age: {p.age}") # Calls TypedAttribute.__get__
# Getting 'age'
# Age: 30
try:
p.age = "thirty" # This will fail validation
except TypeError as e:
print(f"Error: {e}")
# Setting 'age' to 'thirty'
# Error: Expected int, got str
```
Unlock a wealth of tutorials and software downloads available across our platform.