# Effective Python 2E ### Chapter 4: Comprehensions and Generators [David Ye](https://dwye.dev/) @ Houzz --- ## Outline - [Comprehensions (Items 27-29)](#/2) - [Generators (Items 30-35)](#/3) - [`itertools` (Item 36)](#/4) --- ## Comprehensions ```python= > [x ** 3 for x in range(4)] [0, 1, 8, 27] > {x: x ** 3 for x in range(4)} {0: 0, 1: 1, 2: 8, 3: 27} > {x for x in range(10) if x % 3 != 0} {1, 2, 4, 5, 7, 8} ``` ---- ### 27: Use Comprehensions Instead of `map` and `filter` ```python= map(lambda x: x ** 2, filter(lambda x: x % 2 == 0, other_list)) [x ** 2 for x in other_list if x % 2 == 0] # [0, 4, 16, 36, 64, 100, 144, 196, 256, 324] ``` - more clear - easily skip items w/o `filter` - Dictionaries / Sets ---- Multiple conditions at the same loop level have an implicit `and` expression. ```python= a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] > [x for x in a if x > 4 if x % 2 == 0] [6, 8, 10] > [x for x in a if x > 4 and x % 2 == 0] [6, 8, 10] ``` ---- ### 28: Avoid More Than Two Control Subexpressions in Comprehensions ```python= [[x for x in row if x % 3 == 0] for row in matrix if sum(row) >= 10] ``` \>= 2 control subexpressions: difficult to read. ```python= alt = [] for row in matrix: if sum(row) >= 10: alt.append( [x for x in row if x % 3 == 0] ) ``` ---- ### 29: Avoid Repeated Work in Comprehensions by Using Assignment Expressions Assignment Expressions / Walrus Operator `:=` ```python= stock = { 'nails': 125, 'screws': 35, 'wingnuts': 8, 'washers': 24, } > { > name: tenth for name, count in stock.items() > if (tenth := count // 10) > 10 > } {'nails': 12} ``` ---- `:=` will **leak** the loop variable into the containing scope ```python= > half = [(last := count // 2) for count in stock.values()] > f'Last item of {half} is {last}' 'Last item of [62, 17, 4, 12] is 12' ``` `for` loop also leak the variable: ```python= > for count in stack.values(): > pass > f'last item of {list(stock.values())} is {count}' 'last item of [125, 35, 8, 24] is 24' ``` but not for the `for` statement in comprehension: ```python= > half = [count // 2 for count in stock.values()] > f'last item of {stack} is {count}' # NameError: name 'count' is not defined ``` - recommend using assignment expressions only in the condition part --- ## Generators a generator function returns generator iterator when called ```python= def range(n): i = 0 while i < n: yield i i += 1 for i in range(10): print(i) # 0 1 2 3 4 ... 9 print(range(3)) # <generator object range at 0x1088aae40> ``` ---- ### 30: Consider Generators Instead of Returning Lists ```python= def index_words(text): result = [] if text: result.append(0) for index, letter in enumerate(text): if letter == ' ': result.append(index + 1) return result ``` ```python= def index_words_iter(text): if text: yield 0 for index, letter in enumerate(text): if letter == ' ': yield index + 1 > index_words_iter('Four score and seven years ago...') [0, 5, 11, 15, 21, 27] ``` - bounded memory requirements - for infinite generators - works with `itertools` ---- ### 31: Be Defensive When Iterating Over Arguments iterator are stateful, can only be iterated once: - convert to `list` - construct another iterator - check input: ```python= def defensive_iteration(container): if iter(container) is container: # iterator.__iter__ returns itself raise TypeError('Must apply a container') ``` ```python= from collections.abc import Iterator # Abstract Base Classes def normalize_defensive2(numbers): if isinstance(numbers, Iterator): raise TypeError('Must apply a container') ``` ---- ### 32: Consider Generator Expressions for Large List Comprehensions ```python= [len(x) for x in open('my_file.txt')] # list (len(x) for x in open('my_file.txt')) # generator expression ``` ---- ### 33: Compose Multiple Generators with `yield from` `yield from <other generator>` iterate and yield results in that generator ```python= def move(period, speed): for _ in range(period): yield speed def pause(delay): for _ in range(delay): yield 0 def animate_composed(): yield from move(4, 5.0) yield from pause(3) yield from move(2, 3.0) ``` ```python= > run(animate_composed) Delta: 5.0 Delta: 5.0 Delta: 5.0 Delta: 5.0 Delta: 0.0 Delta: 0.0 Delta: 0.0 Delta: 3.0 Delta: 3.0 ``` <aside class="notes"> yield from 可以從別的 generator 抓結果,該 generator 結束後,才往下走<br/> rendor 藏起來了,總之就是會調控速度 </aside> ---- ### 34: Avoid Injecting Data into Generators with `send` `send` upgrades `yield` into a 2-way channel ```python= # send.py def my_generator(): received = yield 1 print(f'received: {received}') it = my_generator() output = it.send(None) # 1st valid value of `send` is `None` print(f'output: {output}') try: while (output := it.send('hello')): print('will not come here') except StopIteration: pass ``` ``` $ python send.py output: 1 received: hello ``` <aside class="notes"> 我猜八成人不知道 send.... 這邊就是要先跟你說,有這種用法,然後叫你不要用 XD<br /> 第一個 send 必須要給 None,因為 generator 剛初始化時,還沒有碰到任何的 yield,所以無法接收變數<br /> 第一個 send 會讓 generator 跑到第一個 yield,而第二次 send 才會傳給第一次 yield<br /> 沒有 send 直接 next() ,就當拿到 None </aside> ---- with composed generator: unexcepted `None`? ```python= def step_mul(steps): amplitude = yield for step in range(steps): amplitude = yield amplitude * step def complex_step_mul(): yield from step_mul(3) yield from step_mul(4) def run_modulating(it): amplitudes = [None, 7, 7, 7, 2, 2, 2, 2] for amplitude in amplitudes: output = it.send(amplitude) print(f'Output: {output}') ``` ```python= > run_modulating(complex_step_mul()) Output: None Output: 0 Output: 7 Output: 14 Output: None # ?? Output: 0 Output: 2 Output: 4 ``` <aside class="notes"> surprise! compose 後還是會出現 none </aside> ---- solution: pass another "amplitude" iterator to generator function: ```python= def step_mul(amplitude_it, steps): for step in range(steps): amplitude = next(amplitude_it) yield amplitude * step def complex_step_mul(amplitude_it): yield from step_mul(amplitude_it, 3) yield from step_mul(amplitude_it, 4) def run_cascading(): amplitudes = [7, 7, 7, 2, 2, 2, 2] it = complex_step_mul(iter(amplitudes)) for _ in amplitudes: output = next(it) print(f'Output: {output}') ``` ```python= > run_cascading() Output: 0 Output: 7 Output: 14 Output: 0 Output: 2 Output: 4 Output: 6 ``` <aside class="notes"> 用另一個 iterator 來做雙向的 channel </aside> ---- ### 35: Avoid Causing State Transitions in Generators with `throw` `iterator.throw()` raise exception on next yield ```python= class MyError(Exception): pass def my_generator(): yield 1 yield 2 yield 3 ``` ```python= it = my_generator() next(it) # Yield 1 next(it) # Yield 2 it.throw(MyError('test error')) # MyError: test error ``` ---- catch exceptions to make state transition: (harder to read) ```python= def timer(period): current = period while current:How current -= 1 try: yield current except Reset: current = period ``` instead, write a container class with `__iter__`: ```python= class Timer: def __init__(self, period): self.current = period self.period = period # reset timer by call reset method def reset(self): self.current = self.period def __iter__(self): while self.current: self.current -= 1 yield self.current ``` <aside class="notes"> 可以 try catch 抓 error type 做 state transition 但不要這樣做 XD </aside> --- ## `itertools` ![](https://i.imgur.com/ypvAi5x.png) https://docs.python.org/3/library/itertools.html ### 36: Consider `itertools` for Working with Iterators and Generators ---- ### Linking iterators ```python= it1 = [1, 2, 3, 4] it2 = [5, 6, 7, 8] it3 = [5, 6, 7] ``` ```python= > itertools.chain(it1, it2) [1, 2, 3, 4, 5, 6, 7, 8] > itertools.repeat('hello', 3) ['hello', 'hello', 'hello'] # cycle: repeat itself forever > it = itertools.cycle(it1) > [next(it) for _ in range(9)] [1, 2, 3, 4, 1, 2, 3, 4, 1] ``` ```python= # zip_longest: zip with padding > zip(it1, it3) [(1, 5), (2, 6), (3, 7)] > itertools.zip_longest(it1, it3, fillvalue='NA') [(1, 5), (2, 6), (3, 7), (4, 'NA')] # tee: parallel iterators > it4, it5 = itertools.tee(it1, 2) > it4 [1, 2, 3, 4] > it5 [1, 2, 3, 4] ``` (all `list` are dropped) ---- ### Filtering items ```python= values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ``` ```python= # islice > itertools.islice(values, 5) # end (inclusive!) [1, 2, 3, 4, 5] > itertools.islice(values, 2, 4) # start + end [3, 4] > itertools.islice(values, 1, 7, 2) # start + end + step [2, 4, 6] ``` ```python= # takewhile / dropwhile > itertools.takewhile(lambda x: x < 4, values) [1, 2, 3] > itertools.dropwhile(lambda x: x > 4, values) [4, 5, 6, 7, 8, 9, 10] # filterfalse > filter(lambda x: x % 3 == 0, values) [3, 6, 9] > itertools.filterfalse(lambda x: x % 3 == 0, values) [1, 2, 4, 5, 7, 8, 10] ``` ---- ### Producing combinations of items from iterators ```python= # accumulate > itertools.accumulate(values) [1, 3, 6, 10, 15, 21, 28, 36, 45, 55] > itertools.accumulate(values, lambda x, y: (x + y) % 20) [1, 3, 6, 10, 15, 1, 8, 16, 5, 15] # product: Cartesian product > it1 = 'abc' > it2 = [1, 2] > itertools.product(it1, it2) [('a', 1), ('a', 2), ('b', 1), ('b', 2), ('c', 1), ('c', 2)] > itertools.product(it2, repeat=3) # self prodcut [(1, 1, 1), (1, 1, 2), (1, 2, 1), (1, 2, 2), (2, 1, 1), (2, 1, 2), (2, 2, 1), (2, 2, 2)] ``` ```python= # Permutations and Combinations > itertools.permutations(it1, 2) [('a', 'b'), ('a', 'c'), ('b', 'a'), ('b', 'c'), ('c', 'a'), ('c', 'b')] > itertools.combinations(it1, 2) [('a', 'b'), ('a', 'c'), ('b', 'c')] > itertools.combinations_with_replacement(it1, 2) [('a', 'a'), ('a', 'b'), ('a', 'c'), ('b', 'b'), ('b', 'c'), ('c', 'c')] ``` <aside class="notes"> 排列組合三大天王 P / C / H </aside> --- # The End Thanks for listening! - [back to outline](#/1)
{"metaMigratedAt":"2023-06-16T19:51:26.866Z","metaMigratedFrom":"YAML","title":"Effective Python Chp 4: Comprehensions and Generators","breaks":true,"slideOptions":"{\"height\":1000,\"width\":1500,\"theme\":\"white\"}","contributors":"[{\"id\":\"915f29e1-3f9c-4908-bbd4-a58795589e48\",\"add\":11125,\"del\":706}]"}
    310 views