🌟 Day 33: Prepare to go *way* beyond surface-level Python! Today, we're deconstructing the interpreter itself. Ever wondered what happens to your code before it runs? We're diving deep into Python's Abstract Syntax Trees (AST) and bytecode. Understanding these internals unlocks immense power for runtime code transformation, static analysis, and even building your own language tools! Get ready to manipulate Python at its very core. 🧙♂️✨
```python
import ast
import dis
import types
# 1. Original Python code string
original_code_str = """
def calculate_secret(x):
y = x * 2 + 5 # The constant '5' will be our target for modification
print(f"Original calculation for {x}: {y}")
return y
"""
# 2. Parse the code into an AST
original_ast = ast.parse(original_code_str)
# 3. Define a NodeTransformer to modify the AST
# We will change the constant '5' to '10' within the AST
class ConstantChanger(ast.NodeTransformer):
def visit_Constant(self, node):
if isinstance(node.value, int) and node.value == 5:
# Return a new Constant node with the modified value
return ast.Constant(value=10)
return node # Return the original node for others
# 4. Apply the transformation
modified_ast = ConstantChanger().visit(original_ast)
# Fix line numbers and other metadata in the modified AST
ast.fix_missing_locations(modified_ast)
# 5. Compile and execute both original and modified ASTs
# Compile the original AST and get the function
original_compiled_code = compile(original_ast, '<string>', 'exec')
original_namespace = {}
exec(original_compiled_code, original_namespace)
original_func = original_namespace['calculate_secret']
# Compile the modified AST and get the function
modified_compiled_code = compile(modified_ast, '<string>', 'exec')
modified_namespace = {}
exec(modified_compiled_code, modified_namespace)
modified_func = modified_namespace['calculate_secret']
print("--- Original Function Bytecode ---")
dis.dis(original_func)
print("n--- Modified Function Bytecode ---")
dis.dis(modified_func)
print("n--- Execution Comparison ---")
# Call both functions to observe the runtime transformation
original_func(10)
modified_func(10) # Observe the result of 'x * 2 + 10' instead of 'x * 2 + 5'
```
Output example:
(Note: `ast.dump` is verbose, so we'll highlight the key change and show `dis` and execution comparison.)
The `ast.dump` for the original AST would show `Constant(value=5)`.
The `ast.dump` for the modified AST would show `Constant(value=10)` in the corresponding place.
```
--- Original Function Bytecode ---
3 0 LOAD_FAST 0 (x)
2 LOAD_CONST 1 (2)
4 BINARY_MULTIPLY
6 LOAD_CONST 2 (5) # Original constant value
8 BINARY_ADD
10 STORE_FAST 1 (y)
...
--- Modified Function Bytecode ---
3 0 LOAD_FAST 0 (x)
2 LOAD_CONST 1 (2)
4 BINARY_MULTIPLY
6 LOAD_CONST 2 (10) # Modified constant value!
8 BINARY_ADD
10 STORE_FAST 1 (y)
...
--- Execution Comparison ---
Original calculation for 10: 25
Original calculation for 10: 30
```
Small challenge:
Can you modify the `ConstantChanger` class to not just change a constant, but to transform an arithmetic operator? For instance, change all `ast.Add` operations to `ast.Mult` operations within a specific function! 😈
---
```python
import ast
import dis
import types
# 1. Original Python code string
original_code_str = """
def calculate_secret(x):
y = x * 2 + 5 # The constant '5' will be our target for modification
print(f"Original calculation for {x}: {y}")
return y
"""
# 2. Parse the code into an AST
original_ast = ast.parse(original_code_str)
# 3. Define a NodeTransformer to modify the AST
# We will change the constant '5' to '10' within the AST
class ConstantChanger(ast.NodeTransformer):
def visit_Constant(self, node):
if isinstance(node.value, int) and node.value == 5:
# Return a new Constant node with the modified value
return ast.Constant(value=10)
return node # Return the original node for others
# 4. Apply the transformation
modified_ast = ConstantChanger().visit(original_ast)
# Fix line numbers and other metadata in the modified AST
ast.fix_missing_locations(modified_ast)
# 5. Compile and execute both original and modified ASTs
# Compile the original AST and get the function
original_compiled_code = compile(original_ast, '<string>', 'exec')
original_namespace = {}
exec(original_compiled_code, original_namespace)
original_func = original_namespace['calculate_secret']
# Compile the modified AST and get the function
modified_compiled_code = compile(modified_ast, '<string>', 'exec')
modified_namespace = {}
exec(modified_compiled_code, modified_namespace)
modified_func = modified_namespace['calculate_secret']
print("--- Original Function Bytecode ---")
dis.dis(original_func)
print("n--- Modified Function Bytecode ---")
dis.dis(modified_func)
print("n--- Execution Comparison ---")
# Call both functions to observe the runtime transformation
original_func(10)
modified_func(10) # Observe the result of 'x * 2 + 10' instead of 'x * 2 + 5'
```
Output example:
(Note: `ast.dump` is verbose, so we'll highlight the key change and show `dis` and execution comparison.)
The `ast.dump` for the original AST would show `Constant(value=5)`.
The `ast.dump` for the modified AST would show `Constant(value=10)` in the corresponding place.
```
--- Original Function Bytecode ---
3 0 LOAD_FAST 0 (x)
2 LOAD_CONST 1 (2)
4 BINARY_MULTIPLY
6 LOAD_CONST 2 (5) # Original constant value
8 BINARY_ADD
10 STORE_FAST 1 (y)
...
--- Modified Function Bytecode ---
3 0 LOAD_FAST 0 (x)
2 LOAD_CONST 1 (2)
4 BINARY_MULTIPLY
6 LOAD_CONST 2 (10) # Modified constant value!
8 BINARY_ADD
10 STORE_FAST 1 (y)
...
--- Execution Comparison ---
Original calculation for 10: 25
Original calculation for 10: 30
```
Small challenge:
Can you modify the `ConstantChanger` class to not just change a constant, but to transform an arithmetic operator? For instance, change all `ast.Add` operations to `ast.Mult` operations within a specific function! 😈
---
Which Python module is primarily used for programmatically inspecting, traversing, and modifying the Abstract Syntax Tree (AST) of Python code?
Anonymous Quiz
0%
A. `sys`
0%
B. `inspect`
0%
C. `ast`
0%
D. `dis`
👻 Day 34: Unmasking the "Ghost in the Machine"! Ever wondered how to manage memory efficiently in Python, especially with caches or large data structures? Today, we dive into **Weak References** and **Ephemeral Objects**! 💡 Discover how to create references to objects without preventing them from being garbage collected, crucial for preventing memory leaks and building robust, memory-sensitive applications. Let's make our Python programs lighter and smarter! 🧠✨
```python
import weakref
import gc # Import gc to manually trigger collection for demonstration
class MyEphemeralObject:
def __init__(self, name):
self.name = name
print(f"✨ Object '{self.name}' created.")
def __del__(self):
print(f"💀 Object '{self.name}' garbage collected.")
print("--- Scenario 1: Object collected when only weak references exist ---")
# 1. Create a strong reference
strong_ref = MyEphemeralObject("Ephemeral A")
# 2. Create a weak reference to the same object
# This reference will not prevent the object from being garbage collected
weak_ref = weakref.ref(strong_ref)
print(f"Weak reference currently points to: {weak_ref()}")
# 3. Delete the strong reference. Now, only the weak_ref points to the object.
print("n🔥 Deleting the strong reference 'strong_ref'...")
del strong_ref
# Manually trigger garbage collection (often happens automatically, but good for demo)
gc.collect()
print("Attempting to access object via weak reference after strong reference deletion:")
retrieved_object = weak_ref()
if retrieved_object is None:
print("✅ Object has been garbage collected!")
else:
print(f"❌ Object still exists: {retrieved_object.name}")
print("n--- Scenario 2: Object NOT collected if a strong reference persists ---")
strong_ref_2 = MyEphemeralObject("Persistent B")
weak_ref_2 = weakref.ref(strong_ref_2)
print(f"Weak reference 2 points to: {weak_ref_2()}")
# We don't delete strong_ref_2 yet.
print("Strong reference 'strong_ref_2' still exists. Object should NOT be collected.")
gc.collect() # Trigger GC, but object is still strongly referenced
retrieved_object_2 = weak_ref_2()
if retrieved_object_2 is None:
print("❌ Object unexpectedly collected!")
else:
print(f"✅ Object still exists: {retrieved_object_2.name}")
# Now, let's delete the remaining strong reference for Persistent B
print("n🔥 Deleting the strong reference 'strong_ref_2' for Persistent B...")
del strong_ref_2
gc.collect() # Trigger GC again
```
Output example:
```
--- Scenario 1: Object collected when only weak references exist ---
✨ Object 'Ephemeral A' created.
Weak reference currently points to: <__main__.MyEphemeralObject object at 0x...>
🔥 Deleting the strong reference 'strong_ref'...
💀 Object 'Ephemeral A' garbage collected.
Attempting to access object via weak reference after strong reference deletion:
✅ Object has been garbage collected!
--- Scenario 2: Object NOT collected if a strong reference persists ---
✨ Object 'Persistent B' created.
Weak reference 2 points to: <__main__.MyEphemeralObject object at 0x...>
Strong reference 'strong_ref_2' still exists. Object should NOT be collected.
✅ Object still exists: Persistent B
🔥 Deleting the strong reference 'strong_ref_2' for Persistent B...
💀 Object 'Persistent B' garbage collected.
```
🤔 **Your Challenge:** Think of a common Python use case, like a large cache, a GUI framework, or a network connection pool, where using weak references would be absolutely critical to prevent memory leaks or excessive memory consumption. How would you apply them? Share your ideas! 👇
```python
import weakref
import gc # Import gc to manually trigger collection for demonstration
class MyEphemeralObject:
def __init__(self, name):
self.name = name
print(f"✨ Object '{self.name}' created.")
def __del__(self):
print(f"💀 Object '{self.name}' garbage collected.")
print("--- Scenario 1: Object collected when only weak references exist ---")
# 1. Create a strong reference
strong_ref = MyEphemeralObject("Ephemeral A")
# 2. Create a weak reference to the same object
# This reference will not prevent the object from being garbage collected
weak_ref = weakref.ref(strong_ref)
print(f"Weak reference currently points to: {weak_ref()}")
# 3. Delete the strong reference. Now, only the weak_ref points to the object.
print("n🔥 Deleting the strong reference 'strong_ref'...")
del strong_ref
# Manually trigger garbage collection (often happens automatically, but good for demo)
gc.collect()
print("Attempting to access object via weak reference after strong reference deletion:")
retrieved_object = weak_ref()
if retrieved_object is None:
print("✅ Object has been garbage collected!")
else:
print(f"❌ Object still exists: {retrieved_object.name}")
print("n--- Scenario 2: Object NOT collected if a strong reference persists ---")
strong_ref_2 = MyEphemeralObject("Persistent B")
weak_ref_2 = weakref.ref(strong_ref_2)
print(f"Weak reference 2 points to: {weak_ref_2()}")
# We don't delete strong_ref_2 yet.
print("Strong reference 'strong_ref_2' still exists. Object should NOT be collected.")
gc.collect() # Trigger GC, but object is still strongly referenced
retrieved_object_2 = weak_ref_2()
if retrieved_object_2 is None:
print("❌ Object unexpectedly collected!")
else:
print(f"✅ Object still exists: {retrieved_object_2.name}")
# Now, let's delete the remaining strong reference for Persistent B
print("n🔥 Deleting the strong reference 'strong_ref_2' for Persistent B...")
del strong_ref_2
gc.collect() # Trigger GC again
```
Output example:
```
--- Scenario 1: Object collected when only weak references exist ---
✨ Object 'Ephemeral A' created.
Weak reference currently points to: <__main__.MyEphemeralObject object at 0x...>
🔥 Deleting the strong reference 'strong_ref'...
💀 Object 'Ephemeral A' garbage collected.
Attempting to access object via weak reference after strong reference deletion:
✅ Object has been garbage collected!
--- Scenario 2: Object NOT collected if a strong reference persists ---
✨ Object 'Persistent B' created.
Weak reference 2 points to: <__main__.MyEphemeralObject object at 0x...>
Strong reference 'strong_ref_2' still exists. Object should NOT be collected.
✅ Object still exists: Persistent B
🔥 Deleting the strong reference 'strong_ref_2' for Persistent B...
💀 Object 'Persistent B' garbage collected.
```
🤔 **Your Challenge:** Think of a common Python use case, like a large cache, a GUI framework, or a network connection pool, where using weak references would be absolutely critical to prevent memory leaks or excessive memory consumption. How would you apply them? Share your ideas! 👇
What is the primary purpose of a weak reference in Python?
Anonymous Quiz
0%
A) To create a new copy of an object without increasing memory usage.
0%
B) To allow an object to be garbage collected even if weak references to it still exist.
0%
C) To explicitly prevent an object from being garbage collected by the Python runtime.
0%
D) To manage memory allocation and deallocation manually, similar to C++ pointers.
Which decorator is essential for enabling `isinstance()` checks against a `typing.Protocol` at runtime in Python?
Anonymous Quiz
0%
0%
What is the primary purpose of `typing.TypeGuard` in Python type hinting?
Anonymous Quiz
0%
To inform a static type checker that a type check function refines the type of one of its arguments.
0%
To define an interface that classes must explicitly inherit from to be considered compatible.
0%
To enable dynamic type changes at runtime based on user input or program state.
0%
To create generic type variables that can represent any type within a function or class.
✨ Welcome to Day 37! Ever wondered how `@property` works its magic? Or how attributes can have custom behavior on access? Today, we pull back the curtain on Python's **Descriptors**! 🤫 These "silent guardians" are objects that control how attributes are accessed, set, or deleted. They're fundamental to many powerful Python features and allow you to craft incredibly flexible and robust classes. Let's unlock their secrets! 🔑
```python
class IntegerValidator:
"""
A descriptor that validates if an attribute is an integer
and falls within a specific range (0-100).
"""
def __set_name__(self, owner, name):
# Called when the descriptor is assigned to an attribute name in a class.
# We store the public name and create a private name for storage.
self.public_name = name
self.private_name = '_' + name
def __get__(self, obj, objtype=None):
# Called when the attribute is accessed (e.g., `instance.attribute`).
# If accessed from the class (obj is None), return the descriptor itself.
if obj is None:
return self
return getattr(obj, self.private_name)
def __set__(self, obj, value):
# Called when the attribute is set (e.g., `instance.attribute = value`).
# This is where our validation logic resides.
if not isinstance(value, int):
raise ValueError(f"{self.public_name} must be an integer!")
if not (0 <= value <= 100):
raise ValueError(f"{self.public_name} must be between 0 and 100!")
setattr(obj, self.private_name, value) # Store the valid value
# __delete__ is optional, but would be called for `del instance.attribute`
class Scoreboard:
# Assigning the descriptor to class attributes
score = IntegerValidator()
level = IntegerValidator()
def __init__(self, initial_score, initial_level):
# When we assign values here, the descriptor's __set__ method is called
self.score = initial_score
self.level = initial_level
# Demonstrate usage
print("--- Valid Scores ---")
player_a = Scoreboard(50, 5)
print(f"Player A - Score: {player_a.score}, Level: {player_a.level}") # __get__ is called
player_a.score = 75 # __set__ is called
print(f"Player A updated score: {player_a.score}")
print("n--- Invalid Score Attempts ---")
try:
player_b = Scoreboard(110, 1) # This will raise an error due to score > 100
except ValueError as e:
print(f"Error creating Player B: {e}")
try:
player_a.level = "rookie" # This will raise an error due to non-integer value
except ValueError as e:
print(f"Error updating Player A level: {e}")
```
Output example:
```
--- Valid Scores ---
Player A - Score: 50, Level: 5
Player A updated score: 75
--- Invalid Score Attempts ---
Error creating Player B: score must be between 0 and 100!
Error updating Player A level: level must be an integer!
```
💡 **Your Challenge:** Modify the `IntegerValidator` descriptor to also accept an optional `min_value` and `max_value` during its definition (e.g., `score = IntegerValidator(min_value=0, max_value=1000)`). How would you pass these values to the descriptor's `__init__` method, and how would you incorporate them into the `__set__` method's validation?
---
```python
class IntegerValidator:
"""
A descriptor that validates if an attribute is an integer
and falls within a specific range (0-100).
"""
def __set_name__(self, owner, name):
# Called when the descriptor is assigned to an attribute name in a class.
# We store the public name and create a private name for storage.
self.public_name = name
self.private_name = '_' + name
def __get__(self, obj, objtype=None):
# Called when the attribute is accessed (e.g., `instance.attribute`).
# If accessed from the class (obj is None), return the descriptor itself.
if obj is None:
return self
return getattr(obj, self.private_name)
def __set__(self, obj, value):
# Called when the attribute is set (e.g., `instance.attribute = value`).
# This is where our validation logic resides.
if not isinstance(value, int):
raise ValueError(f"{self.public_name} must be an integer!")
if not (0 <= value <= 100):
raise ValueError(f"{self.public_name} must be between 0 and 100!")
setattr(obj, self.private_name, value) # Store the valid value
# __delete__ is optional, but would be called for `del instance.attribute`
class Scoreboard:
# Assigning the descriptor to class attributes
score = IntegerValidator()
level = IntegerValidator()
def __init__(self, initial_score, initial_level):
# When we assign values here, the descriptor's __set__ method is called
self.score = initial_score
self.level = initial_level
# Demonstrate usage
print("--- Valid Scores ---")
player_a = Scoreboard(50, 5)
print(f"Player A - Score: {player_a.score}, Level: {player_a.level}") # __get__ is called
player_a.score = 75 # __set__ is called
print(f"Player A updated score: {player_a.score}")
print("n--- Invalid Score Attempts ---")
try:
player_b = Scoreboard(110, 1) # This will raise an error due to score > 100
except ValueError as e:
print(f"Error creating Player B: {e}")
try:
player_a.level = "rookie" # This will raise an error due to non-integer value
except ValueError as e:
print(f"Error updating Player A level: {e}")
```
Output example:
```
--- Valid Scores ---
Player A - Score: 50, Level: 5
Player A updated score: 75
--- Invalid Score Attempts ---
Error creating Player B: score must be between 0 and 100!
Error updating Player A level: level must be an integer!
```
💡 **Your Challenge:** Modify the `IntegerValidator` descriptor to also accept an optional `min_value` and `max_value` during its definition (e.g., `score = IntegerValidator(min_value=0, max_value=1000)`). How would you pass these values to the descriptor's `__init__` method, and how would you incorporate them into the `__set__` method's validation?
---
✨ Day 38: Dive deep into the *Infinite Stream*! 🌊 Today, we unlock the true power of lazy evaluation in Python. Forget memory woes with massive datasets! We'll go beyond basic generators, explore custom iterator protocols, and leverage the incredible `itertools` module to craft highly efficient and composable data processing pipelines. Get ready to master memory-efficient computation! 🚀
```python
import itertools
# Custom infinite generator for even numbers
def infinite_even_numbers():
n = 0
while True:
yield n
n += 2
# Using itertools for a lazy pipeline
print("First 5 even numbers:")
evens = infinite_even_numbers()
first_5_evens = list(itertools.islice(evens, 5))
print(first_5_evens)
# A more complex lazy pipeline:
# Generate infinite numbers, filter out non-multiples of 3,
# then take the first 4 that are greater than 10.
def infinite_numbers():
n = 0
while True:
yield n
n += 1
numbers = infinite_numbers()
# Filter out numbers not divisible by 3 (lazy filtering)
multiples_of_3 = (num for num in numbers if num % 3 == 0)
# Take the first 4 multiples of 3 that are greater than 10 (lazy taking)
pipeline_result = list(
itertools.islice(
(num for num in multiples_of_3 if num > 10),
4
)
)
print("nFirst 4 multiples of 3 greater than 10:")
print(pipeline_result)
```
Output example:
```
First 5 even numbers:
[0, 2, 4, 6, 8]
First 4 multiples of 3 greater than 10:
[12, 15, 18, 21]
```
💡 **Challenge:** Modify the `infinite_even_numbers` generator to create `infinite_prime_numbers()`. Then, use `itertools.islice` to print the first 7 prime numbers. Share your elegant, lazy solution!
---
```python
import itertools
# Custom infinite generator for even numbers
def infinite_even_numbers():
n = 0
while True:
yield n
n += 2
# Using itertools for a lazy pipeline
print("First 5 even numbers:")
evens = infinite_even_numbers()
first_5_evens = list(itertools.islice(evens, 5))
print(first_5_evens)
# A more complex lazy pipeline:
# Generate infinite numbers, filter out non-multiples of 3,
# then take the first 4 that are greater than 10.
def infinite_numbers():
n = 0
while True:
yield n
n += 1
numbers = infinite_numbers()
# Filter out numbers not divisible by 3 (lazy filtering)
multiples_of_3 = (num for num in numbers if num % 3 == 0)
# Take the first 4 multiples of 3 that are greater than 10 (lazy taking)
pipeline_result = list(
itertools.islice(
(num for num in multiples_of_3 if num > 10),
4
)
)
print("nFirst 4 multiples of 3 greater than 10:")
print(pipeline_result)
```
Output example:
```
First 5 even numbers:
[0, 2, 4, 6, 8]
First 4 multiples of 3 greater than 10:
[12, 15, 18, 21]
```
💡 **Challenge:** Modify the `infinite_even_numbers` generator to create `infinite_prime_numbers()`. Then, use `itertools.islice` to print the first 7 prime numbers. Share your elegant, lazy solution!
---
Which of the following statements about Python generators and lazy evaluation is TRUE?
Anonymous Quiz
0%
A. Generators pre-calculate and store all their values in memory before iteration begins.
0%
B. `itertools.islice()` loads the entire iterable into memory before returning any slice.
0%
C. Lazy evaluation in Python primarily aims to reduce CPU cycles at the expense of memory.
0%
D. Generators yield values one by one on demand, consuming memory only for the current state.
✨ **Day 39: Unmasking Descriptors – The Hidden Power Behind Python's Attributes** 🔍
Ever wondered how `property`, `staticmethod`, or even ORM fields work their magic in Python? Meet **Descriptors**! 🐍 These tiny powerhouses are the secret sauce behind attribute access, allowing objects to customize how they're retrieved, set, or deleted. Day 39 dives deep into this fundamental protocol (`__get__`, `__set__`, `__delete__`) to unlock powerful attribute customization. Prepare to see Python attributes in a whole new light! 💫
---
```python
class StringValidator:
"""
A descriptor that ensures an attribute is a string and strips whitespace.
"""
def __set_name__(self, owner, name):
# Called at class creation time, gives the descriptor access to the owner class
# and the name it was assigned to.
self.public_name = name
self.private_name = '_' + name # Store the actual value in a private attribute
def __get__(self, obj, obj_type=None):
# Called when the attribute is accessed (e.g., user.username)
if obj is None: # Accessing via the class (e.g., User.username)
return self
# Return the stored private value, or an empty string if not set
return getattr(obj, self.private_name, '')
def __set__(self, obj, value):
# Called when the attribute is assigned (e.g., user.username = " new ")
if not isinstance(value, str):
raise TypeError(f"Attribute '{self.public_name}' must be a string.")
# Store the stripped string value in the private attribute
setattr(obj, self.private_name, value.strip())
def __delete__(self, obj):
# Called when the attribute is deleted (e.g., del user.username)
try:
delattr(obj, self.private_name)
print(f"Attribute '{self.public_name}' on {obj.__class__.__name__} deleted.")
except AttributeError:
print(f"Attribute '{self.public_name}' on {obj.__class__.__name__} was not set.")
class User:
username = StringValidator() # Apply the descriptor to the 'username' attribute
email = StringValidator() # Apply it to 'email' as well
def __init__(self, username, email):
self.username = username # This triggers __set__ for 'username'
self.email = email # This triggers __set__ for 'email'
# --- Demonstrate usage ---
user1 = User(" Alice ", " alice@example.com ")
print(f"User 1 - Username: '{user1.username}', Email: '{user1.email}'")
user2 = User(" Bob ", "bob@mail.com")
user2.username = " Robert " # Re-setting triggers __set__ again
print(f"User 2 - Username: '{user2.username}'")
# Attempt to set with wrong type - should raise an error
try:
user2.email = 123
except TypeError as e:
print(f"Error setting email: {e}")
# Delete an attribute
del user1.username
print(f"User 1 - Username after deletion: '{user1.username}' (Accessing returns default from __get__ if not set)")
# Accessing a descriptor via the class
print(f"User class username descriptor: {User.username}")
```
---
Output example:
```
User 1 - Username: 'Alice', Email: 'alice@example.com'
User 2 - Username: 'Robert'
Error setting email: Attribute 'email' must be a string.
Attribute 'username' on User deleted.
User 1 - Username after deletion: '' (Accessing returns default from __get__ if not set)
User class username descriptor: <__main__.StringValidator object at 0x...>
```
---
**Small Challenge:**
Challenge: Extend the `StringValidator` descriptor to also enforce a minimum length for the string. For example, `username` must be at least 3 characters long. Think about how to pass this configuration to your descriptor! ✍️
---
**Quiz Time!** 🧠
Ever wondered how `property`, `staticmethod`, or even ORM fields work their magic in Python? Meet **Descriptors**! 🐍 These tiny powerhouses are the secret sauce behind attribute access, allowing objects to customize how they're retrieved, set, or deleted. Day 39 dives deep into this fundamental protocol (`__get__`, `__set__`, `__delete__`) to unlock powerful attribute customization. Prepare to see Python attributes in a whole new light! 💫
---
```python
class StringValidator:
"""
A descriptor that ensures an attribute is a string and strips whitespace.
"""
def __set_name__(self, owner, name):
# Called at class creation time, gives the descriptor access to the owner class
# and the name it was assigned to.
self.public_name = name
self.private_name = '_' + name # Store the actual value in a private attribute
def __get__(self, obj, obj_type=None):
# Called when the attribute is accessed (e.g., user.username)
if obj is None: # Accessing via the class (e.g., User.username)
return self
# Return the stored private value, or an empty string if not set
return getattr(obj, self.private_name, '')
def __set__(self, obj, value):
# Called when the attribute is assigned (e.g., user.username = " new ")
if not isinstance(value, str):
raise TypeError(f"Attribute '{self.public_name}' must be a string.")
# Store the stripped string value in the private attribute
setattr(obj, self.private_name, value.strip())
def __delete__(self, obj):
# Called when the attribute is deleted (e.g., del user.username)
try:
delattr(obj, self.private_name)
print(f"Attribute '{self.public_name}' on {obj.__class__.__name__} deleted.")
except AttributeError:
print(f"Attribute '{self.public_name}' on {obj.__class__.__name__} was not set.")
class User:
username = StringValidator() # Apply the descriptor to the 'username' attribute
email = StringValidator() # Apply it to 'email' as well
def __init__(self, username, email):
self.username = username # This triggers __set__ for 'username'
self.email = email # This triggers __set__ for 'email'
# --- Demonstrate usage ---
user1 = User(" Alice ", " alice@example.com ")
print(f"User 1 - Username: '{user1.username}', Email: '{user1.email}'")
user2 = User(" Bob ", "bob@mail.com")
user2.username = " Robert " # Re-setting triggers __set__ again
print(f"User 2 - Username: '{user2.username}'")
# Attempt to set with wrong type - should raise an error
try:
user2.email = 123
except TypeError as e:
print(f"Error setting email: {e}")
# Delete an attribute
del user1.username
print(f"User 1 - Username after deletion: '{user1.username}' (Accessing returns default from __get__ if not set)")
# Accessing a descriptor via the class
print(f"User class username descriptor: {User.username}")
```
---
Output example:
```
User 1 - Username: 'Alice', Email: 'alice@example.com'
User 2 - Username: 'Robert'
Error setting email: Attribute 'email' must be a string.
Attribute 'username' on User deleted.
User 1 - Username after deletion: '' (Accessing returns default from __get__ if not set)
User class username descriptor: <__main__.StringValidator object at 0x...>
```
---
**Small Challenge:**
Challenge: Extend the `StringValidator` descriptor to also enforce a minimum length for the string. For example, `username` must be at least 3 characters long. Think about how to pass this configuration to your descriptor! ✍️
---
**Quiz Time!** 🧠
Which set of special methods defines Python's descriptor protocol, enabling custom behavior for attribute access (get, set, delete)?
Anonymous Quiz
0%
A) __get__, __set__, __delete__
0%
B) __getattr__, __setattr__, __delattr__
0%
C) __call__, __init__, __repr__
100%
D) __enter__, __exit__, __next__
🚀 Day 40: Time to truly unleash the power of `asyncio`! 🌀 We're moving beyond basic `async`/`await` to peer into the heart of the event loop itself. Today, we'll explore how to build custom network protocols, interact directly with `asyncio`'s transport layer, and gain unparalleled control over concurrent operations. Prepare to architect highly efficient and tailored asynchronous systems!
```python
import asyncio
# --- Custom Echo Protocol ---
class EchoServerProtocol(asyncio.Protocol):
def connection_made(self, transport):
peername = transport.get_extra_info('peername')
print(f"🔗 Connection from {peername}")
self.transport = transport
def data_received(self, data):
message = data.decode()
print(f"👂 Data received: '{message}'")
# Echo the data back
self.transport.write(data)
print("✍️ Data echoed back.")
# If client sends 'close', close the connection
if message.strip().lower() == 'close':
print("🛑 Closing connection on client request.")
self.transport.close()
def connection_lost(self, exc):
print("💔 Client disconnected.")
# --- Server Start Function ---
async def start_echo_server():
# Use create_server with our custom protocol factory
server = await asyncio.get_event_loop().create_server(
EchoServerProtocol, '127.0.0.1', 8888
)
addrs = ', '.join(str(sock.getsockname()) for sock in server.sockets)
print(f"🚀 Serving on {addrs}")
async with server:
await server.serve_forever()
# --- Client Function ---
async def echo_client(message):
reader, writer = await asyncio.open_connection('127.0.0.1', 8888)
print(f"➡️ Sending: '{message}'")
writer.write(message.encode())
await writer.drain() # Ensure data is sent
data = await reader.read(100) # Read up to 100 bytes
print(f"⬅️ Received: '{data.decode()}'")
print("🚪 Closing the connection...")
writer.close()
await writer.wait_closed()
# --- Main Execution ---
async def main():
server_task = asyncio.create_task(start_echo_server())
# Give the server a moment to start
await asyncio.sleep(1)
await echo_client("Hello, Asyncio!")
await asyncio.sleep(0.5)
await echo_client("How are you?")
await asyncio.sleep(0.5)
await echo_client("CLOSE") # Request server to close connection
# For this example, the server will stop when main() exits.
# In a real app, you might explicitly cancel server_task.
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("nServer and client shut down.")
```
Output example:
```
🚀 Serving on ('127.0.0.1', 8888)
🔗 Connection from ('127.0.0.1', 50130)
➡️ Sending: 'Hello, Asyncio!'
👂 Data received: 'Hello, Asyncio!'
✍️ Data echoed back.
⬅️ Received: 'Hello, Asyncio!'
🚪 Closing the connection...
💔 Client disconnected.
🔗 Connection from ('127.0.0.1', 50131)
➡️ Sending: 'How are you?'
👂 Data received: 'How are you?'
✍️ Data echoed back.
⬅️ Received: 'How are you!'
🚪 Closing the connection...
💔 Client disconnected.
🔗 Connection from ('127.0.0.1', 50132)
➡️ Sending: 'CLOSE'
👂 Data received: 'CLOSE'
✍️ Data echoed back.
🛑 Closing connection on client request.
⬅️ Received: 'CLOSE'
🚪 Closing the connection...
💔 Client disconnected.
```
💡 Your Challenge: Modify the `EchoServerProtocol` to implement a simple message prefixer. Instead of just echoing, have it prepend `"[SERVER ECHO] "` to every message before sending it back. Can you also modify the client to *expect* this prefix when reading the response?
---
```python
import asyncio
# --- Custom Echo Protocol ---
class EchoServerProtocol(asyncio.Protocol):
def connection_made(self, transport):
peername = transport.get_extra_info('peername')
print(f"🔗 Connection from {peername}")
self.transport = transport
def data_received(self, data):
message = data.decode()
print(f"👂 Data received: '{message}'")
# Echo the data back
self.transport.write(data)
print("✍️ Data echoed back.")
# If client sends 'close', close the connection
if message.strip().lower() == 'close':
print("🛑 Closing connection on client request.")
self.transport.close()
def connection_lost(self, exc):
print("💔 Client disconnected.")
# --- Server Start Function ---
async def start_echo_server():
# Use create_server with our custom protocol factory
server = await asyncio.get_event_loop().create_server(
EchoServerProtocol, '127.0.0.1', 8888
)
addrs = ', '.join(str(sock.getsockname()) for sock in server.sockets)
print(f"🚀 Serving on {addrs}")
async with server:
await server.serve_forever()
# --- Client Function ---
async def echo_client(message):
reader, writer = await asyncio.open_connection('127.0.0.1', 8888)
print(f"➡️ Sending: '{message}'")
writer.write(message.encode())
await writer.drain() # Ensure data is sent
data = await reader.read(100) # Read up to 100 bytes
print(f"⬅️ Received: '{data.decode()}'")
print("🚪 Closing the connection...")
writer.close()
await writer.wait_closed()
# --- Main Execution ---
async def main():
server_task = asyncio.create_task(start_echo_server())
# Give the server a moment to start
await asyncio.sleep(1)
await echo_client("Hello, Asyncio!")
await asyncio.sleep(0.5)
await echo_client("How are you?")
await asyncio.sleep(0.5)
await echo_client("CLOSE") # Request server to close connection
# For this example, the server will stop when main() exits.
# In a real app, you might explicitly cancel server_task.
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("nServer and client shut down.")
```
Output example:
```
🚀 Serving on ('127.0.0.1', 8888)
🔗 Connection from ('127.0.0.1', 50130)
➡️ Sending: 'Hello, Asyncio!'
👂 Data received: 'Hello, Asyncio!'
✍️ Data echoed back.
⬅️ Received: 'Hello, Asyncio!'
🚪 Closing the connection...
💔 Client disconnected.
🔗 Connection from ('127.0.0.1', 50131)
➡️ Sending: 'How are you?'
👂 Data received: 'How are you?'
✍️ Data echoed back.
⬅️ Received: 'How are you!'
🚪 Closing the connection...
💔 Client disconnected.
🔗 Connection from ('127.0.0.1', 50132)
➡️ Sending: 'CLOSE'
👂 Data received: 'CLOSE'
✍️ Data echoed back.
🛑 Closing connection on client request.
⬅️ Received: 'CLOSE'
🚪 Closing the connection...
💔 Client disconnected.
```
💡 Your Challenge: Modify the `EchoServerProtocol` to implement a simple message prefixer. Instead of just echoing, have it prepend `"[SERVER ECHO] "` to every message before sending it back. Can you also modify the client to *expect* this prefix when reading the response?
---
What is the primary purpose of a custom `asyncio.Protocol` implementation when building network applications?
Anonymous Quiz
0%
To define the high-level business logic that runs inside coroutines.
0%
To handle the byte-level parsing, buffering, and serialization of data transported over the network.
0%
To manage a pool of network connections and distribute requests among them.
0%
To create new event loops for parallel execution of asynchronous tasks.
Here's your Telegram post for Day 42, diving deep into Metaclasses!
---
**Day 42: Metaclasses as Architectural Blueprints! 🏗️**
Ready to unlock Python's deepest secrets? Today, we're not just creating objects, but *designing how classes themselves are built*! 🤯 Metaclasses are powerful architectural tools, allowing you to enforce design patterns, inject common behavior, and even build domain-specific languages right into your class definitions. Think of them as class factories, dictating the very structure and capabilities of the types they create. Let's see how they enforce standards and inject magic! ✨
```python
import datetime
class EnforceableBlueprintMeta(type):
"""
A metaclass acting as an architectural blueprint.
It enforces that classes using it must define a '_VERSION' class attribute
and automatically injects a 'get_blueprint_info' class method.
"""
def __new__(mcs, name, bases, attrs):
# 1. Enforce a specific architectural constraint: '_VERSION' attribute
if '_VERSION' not in attrs:
raise TypeError(
f"Class '{name}' using EnforceableBlueprintMeta must define a '_VERSION' class attribute."
)
# 2. Inject common architectural behavior: 'get_blueprint_info' class method
def get_blueprint_info(cls_instance):
"""Returns the version and creation timestamp for the blueprint class."""
return (
f"Blueprint: {cls_instance.__name__}, "
f"Version: {cls_instance._VERSION}, "
f"Created: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
# Add the injected method to the class attributes
attrs['get_blueprint_info'] = classmethod(get_blueprint_info)
# Let the base 'type' metaclass handle the actual creation of the class
return super().__new__(mcs, name, bases, attrs)
# --- Demonstrate the Metaclass in action ---
class APIResource(metaclass=EnforceableBlueprintMeta):
_VERSION = "1.0.5" # REQUIRED by our metaclass!
def __init__(self, resource_id):
self.resource_id = resource_id
def fetch(self):
print(f"Fetching resource '{self.resource_id}' using API v{self._VERSION}")
class DataValidator(metaclass=EnforceableBlueprintMeta):
_VERSION = "2.1.0-beta" # REQUIRED!
def __init__(self, schema):
self.schema = schema
def validate(self, data):
print(f"Validating data against schema '{self.schema}' with Validator v{self._VERSION}")
# --- Using our Blueprint-enabled Classes ---
print("--- Blueprint-level Information ---")
print(APIResource.get_blueprint_info())
print(DataValidator.get_blueprint_info())
print("n--- Instance Usage ---")
resource = APIResource("user_profile_123")
resource.fetch()
validator = DataValidator("user_schema.json")
validator.validate({"name": "Alice", "age": 30})
# --- What happens if you forget _VERSION? ---
# Uncomment the following block to see the TypeError in action!
# class BrokenComponent(metaclass=EnforceableBlueprintMeta):
# def process(self):
# print("I'm missing my version attribute!")
#
# try:
# _ = BrokenComponent()
# except TypeError as e:
# print(f"nError: {e}")
```
**Output example:**
```
--- Blueprint-level Information ---
Blueprint: APIResource, Version: 1.0.5, Created: 2023-10-27 10:30:00
Blueprint: DataValidator, Version: 2.1.0-beta, Created: 2023-10-27 10:30:00
--- Instance Usage ---
Fetching resource 'user_profile_123' using API v1.0.5
Validating data against schema 'user_schema.json' with Validator v2.1.0-beta
```
*(Note: The 'Created' timestamp will reflect the actual time you run the code.)*
**💡 Small Challenge:**
Modify `EnforceableBlueprintMeta` to also enforce that any class using it *must* implement an instance method named `execute()`. If `execute()` is missing, raise a `TypeError` during class creation, similar to how we handled `_VERSION`! 🚀
---
---
**Day 42: Metaclasses as Architectural Blueprints! 🏗️**
Ready to unlock Python's deepest secrets? Today, we're not just creating objects, but *designing how classes themselves are built*! 🤯 Metaclasses are powerful architectural tools, allowing you to enforce design patterns, inject common behavior, and even build domain-specific languages right into your class definitions. Think of them as class factories, dictating the very structure and capabilities of the types they create. Let's see how they enforce standards and inject magic! ✨
```python
import datetime
class EnforceableBlueprintMeta(type):
"""
A metaclass acting as an architectural blueprint.
It enforces that classes using it must define a '_VERSION' class attribute
and automatically injects a 'get_blueprint_info' class method.
"""
def __new__(mcs, name, bases, attrs):
# 1. Enforce a specific architectural constraint: '_VERSION' attribute
if '_VERSION' not in attrs:
raise TypeError(
f"Class '{name}' using EnforceableBlueprintMeta must define a '_VERSION' class attribute."
)
# 2. Inject common architectural behavior: 'get_blueprint_info' class method
def get_blueprint_info(cls_instance):
"""Returns the version and creation timestamp for the blueprint class."""
return (
f"Blueprint: {cls_instance.__name__}, "
f"Version: {cls_instance._VERSION}, "
f"Created: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
# Add the injected method to the class attributes
attrs['get_blueprint_info'] = classmethod(get_blueprint_info)
# Let the base 'type' metaclass handle the actual creation of the class
return super().__new__(mcs, name, bases, attrs)
# --- Demonstrate the Metaclass in action ---
class APIResource(metaclass=EnforceableBlueprintMeta):
_VERSION = "1.0.5" # REQUIRED by our metaclass!
def __init__(self, resource_id):
self.resource_id = resource_id
def fetch(self):
print(f"Fetching resource '{self.resource_id}' using API v{self._VERSION}")
class DataValidator(metaclass=EnforceableBlueprintMeta):
_VERSION = "2.1.0-beta" # REQUIRED!
def __init__(self, schema):
self.schema = schema
def validate(self, data):
print(f"Validating data against schema '{self.schema}' with Validator v{self._VERSION}")
# --- Using our Blueprint-enabled Classes ---
print("--- Blueprint-level Information ---")
print(APIResource.get_blueprint_info())
print(DataValidator.get_blueprint_info())
print("n--- Instance Usage ---")
resource = APIResource("user_profile_123")
resource.fetch()
validator = DataValidator("user_schema.json")
validator.validate({"name": "Alice", "age": 30})
# --- What happens if you forget _VERSION? ---
# Uncomment the following block to see the TypeError in action!
# class BrokenComponent(metaclass=EnforceableBlueprintMeta):
# def process(self):
# print("I'm missing my version attribute!")
#
# try:
# _ = BrokenComponent()
# except TypeError as e:
# print(f"nError: {e}")
```
**Output example:**
```
--- Blueprint-level Information ---
Blueprint: APIResource, Version: 1.0.5, Created: 2023-10-27 10:30:00
Blueprint: DataValidator, Version: 2.1.0-beta, Created: 2023-10-27 10:30:00
--- Instance Usage ---
Fetching resource 'user_profile_123' using API v1.0.5
Validating data against schema 'user_schema.json' with Validator v2.1.0-beta
```
*(Note: The 'Created' timestamp will reflect the actual time you run the code.)*
**💡 Small Challenge:**
Modify `EnforceableBlueprintMeta` to also enforce that any class using it *must* implement an instance method named `execute()`. If `execute()` is missing, raise a `TypeError` during class creation, similar to how we handled `_VERSION`! 🚀
---
What is the primary purpose of a metaclass in Python?
Anonymous Quiz
0%
To define how classes are created and behave.
0%
To create multiple instances of a class efficiently.
0%
To manage the inheritance hierarchy of objects.
0%
To optimize memory usage for large data structures.
Day 43, Pythonistas! 🚀 Ready to break through the Pythonic abstraction layer? Today, we're diving deep into the system's core, unlocking direct low-level control with `ctypes` and `mmap`. 🐍 Learn how to interface with C libraries and manage shared memory directly, bypassing Python's usual safety nets for extreme performance, precise system interaction, or just to peek behind the curtain. Get ready to truly understand what's happening under the hood! 🛠️
```python
import ctypes
import mmap
import os
import struct
print("--- ctypes: Calling C functions ---")
# Load the standard C library (libc)
# This module allows Python to call functions from dynamic link libraries (DLLs/SOs)
libc = None
try:
if os.name == "posix": # Linux or macOS
try:
libc = ctypes.CDLL("libc.so.6") # Common for Linux
except OSError:
libc = ctypes.CDLL("libc.dylib") # Common for macOS
else: # Windows
libc = ctypes.CDLL("msvcrt.dll")
except OSError as e:
print(f"Could not load libc. Ensure it's available on your system. Error: {e}")
if libc:
# Define the argument and return types for printf (good practice for clarity)
# printf's first argument is a C string (char*), which maps to ctypes.c_char_p
# It returns an integer (number of characters printed)
libc.printf.argtypes = [ctypes.c_char_p]
libc.printf.restype = ctypes.c_int
# Call the printf function directly from the C library
message = "Hello from libc's printf! Python and C dancing together. (Day 43)n"
# C functions expect bytes, so we encode the Python string
libc.printf(message.encode('utf-8'))
else:
print("ctypes example skipped due to libc loading failure.")
print("n--- mmap: Direct memory manipulation ---")
# The mmap module allows you to treat a file or anonymous memory region as a sequence of bytes,
# providing direct, low-level access to memory.
# Create a temporary file to demonstrate file-backed mmap (cross-platform friendly)
temp_file_name = "mmap_data.bin"
with open(temp_file_name, "wb") as f:
f.write(b'x00' * 32) # Initialize with 32 null bytes
with open(temp_file_name, "r+b") as f:
# Map the entire file into memory for read-write access
mm = mmap.mmap(f.fileno(), 0) # length=0 maps the entire file
# Write an integer (e.g., 12345) at the beginning of the mapped memory
# '!i' is network byte order (big-endian) integer
mm.seek(0)
mm.write(struct.pack('!i', 12345))
# Write a short string at offset 4
mm.seek(4)
mm.write(b"MMap rocks!")
# Read back the integer from the mapped memory
mm.seek(0)
read_int = struct.unpack('!i', mm.read(4))[0]
# Read back the string from the mapped memory
mm.seek(4)
read_str_bytes = mm.read(11) # Read 11 bytes for "MMap rocks!"
read_str = read_str_bytes.decode('utf-8')
print(f"Content of mmap (first 16 bytes in hex): {mm[:16].hex()}")
print(f"Integer read from mmap: {read_int}")
print(f"String read from mmap: '{read_str}'")
mm.close() # Unmap the memory region
os.remove(temp_file_name) # Clean up the temporary file
```
Output example:
```
--- ctypes: Calling C functions ---
Hello from libc's printf! Python and C dancing together. (Day 43)
--- mmap: Direct memory manipulation ---
Content of mmap (first 16 bytes in hex): 000030394d4d617020726f636b732100
Integer read from mmap: 12345
String read from mmap: 'MMap rocks!'
```
Challenge: Modify the `ctypes` example to call `libc.time(None)` to get the current Unix timestamp directly from C, and print it. For `mmap`, try mapping a larger region and storing different data types (e.g., floats, more strings) at specific offsets! 🧠
```python
import ctypes
import mmap
import os
import struct
print("--- ctypes: Calling C functions ---")
# Load the standard C library (libc)
# This module allows Python to call functions from dynamic link libraries (DLLs/SOs)
libc = None
try:
if os.name == "posix": # Linux or macOS
try:
libc = ctypes.CDLL("libc.so.6") # Common for Linux
except OSError:
libc = ctypes.CDLL("libc.dylib") # Common for macOS
else: # Windows
libc = ctypes.CDLL("msvcrt.dll")
except OSError as e:
print(f"Could not load libc. Ensure it's available on your system. Error: {e}")
if libc:
# Define the argument and return types for printf (good practice for clarity)
# printf's first argument is a C string (char*), which maps to ctypes.c_char_p
# It returns an integer (number of characters printed)
libc.printf.argtypes = [ctypes.c_char_p]
libc.printf.restype = ctypes.c_int
# Call the printf function directly from the C library
message = "Hello from libc's printf! Python and C dancing together. (Day 43)n"
# C functions expect bytes, so we encode the Python string
libc.printf(message.encode('utf-8'))
else:
print("ctypes example skipped due to libc loading failure.")
print("n--- mmap: Direct memory manipulation ---")
# The mmap module allows you to treat a file or anonymous memory region as a sequence of bytes,
# providing direct, low-level access to memory.
# Create a temporary file to demonstrate file-backed mmap (cross-platform friendly)
temp_file_name = "mmap_data.bin"
with open(temp_file_name, "wb") as f:
f.write(b'x00' * 32) # Initialize with 32 null bytes
with open(temp_file_name, "r+b") as f:
# Map the entire file into memory for read-write access
mm = mmap.mmap(f.fileno(), 0) # length=0 maps the entire file
# Write an integer (e.g., 12345) at the beginning of the mapped memory
# '!i' is network byte order (big-endian) integer
mm.seek(0)
mm.write(struct.pack('!i', 12345))
# Write a short string at offset 4
mm.seek(4)
mm.write(b"MMap rocks!")
# Read back the integer from the mapped memory
mm.seek(0)
read_int = struct.unpack('!i', mm.read(4))[0]
# Read back the string from the mapped memory
mm.seek(4)
read_str_bytes = mm.read(11) # Read 11 bytes for "MMap rocks!"
read_str = read_str_bytes.decode('utf-8')
print(f"Content of mmap (first 16 bytes in hex): {mm[:16].hex()}")
print(f"Integer read from mmap: {read_int}")
print(f"String read from mmap: '{read_str}'")
mm.close() # Unmap the memory region
os.remove(temp_file_name) # Clean up the temporary file
```
Output example:
```
--- ctypes: Calling C functions ---
Hello from libc's printf! Python and C dancing together. (Day 43)
--- mmap: Direct memory manipulation ---
Content of mmap (first 16 bytes in hex): 000030394d4d617020726f636b732100
Integer read from mmap: 12345
String read from mmap: 'MMap rocks!'
```
Challenge: Modify the `ctypes` example to call `libc.time(None)` to get the current Unix timestamp directly from C, and print it. For `mmap`, try mapping a larger region and storing different data types (e.g., floats, more strings) at specific offsets! 🧠
What is the primary purpose of Python's `ctypes` module?
Anonymous Quiz
50%
A) To create high-level graphical user interfaces (GUIs).
0%
B) To enable Python to call functions in shared libraries (DLLs/SOs) and manipulate C data types.
50%
C) To provide a framework for asynchronous programming.
0%
D) To automatically convert Python code into C for faster execution.
🚀 Ready to peel back the layers of Python's execution engine? On Day 44 of our Advanced Python series, we're diving deep into **Dynamic Bytecode Manipulation in CPython**! 🐍 Ever wondered what your Python code *really* looks like to the interpreter? We'll unveil the compiler's secrets, exploring how to inspect (and hint at modifying!) the low-level instructions that power your scripts. Prepare for some serious metaprogramming insights! ✨
```python
import dis
import types
def greet_original(name):
"""A simple function to greet someone."""
message = f"Hello, {name}!"
print(message)
return message
# Inspect the bytecode of our simple function
print("--- Bytecode of 'greet_original' function ---")
dis.dis(greet_original)
# --- Demonstrating a form of runtime bytecode manipulation (via code object swap) ---
# This simulates patching a function's behavior at runtime.
# In real advanced scenarios, you might modify the bytecode array directly!
def greet_patched(name):
"""A 'patched' version of the greet function."""
msg = f"Hola, {name}! (Patched)"
print(msg)
return msg
print("n--- Original greet call ---")
greet_original("Alice")
# WARNING: Directly manipulating `__code__` should be done with extreme care.
# This is for illustrative purposes to show how the underlying code can change.
# Assigning the code object of `greet_patched` to `greet_original` effectively changes its behavior.
greet_original.__code__ = greet_patched.__code__
print("n--- Modified greet call (via original function name) ---")
greet_original("Bob")
print("n--- Bytecode of 'greet_original' after modification ---")
# Now, inspecting `greet_original` shows the bytecode of `greet_patched`
dis.dis(greet_original)
```
Output example:
```
--- Bytecode of 'greet_original' function ---
5 0 LOAD_CONST 1 ('Hello, ')
2 LOAD_FAST 0 (name)
4 FORMAT_VALUE 0
6 LOAD_CONST 2 ('!')
8 BUILD_STRING 3
10 STORE_FAST 1 (message)
6 12 LOAD_GLOBAL 0 (print)
14 LOAD_FAST 1 (message)
16 CALL_FUNCTION 1
18 POP_TOP
7 20 LOAD_FAST 1 (message)
22 RETURN_VALUE
--- Original greet call ---
Hello, Alice!
--- Modified greet call (via original function name) ---
Hola, Bob! (Patched)
--- Bytecode of 'greet_original' after modification ---
13 0 LOAD_CONST 1 ('Hola, ')
2 LOAD_FAST 0 (name)
4 FORMAT_VALUE 0
6 LOAD_CONST 2 ('! (Patched)')
8 BUILD_STRING 3
10 STORE_FAST 1 (msg)
14 12 LOAD_GLOBAL 0 (print)
14 LOAD_FAST 1 (msg)
16 CALL_FUNCTION 1
18 POP_TOP
15 20 LOAD_FAST 1 (msg)
22 RETURN_VALUE
```
💡 **Challenge:**
1. Try running `dis.dis()` on a function that includes a `for` loop, an `if` statement, and a `try-except` block. How do control flow opcodes like `JUMP_ABSOLUTE`, `POP_JUMP_IF_FALSE`, and exception handling opcodes appear in the bytecode?
2. What does the `LOAD_GLOBAL` opcode typically retrieve during execution?
---
```python
import dis
import types
def greet_original(name):
"""A simple function to greet someone."""
message = f"Hello, {name}!"
print(message)
return message
# Inspect the bytecode of our simple function
print("--- Bytecode of 'greet_original' function ---")
dis.dis(greet_original)
# --- Demonstrating a form of runtime bytecode manipulation (via code object swap) ---
# This simulates patching a function's behavior at runtime.
# In real advanced scenarios, you might modify the bytecode array directly!
def greet_patched(name):
"""A 'patched' version of the greet function."""
msg = f"Hola, {name}! (Patched)"
print(msg)
return msg
print("n--- Original greet call ---")
greet_original("Alice")
# WARNING: Directly manipulating `__code__` should be done with extreme care.
# This is for illustrative purposes to show how the underlying code can change.
# Assigning the code object of `greet_patched` to `greet_original` effectively changes its behavior.
greet_original.__code__ = greet_patched.__code__
print("n--- Modified greet call (via original function name) ---")
greet_original("Bob")
print("n--- Bytecode of 'greet_original' after modification ---")
# Now, inspecting `greet_original` shows the bytecode of `greet_patched`
dis.dis(greet_original)
```
Output example:
```
--- Bytecode of 'greet_original' function ---
5 0 LOAD_CONST 1 ('Hello, ')
2 LOAD_FAST 0 (name)
4 FORMAT_VALUE 0
6 LOAD_CONST 2 ('!')
8 BUILD_STRING 3
10 STORE_FAST 1 (message)
6 12 LOAD_GLOBAL 0 (print)
14 LOAD_FAST 1 (message)
16 CALL_FUNCTION 1
18 POP_TOP
7 20 LOAD_FAST 1 (message)
22 RETURN_VALUE
--- Original greet call ---
Hello, Alice!
--- Modified greet call (via original function name) ---
Hola, Bob! (Patched)
--- Bytecode of 'greet_original' after modification ---
13 0 LOAD_CONST 1 ('Hola, ')
2 LOAD_FAST 0 (name)
4 FORMAT_VALUE 0
6 LOAD_CONST 2 ('! (Patched)')
8 BUILD_STRING 3
10 STORE_FAST 1 (msg)
14 12 LOAD_GLOBAL 0 (print)
14 LOAD_FAST 1 (msg)
16 CALL_FUNCTION 1
18 POP_TOP
15 20 LOAD_FAST 1 (msg)
22 RETURN_VALUE
```
💡 **Challenge:**
1. Try running `dis.dis()` on a function that includes a `for` loop, an `if` statement, and a `try-except` block. How do control flow opcodes like `JUMP_ABSOLUTE`, `POP_JUMP_IF_FALSE`, and exception handling opcodes appear in the bytecode?
2. What does the `LOAD_GLOBAL` opcode typically retrieve during execution?
---
What is the primary purpose of the `dis` module in Python?
Anonymous Quiz
0%
A. To convert Python source code into machine code.
0%
B. To disassemble Python bytecode into a human-readable form.
0%
C. To compile Python code just-in-time (JIT).
0%
D. To optimize Python code for faster execution.
Here's your Telegram post for Day 45 of your Python learning series!
---
Day 45: Ever wondered how Python attributes can do more than just store values? 🤔 Today, we dive into **Advanced Descriptors**! Forget simple `property` – we'll learn to craft "self-aware" attributes that can validate input, trigger side effects, and perform introspection automatically. Unleash the true power behind Python's object model and build more robust, expressive classes! 🚀 #PythonLearning #AdvancedPython #Descriptors
```python
class PositiveNumber:
"""
A descriptor that ensures the attribute it manages is a positive number.
It also prints messages on access and modification.
"""
def __set_name__(self, owner, name):
# __set_name__ allows the descriptor to know the name of the attribute
# it's managing on the owner class.
self.public_name = name
self.private_name = '_' + name # Store the actual value in a private attribute
def __get__(self, obj, obj_type=None):
if obj is None:
return self # Accessing the descriptor itself via the class
print(f"🔍 Accessing attribute '{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 attribute '{self.public_name}' to {value}...")
if not isinstance(value, (int, float)):
raise TypeError(f"Error: '{self.public_name}' must be a number, got {type(value).__name__}.")
if value <= 0:
raise ValueError(f"Error: '{self.public_name}' must be a positive number.")
# 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}.")
class Product:
# Assign descriptors to class attributes
price = PositiveNumber()
quantity = PositiveNumber()
def __init__(self, name, price, quantity):
self.name = name
# Setting these attributes triggers the __set__ method of our descriptors
self.price = price
self.quantity = quantity
print("--- Initializing Product ---")
product1 = Product("Laptop", 1200, 5)
print(f"nProduct details: {product1.name}, Price: {product1.price}, Quantity: {product1.quantity}")
print("n--- Testing Invalid Assignments ---")
try:
product1.price = -100 # This should raise a ValueError
except ValueError as e:
print(e)
try:
product1.quantity = "ten" # This should raise a TypeError
except TypeError as e:
print(e)
print("n--- Testing Valid Assignment ---")
product1.price = 1250.75
print(f"nUpdated Price: {product1.price}")
```
Output example:
```
--- Initializing Product ---
✏️ Attempting to set attribute 'price' to 1200...
✅ Successfully set 'price' to 1200.
✏️ Attempting to set attribute 'quantity' to 5...
✅ Successfully set 'quantity' to 5.
🔍 Accessing attribute 'price'...
🔍 Accessing attribute 'quantity'...
Product details: Laptop, Price: 1200, Quantity: 5
--- Testing Invalid Assignments ---
✏️ Attempting to set attribute 'price' to -100...
Error: 'price' must be a positive number.
✏️ Attempting to set attribute 'quantity' to ten...
Error: 'quantity' must be a number, got str.
--- Testing Valid Assignment ---
✏️ Attempting to set attribute 'price' to 1250.75...
✅ Successfully set 'price' to 1250.75.
🔍 Accessing attribute 'price'...
Updated Price: 1250.75
```
Small challenge:
How would you modify the `PositiveNumber` descriptor to also keep a history of all previously set values for that attribute in a list? Share your ideas! 👇
---
---
Day 45: Ever wondered how Python attributes can do more than just store values? 🤔 Today, we dive into **Advanced Descriptors**! Forget simple `property` – we'll learn to craft "self-aware" attributes that can validate input, trigger side effects, and perform introspection automatically. Unleash the true power behind Python's object model and build more robust, expressive classes! 🚀 #PythonLearning #AdvancedPython #Descriptors
```python
class PositiveNumber:
"""
A descriptor that ensures the attribute it manages is a positive number.
It also prints messages on access and modification.
"""
def __set_name__(self, owner, name):
# __set_name__ allows the descriptor to know the name of the attribute
# it's managing on the owner class.
self.public_name = name
self.private_name = '_' + name # Store the actual value in a private attribute
def __get__(self, obj, obj_type=None):
if obj is None:
return self # Accessing the descriptor itself via the class
print(f"🔍 Accessing attribute '{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 attribute '{self.public_name}' to {value}...")
if not isinstance(value, (int, float)):
raise TypeError(f"Error: '{self.public_name}' must be a number, got {type(value).__name__}.")
if value <= 0:
raise ValueError(f"Error: '{self.public_name}' must be a positive number.")
# 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}.")
class Product:
# Assign descriptors to class attributes
price = PositiveNumber()
quantity = PositiveNumber()
def __init__(self, name, price, quantity):
self.name = name
# Setting these attributes triggers the __set__ method of our descriptors
self.price = price
self.quantity = quantity
print("--- Initializing Product ---")
product1 = Product("Laptop", 1200, 5)
print(f"nProduct details: {product1.name}, Price: {product1.price}, Quantity: {product1.quantity}")
print("n--- Testing Invalid Assignments ---")
try:
product1.price = -100 # This should raise a ValueError
except ValueError as e:
print(e)
try:
product1.quantity = "ten" # This should raise a TypeError
except TypeError as e:
print(e)
print("n--- Testing Valid Assignment ---")
product1.price = 1250.75
print(f"nUpdated Price: {product1.price}")
```
Output example:
```
--- Initializing Product ---
✏️ Attempting to set attribute 'price' to 1200...
✅ Successfully set 'price' to 1200.
✏️ Attempting to set attribute 'quantity' to 5...
✅ Successfully set 'quantity' to 5.
🔍 Accessing attribute 'price'...
🔍 Accessing attribute 'quantity'...
Product details: Laptop, Price: 1200, Quantity: 5
--- Testing Invalid Assignments ---
✏️ Attempting to set attribute 'price' to -100...
Error: 'price' must be a positive number.
✏️ Attempting to set attribute 'quantity' to ten...
Error: 'quantity' must be a number, got str.
--- Testing Valid Assignment ---
✏️ Attempting to set attribute 'price' to 1250.75...
✅ Successfully set 'price' to 1250.75.
🔍 Accessing attribute 'price'...
Updated Price: 1250.75
```
Small challenge:
How would you modify the `PositiveNumber` descriptor to also keep a history of all previously set values for that attribute in a list? Share your ideas! 👇
---