Python basic to advance
43 subscribers
131 photos
98 links
Learn Python daily with bite-sized lessons, code examples, output previews, and fun quizzes. Perfect for beginners and enthusiasts!
Download Telegram
Here's your Telegram post for Day 38 of the Python learning series! 🚀

---

**Day 38: Static Contract Enforcement 🛡️ Mastering Protocols and Generic Typevars for Robust APIs**

Ever wanted to define an interface without inheritance, enforcing specific behaviors across diverse classes? Python's `typing.Protocol` is your answer! 🦆 It brings structural subtyping (duck typing) to the type checker, ensuring your APIs are robust and predictable. Combine this with `TypeVar` for truly generic, type-safe functions. Build libraries that scream reliability! ✨

```python
from typing import Protocol, TypeVar, runtime_checkable

# 1. Define a Protocol for objects that can be 'printed'.
# @runtime_checkable allows isinstance() checks at runtime, though Protocols
# are primarily for static analysis by tools like mypy.
@runtime_checkable
class Printable(Protocol):
"""
A protocol for objects that provide a string representation
via a 'to_string' method.
"""
def to_string(self) -> str:
# Ellipsis indicates an abstract method signature; no implementation needed here.
...

# 2. Implementations (implicit: no explicit inheritance from Printable required!)
class MyObject:
def __init__(self, value: int):
self.value = value

def to_string(self) -> str:
return f"MyObject(value={self.value})"

class AnotherType:
def __init__(self, name: str):
self.name = name

def to_string(self) -> str:
return f"AnotherType(name='{self.name}')"

class NotPrintable:
def __init__(self, data: list):
self.data = data
# This class *does not* have a 'to_string' method, so it does not conform.

# 3. A Generic Function that expects any type conforming to the Printable protocol.
# 'bound=Printable' ensures that P must be Printable or a subtype thereof.
P = TypeVar('P', bound=Printable)

def process_and_print(item: P) -> None:
"""
Takes any object conforming to the Printable protocol and prints its
string representation. Static type checkers will ensure 'item' has 'to_string'.
"""
print(f"Processing and printing: {item.to_string()}")

# 4. Demonstrate usage and type checking
if __name__ == "__main__":
obj1 = MyObject(123)
obj2 = AnotherType("Protocol Example")
obj_not_printable = NotPrintable([1, 2, 3])

process_and_print(obj1)
process_and_print(obj2)

# Check conformity at runtime (thanks to @runtime_checkable)
print(f"nIs obj1 a Printable? {isinstance(obj1, Printable)}")
print(f"Is obj_not_printable a Printable? {isinstance(obj_not_printable, Printable)}")

# If you tried to uncomment the line below and run mypy, it would flag a type error!
# process_and_print(obj_not_printable)
# At runtime, calling process_and_print with obj_not_printable would raise an AttributeError.
```

Output example:
```
Processing and printing: MyObject(value=123)
Processing and printing: AnotherType(name='Protocol Example')

Is obj1 a Printable? True
Is obj_not_printable a Printable? False
```

**Small challenge:**
Extend the `Printable` protocol to also require a `get_id()` method that returns an `int`. Then, create a new class that conforms to this extended protocol and try passing it to `process_and_print`. How would mypy react if `get_id()` were missing from one of your conforming classes?

---
Here's your Telegram post for Day 40!

---

🚀 **Day 40: Unlocking Python's Mighty Descriptor Protocol!** 🎩

Ready to peek behind the curtain of Python's object model? Today, we're diving deep into *Descriptors*! Far beyond simple `@property` decorators, descriptors are a powerful mechanism that allows you to customize *how* attributes are accessed, set, or deleted on an object. They're the secret sauce behind many advanced features, enabling robust validation, lazy loading, and dynamic behavior. Let's wield this mighty tool and craft smarter, more controlled classes! ✨

```python
class ValidatedInt:
"""
A descriptor that ensures an attribute is an integer within a specified range.
"""
def __set_name__(self, owner, name):
# Called when the descriptor is assigned. It lets us know its own name!
self.public_name = name
self.private_name = f'_{name}' # Convention for storing the actual data

def __init__(self, min_val, max_val):
self.min_val = min_val
self.max_val = max_val

def __get__(self, obj, objtype=None):
if obj is None:
# Accessing the descriptor from the class itself (e.g., GameCharacter.level)
return self

# Retrieve the value from the instance's dictionary
value = getattr(obj, self.private_name, None)
if value is None:
raise AttributeError(f"'{self.public_name}' has not been set.")
return value

def __set__(self, obj, value):
# Validate the value before setting
if not isinstance(value, int):
raise ValueError(f"'{self.public_name}' must be an integer.")
if not (self.min_val <= value <= self.max_val):
raise ValueError(f"'{self.public_name}' must be between {self.min_val} and {self.max_val}.")

# Store the validated value in the instance's dictionary
setattr(obj, self.private_name, value)

def __delete__(self, obj):
# Prevent deletion of the attribute
raise AttributeError(f"'{self.public_name}' cannot be deleted.")

class GameCharacter:
level = ValidatedInt(1, 100) # Our level attribute now uses the descriptor!
health = ValidatedInt(0, 1000) # Same for health!

def __init__(self, level, health):
self.level = level # This calls __set__ on the descriptor
self.health = health # This calls __set__ on the descriptor

print("--- Creating characters ---")
hero = GameCharacter(10, 500)
print(f"Hero Level: {hero.level}, Health: {hero.health}")

villain = GameCharacter(99, 900)
print(f"Villain Level: {villain.level}, Health: {villain.health}")

print("n--- Testing validation ---")
try:
hero.level = 101 # Too high, will raise ValueError
except ValueError as e:
print(f"Error setting hero level: {e}")

try:
hero.health = "low" # Not an int, will raise ValueError
except ValueError as e:
print(f"Error setting hero health: {e}")

try:
del hero.level # Attempt to delete, will raise AttributeError
except AttributeError as e:
print(f"Error deleting hero level: {e}")

print(f"nHero (after attempts): Level: {hero.level}, Health: {hero.health}")
```

Output example:
```
--- Creating characters ---
Hero Level: 10, Health: 500
Villain Level: 99, Health: 900

--- Testing validation ---
Error setting hero level: 'level' must be between 1 and 100.
Error setting hero health: 'health' must be an integer.
Error deleting hero level: 'level' cannot be deleted.

Hero (after attempts): Level: 10, Health: 500
```

💡 **Challenge:** Modify the `ValidatedInt` descriptor to also accept an optional `default_value` in its `__init__`. If the attribute is accessed but hasn't been set yet (i.e., `getattr(obj, self.private_name, None)` returns `None`), it should return this `default_value` instead of raising an `AttributeError`.

---

Here's a quick quiz to test your descriptor knowledge! 👇
🚀 Day 42 is here, and we're tackling one of Python's most debated topics: the Global Interpreter Lock (GIL)! 🔐 Ever wondered why your multi-threaded CPU-bound Python code doesn't run faster? The GIL is the answer, restricting CPython to execute only one thread at a time.

Today, we deconstruct its impact, explore its origins, and search for true parallelism! Get ready for an ultimate question! 🤔

```python
import time
import threading
import multiprocessing
import os

# A CPU-bound task that performs calculations
def cpu_bound_task(n):
count = 0
for i in range(n):
count += i * i # Simple calculation to keep CPU busy
return count

# Function to run multiple CPU-bound tasks using threads
def run_with_threads(n_tasks, iterations):
start_time = time.perf_counter()
threads = []
print(f" Starting {n_tasks} threads in PID: {os.getpid()}")
for _ in range(n_tasks):
thread = threading.Thread(target=cpu_bound_task, args=(iterations,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
end_time = time.perf_counter()
print(f" Threads finished in: {end_time - start_time:.4f} seconds")

# Function to run multiple CPU-bound tasks using processes
def run_with_processes(n_tasks, iterations):
start_time = time.perf_counter()
processes = []
print(f" Starting {n_tasks} processes (each with its own GIL)")
for _ in range(n_tasks):
process = multiprocessing.Process(target=cpu_bound_task, args=(iterations,))
processes.append(process)
process.start()
for process in processes:
process.join()
end_time = time.perf_counter()
print(f" Processes finished in: {end_time - start_time:.4f} seconds")

if __name__ == "__main__":
NUM_TASKS = 4 # Number of threads/processes to create
ITERATIONS = 10_000_000 # Complexity for each CPU-bound task

print("--- Testing with Threads (affected by GIL) ---")
run_with_threads(NUM_TASKS, ITERATIONS)

print("n--- Testing with Processes (bypasses GIL) ---")
run_with_processes(NUM_TASKS, ITERATIONS)

print("nObservation: For CPU-bound tasks in CPython, multi-processing typically shows better performance, as each process gets its own interpreter and GIL, allowing true parallel execution.")
```

Output example:
```
--- Testing with Threads (affected by GIL) ---
Starting 4 threads in PID: 12345
Threads finished in: 3.1234 seconds

--- Testing with Processes (bypasses GIL) ---
Starting 4 processes (each with its own GIL)
Processes finished in: 0.8765 seconds

Observation: For CPU-bound tasks in CPython, multi-processing typically shows better performance, as each process gets its own interpreter and GIL, allowing true parallel execution.
```

Small challenge:
Challenge: Modify the `cpu_bound_task` to simulate an I/O-bound operation (e.g., replace `count += i * i` with `time.sleep(0.01)` inside the loop, or better, `time.sleep(0.1)` outside the loop once). How do the execution times for threads vs. processes change? Why? 👇 Let us know your insights!
Which of the following methods, when implemented in a class, makes an instance of that class a 'descriptor' in Python?
Anonymous Quiz
0%
A) Only `__init__`
0%
B) At least one of `__get__`, `__set__`, or `__delete__`
0%
C) Only `__call__`
0%
D) At least one of `__enter__` or `__exit__`
🚀 **Day 44: The Zen of Zero-Copy with `memoryview` and Protocols!** 🚀

Ever wondered how to handle large datasets in Python without constantly copying memory? Today, we unlock a powerful secret: `memoryview` and Python's Buffer Protocol! 💡 This advanced topic allows you to create "views" into an existing object's memory, enabling high-performance, low-overhead data manipulation. Forget unnecessary copying; manipulate data directly, efficiently, and with incredible speed, especially when dealing with bytes, bytearrays, or integrating with C extensions. Let's make your Python code truly fly! 🚀

```python
import array

# 1. Start with a mutable data source (e.g., bytearray)
original_data = bytearray(b'Hello Python Learners!')
print(f"Original data: {original_data}")

# 2. Create a memoryview of the data
mv = memoryview(original_data)
print(f"Memoryview as string (initial): {mv.tobytes().decode()}")

# 3. Modify the data through the memoryview - no copy is made!
# The changes directly reflect in the original_data.
mv[6:12] = b'WORLD!' # Change 'Python' to 'WORLD!'
mv[0] = ord('J') # Change 'H' to 'J'

print(f"Memoryview as string (modified): {mv.tobytes().decode()}")
print(f"Original data after memoryview modification: {original_data}")

# 4. Memoryviews can be sliced, and these slices are also views!
sub_mv = mv[6:12]
print(f"Sliced memoryview (sub_mv): {sub_mv.tobytes().decode()}")

# 5. Modifying the sub-view also modifies the original data
sub_mv[0] = ord('W') # Change 'W' to 'W' (no visible change, but demonstrates ability)
print(f"Original data after sub_mv modification: {original_data}")

# 6. Attempting to create a memoryview of an immutable object (like bytes)
# and then trying to modify it will raise an error.
immutable_bytes = b'Immutable'
mv_immutable = memoryview(immutable_bytes)
try:
# This line would raise an error if uncommented
# mv_immutable[0] = ord('X')
pass
except TypeError as e:
print(f"nAttempting to modify immutable bytes via memoryview leads to: {e}")
```

Output example:
```
Original data: bytearray(b'Hello Python Learners!')
Memoryview as string (initial): Hello Python Learners!
Memoryview as string (modified): Jello WORLD! Learners!
Original data after memoryview modification: bytearray(b'Jello WORLD! Learners!')
Sliced memoryview (sub_mv): WORLD!
Original data after sub_mv modification: bytearray(b'Jello WORLD! Learners!')

Attempting to modify immutable bytes via memoryview leads to: memoryview: underlying object is not writable
```

💡 **Challenge:**
Imagine you have a `bytearray` containing a large stream of sensor data, and you need to process only the segments where a specific flag is set. How would you use `memoryview` to efficiently iterate through these segments, creating views for each, without creating multiple copies of data? Share your approach! 👇

---
Alright, Pythonistas! 🐍 On Day 45, we're diving deep into the very soul of CPython: its bytecode! ✨ Ever wondered what truly happens when your Python code runs? We're peeling back the layers to explore the low-level instructions CPython executes. Understanding bytecode unlocks a new dimension of performance optimization, advanced introspection, and even dynamic code generation. Get ready to witness some true Python magic! 🔥

```python
import dis
import types

# A simple function to inspect
def greet_person(name):
# Line 2: Defines a formatted string
message = f"Hello, {name}!"
# Line 3: Prints the message
print(message)
# Line 4: Returns the length of the message
return len(message)

print("--- Disassembling greet_person ---")
dis.dis(greet_person)

print("n--- Raw Bytecode and Constants ---")
# Accessing the code object, which contains all function details
code_obj = greet_person.__code__

# Raw bytecode bytes (opcodes and their arguments, packed)
print(f"Raw Bytecode (hex): {code_obj.co_code.hex()}")

# Constants used by the function (e.g., strings, numbers)
print(f"Constants: {code_obj.co_consts}")

# Bytecode is a sequence of opcodes, each potentially followed by an argument.
# The 'dis' module makes sense of these raw bytes for us!
```

**Output example:**
```
--- Disassembling greet_person ---
2 0 RESUME 0
3 2 LOAD_CONST 1 ('Hello, ')
4 LOAD_FAST 0 (name)
6 FORMAT_VALUE 128 (f-string)
8 LOAD_CONST 2 ('!')
10 BUILD_STRING 3
12 STORE_FAST 1 (message)

4 14 LOAD_GLOBAL 0 (print)
16 LOAD_FAST 1 (message)
18 PRECALL 1
22 CALL 1
32 POP_TOP

5 34 LOAD_GLOBAL 1 (len)
36 LOAD_FAST 1 (message)
38 PRECALL 1
42 CALL 1
52 RETURN_VALUE

--- Raw Bytecode and Constants ---
Raw Bytecode (hex): 000002640100650080006402008303005a0100640000650100be0100b401000100640100650100be0100b401005300
Constants: (None, 'Hello, ', '!', <class 'len'>)
```

**Small challenge:**
Your turn! 🚀 Disassemble the following function using `dis.dis()`:

```python
def calculate_sum(numbers):
total = 0
for num in numbers:
total += num
return total
```
Identify the opcodes responsible for:
1. Initializing `total = 0`.
2. Iterating through `numbers`.
3. Adding `num` to `total`.
4. Returning the final `total`.

Share your insights below! 👇
❤1
🚀 Day 46: The Miniature API! ✨

Ever wondered how `@property` works, or how powerful libraries like ORMs manage database fields? Meet Python's Descriptors! These fundamental objects allow you to customize attribute access (get, set, delete) at a low level, giving you incredible control over how your class attributes behave.

With the magic of `__set_name__`, your descriptor instance even knows the name of the attribute it's managing within the owner class. This makes your descriptors self-aware, enabling more generic error messages, logging, or complex setup without needing manual configuration. Dive in to craft elegant, robust, and reusable attribute behavior! 🐍

```python
class ValidatedString:
"""
A descriptor that ensures an attribute is a non-empty string and
can enforce min/max length, using __set_name__ for self-awareness.
"""
def __init__(self, min_len=1, max_len=float('inf')):
self.min_len = min_len
self.max_len = max_len

def __set_name__(self, owner, name):
# 'owner' is the class the descriptor is attached to (e.g., User)
# 'name' is the attribute name it's assigned to (e.g., 'username')
self.public_name = name
self.private_name = f'_{name}' # Store the actual value in a private attribute

def __get__(self, obj, objtype=None):
if obj is None:
return self # Accessing the descriptor itself via the class
return getattr(obj, self.private_name, '') # Return the private value

def __set__(self, obj, value):
if not isinstance(value, str):
raise TypeError(f"'{self.public_name}' must be a string.")
if not (self.min_len <= len(value) <= self.max_len):
if self.max_len == float('inf'):
raise ValueError(
f"'{self.public_name}' must be at least {self.min_len} characters long."
)
else:
raise ValueError(
f"'{self.public_name}' must be between {self.min_len} and "
f"{self.max_len} characters long."
)
setattr(obj, self.private_name, value) # Set the private value

class User:
username = ValidatedString(min_len=3, max_len=20)
email = ValidatedString(min_len=5) # No max_len constraint here

def __init__(self, username, email):
self.username = username
self.email = email

# --- Demonstration ---
try:
user1 = User("Alice", "alice@example.com")
print(f"User 1: {user1.username}, {user1.email}")

user1.username = "BobTheBuilder" # Valid update
print(f"User 1 (updated username): {user1.username}")

# Attempt to set an invalid username (too short)
user1.username = "Bo"
except ValueError as e:
print(f"Error caught: {e}")

try:
# Attempt to create a user with invalid email (too short)
user2 = User("Charlie", "c@ex")
except ValueError as e:
print(f"Error caught: {e}")

try:
# Attempt to create a user with non-string username
user3 = User(123, "test@example.com")
except TypeError as e:
print(f"Error caught: {e}")
```

Output example:
```
User 1: Alice, alice@example.com
User 1 (updated username): BobTheBuilder
Error caught: 'username' must be at least 3 characters long.
Error caught: 'email' must be at least 5 characters long.
Error caught: 'username' must be a string.
```

💡 Your Challenge: Can you create a `PositiveNumber` descriptor that ensures an attribute (like `age` or `quantity`) is always an integer greater than zero? Or perhaps a `ReadOnly` descriptor that allows setting a value only once during initialization? Go ahead, build your own miniature API! 🛠️

---
🚀 Day 48: Level up your Python! ✨ Ever wondered how frameworks enforce patterns or create domain-specific languages? Today, we're diving into the heart of Python's type system with **Metaclasses and `__init_subclass__`**! 🤯

We'll explore how to programmatically control class creation itself, ensuring your classes adhere to specific rules *before* they even exist. `__init_subclass__` offers a powerful hook for class-level validation and modification, a perfect stepping stone into the world of metaprogramming. Get ready to master the art of class factories!

```python
class PluginBase:
"""
Base class for plugins, enforcing that all subclasses define
a 'PLUGIN_NAME' attribute and an 'execute' method.
This logic runs when the subclass itself is defined, not when an instance is created.
"""
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs) # Always call super()

# Enforce PLUGIN_NAME attribute
if not hasattr(cls, 'PLUGIN_NAME') or not isinstance(cls.PLUGIN_NAME, str):
raise TypeError(f"Subclass '{cls.__name__}' must define a string 'PLUGIN_NAME'.")

# Enforce 'execute' method
if not callable(getattr(cls, 'execute', None)):
raise TypeError(f"Subclass '{cls.__name__}' must define an 'execute' method.")

print(f"✅ Plugin '{cls.PLUGIN_NAME}' validated and ready!")

# --- Valid Plugin Definition ---
class MyAwesomePlugin(PluginBase):
PLUGIN_NAME = "AwesomePlugin"

def execute(self, data):
print(f"Executing {self.PLUGIN_NAME} with data: {data}")
return data.upper()

# --- Instantiation of a valid plugin ---
print("n--- Creating MyAwesomePlugin instance ---")
plugin_instance = MyAwesomePlugin()
result = plugin_instance.execute("hello metaprogramming!")
print(f"Result: {result}")

# --- Invalid Plugin Definition (will raise an error) ---
print("n--- Attempting to define BadPlugin (will fail) ---")
try:
class BadPlugin(PluginBase):
# Missing PLUGIN_NAME and execute method
pass
except TypeError as e:
print(f"Error caught as expected: {e}")

# --- Another Invalid Plugin (missing execute) ---
print("n--- Attempting to define AnotherBadPlugin (will fail) ---")
try:
class AnotherBadPlugin(PluginBase):
PLUGIN_NAME = "MissingExecute"
# Missing execute method
pass
except TypeError as e:
print(f"Error caught as expected: {e}")
```

Output example:
```
✅ Plugin 'AwesomePlugin' validated and ready!

--- Creating MyAwesomePlugin instance ---
Executing AwesomePlugin with data: hello metaprogramming!
Result: HELLO METAPROGRAMMING!

--- Attempting to define BadPlugin (will fail) ---
Error caught as expected: Subclass 'BadPlugin' must define a string 'PLUGIN_NAME'.

--- Attempting to define AnotherBadPlugin (will fail) ---
Error caught as expected: Subclass 'AnotherBadPlugin' must define an 'execute' method.
```

🧠 **Small Challenge:** Modify `PluginBase` to automatically register all valid subclasses in a global dictionary (e.g., `_REGISTERED_PLUGINS = {}`) upon their definition via `__init_subclass__`. This is a very common pattern for building extensible architectures!

---
Which special method is automatically called when a class is created as a subclass of another class, allowing for class-level validation or registration *before* the subclass is fully defined?
Anonymous Quiz
0%
__new__
33%
__init__
67%
__init_subclass__
0%
__call__
🚀 **Python Learning Series - Day 49: Disassembling the Zen!** 🤯

Ever wondered what truly happens beneath the elegant surface of your Python code? Today, we're not just peeking; we're diving deep into the CPython interpreter's brain! 🧠 Welcome to the world of runtime bytecode manipulation and dynamic code generation. This isn't just about writing Python; it's about understanding how Python itself *runs* and how you can bend its internal mechanics to your will for advanced optimization, instrumentation, or even building truly adaptive systems. Get ready to deconstruct and reconstruct the very essence of your functions! ✨

```python
import dis
import types

def original_add(a, b):
"""A simple function to add two numbers."""
return a + b

def original_subtract(a, b):
"""A simple function to subtract two numbers."""
return a - b

print("--- Original add function bytecode ---")
dis.dis(original_add)

print("n--- Original subtract function bytecode ---")
dis.dis(original_subtract)

print(f"nBefore manipulation: original_add(5, 3) = {original_add(5, 3)}")

# Deep dive: Runtime bytecode manipulation!
# We can dynamically change a function's underlying code object.
# Here, we're making 'original_add' execute 'original_subtract's logic!
original_add.__code__ = original_subtract.__code__

print(f"nAfter manipulation: original_add(5, 3) = {original_add(5, 3)}")

# Let's verify by disassembling 'original_add' again
print("n--- original_add function bytecode after manipulation ---")
dis.dis(original_add)
```

**Output example:**
```
--- Original add function bytecode ---
9 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_ADD
6 RETURN_VALUE

--- Original subtract function bytecode ---
13 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_SUBTRACT
6 RETURN_VALUE

Before manipulation: original_add(5, 3) = 8

After manipulation: original_add(5, 3) = 2

--- original_add function bytecode after manipulation ---
13 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_SUBTRACT
6 RETURN_VALUE
```

### 🎯 **Small Challenge:**

Can you define a third function, `original_multiply(a, b)`, and then dynamically change `original_add`'s behavior *again* so that it performs multiplication when called? Show the output before and after your modification! Good luck, bytecode wizard! 🧙‍♂️

---
🚀 Day 50: Dive into the heart of Python's object model! Today, we're not just writing code, we're *designing* its very behavior. Welcome to **The Architect's Loom: Weaving Runtime Behavior with Advanced Metaclasses and Descriptors**. 🤯 Discover how to craft custom attribute access, enforce validation, and build dynamic systems that adapt at runtime. Descriptors and metaclasses are your powerful tools to unlock Python's deepest metaprogramming capabilities. Get ready to reshape how your objects behave! 👇

```python
class PositiveNumber:
"""
A descriptor that ensures an attribute is a positive number and logs access.
"""
def __set_name__(self, owner, name):
# Store the name of the attribute it manages (e.g., '_price')
self._private_name = '_' + name

def __get__(self, obj, objtype=None):
if obj is None: # Accessing the descriptor directly from the class
return self
print(f"🔍 Accessing attribute '{self._private_name}'...")
return obj.__dict__.get(self._private_name, None)

def __set__(self, obj, value):
if not isinstance(value, (int, float)) or value <= 0:
raise ValueError(f"🛑 Attribute '{self._private_name}' must be a positive number.")
print(f"✏️ Setting attribute '{self._private_name}' to {value}...")
obj.__dict__[self._private_name] = value

class Product:
# Assigning a descriptor instance to a class attribute
price = PositiveNumber()
quantity = PositiveNumber()

def __init__(self, name, price, quantity):
self.name = name
self.price = price # This assignment uses the 'price' descriptor
self.quantity = quantity # This assignment uses the 'quantity' descriptor

# --- Usage Example ---
print("--- Initializing Product ---")
p = Product("SuperWidget", 1200, 5)
print(f"nProduct: {p.name}")
print(f"Price: {p.price}") # Accessing uses the descriptor's __get__
print(f"Quantity: {p.quantity}") # Accessing uses the descriptor's __get__

print("n--- Testing Invalid Assignments ---")
try:
p.price = -100 # This will raise a ValueError
except ValueError as e:
print(e)

try:
p.quantity = "five" # This will raise a ValueError
except ValueError as e:
print(e)

print("n--- Testing Valid Assignment ---")
p.price = 1500 # This assignment uses the descriptor's __set__
print(f"New price: {p.price}")
```

Output example:
```
--- Initializing Product ---
✏️ Setting attribute '_price' to 1200...
✏️ Setting attribute '_quantity' to 5...

Product: SuperWidget
🔍 Accessing attribute '_price'...
Price: 1200
🔍 Accessing attribute '_quantity'...
Quantity: 5

--- Testing Invalid Assignments ---
🛑 Attribute '_price' must be a positive number.
🛑 Attribute '_quantity' must be a positive number.

--- Testing Valid Assignment ---
✏️ Setting attribute '_price' to 1500...
🔍 Accessing attribute '_price'...
New price: 1500
```

💡 **Your Challenge:** How would you modify the `PositiveNumber` descriptor to allow a default value of `1` if no value has been explicitly set for the attribute yet, without breaking the positive number constraint? Think about when `__get__` is called and what information it receives! Share your ideas! 👇

---
Which special method is invoked when an attribute managed by a descriptor is *accessed* (read) on an instance?
Anonymous Quiz
0%
__get__(self, obj, objtype=None)
0%
__set__(self, obj, value)
0%
__delete__(self, obj)
0%
__set_name__(self, owner, name)
Hey Pythonistas! 👋 Welcome to Day 52 of our deep dive! 🚀 Today, we're twisting Python itself with a mind-bending topic: **"The Malleable Codebase: Dynamic Class Generation and Runtime Code Alteration for Self-Modifying Python."**

Ever wondered if your code could change its own rules on the fly? We'll explore how to not just write classes, but *generate* them and *alter* their behavior *after* they've been defined, even affecting existing instances! Get ready for some serious metaprogramming magic that pushes Python's dynamic nature to its limits. ✨

```python
import types

# Day 52: The Malleable Codebase 🧩
# 1. Dynamic Class Generation: Creating a class 'TaskProcessor' on the fly.
# The `type()` function allows us to define classes programmatically.
TaskProcessor = type(
"TaskProcessor", # The name of our new class
(), # A tuple of base classes (empty means no inheritance)
{ # A dictionary of attributes and methods for the class
"__init__": lambda self, task_id: setattr(self, 'task_id', task_id),
"process": lambda self: f"Processing task {self.task_id} with basic logic."
}
)

# Create an instance of our dynamically generated class
task1 = TaskProcessor("alpha-123")
print(f"Task 1 initial: {task1.process()}")

# 2. Runtime Code Alteration / Self-Modification:
# Now, imagine we need to add advanced logic or change behavior dynamically.
# We can redefine methods on the class *after* it's been created.

def advanced_process_logic(self):
"""A new function that represents our 'advanced' processing logic."""
print(f"🔍 Task {self.task_id}: Activating advanced processing protocols!")
return f"Processing task {self.task_id} with *advanced* logic now!"

# Assign the new function to the 'process' method of the TaskProcessor class.
# This immediately modifies the class's behavior and affects all existing
# instances (that haven't overridden 'process' individually) and new ones!
TaskProcessor.process = advanced_process_logic

# Observe the change in the *existing* instance!
print(f"Task 1 modified: {task1.process()}")

# New instances also get this new behavior by default
task2 = TaskProcessor("beta-456")
print(f"Task 2 new: {task2.process()}")
```

Output example:
```
Task 1 initial: Processing task alpha-123 with basic logic.
🔍 Task alpha-123: Activating advanced processing protocols!
Task 1 modified: Processing task alpha-123 with *advanced* logic now!
🔍 Task beta-456: Activating advanced processing protocols!
Task 2 new: Processing task beta-456 with *advanced* logic now!
```

---
**Small challenge:**
How would you dynamically *remove* the `process` method from `TaskProcessor` at runtime, or replace it with a placeholder that raises `NotImplementedError`? 🤔 Share your thoughts!

---
Which of the following built-in functions is primarily used in Python for dynamically creating new class types at runtime?
Anonymous Quiz
0%
`exec()`
0%
`eval()`
0%
`type()`
0%
`compile()`
Which of the following magic methods must a descriptor implement to control attribute assignment (setting a value)?
Anonymous Quiz
0%
__set__(self, instance, value)
0%
__get__(self, instance, owner)
0%
__delete__(self, instance)
0%
__init__(self, ...)
Here's your Telegram post for Day 55, focusing on "Zero-Copy Python"!

---

**Day 55: Zero-Copy Python: Leveraging `mmap` and `memoryview` for High-Throughput Data Pipelines 🚀**

Ever needed to process massive datasets without unnecessary memory overhead? Today, we're diving into `mmap` and `memoryview` – your power tools for high-throughput data pipelines! Learn how to directly access and manipulate data buffers, bypassing costly data copies. This technique is crucial for performance-critical applications, from network programming to large-scale data processing. Get ready to supercharge your Python code with true zero-copy operations! ⚡💾

```python
import mmap
import os
import tempfile

# 1. Create a temporary file and mmap it
FILE_SIZE = 64 # Small size for demo purposes
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
filepath = tmp_file.name
tmp_file.write(b'x00' * FILE_SIZE) # Pre-allocate space
tmp_file.flush() # Ensure data is written to disk

try:
with open(filepath, 'rb+') as f:
# Map the entire file into memory
with mmap.mmap(f.fileno(), 0) as mm:
print(f"Initial mmap content (first 10 bytes): {mm[:10]}")

# 2. Create a memoryview for zero-copy access
mv = memoryview(mm)

# Write data directly through the memoryview buffer
data_to_write = b"Hello, Zero-Copy World!"
mv[:len(data_to_write)] = data_to_write
print(f"Content after writing (through mv): {mv[:len(data_to_write)]}")

# 3. Create a sliced memoryview – still no copy!
# Let's extract "Zero-Copy" part
zero_copy_slice = mv[7:17]
print(f"Sliced view content ('Zero-Copy'): {zero_copy_slice.tobytes().decode()}")

# 4. Prove zero-copy: Modify the original mmap,
# and observe the change directly in the slice
mm[7:8] = b'X' # Change 'Z' to 'X' in "Zero-Copy"
print(f"Slice reflects mmap change ('Xero-Copy'): {zero_copy_slice.tobytes().decode()}")

# Read directly from mmap again to confirm its current state
print(f"Original mmap (start) confirms change: {mm[:len(data_to_write)].decode()}")

finally:
os.remove(filepath) # Clean up the temporary file
```

**Output example:**
```
Initial mmap content (first 10 bytes): b'x00x00x00x00x00x00x00x00x00x00'
Content after writing (through mv): b'Hello, Zero-Copy World!'
Sliced view content ('Zero-Copy'): Zero-Copy
Slice reflects mmap change ('Xero-Copy'): Xero-Copy
Original mmap (start) confirms change: Hello, Xero-Copy World!
```

**Small challenge:**
Explore how `memoryview` can wrap `bytes`, `bytearray`, `array.array`, and `numpy` arrays. How does this `buffer protocol` enhance interoperability for zero-copy operations across different data types? Share your findings! 👇

---