-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path27_generators_tutorial.py
More file actions
306 lines (243 loc) · 7.28 KB
/
27_generators_tutorial.py
File metadata and controls
306 lines (243 loc) · 7.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# Generators - functions that return an iterator using yield
# Generators are lazy - they create values on demand, not all at once
# ========== BASIC GENERATORS ==========
# 1. Simple generator
def count_up_to(n):
count = 1
while count <= n:
yield count # yield returns a value and pauses execution
count += 1
# Generator creates an iterator
counter = count_up_to(5)
print(f"Type: {type(counter)}") # <class 'generator'>
# Iterating over generator
for num in count_up_to(5):
print(num) # 1, 2, 3, 4, 5
# 2. Comparison: list vs generator
# List - created all at once (takes memory)
def numbers_list(n):
result = []
for i in range(n):
result.append(i)
return result
# Generator - creates elements on demand (saves memory!)
def numbers_generator(n):
for i in range(n):
yield i
# For large data, generator is much more efficient
big_list = numbers_list(1000000) # Creates a list of 1M elements in memory
big_gen = numbers_generator(1000000) # Creates only an iterator (low memory)
# 3. Generator is exhausted after one pass
gen = count_up_to(3)
print(list(gen)) # [1, 2, 3]
print(list(gen)) # [] - generator exhausted!
# 4. next() - get next value
gen = count_up_to(3)
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
# print(next(gen)) # StopIteration - no more elements
# ========== GENERATOR EXAMPLES ==========
# 5. Fibonacci generator
def fibonacci(n):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
for num in fibonacci(10):
print(num, end=" ")
print()
# 6. Infinite generator
def infinite_sequence():
num = 0
while True:
yield num
num += 1
# Use with limit
gen = infinite_sequence()
for i in range(5):
print(next(gen)) # 0, 1, 2, 3, 4
# 7. Even numbers generator
def even_numbers(max_num):
num = 0
while num <= max_num:
yield num
num += 2
for num in even_numbers(10):
print(num) # 0, 2, 4, 6, 8, 10
# 8. Generator for reading large file
def read_large_file(file_path):
"""Reads file line by line (doesn't load entire file into memory)"""
with open(file_path, 'r') as file:
for line in file:
yield line.strip()
# Usage:
# for line in read_large_file('big_file.txt'):
# print(line)
# 9. Countdown generator
def countdown(n):
while n > 0:
yield n
n -= 1
for num in countdown(5):
print(num) # 5, 4, 3, 2, 1
# ========== GENERATOR EXPRESSIONS ==========
# 10. Generator expression (analogous to list comprehension)
# List comprehension - creates a list
squares_list = [x**2 for x in range(5)]
print(f"List: {squares_list}")
# Generator expression - creates a generator
squares_gen = (x**2 for x in range(5))
print(f"Generator: {squares_gen}")
print(f"Elements: {list(squares_gen)}")
# 11. Advantage of generator expression
# Memory: lists take a lot of memory
big_list = [x for x in range(1000000)]
# Generators save memory
big_gen = (x for x in range(1000000))
# For operations that don't require the entire list at once
total = sum(x for x in range(1000000)) # Efficient!
print(f"Sum: {total}")
# ========== YIELD FROM ==========
# 12. yield from - delegate to another generator
def generator1():
yield 1
yield 2
def generator2():
yield 3
yield 4
def combined_generator():
yield from generator1()
yield from generator2()
for num in combined_generator():
print(num) # 1, 2, 3, 4
# 13. Flatten (flat list) with yield from
def flatten(nested_list):
for item in nested_list:
if isinstance(item, list):
yield from flatten(item) # Recursively
else:
yield item
nested = [1, [2, 3, [4, 5]], 6, [7, 8]]
flat = list(flatten(nested))
print(f"Flat list: {flat}")
# ========== SENDING DATA TO GENERATOR ==========
# 14. Generator as coroutine (send)
def echo_generator():
while True:
received = yield # Receive value through send()
print(f"Received: {received}")
gen = echo_generator()
next(gen) # Start generator until first yield
gen.send("Hello")
gen.send("World")
# 15. Generator with bidirectional communication
def running_average():
total = 0
count = 0
average = None
while True:
value = yield average # Return average and receive value
total += value
count += 1
average = total / count
avg_gen = running_average()
next(avg_gen) # Start generator
print(avg_gen.send(10)) # 10.0
print(avg_gen.send(20)) # 15.0
print(avg_gen.send(30)) # 20.0
# ========== PRACTICAL EXAMPLES ==========
# 16. Range generator with step
def custom_range(start, stop, step=1):
current = start
while current < stop:
yield current
current += step
for num in custom_range(0, 10, 2):
print(num) # 0, 2, 4, 6, 8
# 17. Permutations generator
def permutations(items):
if len(items) <= 1:
yield items
else:
for i, item in enumerate(items):
for perm in permutations(items[:i] + items[i+1:]):
yield [item] + perm
for perm in permutations([1, 2, 3]):
print(perm)
# 18. Sliding window generator
def sliding_window(iterable, size):
from collections import deque
window = deque(maxlen=size)
for item in iterable:
window.append(item)
if len(window) == size:
yield list(window)
for window in sliding_window([1, 2, 3, 4, 5, 6], 3):
print(window)
# [1, 2, 3]
# [2, 3, 4]
# [3, 4, 5]
# [4, 5, 6]
# 19. Batches generator
def batches(iterable, batch_size):
batch = []
for item in iterable:
batch.append(item)
if len(batch) == batch_size:
yield batch
batch = []
if batch: # Remainder
yield batch
for batch in batches(range(10), 3):
print(batch)
# [0, 1, 2]
# [3, 4, 5]
# [6, 7, 8]
# [9]
# 20. Prime numbers generator
def primes():
"""Infinite prime numbers generator"""
num = 2
while True:
is_prime = True
for i in range(2, int(num ** 0.5) + 1):
if num % i == 0:
is_prime = False
break
if is_prime:
yield num
num += 1
# First 10 prime numbers
prime_gen = primes()
first_10_primes = [next(prime_gen) for _ in range(10)]
print(f"First 10 primes: {first_10_primes}")
# 21. Generator chains
from itertools import chain
def chain_generators(*generators):
for gen in generators:
yield from gen
gen1 = (x for x in range(3))
gen2 = (x for x in range(3, 6))
gen3 = (x for x in range(6, 9))
for num in chain_generators(gen1, gen2, gen3):
print(num) # 0, 1, 2, 3, 4, 5, 6, 7, 8
# ========== WHEN TO USE GENERATORS ==========
# ✅ Use generators when:
# - Working with large amounts of data (files, databases)
# - Need lazy evaluation (not all data at once)
# - Creating infinite sequences
# - Want to save memory
# - Data is processed once (single-pass iteration)
# ❌ DON'T use generators when:
# - Need multiple access to data (generator gets exhausted)
# - Need indexing or slicing (gen[0] doesn't work)
# - Need len() (generators don't have length)
# - Data is small and fits in memory
# ========== PERFORMANCE ==========
# Generators vs lists (memory)
import sys
list_comp = [x for x in range(10000)]
gen_exp = (x for x in range(10000))
print(f"List size: {sys.getsizeof(list_comp)} bytes")
print(f"Generator size: {sys.getsizeof(gen_exp)} bytes")