Which of the following special methods is *not* considered a fundamental part of the Python descriptor protocol for controlling attribute access?
Anonymous Quiz
0%
__get__
50%
__set__
0%
__call__
50%
__delete__
🌟 Day 53: Beyond Inheritance – Dive into Protocol-Oriented Design! 🚀
Tired of rigid class hierarchies? Today, we're unlocking the power of `typing.Protocol` to build incredibly flexible and pluggable APIs. This advanced concept lets you define "what an object can *do*" rather than "what it *is*," enabling true duck-typing with the safety net of static type checking. Craft systems that are easier to extend, test, and understand, fostering unparalleled modularity. Get ready to elevate your Python design patterns! 👇
```python
from typing import Protocol, List, Dict, Any
class DataSource(Protocol):
"""
A protocol defining the interface for any data source.
Objects adhering to this protocol can fetch and process data.
"""
def fetch_data(self, query: str) -> List[Dict[str, Any]]:
"""Fetches data based on a query."""
... # Ellipsis indicates an abstract method, to be implemented by concrete classes
def process_data(self, raw_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Processes raw data into a standardized format."""
...
class DatabaseSource:
"""A concrete data source interacting with a database."""
def fetch_data(self, query: str) -> List[Dict[str, Any]]:
print(f"🌐 Fetching from Database with query: '{query}'")
# Simulate fetching from a DB
if "users" in query:
return [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
return []
def process_data(self, raw_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
print("⚙️ Processing database data...")
return [{"db_id": item["id"], "db_name": item["name"].upper()} for item in raw_data]
class APISource:
"""A concrete data source interacting with an external API."""
def fetch_data(self, query: str) -> List[Dict[str, Any]]:
print(f"☁️ Fetching from API with query: '{query}'")
# Simulate fetching from an API
if "products" in query:
return [{"pid": "P001", "name": "Laptop", "price": 1200}, {"pid": "P002", "name": "Mouse", "price": 25}]
return []
def process_data(self, raw_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
print("⚙️ Processing API data...")
return [{"product_id": item["pid"], "product_name": item["name"].capitalize(), "price_usd": item["price"]} for item in raw_data]
def analyze_data(source: DataSource, query: str) -> None:
"""
A function that can work with ANY object that adheres to the DataSource protocol.
Note: DatabaseSource and APISource don't explicitly 'inherit' from DataSource,
but they implicitly implement its methods, satisfying the protocol! ✨
"""
print(f"n--- 📊 Analyzing data from {source.__class__.__name__} ---")
raw = source.fetch_data(query)
processed = source.process_data(raw)
print("Final Processed Data:", processed)
print("------------------------------------------")
# Demonstrate with different sources
db_source = DatabaseSource()
api_source = APISource()
analyze_data(db_source, "users")
analyze_data(api_source, "products")
```
Output example:
```
--- 📊 Analyzing data from DatabaseSource ---
🌐 Fetching from Database with query: 'users'
⚙️ Processing database data...
Final Processed Data: [{'db_id': 1, 'db_name': 'ALICE'}, {'db_id': 2, 'db_name': 'BOB'}]
------------------------------------------
--- 📊 Analyzing data from APISource ---
☁️ Fetching from API with query: 'products'
⚙️ Processing API data...
Final Processed Data: [{'product_id': 'P001', 'product_name': 'Laptop', 'price_usd': 1200}, {'product_id': 'P002', 'product_name': 'Mouse', 'price_usd': 25}]
------------------------------------------
```
🧠 **Small Challenge:**
Create a new `Loggable` protocol with a `log(message: str)` method. Implement this protocol in a `FileLogger` class (which prints to console for simplicity) and a `ConsoleLogger` class. Then, write a function `debug_message(logger: Loggable, msg: str)` that uses your protocol to send a debug message. How flexible is your new logging system? 🚀
Tired of rigid class hierarchies? Today, we're unlocking the power of `typing.Protocol` to build incredibly flexible and pluggable APIs. This advanced concept lets you define "what an object can *do*" rather than "what it *is*," enabling true duck-typing with the safety net of static type checking. Craft systems that are easier to extend, test, and understand, fostering unparalleled modularity. Get ready to elevate your Python design patterns! 👇
```python
from typing import Protocol, List, Dict, Any
class DataSource(Protocol):
"""
A protocol defining the interface for any data source.
Objects adhering to this protocol can fetch and process data.
"""
def fetch_data(self, query: str) -> List[Dict[str, Any]]:
"""Fetches data based on a query."""
... # Ellipsis indicates an abstract method, to be implemented by concrete classes
def process_data(self, raw_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Processes raw data into a standardized format."""
...
class DatabaseSource:
"""A concrete data source interacting with a database."""
def fetch_data(self, query: str) -> List[Dict[str, Any]]:
print(f"🌐 Fetching from Database with query: '{query}'")
# Simulate fetching from a DB
if "users" in query:
return [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
return []
def process_data(self, raw_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
print("⚙️ Processing database data...")
return [{"db_id": item["id"], "db_name": item["name"].upper()} for item in raw_data]
class APISource:
"""A concrete data source interacting with an external API."""
def fetch_data(self, query: str) -> List[Dict[str, Any]]:
print(f"☁️ Fetching from API with query: '{query}'")
# Simulate fetching from an API
if "products" in query:
return [{"pid": "P001", "name": "Laptop", "price": 1200}, {"pid": "P002", "name": "Mouse", "price": 25}]
return []
def process_data(self, raw_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
print("⚙️ Processing API data...")
return [{"product_id": item["pid"], "product_name": item["name"].capitalize(), "price_usd": item["price"]} for item in raw_data]
def analyze_data(source: DataSource, query: str) -> None:
"""
A function that can work with ANY object that adheres to the DataSource protocol.
Note: DatabaseSource and APISource don't explicitly 'inherit' from DataSource,
but they implicitly implement its methods, satisfying the protocol! ✨
"""
print(f"n--- 📊 Analyzing data from {source.__class__.__name__} ---")
raw = source.fetch_data(query)
processed = source.process_data(raw)
print("Final Processed Data:", processed)
print("------------------------------------------")
# Demonstrate with different sources
db_source = DatabaseSource()
api_source = APISource()
analyze_data(db_source, "users")
analyze_data(api_source, "products")
```
Output example:
```
--- 📊 Analyzing data from DatabaseSource ---
🌐 Fetching from Database with query: 'users'
⚙️ Processing database data...
Final Processed Data: [{'db_id': 1, 'db_name': 'ALICE'}, {'db_id': 2, 'db_name': 'BOB'}]
------------------------------------------
--- 📊 Analyzing data from APISource ---
☁️ Fetching from API with query: 'products'
⚙️ Processing API data...
Final Processed Data: [{'product_id': 'P001', 'product_name': 'Laptop', 'price_usd': 1200}, {'product_id': 'P002', 'product_name': 'Mouse', 'price_usd': 25}]
------------------------------------------
```
🧠 **Small Challenge:**
Create a new `Loggable` protocol with a `log(message: str)` method. Implement this protocol in a `FileLogger` class (which prints to console for simplicity) and a `ConsoleLogger` class. Then, write a function `debug_message(logger: Loggable, msg: str)` that uses your protocol to send a debug message. How flexible is your new logging system? 🚀
🎉 Day 54: Mastering Advanced Python! 🎉
Today, we're diving deep into a powerful feature that brings type-safety to Python's beloved duck typing: **Structural Subtyping with `Protocols`**. 🦆🛠️ `typing.Protocol` (PEP 544) allows you to formally define an interface based on a class's structure, not its inheritance. This means more robust static analysis, clearer code, and fewer runtime surprises, even when classes don't explicitly inherit from a common base. Let's make our "duck typing" explicitly checked! ✨
```python
from typing import Protocol
# 1. Define a Protocol
class Printable(Protocol):
"""
A protocol for objects that can print their details.
Must have a 'title' attribute and a 'print_details' method.
"""
title: str # Protocol expects this attribute
def print_details(self) -> None:
... # Ellipsis indicates no implementation required in the protocol
# 2. Implementations that implicitly conform to the Protocol
class Book:
def __init__(self, title: str, author: str):
self.title = title
self.author = author
def print_details(self) -> None:
print(f"Book: '{self.title}' by {self.author}")
class Movie:
def __init__(self, title: str, director: str, year: int):
self.title = title
self.director = director
self.year = year
def print_details(self) -> None:
print(f"Movie: '{self.title}' ({self.year}) directed by {self.director}")
# 3. A function that expects the Protocol type
def display_item(item: Printable) -> None:
print(f"--- Displaying '{item.title}' ---")
item.print_details()
print("--------------------")
# Using our conforming objects
my_book = Book("The Hitchhiker's Guide to the Galaxy", "Douglas Adams")
my_movie = Movie("Inception", "Christopher Nolan", 2010)
display_item(my_book)
display_item(my_movie)
# Example of what mypy would flag (if uncommented):
# class SimpleObject:
# # Missing 'print_details' method
# def __init__(self, title: str):
# self.title = title
#
# invalid_item = SimpleObject("Just a title")
# # mypy would error here: Argument "item" has incompatible type "SimpleObject"; expected "Printable"
# # display_item(invalid_item)
```
Output example:
```
--- Displaying 'The Hitchhiker's Guide to the Galaxy' ---
Book: 'The Hitchhiker's Guide to the Galaxy' by Douglas Adams
--------------------
--- Displaying 'Inception' ---
Movie: 'Inception' (2010) directed by Christopher Nolan
--------------------
```
**Small Challenge:**
Create a new class, `Song`, with `title` and `artist` attributes, and a `print_details` method. Pass an instance of `Song` to `display_item`. What happens if you remove the `print_details` method from `Song` and then run `mypy` on your code? (Hint: `pip install mypy` and then run `mypy your_script_name.py`)
---
Today, we're diving deep into a powerful feature that brings type-safety to Python's beloved duck typing: **Structural Subtyping with `Protocols`**. 🦆🛠️ `typing.Protocol` (PEP 544) allows you to formally define an interface based on a class's structure, not its inheritance. This means more robust static analysis, clearer code, and fewer runtime surprises, even when classes don't explicitly inherit from a common base. Let's make our "duck typing" explicitly checked! ✨
```python
from typing import Protocol
# 1. Define a Protocol
class Printable(Protocol):
"""
A protocol for objects that can print their details.
Must have a 'title' attribute and a 'print_details' method.
"""
title: str # Protocol expects this attribute
def print_details(self) -> None:
... # Ellipsis indicates no implementation required in the protocol
# 2. Implementations that implicitly conform to the Protocol
class Book:
def __init__(self, title: str, author: str):
self.title = title
self.author = author
def print_details(self) -> None:
print(f"Book: '{self.title}' by {self.author}")
class Movie:
def __init__(self, title: str, director: str, year: int):
self.title = title
self.director = director
self.year = year
def print_details(self) -> None:
print(f"Movie: '{self.title}' ({self.year}) directed by {self.director}")
# 3. A function that expects the Protocol type
def display_item(item: Printable) -> None:
print(f"--- Displaying '{item.title}' ---")
item.print_details()
print("--------------------")
# Using our conforming objects
my_book = Book("The Hitchhiker's Guide to the Galaxy", "Douglas Adams")
my_movie = Movie("Inception", "Christopher Nolan", 2010)
display_item(my_book)
display_item(my_movie)
# Example of what mypy would flag (if uncommented):
# class SimpleObject:
# # Missing 'print_details' method
# def __init__(self, title: str):
# self.title = title
#
# invalid_item = SimpleObject("Just a title")
# # mypy would error here: Argument "item" has incompatible type "SimpleObject"; expected "Printable"
# # display_item(invalid_item)
```
Output example:
```
--- Displaying 'The Hitchhiker's Guide to the Galaxy' ---
Book: 'The Hitchhiker's Guide to the Galaxy' by Douglas Adams
--------------------
--- Displaying 'Inception' ---
Movie: 'Inception' (2010) directed by Christopher Nolan
--------------------
```
**Small Challenge:**
Create a new class, `Song`, with `title` and `artist` attributes, and a `print_details` method. Pass an instance of `Song` to `display_item`. What happens if you remove the `print_details` method from `Song` and then run `mypy` on your code? (Hint: `pip install mypy` and then run `mypy your_script_name.py`)
---
What is the primary benefit of using `typing.Protocol` in Python?
Anonymous Quiz
0%
To enforce structural subtyping and provide better static type checking for duck-typed objects.
0%
To create abstract base classes that require explicit inheritance and method implementation.
0%
To define runtime interfaces that prevent method calls on non-conforming objects.
0%
To generate dynamic proxy classes that intercept method calls at runtime.
🚀 Day 56: Ever wondered how Python frameworks magically add methods or enforce rules on your classes *before* they even exist? Enter the "Invisible Hand" of Metaclasses! 👻 They're the ultimate runtime architects, allowing you to intercept and modify class creation itself. Today, we'll peel back the layers to see how metaclasses shape your code's behavior dynamically, going far beyond simple inheritance. Get ready to control the very definition of your classes! 🛠️
```python
class EnforcingMeta(type):
"""
A metaclass that enforces the presence of specific attributes
in any class that uses it.
"""
def __new__(mcs, name, bases, namespace):
# Get the list of required attributes from the class's namespace
required_attributes = namespace.get('REQUIRED_ATTRIBUTES', ())
# Ensure REQUIRED_ATTRIBUTES is a tuple or list
if not isinstance(required_attributes, (list, tuple)):
raise TypeError(
f"Class '{name}' using EnforcingMeta must define "
"'REQUIRED_ATTRIBUTES' as a list or tuple of strings."
)
# Check if all required attributes are present in the class's namespace
for attr in required_attributes:
if attr not in namespace:
raise AttributeError(
f"Class '{name}' is missing the required attribute: '{attr}'."
)
# Create the class using the standard type.__new__ if all checks pass
return super().__new__(mcs, name, bases, namespace)
# --- Example Usage ---
print("--- Creating ValidConfig ---")
class ValidConfig(metaclass=EnforcingMeta):
REQUIRED_ATTRIBUTES = ('database_url', 'api_key')
database_url = "sqlite:///app.db"
api_key = "your_secret_key"
def __init__(self, some_value):
self.some_value = some_value
config_instance = ValidConfig(123)
print(f"ValidConfig created successfully! DB URL: {config_instance.database_url}")
print(f"API Key: {config_instance.api_key}")
print("n")
print("--- Attempting to create InvalidConfig (expecting error) ---")
try:
class InvalidConfig(metaclass=EnforcingMeta):
REQUIRED_ATTRIBUTES = ('database_url', 'api_key', 'server_port')
database_url = "postgresql://user:pass@host/db"
api_key = "another_secret"
# 'server_port' is intentionally missing, causing an error
# This line should not be reached
print("InvalidConfig created unexpectedly!")
except AttributeError as e:
print(f"Caught expected error: {e}")
except TypeError as e:
print(f"Caught unexpected TypeError: {e}")
print("n--- Attempting to create MalformedConfig (expecting error for bad REQUIRED_ATTRIBUTES type) ---")
try:
class MalformedConfig(metaclass=EnforcingMeta):
# REQUIRED_ATTRIBUTES is not a tuple/list, causing a TypeError
REQUIRED_ATTRIBUTES = "database_url"
database_url = "mongo_url"
print("MalformedConfig created unexpectedly!")
except TypeError as e:
print(f"Caught expected error: {e}")
except AttributeError as e:
print(f"Caught unexpected AttributeError: {e}")
```
Output example:
```
--- Creating ValidConfig ---
ValidConfig created successfully! DB URL: sqlite:///app.db
API Key: your_secret_key
--- Attempting to create InvalidConfig (expecting error) ---
Caught expected error: Class 'InvalidConfig' is missing the required attribute: 'server_port'.
--- Attempting to create MalformedConfig (expecting error for bad REQUIRED_ATTRIBUTES type) ---
Caught expected error: Class 'MalformedConfig' using EnforcingMeta must define 'REQUIRED_ATTRIBUTES' as a list or tuple of strings.
```
💡 **Your Challenge:** Modify `EnforcingMeta` to automatically set a default value (e.g., `None` or an empty string) for any `REQUIRED_ATTRIBUTES` that are missing in the class, instead of raising an `AttributeError`. Alternatively, make the metaclass automatically register every class it creates into a global dictionary keyed by the class name.
---
```python
class EnforcingMeta(type):
"""
A metaclass that enforces the presence of specific attributes
in any class that uses it.
"""
def __new__(mcs, name, bases, namespace):
# Get the list of required attributes from the class's namespace
required_attributes = namespace.get('REQUIRED_ATTRIBUTES', ())
# Ensure REQUIRED_ATTRIBUTES is a tuple or list
if not isinstance(required_attributes, (list, tuple)):
raise TypeError(
f"Class '{name}' using EnforcingMeta must define "
"'REQUIRED_ATTRIBUTES' as a list or tuple of strings."
)
# Check if all required attributes are present in the class's namespace
for attr in required_attributes:
if attr not in namespace:
raise AttributeError(
f"Class '{name}' is missing the required attribute: '{attr}'."
)
# Create the class using the standard type.__new__ if all checks pass
return super().__new__(mcs, name, bases, namespace)
# --- Example Usage ---
print("--- Creating ValidConfig ---")
class ValidConfig(metaclass=EnforcingMeta):
REQUIRED_ATTRIBUTES = ('database_url', 'api_key')
database_url = "sqlite:///app.db"
api_key = "your_secret_key"
def __init__(self, some_value):
self.some_value = some_value
config_instance = ValidConfig(123)
print(f"ValidConfig created successfully! DB URL: {config_instance.database_url}")
print(f"API Key: {config_instance.api_key}")
print("n")
print("--- Attempting to create InvalidConfig (expecting error) ---")
try:
class InvalidConfig(metaclass=EnforcingMeta):
REQUIRED_ATTRIBUTES = ('database_url', 'api_key', 'server_port')
database_url = "postgresql://user:pass@host/db"
api_key = "another_secret"
# 'server_port' is intentionally missing, causing an error
# This line should not be reached
print("InvalidConfig created unexpectedly!")
except AttributeError as e:
print(f"Caught expected error: {e}")
except TypeError as e:
print(f"Caught unexpected TypeError: {e}")
print("n--- Attempting to create MalformedConfig (expecting error for bad REQUIRED_ATTRIBUTES type) ---")
try:
class MalformedConfig(metaclass=EnforcingMeta):
# REQUIRED_ATTRIBUTES is not a tuple/list, causing a TypeError
REQUIRED_ATTRIBUTES = "database_url"
database_url = "mongo_url"
print("MalformedConfig created unexpectedly!")
except TypeError as e:
print(f"Caught expected error: {e}")
except AttributeError as e:
print(f"Caught unexpected AttributeError: {e}")
```
Output example:
```
--- Creating ValidConfig ---
ValidConfig created successfully! DB URL: sqlite:///app.db
API Key: your_secret_key
--- Attempting to create InvalidConfig (expecting error) ---
Caught expected error: Class 'InvalidConfig' is missing the required attribute: 'server_port'.
--- Attempting to create MalformedConfig (expecting error for bad REQUIRED_ATTRIBUTES type) ---
Caught expected error: Class 'MalformedConfig' using EnforcingMeta must define 'REQUIRED_ATTRIBUTES' as a list or tuple of strings.
```
💡 **Your Challenge:** Modify `EnforcingMeta` to automatically set a default value (e.g., `None` or an empty string) for any `REQUIRED_ATTRIBUTES` that are missing in the class, instead of raising an `AttributeError`. Alternatively, make the metaclass automatically register every class it creates into a global dictionary keyed by the class name.
---
What is the primary purpose of a Python metaclass?
Anonymous Quiz
0%
To define static methods that belong to a class.
0%
To control and customize the creation of classes.
0%
To manage instances of a class after they are created.
0%
To handle exceptions that occur within class methods.
🚀 Day 57: Dive deep into Python's object model! Today, we're not just using `@property`, we're crafting the very fabric of attribute interaction. Get ready to master **Descriptors**! 🛠️ These powerful objects allow you to define custom logic for what happens when an attribute is accessed, modified, or deleted. Think robust validation, lazy loading, or even dynamic attribute generation, all neatly encapsulated. Elevate your object design skills and build truly sophisticated Python classes! ✨
```python
class AgeValidator:
"""
A descriptor to ensure an 'age' attribute is an integer and non-negative.
It manages the actual storage in a private attribute.
"""
def __set_name__(self, owner, name):
# This method is called when the descriptor is assigned to an attribute
# It's useful for knowing the name of the attribute it will manage.
self.public_name = name
self.private_name = '_' + name # Store the actual value privately
def __get__(self, obj, objtype=None):
if obj is None:
return self # Accessing the descriptor itself via the class
print(f"Accessing '{self.public_name}'...")
# Retrieve the value from the instance's private attribute
return getattr(obj, self.private_name)
def __set__(self, obj, value):
print(f"Attempting to set '{self.public_name}' to {value}...")
if not isinstance(value, int):
raise ValueError(f"'{self.public_name}' must be an integer.")
if value < 0:
raise ValueError(f"'{self.public_name}' cannot be negative.")
# Store the validated value in the instance's private attribute
setattr(obj, self.private_name, value)
print(f"Successfully set '{self.public_name}' to {value}.")
def __delete__(self, obj):
print(f"Deleting '{self.public_name}'...")
# Delete the private attribute from the instance
delattr(obj, self.private_name)
class Person:
age = AgeValidator() # Assigning the descriptor to the 'age' attribute
def __init__(self, name, age):
self.name = name
self.age = age # This assignment uses the descriptor's __set__
# --- Demonstrate usage ---
p = Person("Alice", 30)
print(f"Alice's age is currently: {p.age}n") # Access uses descriptor's __get__
# Try to set an invalid age
try:
p.age = -5
except ValueError as e:
print(f"Error: {e}n")
try:
p.age = "thirty"
except ValueError as e:
print(f"Error: {e}n")
print(f"Alice's age is still: {p.age}n") # It remains 30 because of validation
# Demonstrate deletion
del p.age # This uses the descriptor's __delete__
try:
print(p.age) # Accessing after deletion
except AttributeError as e:
print(f"Error: {e}n")
```
Output example:
```
Attempting to set 'age' to 30...
Successfully set 'age' to 30.
Accessing 'age'...
Alice's age is currently: 30
Attempting to set 'age' to -5...
Error: 'age' cannot be negative.
Attempting to set 'age' to thirty...
Error: 'age' must be an integer.
Accessing 'age'...
Alice's age is still: 30
Deleting 'age'...
Accessing 'age'...
Error: 'Person' object has no attribute '_age'
```
💡 **Challenge:** Enhance the `AgeValidator` descriptor!
1. Modify it to also validate that the `age` is within a reasonable range (e.g., 0-120).
2. Create another descriptor, `EmailValidator`, to ensure an email attribute contains both `@` and `.` characters. Then, use it in the `Person` class.
---
```python
class AgeValidator:
"""
A descriptor to ensure an 'age' attribute is an integer and non-negative.
It manages the actual storage in a private attribute.
"""
def __set_name__(self, owner, name):
# This method is called when the descriptor is assigned to an attribute
# It's useful for knowing the name of the attribute it will manage.
self.public_name = name
self.private_name = '_' + name # Store the actual value privately
def __get__(self, obj, objtype=None):
if obj is None:
return self # Accessing the descriptor itself via the class
print(f"Accessing '{self.public_name}'...")
# Retrieve the value from the instance's private attribute
return getattr(obj, self.private_name)
def __set__(self, obj, value):
print(f"Attempting to set '{self.public_name}' to {value}...")
if not isinstance(value, int):
raise ValueError(f"'{self.public_name}' must be an integer.")
if value < 0:
raise ValueError(f"'{self.public_name}' cannot be negative.")
# Store the validated value in the instance's private attribute
setattr(obj, self.private_name, value)
print(f"Successfully set '{self.public_name}' to {value}.")
def __delete__(self, obj):
print(f"Deleting '{self.public_name}'...")
# Delete the private attribute from the instance
delattr(obj, self.private_name)
class Person:
age = AgeValidator() # Assigning the descriptor to the 'age' attribute
def __init__(self, name, age):
self.name = name
self.age = age # This assignment uses the descriptor's __set__
# --- Demonstrate usage ---
p = Person("Alice", 30)
print(f"Alice's age is currently: {p.age}n") # Access uses descriptor's __get__
# Try to set an invalid age
try:
p.age = -5
except ValueError as e:
print(f"Error: {e}n")
try:
p.age = "thirty"
except ValueError as e:
print(f"Error: {e}n")
print(f"Alice's age is still: {p.age}n") # It remains 30 because of validation
# Demonstrate deletion
del p.age # This uses the descriptor's __delete__
try:
print(p.age) # Accessing after deletion
except AttributeError as e:
print(f"Error: {e}n")
```
Output example:
```
Attempting to set 'age' to 30...
Successfully set 'age' to 30.
Accessing 'age'...
Alice's age is currently: 30
Attempting to set 'age' to -5...
Error: 'age' cannot be negative.
Attempting to set 'age' to thirty...
Error: 'age' must be an integer.
Accessing 'age'...
Alice's age is still: 30
Deleting 'age'...
Accessing 'age'...
Error: 'Person' object has no attribute '_age'
```
💡 **Challenge:** Enhance the `AgeValidator` descriptor!
1. Modify it to also validate that the `age` is within a reasonable range (e.g., 0-120).
2. Create another descriptor, `EmailValidator`, to ensure an email attribute contains both `@` and `.` characters. Then, use it in the `Person` class.
---
What is the primary purpose of a Python descriptor?
Anonymous Quiz
0%
To define custom logic for attribute access (get, set, delete) for managed attributes.
0%
To create static methods and class methods within a class.
0%
To serialize and deserialize objects to and from JSON.
0%
To implement abstract base classes.
Here's your Telegram post for Day 58!
---
**Day 58: The Dynamic Type Factory: Metaclasses as Code Generators and Language Extensions** ✨
What if you could control how classes are born? 🤯 Metaclasses are Python's ultimate "type factory," allowing you to programmatically define how new classes are created. Forget `__init__`, we're talking about `__new__` for classes! On Day 58, we dive into how metaclasses can generate code, enforce patterns, and even extend Python's syntax, turning your classes into dynamic powerhouses. Get ready to build your own mini-language right within Python! 🚀
```python
# Day 58: The Dynamic Type Factory: Metaclasses as Code Generators
class AutoBoilerplateMeta(type):
"""
A metaclass that automatically adds a '_version' attribute and
a custom __str__ method to any class it creates, acting as
boilerplate code generation.
"""
def __new__(mcs, name, bases, namespace):
# 1. Inject a default attribute if not present
namespace['_version'] = namespace.get('_version', "1.0.0")
# 2. Inject a custom __str__ method if not already defined
if '__str__' not in namespace:
def custom_str(self):
return f"<{self.__class__.__name__} (v{self._version}) object>"
namespace['__str__'] = custom_str
# 3. Add a class method for introspection
if 'get_class_info' not in namespace:
@classmethod
def get_class_info(cls):
return f"Class '{cls.__name__}' created by AutoBoilerplateMeta (version: {cls._version})"
namespace['get_class_info'] = get_class_info
# Finally, call the base type.__new__ to actually create the class
cls = super().__new__(mcs, name, bases, namespace)
print(f"Metaclass created class: {cls.__name__}")
return cls
class MyService(metaclass=AutoBoilerplateMeta):
"""A service benefiting from metaclass-injected boilerplate."""
def __init__(self, task):
self.task = task
def execute(self):
return f"Executing '{self.task}' with service version {self._version}"
class DatabaseConnector(metaclass=AutoBoilerplateMeta):
"""Another component showing injected attributes and methods."""
# We can override _version here, if needed
_version = "2.1.0-alpha"
def __init__(self, config_str):
self.config = config_str
# --- Example Usage ---
service_instance = MyService("data processing")
db_instance = DatabaseConnector("prod_db_config")
print("n--- Instances ---")
print(service_instance)
print(service_instance.execute())
print(f"MyService class info: {MyService.get_class_info()}")
print("n--- Another Instance ---")
print(db_instance)
print(f"DatabaseConnector has version: {db_instance._version}")
print(f"DatabaseConnector class info: {DatabaseConnector.get_class_info()}")
# Notice how DatabaseConnector overrode the default _version, but still got the __str__ and get_class_info!
```
Output example:
```
Metaclass created class: MyService
Metaclass created class: DatabaseConnector
--- Instances ---
<MyService (v1.0.0) object>
Executing 'data processing' with service version 1.0.0
MyService class info: Class 'MyService' created by AutoBoilerplateMeta (version: 1.0.0)
--- Another Instance ---
<DatabaseConnector (v2.1.0-alpha) object>
DatabaseConnector has version: 2.1.0-alpha
DatabaseConnector class info: Class 'DatabaseConnector' created by AutoBoilerplateMeta (version: 2.1.0-alpha)
```
**Small Challenge:**
Can you modify `AutoBoilerplateMeta` to automatically add a *warning* to the `get_class_info` output if a class has *not* explicitly defined its `_version` attribute (i.e., it's using the default one from the metaclass)? 🕵️♀️
---
---
**Day 58: The Dynamic Type Factory: Metaclasses as Code Generators and Language Extensions** ✨
What if you could control how classes are born? 🤯 Metaclasses are Python's ultimate "type factory," allowing you to programmatically define how new classes are created. Forget `__init__`, we're talking about `__new__` for classes! On Day 58, we dive into how metaclasses can generate code, enforce patterns, and even extend Python's syntax, turning your classes into dynamic powerhouses. Get ready to build your own mini-language right within Python! 🚀
```python
# Day 58: The Dynamic Type Factory: Metaclasses as Code Generators
class AutoBoilerplateMeta(type):
"""
A metaclass that automatically adds a '_version' attribute and
a custom __str__ method to any class it creates, acting as
boilerplate code generation.
"""
def __new__(mcs, name, bases, namespace):
# 1. Inject a default attribute if not present
namespace['_version'] = namespace.get('_version', "1.0.0")
# 2. Inject a custom __str__ method if not already defined
if '__str__' not in namespace:
def custom_str(self):
return f"<{self.__class__.__name__} (v{self._version}) object>"
namespace['__str__'] = custom_str
# 3. Add a class method for introspection
if 'get_class_info' not in namespace:
@classmethod
def get_class_info(cls):
return f"Class '{cls.__name__}' created by AutoBoilerplateMeta (version: {cls._version})"
namespace['get_class_info'] = get_class_info
# Finally, call the base type.__new__ to actually create the class
cls = super().__new__(mcs, name, bases, namespace)
print(f"Metaclass created class: {cls.__name__}")
return cls
class MyService(metaclass=AutoBoilerplateMeta):
"""A service benefiting from metaclass-injected boilerplate."""
def __init__(self, task):
self.task = task
def execute(self):
return f"Executing '{self.task}' with service version {self._version}"
class DatabaseConnector(metaclass=AutoBoilerplateMeta):
"""Another component showing injected attributes and methods."""
# We can override _version here, if needed
_version = "2.1.0-alpha"
def __init__(self, config_str):
self.config = config_str
# --- Example Usage ---
service_instance = MyService("data processing")
db_instance = DatabaseConnector("prod_db_config")
print("n--- Instances ---")
print(service_instance)
print(service_instance.execute())
print(f"MyService class info: {MyService.get_class_info()}")
print("n--- Another Instance ---")
print(db_instance)
print(f"DatabaseConnector has version: {db_instance._version}")
print(f"DatabaseConnector class info: {DatabaseConnector.get_class_info()}")
# Notice how DatabaseConnector overrode the default _version, but still got the __str__ and get_class_info!
```
Output example:
```
Metaclass created class: MyService
Metaclass created class: DatabaseConnector
--- Instances ---
<MyService (v1.0.0) object>
Executing 'data processing' with service version 1.0.0
MyService class info: Class 'MyService' created by AutoBoilerplateMeta (version: 1.0.0)
--- Another Instance ---
<DatabaseConnector (v2.1.0-alpha) object>
DatabaseConnector has version: 2.1.0-alpha
DatabaseConnector class info: Class 'DatabaseConnector' created by AutoBoilerplateMeta (version: 2.1.0-alpha)
```
**Small Challenge:**
Can you modify `AutoBoilerplateMeta` to automatically add a *warning* to the `get_class_info` output if a class has *not* explicitly defined its `_version` attribute (i.e., it's using the default one from the metaclass)? 🕵️♀️
---
What is the primary role of a metaclass in Python?
Anonymous Quiz
0%
To define the instance methods and attributes for objects.
0%
To control and customize the creation process of classes themselves.
100%
To manage memory allocation and deallocation for Python objects.
0%
To implement asynchronous programming patterns using `asyncio`.
Here's your Telegram post for Day 59!
---
Hey Pythonistas! 👋 Day 59 is here, and we're diving deep into the very core of how Python works! 🚀
Ever wondered what Python does *before* it executes your code? It compiles it into an intermediate representation called **bytecode**. Understanding this isn't just for language geeks; it offers profound insights into performance, optimization, and even how to build powerful metaprogramming tools. Today, we'll peek under the hood with Python's built-in `dis` module! 🔍 Get ready to see the machine code that powers your scripts.
```python
import dis
def process_data(value):
"""
A simple function demonstrating string concatenation and a conditional.
"""
if isinstance(value, str):
message = "Input is a string: " + value
else:
message = f"Input is not a string, type: {type(value).__name__}"
print(message)
return message
print("✨ Disassembling the 'process_data' function: ✨n")
dis.dis(process_data)
```
Output example:
```
✨ Disassembling the 'process_data' function: ✨
7 0 LOAD_GLOBAL 0 (isinstance)
2 LOAD_FAST 0 (value)
4 LOAD_GLOBAL 1 (str)
6 CALL_FUNCTION 2
8 POP_JUMP_IF_FALSE 20
8 10 LOAD_CONST 1 ('Input is a string: ')
12 LOAD_FAST 0 (value)
14 BINARY_ADD
16 STORE_FAST 1 (message)
18 JUMP_FORWARD 30 (to 50)
10 >> 20 LOAD_CONST 2 ('Input is not a string, type: ')
22 LOAD_GLOBAL 2 (type)
24 LOAD_FAST 0 (value)
26 CALL_FUNCTION 1
28 LOAD_ATTR 3 (__name__)
30 FORMAT_VALUE 0
32 BUILD_STRING 2
34 STORE_FAST 1 (message)
11 >> 36 LOAD_GLOBAL 4 (print)
38 LOAD_FAST 1 (message)
40 CALL_FUNCTION 1
42 POP_TOP
12 44 LOAD_FAST 1 (message)
46 RETURN_VALUE
```
⚙️ **Small Challenge:** The output above shows operations like `POP_JUMP_IF_FALSE` (for the `if` statement), `BINARY_ADD` (for string concatenation), and `FORMAT_VALUE`/`BUILD_STRING` (for f-strings). Define your own simple Python function that includes a `for` loop or a `while` loop. Use `dis.dis()` on it and try to identify the corresponding opcodes like `SETUP_LOOP`, `FOR_ITER`, `JUMP_ABSOLUTE`, or `GET_ITER`! What other interesting opcodes do you discover?
---
---
Hey Pythonistas! 👋 Day 59 is here, and we're diving deep into the very core of how Python works! 🚀
Ever wondered what Python does *before* it executes your code? It compiles it into an intermediate representation called **bytecode**. Understanding this isn't just for language geeks; it offers profound insights into performance, optimization, and even how to build powerful metaprogramming tools. Today, we'll peek under the hood with Python's built-in `dis` module! 🔍 Get ready to see the machine code that powers your scripts.
```python
import dis
def process_data(value):
"""
A simple function demonstrating string concatenation and a conditional.
"""
if isinstance(value, str):
message = "Input is a string: " + value
else:
message = f"Input is not a string, type: {type(value).__name__}"
print(message)
return message
print("✨ Disassembling the 'process_data' function: ✨n")
dis.dis(process_data)
```
Output example:
```
✨ Disassembling the 'process_data' function: ✨
7 0 LOAD_GLOBAL 0 (isinstance)
2 LOAD_FAST 0 (value)
4 LOAD_GLOBAL 1 (str)
6 CALL_FUNCTION 2
8 POP_JUMP_IF_FALSE 20
8 10 LOAD_CONST 1 ('Input is a string: ')
12 LOAD_FAST 0 (value)
14 BINARY_ADD
16 STORE_FAST 1 (message)
18 JUMP_FORWARD 30 (to 50)
10 >> 20 LOAD_CONST 2 ('Input is not a string, type: ')
22 LOAD_GLOBAL 2 (type)
24 LOAD_FAST 0 (value)
26 CALL_FUNCTION 1
28 LOAD_ATTR 3 (__name__)
30 FORMAT_VALUE 0
32 BUILD_STRING 2
34 STORE_FAST 1 (message)
11 >> 36 LOAD_GLOBAL 4 (print)
38 LOAD_FAST 1 (message)
40 CALL_FUNCTION 1
42 POP_TOP
12 44 LOAD_FAST 1 (message)
46 RETURN_VALUE
```
⚙️ **Small Challenge:** The output above shows operations like `POP_JUMP_IF_FALSE` (for the `if` statement), `BINARY_ADD` (for string concatenation), and `FORMAT_VALUE`/`BUILD_STRING` (for f-strings). Define your own simple Python function that includes a `for` loop or a `while` loop. Use `dis.dis()` on it and try to identify the corresponding opcodes like `SETUP_LOOP`, `FOR_ITER`, `JUMP_ABSOLUTE`, or `GET_ITER`! What other interesting opcodes do you discover?
---
What is the primary function of Python's `dis` module?
Anonymous Quiz
0%
To optimize the Python interpreter's performance.
0%
To convert Python source code into an executable binary.
0%
To display the low-level bytecode instructions for Python functions.
0%
To provide tools for remote debugging of Python applications.
Here's your Telegram post for Day 60, diving into the advanced world of Python descriptors!
---
✨ Day 60: The Descriptor Protocol Unleashed! ✨
Today, we're diving deep into one of Python's most powerful, yet often misunderstood, mechanisms: The Descriptor Protocol. Forget simple `@property`! Descriptors allow you to create dynamic attribute behavior, implement robust validation pipelines, or even inject dependencies directly into your classes. 🚀
We'll explore how `__get__`, `__set__`, and `__delete__` methods, combined with `__set_name__`, unlock a new level of control over attribute access. Get ready to sculpt your objects with precision! 💪 Let's build reusable attribute "factories" that make your code both powerful and maintainable.
```python
class TypeChecked:
"""A descriptor for enforcing attribute types."""
def __init__(self, expected_type):
self.expected_type = expected_type
self.private_name = None # Will store the actual attribute name prefixed with '_'
def __set_name__(self, owner, name):
"""Called once when the descriptor is assigned to a class attribute."""
self.private_name = f'_{name}'
print(f"Descriptor assigned to '{name}' in class '{owner.__name__}'")
def __get__(self, obj, obj_type=None):
"""Called when accessing the attribute."""
if obj is None:
return self # Accessing descriptor directly via class (e.g., Product.name)
# Return the value stored in the instance's dictionary
return obj.__dict__.get(self.private_name)
def __set__(self, obj, value):
"""Called when setting the attribute."""
if not isinstance(value, self.expected_type):
raise TypeError(f"Expected {self.expected_type.__name__}, got {type(value).__name__} for attribute '{self.private_name}'")
# Store the value in the instance's dictionary using the private name
obj.__dict__[self.private_name] = value
class Product:
name = TypeChecked(str)
price = TypeChecked(float)
quantity = TypeChecked(int)
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
# --- Usage Example ---
print("--- Creating a valid product ---")
product1 = Product("Laptop", 1200.50, 10)
print(f"Product 1: Name={product1.name}, Price={product1.price}, Quantity={product1.quantity}")
print("n--- Attempting to create an invalid product ---")
try:
product2 = Product("Monitor", 300, "five") # Incorrect type for quantity
except TypeError as e:
print(f"Caught expected error: {e}")
print("n--- Accessing descriptor directly ---")
print(f"Product.name descriptor: {Product.name}")
```
Output example:
```
Descriptor assigned to 'name' in class 'Product'
Descriptor assigned to 'price' in class 'Product'
Descriptor assigned to 'quantity' in class 'Product'
--- Creating a valid product ---
Product 1: Name=Laptop, Price=1200.5, Quantity=10
--- Attempting to create an invalid product ---
Caught expected error: Expected int, got str for attribute '_quantity'
--- Accessing descriptor directly ---
Product.name descriptor: <__main__.TypeChecked object at 0x...>
```
Small Challenge: Can you enhance our `TypeChecked` descriptor? 🤔
Modify it to accept an optional `default` argument in its `__init__`. If a value isn't explicitly set for an attribute, `__get__` should return this default. Hint: You might need to check if `self.private_name` exists in `obj.__dict__` and, if not, return the `default`.
---
---
✨ Day 60: The Descriptor Protocol Unleashed! ✨
Today, we're diving deep into one of Python's most powerful, yet often misunderstood, mechanisms: The Descriptor Protocol. Forget simple `@property`! Descriptors allow you to create dynamic attribute behavior, implement robust validation pipelines, or even inject dependencies directly into your classes. 🚀
We'll explore how `__get__`, `__set__`, and `__delete__` methods, combined with `__set_name__`, unlock a new level of control over attribute access. Get ready to sculpt your objects with precision! 💪 Let's build reusable attribute "factories" that make your code both powerful and maintainable.
```python
class TypeChecked:
"""A descriptor for enforcing attribute types."""
def __init__(self, expected_type):
self.expected_type = expected_type
self.private_name = None # Will store the actual attribute name prefixed with '_'
def __set_name__(self, owner, name):
"""Called once when the descriptor is assigned to a class attribute."""
self.private_name = f'_{name}'
print(f"Descriptor assigned to '{name}' in class '{owner.__name__}'")
def __get__(self, obj, obj_type=None):
"""Called when accessing the attribute."""
if obj is None:
return self # Accessing descriptor directly via class (e.g., Product.name)
# Return the value stored in the instance's dictionary
return obj.__dict__.get(self.private_name)
def __set__(self, obj, value):
"""Called when setting the attribute."""
if not isinstance(value, self.expected_type):
raise TypeError(f"Expected {self.expected_type.__name__}, got {type(value).__name__} for attribute '{self.private_name}'")
# Store the value in the instance's dictionary using the private name
obj.__dict__[self.private_name] = value
class Product:
name = TypeChecked(str)
price = TypeChecked(float)
quantity = TypeChecked(int)
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
# --- Usage Example ---
print("--- Creating a valid product ---")
product1 = Product("Laptop", 1200.50, 10)
print(f"Product 1: Name={product1.name}, Price={product1.price}, Quantity={product1.quantity}")
print("n--- Attempting to create an invalid product ---")
try:
product2 = Product("Monitor", 300, "five") # Incorrect type for quantity
except TypeError as e:
print(f"Caught expected error: {e}")
print("n--- Accessing descriptor directly ---")
print(f"Product.name descriptor: {Product.name}")
```
Output example:
```
Descriptor assigned to 'name' in class 'Product'
Descriptor assigned to 'price' in class 'Product'
Descriptor assigned to 'quantity' in class 'Product'
--- Creating a valid product ---
Product 1: Name=Laptop, Price=1200.5, Quantity=10
--- Attempting to create an invalid product ---
Caught expected error: Expected int, got str for attribute '_quantity'
--- Accessing descriptor directly ---
Product.name descriptor: <__main__.TypeChecked object at 0x...>
```
Small Challenge: Can you enhance our `TypeChecked` descriptor? 🤔
Modify it to accept an optional `default` argument in its `__init__`. If a value isn't explicitly set for an attribute, `__get__` should return this default. Hint: You might need to check if `self.private_name` exists in `obj.__dict__` and, if not, return the `default`.
---
Which special method in a Python descriptor is primarily responsible for allowing the descriptor to know the name of the attribute it was assigned to within the owner class?
Anonymous Quiz
0%
A. __get__
0%
B. __set__
0%
C. __set_name__
0%
D. __delete__
Here's your Telegram post for Day 63 of your Python learning series! 🚀
---
📚 Day 63: The Quantum Leap – Navigating CPython's Bytecode and Frame Objects for Runtime Introspection 🔬✨
Ready to peek under Python's hood? Today, we're not just using Python; we're exploring *how* Python works internally! Understanding CPython's bytecode and frame objects unlocks incredible powers for advanced debugging, profiling, and even building custom DSLs. We'll use the `inspect` module to explore active execution frames and `dis` to peer into the machine-like instructions Python executes. Get ready to truly understand your code's runtime behavior!
```python
import sys
import inspect
import dis
def calculate_sum(a, b):
# This function's bytecode we will inspect
x = a + 1
y = b * 2
result = x + y
return result
def analyze_runtime_context(arg1, arg2):
# This function demonstrates frame introspection
local_context_var = "I am in analyze_runtime_context"
print(f"Executing analyze_runtime_context with args: {arg1}, {arg2}")
# 1. Get the current frame
current_frame = inspect.currentframe()
print(f"n🔬 Current Frame Details ({current_frame.f_code.co_name}):")
print(f" Line number: {current_frame.f_lineno}")
print(f" Local variables: {current_frame.f_locals}")
# 2. Get the caller's frame (the one that called analyze_runtime_context)
# sys._getframe(1) is a common way, or inspect.getouterframes
outer_frames = inspect.getouterframes(current_frame)
if len(outer_frames) > 1:
caller_frame_info = outer_frames[1] # [0] is current, [1] is immediate caller
caller_frame = caller_frame_info.frame
print(f"n🔍 Caller's Frame Details ({caller_frame.f_code.co_name}):")
print(f" Line number: {caller_frame.f_lineno}")
# Note: f_locals for caller's frame might not always be fully populated due to optimization
print(f" Caller's visible locals: {{k:v for k,v in caller_frame.f_locals.items() if not k.startswith('_')}}")
# 3. Disassemble the bytecode of calculate_sum
print(f"n📜 Bytecode for 'calculate_sum' function:")
dis.dis(calculate_sum)
# The main entry point
def main_program():
main_val_x = 10
main_val_y = 20
analyze_runtime_context(main_val_x, main_val_y)
_ = calculate_sum(1, 2) # Call it to make sure it's compiled, though dis() works on func object
print("nIntrospection complete! ✨")
if __name__ == "__main__":
main_program()
```
Output example:
```
Executing analyze_runtime_context with args: 10, 20
🔬 Current Frame Details (analyze_runtime_context):
Line number: 20
Local variables: {'arg1': 10, 'arg2': 20, 'local_context_var': 'I am in analyze_runtime_context', 'current_frame': <frame object at 0x...>}
🔍 Caller's Frame Details (main_program):
Line number: 40
Caller's visible locals: {'main_val_x': 10, 'main_val_y': 20}
📜 Bytecode for 'calculate_sum' function:
11 0 LOAD_FAST 0 (a)
2 LOAD_CONST 1 (1)
4 BINARY_ADD
6 STORE_FAST 2 (x)
12 8 LOAD_FAST 1 (b)
10 LOAD_CONST 2 (2)
12 BINARY_MULTIPLY
14 STORE_FAST 3 (y)
13 16 LOAD_FAST 2 (x)
18 LOAD_FAST 3 (y)
20 BINARY_ADD
22 STORE_FAST 4 (result)
14 24 LOAD_FAST 4 (result)
26 RETURN_VALUE
Introspection complete! ✨
```
💡 **Small Challenge:**
Modify the `analyze_runtime_context` function. Instead of just showing the immediate caller's locals, try to traverse up the call stack using `inspect.getouterframes()` to find and print the names of *all* functions in the current call stack, from the innermost to the outermost! Can you also identify which line number in `main_program` calls `analyze_runtime_context` from the frame info?
---
---
📚 Day 63: The Quantum Leap – Navigating CPython's Bytecode and Frame Objects for Runtime Introspection 🔬✨
Ready to peek under Python's hood? Today, we're not just using Python; we're exploring *how* Python works internally! Understanding CPython's bytecode and frame objects unlocks incredible powers for advanced debugging, profiling, and even building custom DSLs. We'll use the `inspect` module to explore active execution frames and `dis` to peer into the machine-like instructions Python executes. Get ready to truly understand your code's runtime behavior!
```python
import sys
import inspect
import dis
def calculate_sum(a, b):
# This function's bytecode we will inspect
x = a + 1
y = b * 2
result = x + y
return result
def analyze_runtime_context(arg1, arg2):
# This function demonstrates frame introspection
local_context_var = "I am in analyze_runtime_context"
print(f"Executing analyze_runtime_context with args: {arg1}, {arg2}")
# 1. Get the current frame
current_frame = inspect.currentframe()
print(f"n🔬 Current Frame Details ({current_frame.f_code.co_name}):")
print(f" Line number: {current_frame.f_lineno}")
print(f" Local variables: {current_frame.f_locals}")
# 2. Get the caller's frame (the one that called analyze_runtime_context)
# sys._getframe(1) is a common way, or inspect.getouterframes
outer_frames = inspect.getouterframes(current_frame)
if len(outer_frames) > 1:
caller_frame_info = outer_frames[1] # [0] is current, [1] is immediate caller
caller_frame = caller_frame_info.frame
print(f"n🔍 Caller's Frame Details ({caller_frame.f_code.co_name}):")
print(f" Line number: {caller_frame.f_lineno}")
# Note: f_locals for caller's frame might not always be fully populated due to optimization
print(f" Caller's visible locals: {{k:v for k,v in caller_frame.f_locals.items() if not k.startswith('_')}}")
# 3. Disassemble the bytecode of calculate_sum
print(f"n📜 Bytecode for 'calculate_sum' function:")
dis.dis(calculate_sum)
# The main entry point
def main_program():
main_val_x = 10
main_val_y = 20
analyze_runtime_context(main_val_x, main_val_y)
_ = calculate_sum(1, 2) # Call it to make sure it's compiled, though dis() works on func object
print("nIntrospection complete! ✨")
if __name__ == "__main__":
main_program()
```
Output example:
```
Executing analyze_runtime_context with args: 10, 20
🔬 Current Frame Details (analyze_runtime_context):
Line number: 20
Local variables: {'arg1': 10, 'arg2': 20, 'local_context_var': 'I am in analyze_runtime_context', 'current_frame': <frame object at 0x...>}
🔍 Caller's Frame Details (main_program):
Line number: 40
Caller's visible locals: {'main_val_x': 10, 'main_val_y': 20}
📜 Bytecode for 'calculate_sum' function:
11 0 LOAD_FAST 0 (a)
2 LOAD_CONST 1 (1)
4 BINARY_ADD
6 STORE_FAST 2 (x)
12 8 LOAD_FAST 1 (b)
10 LOAD_CONST 2 (2)
12 BINARY_MULTIPLY
14 STORE_FAST 3 (y)
13 16 LOAD_FAST 2 (x)
18 LOAD_FAST 3 (y)
20 BINARY_ADD
22 STORE_FAST 4 (result)
14 24 LOAD_FAST 4 (result)
26 RETURN_VALUE
Introspection complete! ✨
```
💡 **Small Challenge:**
Modify the `analyze_runtime_context` function. Instead of just showing the immediate caller's locals, try to traverse up the call stack using `inspect.getouterframes()` to find and print the names of *all* functions in the current call stack, from the innermost to the outermost! Can you also identify which line number in `main_program` calls `analyze_runtime_context` from the frame info?
---
🚀 Day 64: Ever wondered how Python truly works under the hood? Today, we're diving deep into the very blueprint of your code: the Abstract Syntax Tree (AST)! 🤯 Learn to unmask, transform, and even generate Python code at runtime by directly manipulating its AST. This isn't just theory; it's a powerful technique for advanced metaprogramming, dynamic code generation, and building highly adaptable systems. Prepare to truly understand your code's structure! ✨
In this example, we'll parse a simple Python function, modify its AST to inject a logging `print` statement at the beginning of the function, and then execute the transformed code. See how we can alter code's behavior without touching the source string directly!
```python
import ast
import inspect
# Original Python code as a string
original_code = """
def calculate_sum(a, b):
# This is the original function body
result = a + b
return result
# This will print the result using the original function definition
print(f"Original sum result: {calculate_sum(5, 3)}")
"""
class FunctionLoggerTransformer(ast.NodeTransformer):
"""
A custom AST transformer that adds a print statement to the beginning
of any function it processes, logging its execution.
"""
def visit_FunctionDef(self, node):
# Create a new print statement node: print(f"--- Executing function: '{node.name}' ---")
# We embed the function's name (from AST transformation time) directly into the string.
log_message = ast.Constant(value=f"--- Executing function: '{node.name}' ---")
print_statement = ast.Expr(
value=ast.Call(
func=ast.Name(id='print', ctx=ast.Load()), # Call to the 'print' built-in
args=[log_message], # Argument is our logging message
keywords=[]
)
)
# Prepend the new print statement to the function's body
node.body.insert(0, print_statement)
# Important: Call generic_visit to ensure child nodes are also visited and potentially transformed
return self.generic_visit(node)
print("Original code before transformation:n" + original_code)
# 1. Parse the original code into an AST
tree = ast.parse(original_code)
# 2. Transform the AST using our custom transformer
transformer = FunctionLoggerTransformer()
new_tree = transformer.visit(tree)
# 3. Fix missing line numbers and column offsets in the new tree.
# This is crucial for compilation to work correctly and prevent errors.
ast.fix_missing_locations(new_tree)
# 4. Compile the modified AST back into executable code
# filename='<string>' is standard for in-memory code
# mode='exec' for a sequence of statements or a module
compiled_code = compile(new_tree, filename='<string>', mode='exec')
print("n--- Executing Transformed Code ---")
# 5. Execute the compiled code in a new scope
# This will run the `print` statement that calls `calculate_sum` from the original_code,
# but now `calculate_sum` has our injected logger.
exec_scope = {} # Define a scope for execution to catch definitions if needed
exec(compiled_code, exec_scope)
```
Output example:
```
Original code before transformation:
def calculate_sum(a, b):
# This is the original function body
result = a + b
return result
# This will print the result using the original function definition
print(f"Original sum result: {calculate_sum(5, 3)}")
--- Executing Transformed Code ---
--- Executing function: 'calculate_sum' ---
Original sum result: 8
```
💡 **Your Challenge:** Modify the `FunctionLoggerTransformer` to not just log the function execution, but also log the *arguments* passed to the function! Hint: You'll need to inspect `node.args` within `visit_FunctionDef` and create an AST node for a dictionary or a formatted string that prints argument names and values. Good luck!
---
📚 Quiz Question:
In this example, we'll parse a simple Python function, modify its AST to inject a logging `print` statement at the beginning of the function, and then execute the transformed code. See how we can alter code's behavior without touching the source string directly!
```python
import ast
import inspect
# Original Python code as a string
original_code = """
def calculate_sum(a, b):
# This is the original function body
result = a + b
return result
# This will print the result using the original function definition
print(f"Original sum result: {calculate_sum(5, 3)}")
"""
class FunctionLoggerTransformer(ast.NodeTransformer):
"""
A custom AST transformer that adds a print statement to the beginning
of any function it processes, logging its execution.
"""
def visit_FunctionDef(self, node):
# Create a new print statement node: print(f"--- Executing function: '{node.name}' ---")
# We embed the function's name (from AST transformation time) directly into the string.
log_message = ast.Constant(value=f"--- Executing function: '{node.name}' ---")
print_statement = ast.Expr(
value=ast.Call(
func=ast.Name(id='print', ctx=ast.Load()), # Call to the 'print' built-in
args=[log_message], # Argument is our logging message
keywords=[]
)
)
# Prepend the new print statement to the function's body
node.body.insert(0, print_statement)
# Important: Call generic_visit to ensure child nodes are also visited and potentially transformed
return self.generic_visit(node)
print("Original code before transformation:n" + original_code)
# 1. Parse the original code into an AST
tree = ast.parse(original_code)
# 2. Transform the AST using our custom transformer
transformer = FunctionLoggerTransformer()
new_tree = transformer.visit(tree)
# 3. Fix missing line numbers and column offsets in the new tree.
# This is crucial for compilation to work correctly and prevent errors.
ast.fix_missing_locations(new_tree)
# 4. Compile the modified AST back into executable code
# filename='<string>' is standard for in-memory code
# mode='exec' for a sequence of statements or a module
compiled_code = compile(new_tree, filename='<string>', mode='exec')
print("n--- Executing Transformed Code ---")
# 5. Execute the compiled code in a new scope
# This will run the `print` statement that calls `calculate_sum` from the original_code,
# but now `calculate_sum` has our injected logger.
exec_scope = {} # Define a scope for execution to catch definitions if needed
exec(compiled_code, exec_scope)
```
Output example:
```
Original code before transformation:
def calculate_sum(a, b):
# This is the original function body
result = a + b
return result
# This will print the result using the original function definition
print(f"Original sum result: {calculate_sum(5, 3)}")
--- Executing Transformed Code ---
--- Executing function: 'calculate_sum' ---
Original sum result: 8
```
💡 **Your Challenge:** Modify the `FunctionLoggerTransformer` to not just log the function execution, but also log the *arguments* passed to the function! Hint: You'll need to inspect `node.args` within `visit_FunctionDef` and create an AST node for a dictionary or a formatted string that prints argument names and values. Good luck!
---
📚 Quiz Question:
Which Python module is primarily used for parsing Python code into an Abstract Syntax Tree (AST) for programmatic manipulation?
Anonymous Quiz
0%
A. `parser`
0%
B. `ast`
0%
C. `tokenize`
0%
D. `compiler`
🚀 Day 65: Ever wanted to bend Python's rules? 🤯 Today, we dive into the Abstract Syntax Tree (AST)! No, it's not just for linters. We'll learn how to programmatically *rewrite* your code before it even runs, opening doors to custom language features, powerful static analysis, or even runtime optimizations. Get ready to transform your Python! 🛠️✨
Let's craft a simple AST transformer to automatically log when any function is entered.
```python
import ast
import sys
from io import StringIO
# Original code to transform
original_code = """
def greet(name):
print(f"Hello, {name}!")
return f"Greeting for {name}."
def calculate_sum(a, b):
result = a + b
print(f"Sum is {result}")
return result
print("Before AST transformation")
greet("Alice")
calculate_sum(10, 20)
"""
class FunctionCallLogger(ast.NodeTransformer):
"""
AST transformer to inject a print statement at the beginning
of every function definition, logging function entry.
"""
def visit_FunctionDef(self, node):
# Create a print statement node to log function entry
log_message = ast.Constant(value=f"Entering function: {node.name}()")
print_call = ast.Expr(
value=ast.Call(
func=ast.Name(id='print', ctx=ast.Load()),
args=[log_message],
keywords=[]
)
)
# Insert the print statement at the beginning of the function's body
node.body.insert(0, print_call)
# Ensure we visit child nodes (e.g., nested functions)
return self.generic_visit(node)
# Parse the original code into an AST
tree = ast.parse(original_code)
# Apply the transformer to modify the AST
transformer = FunctionCallLogger()
new_tree = transformer.visit(tree)
# --- Compile and execute the transformed code ---
# Redirect stdout to capture the output for demonstration
old_stdout = sys.stdout
redirected_output = StringIO()
sys.stdout = redirected_output
try:
print("--- Rewritten Code Preview (Python 3.9+) ---")
# ast.unparse is great for viewing the transformed code,
# but only available in Python 3.9 and later.
try:
print(ast.unparse(new_tree))
except AttributeError:
print("# ast.unparse not available. The transformed code below will be executed.")
print("n--- Executing Transformed Code ---")
# Compile the modified AST back into executable code and run it
exec(compile(new_tree, '<string>', 'exec'))
except Exception as e:
print(f"Error during execution: {e}")
finally:
sys.stdout = old_stdout # Restore stdout
print(redirected_output.getvalue())
```
Output example:
```
--- Rewritten Code Preview (Python 3.9+) ---
def greet(name):
print('Entering function: greet()')
print(f"Hello, {name}!")
return f"Greeting for {name}."
def calculate_sum(a, b):
print('Entering function: calculate_sum()')
result = a + b
print(f"Sum is {result}")
return result
print('Before AST transformation')
greet('Alice')
calculate_sum(10, 20)
--- Executing Transformed Code ---
Before AST transformation
Entering function: greet()
Hello, Alice!
Entering function: calculate_sum()
Sum is 30
```
**Challenge:** Modify the `FunctionCallLogger` transformer to also inject a `print` statement at the *end* of each function, showing an "Exiting function: {func_name}()" message! 🔚
---
Let's craft a simple AST transformer to automatically log when any function is entered.
```python
import ast
import sys
from io import StringIO
# Original code to transform
original_code = """
def greet(name):
print(f"Hello, {name}!")
return f"Greeting for {name}."
def calculate_sum(a, b):
result = a + b
print(f"Sum is {result}")
return result
print("Before AST transformation")
greet("Alice")
calculate_sum(10, 20)
"""
class FunctionCallLogger(ast.NodeTransformer):
"""
AST transformer to inject a print statement at the beginning
of every function definition, logging function entry.
"""
def visit_FunctionDef(self, node):
# Create a print statement node to log function entry
log_message = ast.Constant(value=f"Entering function: {node.name}()")
print_call = ast.Expr(
value=ast.Call(
func=ast.Name(id='print', ctx=ast.Load()),
args=[log_message],
keywords=[]
)
)
# Insert the print statement at the beginning of the function's body
node.body.insert(0, print_call)
# Ensure we visit child nodes (e.g., nested functions)
return self.generic_visit(node)
# Parse the original code into an AST
tree = ast.parse(original_code)
# Apply the transformer to modify the AST
transformer = FunctionCallLogger()
new_tree = transformer.visit(tree)
# --- Compile and execute the transformed code ---
# Redirect stdout to capture the output for demonstration
old_stdout = sys.stdout
redirected_output = StringIO()
sys.stdout = redirected_output
try:
print("--- Rewritten Code Preview (Python 3.9+) ---")
# ast.unparse is great for viewing the transformed code,
# but only available in Python 3.9 and later.
try:
print(ast.unparse(new_tree))
except AttributeError:
print("# ast.unparse not available. The transformed code below will be executed.")
print("n--- Executing Transformed Code ---")
# Compile the modified AST back into executable code and run it
exec(compile(new_tree, '<string>', 'exec'))
except Exception as e:
print(f"Error during execution: {e}")
finally:
sys.stdout = old_stdout # Restore stdout
print(redirected_output.getvalue())
```
Output example:
```
--- Rewritten Code Preview (Python 3.9+) ---
def greet(name):
print('Entering function: greet()')
print(f"Hello, {name}!")
return f"Greeting for {name}."
def calculate_sum(a, b):
print('Entering function: calculate_sum()')
result = a + b
print(f"Sum is {result}")
return result
print('Before AST transformation')
greet('Alice')
calculate_sum(10, 20)
--- Executing Transformed Code ---
Before AST transformation
Entering function: greet()
Hello, Alice!
Entering function: calculate_sum()
Sum is 30
```
**Challenge:** Modify the `FunctionCallLogger` transformer to also inject a `print` statement at the *end* of each function, showing an "Exiting function: {func_name}()" message! 🔚
---
Which `ast` module class is specifically designed for visiting and *modifying* nodes within an Abstract Syntax Tree?
Anonymous Quiz
0%
ast.NodeTransformer
0%
ast.NodeVisitor
0%
ast.parse
0%
ast.dump
Which of the following methods is primarily responsible for *retrieving* an attribute's value when a descriptor is accessed on an instance (e.g., `obj.attribute`)?
Anonymous Quiz
0%
__set__
0%
__get__
0%
__call__
0%
__set_name__