Loading experience... Please wait.
Advanced Coding Tutorial

Advanced Coding Tutorials: Cython

Image description
Section 1: Introduction +

Welcome to our advanced coding tutorials. Here, you'll dive deep into programming concepts, best practices, and more.

Introduction
    
 <!DOCTYPE html>
<html>
<head>
    <title>Stay Tuned this Tutorial is Coming Soon</title>
</head>
<body>
    <h1>Helloo, World!</h1>
</body>
</html>
</html>
</html>
</html>
</html>
</html>
</html>
                



                Section 1: The Global Interpreter Lock (GIL)+

The GIL is a mutex in CPython that protects access to Python objects, preventing multiple native threads from executing Python bytecode simultaneously within a single process. While it simplifies memory management, it limits parallelism for CPU-bound tasks in multi-threaded applications. I/O-bound threads still benefit as the GIL is released during blocking I/O calls.

  

import time
import threading

# A CPU-bound task (counting)
def cpu_bound_task(count_to):
    count = 0
    while count < count_to:
        count += 1

COUNT = 100_000_000 # A large number to make the task take time

# --- Run Sequentially ---
start_seq = time.time()
cpu_bound_task(COUNT)
cpu_bound_task(COUNT)
end_seq = time.time()
print(f"Sequential execution time: {end_seq - start_seq:.4f} seconds")

# --- Run with Threads ---
thread1 = threading.Thread(target=cpu_bound_task, args=(COUNT,))
thread2 = threading.Thread(target=cpu_bound_task, args=(COUNT,))

start_thread = time.time()
thread1.start()
thread2.start()
thread1.join() # Wait for thread 1 to finish
thread2.join() # Wait for thread 2 to finish
end_thread = time.time()
print(f"Threaded execution time: {end_thread - start_thread:.4f} seconds")

# Note: Due to the GIL, the threaded execution time for this CPU-bound task
# will likely be similar to or even slightly longer than the sequential time,
# not significantly faster as one might expect from true parallelism.




            Section 2: Introduction to Cython+

Cython is a superset of Python that allows writing C extensions. It lets you compile Python-like code (with optional static type declarations) into efficient C code. This is particularly useful for speeding up CPU-bound bottlenecks in Python applications or for interfacing directly with C/C++ libraries without writing complex C API code manually.

  

# --- Example Cython code (save as example.pyx) ---
# This file needs to be compiled using a setup.py script

# cdef allows defining C variables for speed
# cpdef creates both C and Python accessible versions
# cython: language_level=3 # Specify Python 3 semantics

cpdef int sum_of_squares_cython(int n):
    # Declare C integer type for loop variable and result
    cdef int i, total = 0
    for i in range(n):
        total += i * i
    return total

# --- Example setup.py (save as setup.py) ---
# from setuptools import setup
# from Cython.Build import cythonize
#
# setup(
#     ext_modules = cythonize("example.pyx")
# )
# Run: python setup.py build_ext --inplace

# --- Example Python usage (after compiling) ---
# import example # Import the compiled module
# import time
#
# N = 10000
#
# start = time.time()
# result = example.sum_of_squares_cython(N)
# end = time.time()
# print(f"Cython result: {result}, Time: {end - start:.6f}s")
#
# # Compare with pure Python (usually slower)
# def sum_of_squares_python(n):
#     total = 0
#     for i in range(n):
#         total += i * i
#     return total
#
# start_py = time.time()
# result_py = sum_of_squares_python(N)
# end_py = time.time()
# print(f"Python result: {result_py}, Time: {end_py - start_py:.6f}s")

# Placeholder message as direct execution isn't possible here:
print("Cython code needs compilation. See comments for structure.")




            Section 3: CPython Memory Management+

CPython primarily uses reference counting for memory management. Each object keeps track of how many references point to it. When the count drops to zero, the object's memory is deallocated. A cyclic garbage collector runs periodically to detect and break reference cycles (objects referring to each other) that reference counting alone cannot handle.

  

import sys
import gc # Garbage Collector interface

# --- Reference Counting Example ---
a = [1, 2, 3] # Object [1, 2, 3] created, ref count = 1 (a)
print(f"Initial ref count for a: {sys.getrefcount(a) - 1}") # -1 because getrefcount itself adds a temporary ref

b = a         # Ref count = 2 (a, b)
print(f"Ref count after b = a: {sys.getrefcount(a) - 1}")

del b         # Ref count = 1 (a)
print(f"Ref count after del b: {sys.getrefcount(a) - 1}")

del a         # Ref count = 0, object [1, 2, 3] is deallocated
# print(sys.getrefcount(a)) # This would raise NameError as 'a' is deleted

# --- Cyclic Reference Example (handled by GC) ---
class Node:
    def __init__(self, name):
        self.name = name
        self.neighbor = None
        print(f"Node '{self.name}' created.")

    def __del__(self):
        # This is called when the object is about to be destroyed
        print(f"Node '{self.name}' being destroyed.")

# Create a cycle
node1 = Node("A")
node2 = Node("B")
node1.neighbor = node2 # node1 references node2
node2.neighbor = node1 # node2 references node1

# Check initial reference counts (will be 2 due to self + neighbor)
print(f"Ref count node1: {sys.getrefcount(node1) - 1}")
print(f"Ref count node2: {sys.getrefcount(node2) - 1}")

# Remove external references
del node1
del node2

# At this point, the nodes still reference each other (ref count > 0)
# They are unreachable but not deallocated by ref counting alone.
print("\nNodes deleted from namespace, but cycle exists.")
print("Running garbage collection manually...")

collected_count = gc.collect() # Manually run the cyclic GC
print(f"Garbage collector collected {collected_count} objects.")
# Now the __del__ methods should be called as the GC breaks the cycle.




            Section 4: Abstract Base Classes (ABCs)+

Abstract Base Classes (ABCs) from the `abc` module define interfaces or contracts that subclasses must implement. They use the `@abstractmethod` decorator to specify methods that must be overridden. This enforces a common structure across different implementations, promoting better code organization and polymorphism.

  

from abc import ABC, abstractmethod

# Define an Abstract Base Class
class Vehicle(ABC):
    @abstractmethod
    def start_engine(self):
        """Abstract method to start the vehicle's engine."""
        pass

    @abstractmethod
    def stop_engine(self):
        """Abstract method to stop the vehicle's engine."""
        pass

    def honk(self):
        """A concrete method available to all subclasses."""
        print("Generic honk!")

# Concrete subclass implementing the abstract methods
class Car(Vehicle):
    def start_engine(self):
        print("Car engine started with a key turn.")

    def stop_engine(self):
        print("Car engine stopped.")

    # Can override concrete methods too
    def honk(self):
        print("Beep beep!")

# Another concrete subclass
class Motorcycle(Vehicle):
    def start_engine(self):
        print("Motorcycle engine started with a kick.")

    def stop_engine(self):
        print("Motorcycle engine stopped.")

# Usage
my_car = Car()
my_moto = Motorcycle()

vehicles = [my_car, my_moto]

for v in vehicles:
    v.start_engine()
    v.honk()
    v.stop_engine()
    print("-" * 10)

# Attempting to instantiate the ABC directly will fail
try:
    generic_vehicle = Vehicle()
except TypeError as e:
    print(f"Error trying to instantiate ABC: {e}")

# Attempting to instantiate a subclass that *doesn't* implement
# all abstract methods will also fail:
# class BrokenCar(Vehicle):
#     def start_engine(self): pass
# try:
#    bad_car = BrokenCar() # Raises TypeError
# except TypeError as e:
#    print(f"Error with incomplete subclass: {e}")




            Section 5: Threading vs Multiprocessing+

Python's `threading` module allows concurrent execution within a single process, sharing memory but limited by the GIL for CPU-bound tasks. The `multiprocessing` module bypasses the GIL by creating separate processes, each with its own memory space, enabling true parallelism for CPU-bound work but incurring higher communication overhead.

  

import time
import threading
import multiprocessing
import os

# Shared function (can be CPU or I/O bound depending on content)
def worker_function(worker_id, duration):
    pid = os.getpid()
    thread_id = threading.get_ident()
    print(f"Worker {worker_id}: START - PID {pid}, Thread {thread_id}")
    time.sleep(duration) # Simulate work (I/O bound)
    # For CPU-bound, replace sleep with heavy computation
    print(f"Worker {worker_id}: END   - PID {pid}, Thread {thread_id}")
    return f"Result from worker {worker_id}"

NUM_WORKERS = 3
DURATION = 1

# --- Using Threading ---
print("--- Starting with Threading ---")
threads = []
start_th = time.time()
for i in range(NUM_WORKERS):
    t = threading.Thread(target=worker_function, args=(f"T{i}", DURATION))
    threads.append(t)
    t.start()

for t in threads:
    t.join() # Wait for threads to complete
end_th = time.time()
print(f"Threading finished in {end_th - start_th:.4f}s\n")
# Note: All threads run within the same PID.

# --- Using Multiprocessing ---
# Required for multiprocessing on some platforms (like Windows)
if __name__ == "__main__": # Guard clause
    print("--- Starting with Multiprocessing ---")
    processes = []
    start_mp = time.time()
    # Use a Pool for easier management (optional)
    # with multiprocessing.Pool(processes=NUM_WORKERS) as pool:
    #     results = pool.starmap(worker_function, [(f"P{i}", DURATION) for i in range(NUM_WORKERS)])

    # Manual process creation:
    for i in range(NUM_WORKERS):
        p = multiprocessing.Process(target=worker_function, args=(f"P{i}", DURATION))
        processes.append(p)
        p.start()

    results = [] # Placeholder if not using Pool.join() implicitly handles results
    for p in processes:
        p.join() # Wait for processes to complete
        # results.append(p.exitcode) # Can get exit code

    end_mp = time.time()
    print(f"Multiprocessing finished in {end_mp - start_mp:.4f}s")
    # Note: Each process will have a different PID.

# Key takeaway: For I/O-bound tasks (like time.sleep), both can offer concurrency.
# For CPU-bound tasks, multiprocessing typically offers true parallelism (speedup),
# while threading is limited by the GIL in CPython.




            Section 6: Functional Programming Tools (`functools`)+

Python supports functional programming paradigms. The `functools` module provides higher-order functions and operations on callable objects. Key tools include `partial` for freezing function arguments, `reduce` for cumulative operations, `lru_cache` for memoization, and `wraps` for creating well-behaved decorators.

  

import functools
import time
import operator

# --- functools.partial ---
def power(base, exponent):
    return base ** exponent

# Create specialized functions by freezing arguments
square = functools.partial(power, exponent=2)
cube = functools.partial(power, exponent=3)

print(f"Square of 5: {square(5)}") # Output: 25
print(f"Cube of 3: {cube(3)}")     # Output: 27

# --- functools.reduce ---
# Performs cumulative operations on a sequence
numbers = [1, 2, 3, 4, 5]
product = functools.reduce(operator.mul, numbers) # Multiply all numbers
# Equivalent to (((1*2)*3)*4)*5
print(f"Product of {numbers}: {product}") # Output: 120

# --- functools.lru_cache ---
# Memoization decorator - caches results of expensive function calls
@functools.lru_cache(maxsize=128) # Least Recently Used cache
def slow_fibonacci(n):
    """Calculates Fibonacci recursively (inefficient without cache)."""
    if n < 2:
        return n
    # print(f"Calculating fib({n})") # Uncomment to see cache effect
    time.sleep(0.01) # Simulate slowness
    return slow_fibonacci(n-1) + slow_fibonacci(n-2)

start_cache = time.time()
result_fib = slow_fibonacci(15) # Calculation happens
end_cache = time.time()
print(f"Fib(15) = {result_fib}. First call time: {end_cache - start_cache:.4f}s")

start_cache2 = time.time()
result_fib2 = slow_fibonacci(15) # Result retrieved from cache instantly
end_cache2 = time.time()
print(f"Fib(15) = {result_fib2}. Second call time: {end_cache2 - start_cache2:.4f}s")

# --- functools.wraps ---
# (Already demonstrated in Section 2: Advanced Decorators)
# Ensures decorators preserve metadata of the original function.



Grab the great 7-zip compression program from here: 7-zip



Discover Comprehensive Tutorials & Software Downloads

Unlock a wealth of tutorials and software downloads available across our platform.

Image descriptionStart Exploring
 Welcome to your all-in-one destination in the Philippines! Settle into our welcoming accommodations, then treat your taste buds at our restaurant, featuring both elegant fine dining and authentic local cuisine, including special 8-course meals. Explore the islands with our tourist tours and car rentals, or let our shuttle service handle your transport. Find unique treasures in our handcrafted clothing line, unwind with yoga and massage, or catch some waves with our surfboard rentals. Join us at the bar, order takeaway, or book us for your next celebration or corporate event – we even offer magical beach dining setups. We also have a selection of reads and software in our bookstore and software store.