Skip to content

Commit fe630fb

Browse files
committed
How to Split a Python List - Initial commit (materials)
1 parent a886108 commit fe630fb

File tree

7 files changed

+409
-0
lines changed

7 files changed

+409
-0
lines changed

python-split-list/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# How to Split a Python List
2+
3+
This folder holds the code for the Real Python How to Split a Python List tutorial.
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"""
2+
Plot the Mandelbrot set using parallel workers.
3+
4+
Usage:
5+
$ python parallelization.py
6+
"""
7+
8+
import functools
9+
import multiprocessing
10+
import os
11+
import time
12+
from dataclasses import dataclass
13+
from math import log
14+
from typing import Callable, Iterable
15+
16+
import numpy as np
17+
from PIL import Image
18+
19+
from splitting.multidimensional import Bounds, split_multi
20+
21+
IMAGE_WIDTH, IMAGE_HEIGHT = 1280, 720
22+
CENTER = -0.7435 + 0.1314j
23+
SCALE = 0.0000025
24+
MAX_ITERATIONS = 256
25+
ESCAPE_RADIUS = 1000
26+
NUM_CHUNKS = os.cpu_count() or 4
27+
28+
29+
class Chunk:
30+
"""A chunk of the image to be computed and rendered."""
31+
32+
def __init__(self, bounds: Bounds) -> None:
33+
self.bounds = bounds
34+
self.height = bounds.size[0]
35+
self.width = bounds.size[1]
36+
self.pixels = np.zeros((self.height, self.width), dtype=np.uint8)
37+
38+
def __getitem__(self, coordinates) -> int:
39+
"""Get the value of a pixel at the given absolute coordinates."""
40+
return self.pixels[self.bounds.offset(*coordinates)]
41+
42+
def __setitem__(self, coordinates, value: int) -> None:
43+
"""Set the value of a pixel at the given absolute coordinates."""
44+
self.pixels[self.bounds.offset(*coordinates)] = value
45+
46+
47+
@dataclass
48+
class MandelbrotSet:
49+
max_iterations: int
50+
escape_radius: float = 2.0
51+
52+
def __contains__(self, c: complex) -> bool:
53+
return self.stability(c) == 1
54+
55+
def stability(self, c: complex, smooth=False, clamp=True) -> float:
56+
value = self.escape_count(c, smooth) / self.max_iterations
57+
return max(0.0, min(value, 1.0)) if clamp else value
58+
59+
def escape_count(self, c: complex, smooth=False) -> int | float:
60+
z = 0 + 0j
61+
for iteration in range(self.max_iterations):
62+
z = z**2 + c
63+
if abs(z) > self.escape_radius:
64+
if smooth:
65+
return iteration + 1 - log(log(abs(z))) / log(2)
66+
return iteration
67+
return self.max_iterations
68+
69+
70+
def timed(function: Callable) -> Callable:
71+
@functools.wraps(function)
72+
def wrapper(*args, **kwargs):
73+
start = time.perf_counter()
74+
result = function(*args, **kwargs)
75+
end = time.perf_counter()
76+
print(f"{function.__name__}() took {end - start:.2f} seconds")
77+
return result
78+
79+
return wrapper
80+
81+
82+
def main() -> None:
83+
for worker in (process_chunks_parallel, process_chunks_sequential):
84+
image = compute(worker)
85+
image.show()
86+
87+
88+
@timed
89+
def process_chunks_sequential(chunked_bounds: Iterable[Bounds]) -> list[Chunk]:
90+
return list(map(generate_chunk, chunked_bounds))
91+
92+
93+
@timed
94+
def process_chunks_parallel(chunked_bounds: Iterable[Bounds]) -> list[Chunk]:
95+
with multiprocessing.Pool() as pool:
96+
return pool.map(generate_chunk, chunked_bounds)
97+
98+
99+
def generate_chunk(bounds: Bounds) -> Chunk:
100+
"""Generate a chunk of pixels for the given bounds."""
101+
chunk = Chunk(bounds)
102+
mandelbrot_set = MandelbrotSet(MAX_ITERATIONS, ESCAPE_RADIUS)
103+
for y, x in bounds:
104+
c = transform(y, x)
105+
instability = 1 - mandelbrot_set.stability(c, smooth=True)
106+
chunk[y, x] = int(instability * 255)
107+
return chunk
108+
109+
110+
def transform(y: int, x: int) -> complex:
111+
"""Transform the given pixel coordinates to the complex plane."""
112+
im = SCALE * (IMAGE_HEIGHT / 2 - y)
113+
re = SCALE * (x - IMAGE_WIDTH / 2)
114+
return complex(re, im) + CENTER
115+
116+
117+
def compute(worker: Callable) -> Image.Image:
118+
"""Render the image using the given worker function."""
119+
chunks = worker(split_multi(NUM_CHUNKS, IMAGE_HEIGHT, IMAGE_WIDTH))
120+
return combine(chunks)
121+
122+
123+
def combine(chunks: list[Chunk]) -> Image.Image:
124+
"""Combine the chunks into a single image."""
125+
pixels = np.zeros((IMAGE_HEIGHT, IMAGE_WIDTH), dtype=np.uint8)
126+
for chunk in chunks:
127+
pixels[chunk.bounds.slices()] = chunk.pixels
128+
return Image.fromarray(pixels, mode="L")
129+
130+
131+
if __name__ == "__main__":
132+
main()

python-split-list/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Pillow==9.3.0
2+
more-itertools==9.0.0
3+
numpy==1.23.5

python-split-list/splitting/__init__.py

Whitespace-only changes.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""
2+
Split a Python List Into a Fixed Number of Chunks With Roughly Equal Sizes
3+
"""
4+
5+
from itertools import zip_longest
6+
from typing import Any, Iterable, Iterator, Sequence
7+
8+
import numpy as np
9+
10+
11+
def split_with_numpy(array: np.array, num_chunks: int) -> Iterable[np.array]:
12+
return np.array_split(array, num_chunks)
13+
14+
15+
def split_transposed(
16+
sequence: Sequence[Any], num_chunks: int
17+
) -> Iterator[tuple[Any]]:
18+
for i in range(num_chunks):
19+
yield sequence[i::num_chunks]
20+
21+
22+
def transpose(chunks, padding: Any = None) -> Iterator[tuple[Any]]:
23+
return zip_longest(*chunks, value=padding)
24+
25+
26+
def split(sequence: Sequence[Any], num_chunks: int) -> Iterator[Sequence[Any]]:
27+
chunk_size, remaining = divmod(len(sequence), num_chunks)
28+
for i in range(num_chunks):
29+
begin = i * chunk_size + min(i, remaining)
30+
end = (i + 1) * chunk_size + min(i + 1, remaining)
31+
yield sequence[begin:end]
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""
2+
Split a Python List Into Fixed-Sized Chunks
3+
"""
4+
5+
import math
6+
import sys
7+
from itertools import islice, zip_longest
8+
from typing import Any, Iterable, Iterator, Sequence
9+
10+
import numpy as np
11+
12+
13+
def batched(
14+
iterable: Iterable[Any], max_chunk_size: int
15+
) -> Iterator[list[Any]]:
16+
if sys.version_info >= (3, 12):
17+
from itertools import batched
18+
19+
return batched(iterable, max_chunk_size)
20+
try:
21+
from more_itertools import batched
22+
23+
return batched(iterable, max_chunk_size)
24+
except ImportError:
25+
iterator = iter(iterable)
26+
while chunk := list(islice(iterator, max_chunk_size)):
27+
yield chunk
28+
29+
30+
def batched_functional(
31+
iterable: Iterable[Any], max_chunk_size: int
32+
) -> Iterator[list[Any]]:
33+
iterator = iter(iterable)
34+
return iter(lambda: list(islice(iterator, max_chunk_size)), [])
35+
36+
37+
def split_with_padding(
38+
sequence: Iterable[Any], chunk_size: int, padding: Any = None
39+
) -> Iterable[tuple[Any]]:
40+
return zip_longest(*[iter(sequence)] * chunk_size, fillvalue=padding)
41+
42+
43+
def split_drop_last(
44+
sequence: Sequence[Any], chunk_size: int
45+
) -> Iterator[Sequence[tuple[Any]]]:
46+
return zip(*[sequence[i::chunk_size] for i in range(chunk_size)])
47+
48+
49+
def split_strings(
50+
sequence: Sequence[Any], max_chunk_size: int
51+
) -> Iterator[Sequence[Any]]:
52+
for i in range(0, len(sequence), max_chunk_size):
53+
yield sequence[i : i + max_chunk_size]
54+
55+
56+
def split_into_slices(
57+
sequence: Sequence[Any], max_chunk_size: int
58+
) -> Iterator[slice]:
59+
for i in range(0, len(sequence), max_chunk_size):
60+
yield slice(i, i + max_chunk_size)
61+
62+
63+
def split_list_with_padding(
64+
sequence: Sequence[Any], chunk_size: int, fill_with: Any
65+
) -> Iterable[Sequence[Any]]:
66+
for i in range(0, len(sequence), chunk_size):
67+
chunk = list(sequence[i : i + chunk_size])
68+
padding = [fill_with] * (chunk_size - len(chunk))
69+
yield chunk + padding
70+
71+
72+
def split_with_numpy(array: np.array, chunk_size: int) -> Iterable[np.array]:
73+
return np.array_split(array, math.ceil(len(array) / chunk_size))

0 commit comments

Comments
 (0)