Skip to content

Commit c9060f4

Browse files
committed
How to Split a Python List - Update materials
1 parent fe630fb commit c9060f4

File tree

8 files changed

+172
-162
lines changed

8 files changed

+172
-162
lines changed

python-split-list/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
# How to Split a Python List
1+
# How to Split a Python List Into Chunks
22

3-
This folder holds the code for the Real Python How to Split a Python List tutorial.
3+
This folder holds sample code that supplements the Real Python tutorial on [How to Split a Python List Into Chunks](https://realpython.com/how-to-split-a-python-list-into-chunks/).
Lines changed: 48 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
11
"""
2-
Plot the Mandelbrot set using parallel workers.
2+
Synthesize an image in chunks using parallel workers.
33
44
Usage:
5-
$ python parallelization.py
5+
$ python parallel_demo.py
66
"""
77

88
import functools
99
import multiprocessing
10-
import os
1110
import time
1211
from dataclasses import dataclass
1312
from math import log
14-
from typing import Callable, Iterable
13+
from os import cpu_count
14+
from typing import Callable, Iterable, Iterator
1515

1616
import numpy as np
1717
from PIL import Image
1818

19-
from splitting.multidimensional import Bounds, split_multi
19+
from spatial_splitting import Bounds, split_multi
2020

21-
IMAGE_WIDTH, IMAGE_HEIGHT = 1280, 720
21+
IMAGE_WIDTH, IMAGE_HEIGHT = 1920, 1080
2222
CENTER = -0.7435 + 0.1314j
23-
SCALE = 0.0000025
23+
SCALE = 0.0000015
2424
MAX_ITERATIONS = 256
2525
ESCAPE_RADIUS = 1000
26-
NUM_CHUNKS = os.cpu_count() or 4
26+
NUM_CHUNKS = cpu_count() or 4
2727

2828

2929
class Chunk:
@@ -35,12 +35,10 @@ def __init__(self, bounds: Bounds) -> None:
3535
self.width = bounds.size[1]
3636
self.pixels = np.zeros((self.height, self.width), dtype=np.uint8)
3737

38-
def __getitem__(self, coordinates) -> int:
39-
"""Get the value of a pixel at the given absolute coordinates."""
38+
def __getitem__(self, coordinates: tuple[int, int]) -> int:
4039
return self.pixels[self.bounds.offset(*coordinates)]
4140

42-
def __setitem__(self, coordinates, value: int) -> None:
43-
"""Set the value of a pixel at the given absolute coordinates."""
41+
def __setitem__(self, coordinates: tuple[int, int], value: int) -> None:
4442
self.pixels[self.bounds.offset(*coordinates)] = value
4543

4644

@@ -49,14 +47,14 @@ class MandelbrotSet:
4947
max_iterations: int
5048
escape_radius: float = 2.0
5149

52-
def __contains__(self, c: complex) -> bool:
50+
def __contains__(self, c):
5351
return self.stability(c) == 1
5452

55-
def stability(self, c: complex, smooth=False, clamp=True) -> float:
53+
def stability(self, c, smooth=False, clamp=True):
5654
value = self.escape_count(c, smooth) / self.max_iterations
5755
return max(0.0, min(value, 1.0)) if clamp else value
5856

59-
def escape_count(self, c: complex, smooth=False) -> int | float:
57+
def escape_count(self, c, smooth=False):
6058
z = 0 + 0j
6159
for iteration in range(self.max_iterations):
6260
z = z**2 + c
@@ -67,6 +65,32 @@ def escape_count(self, c: complex, smooth=False) -> int | float:
6765
return self.max_iterations
6866

6967

68+
def transform(y: int, x: int) -> complex:
69+
"""Transform the given pixel coordinates to the complex plane."""
70+
im = SCALE * (IMAGE_HEIGHT / 2 - y)
71+
re = SCALE * (x - IMAGE_WIDTH / 2)
72+
return complex(re, im) + CENTER
73+
74+
75+
def generate_chunk(bounds: Bounds) -> Chunk:
76+
"""Generate a chunk of pixels for the given bounds."""
77+
chunk = Chunk(bounds)
78+
mandelbrot_set = MandelbrotSet(MAX_ITERATIONS, ESCAPE_RADIUS)
79+
for y, x in bounds:
80+
c = transform(y, x)
81+
instability = 1 - mandelbrot_set.stability(c, smooth=True)
82+
chunk[y, x] = int(instability * 255)
83+
return chunk
84+
85+
86+
def combine(chunks: Iterable[Chunk]) -> Image.Image:
87+
"""Combine the chunks into a single image."""
88+
pixels = np.zeros((IMAGE_HEIGHT, IMAGE_WIDTH), dtype=np.uint8)
89+
for chunk in chunks:
90+
pixels[chunk.bounds.slices()] = chunk.pixels
91+
return Image.fromarray(pixels, mode="L")
92+
93+
7094
def timed(function: Callable) -> Callable:
7195
@functools.wraps(function)
7296
def wrapper(*args, **kwargs):
@@ -79,53 +103,23 @@ def wrapper(*args, **kwargs):
79103
return wrapper
80104

81105

82-
def main() -> None:
83-
for worker in (process_chunks_parallel, process_chunks_sequential):
84-
image = compute(worker)
85-
image.show()
86-
106+
def process_sequentially(bounds_iter: Iterator[Bounds]) -> Iterator[Chunk]:
107+
return map(generate_chunk, bounds_iter)
87108

88-
@timed
89-
def process_chunks_sequential(chunked_bounds: Iterable[Bounds]) -> list[Chunk]:
90-
return list(map(generate_chunk, chunked_bounds))
91109

92-
93-
@timed
94-
def process_chunks_parallel(chunked_bounds: Iterable[Bounds]) -> list[Chunk]:
110+
def process_in_parallel(bounds_iter: Iterator[Bounds]) -> list[Chunk]:
95111
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
112+
return pool.map(generate_chunk, bounds_iter)
115113

116114

115+
@timed
117116
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)
117+
return combine(worker(split_multi(NUM_CHUNKS, IMAGE_HEIGHT, IMAGE_WIDTH)))
121118

122119

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")
120+
def main() -> None:
121+
for worker in (process_sequentially, process_in_parallel):
122+
compute(worker).show()
129123

130124

131125
if __name__ == "__main__":

python-split-list/requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
Pillow==9.3.0
1+
Pillow==9.4.0
22
more-itertools==9.0.0
3-
numpy==1.23.5
3+
numpy==1.24.1
File renamed without changes.

python-split-list/splitting.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""
2+
Split into fixed-size chunks or a fixed number of chunks.
3+
"""
4+
5+
import sys
6+
from itertools import cycle, islice, zip_longest
7+
from typing import Any, Iterable, Iterator, Sequence
8+
9+
import numpy as np
10+
11+
if sys.version_info >= (3, 12):
12+
from itertools import batched
13+
else:
14+
try:
15+
from more_itertools import batched
16+
except ImportError:
17+
18+
def batched(
19+
iterable: Iterable[Any], chunk_size: int
20+
) -> Iterator[tuple[Any, ...]]:
21+
iterator = iter(iterable)
22+
while chunk := tuple(islice(iterator, chunk_size)):
23+
yield chunk
24+
25+
26+
def batched_with_padding(
27+
iterable: Iterable[Any], batch_size: int, fill_value: Any = None
28+
) -> Iterator[tuple[Any, ...]]:
29+
for batch in batched(iterable, batch_size):
30+
yield batch + (fill_value,) * (batch_size - len(batch))
31+
32+
33+
def batched_functional(
34+
iterable: Iterable[Any], chunk_size: int
35+
) -> Iterator[tuple[Any, ...]]:
36+
iterator = iter(iterable)
37+
return iter(lambda: tuple(islice(iterator, chunk_size)), tuple())
38+
39+
40+
def split_with_padding(
41+
iterable: Iterable[Any], chunk_size: int, fill_value: Any = None
42+
) -> Iterator[tuple[Any, ...]]:
43+
return zip_longest(*[iter(iterable)] * chunk_size, fillvalue=fill_value)
44+
45+
46+
def split_into_pairs(
47+
iterable: Iterable[Any], fill_value: Any = None
48+
) -> Iterator[tuple[Any, Any]]:
49+
iterator = iter(iterable)
50+
return zip_longest(iterator, iterator, fillvalue=fill_value)
51+
52+
53+
def split_drop_last(
54+
iterable: Iterable[Any], chunk_size: int
55+
) -> Iterator[tuple[Any, ...]]:
56+
return zip(*[iter(iterable)] * chunk_size)
57+
58+
59+
def split_sequence(
60+
sequence: Sequence[Any], chunk_size: int
61+
) -> Iterator[Sequence[Any]]:
62+
for i in range(0, len(sequence), chunk_size):
63+
yield sequence[i : i + chunk_size]
64+
65+
66+
def split_str(
67+
sequence: str, chunk_size: int, fill_value: str = " "
68+
) -> Iterator[str]:
69+
for chunk in split_sequence(sequence, chunk_size):
70+
padding = "".join([fill_value] * (chunk_size - len(chunk)))
71+
yield chunk + padding
72+
73+
74+
def split_list(
75+
sequence: list[Any], chunk_size: int, fill_value: Any = None
76+
) -> Iterator[list[Any]]:
77+
for chunk in split_sequence(sequence, chunk_size):
78+
padding = [fill_value] * (chunk_size - len(chunk))
79+
yield chunk + padding
80+
81+
82+
def split_into_slices(
83+
sequence: Sequence[Any], slice_size: int
84+
) -> Iterator[slice]:
85+
for i in range(0, len(sequence), slice_size):
86+
yield slice(i, i + slice_size)
87+
88+
89+
def split_strides(
90+
sequence: Sequence[Any], num_chunks: int
91+
) -> Iterator[Sequence[Any]]:
92+
for i in range(num_chunks):
93+
yield sequence[i::num_chunks]
94+
95+
96+
def split_round_robin(
97+
iterable: Iterable[Any], num_chunks: int
98+
) -> list[list[Any]]:
99+
chunks: list[list[Any]] = [list() for _ in range(num_chunks)]
100+
index = cycle(range(num_chunks))
101+
for value in iterable:
102+
chunks[next(index)].append(value)
103+
return chunks
104+
105+
106+
def split_n(
107+
sequence: Sequence[Any], num_chunks: int
108+
) -> Iterator[Sequence[Any]]:
109+
chunk_size, remaining = divmod(len(sequence), num_chunks)
110+
for i in range(num_chunks):
111+
begin = i * chunk_size + min(i, remaining)
112+
end = (i + 1) * chunk_size + min(i + 1, remaining)
113+
yield sequence[begin:end]
114+
115+
116+
def split_with_numpy(
117+
numbers: Sequence[float | int], chunk_size: int
118+
) -> list[np.ndarray]:
119+
indices = np.arange(chunk_size, len(numbers), chunk_size)
120+
return np.array_split(numbers, indices)

python-split-list/splitting/__init__.py

Whitespace-only changes.

python-split-list/splitting/fixednumber.py

Lines changed: 0 additions & 31 deletions
This file was deleted.

0 commit comments

Comments
 (0)