- rtshkmr's digital garden/
- Readings/
- Books/
- Fluent Python: Clear, Concise, and Effective Programming – Luciano Ramalho/
- Chapter 18. with, match, and else Blocks/
Chapter 18. with, match, and else Blocks
Table of Contents
this chapter is about control flow structures that are especially powerful in python
magic of
withstatements and how the context manager gives safetythe magic of
matchstatements and how that is expressive for languages (including custom DSLs)
What’s New in This Chapter #
Context Managers and with Blocks #
context managers exist to control a
withstatementanalogous to
forstatements controlled by iteratorsMISCONCEPTIONS:
this is correct: a
finallyblock is always guaranteed to run, even if the try block has areturn,sys.exit()or an exception raised.I just never paid attention to this.
That’s why it’s good for cleanup: resource release / reverting or undoing temporary state changes
withblocks don’t define a new scope like how functions do, that’s why the names are accessible outside of the blocksyntax:
in
with open('mirror.py') as fp:,evaluating the expression after the
withgives the context manager object, i.e.open('mirror.py')the context manager object here is an instance of
TextIOWrapper, this is what theopen()function returns.the
__enter__method ofTextIOWrapperreturnsselfthe target variable is within the
asclause is bound to somethingthe
asclause is optionalthat something is the result returned by the
__enter__method of the context manager object (TextIOWrapper), which we determined wasself(i.e. the context manager instance)
for any reason, when the control flow exists the
withblock, then__exit__is called on the context manager object.This is NOT called on whatever that was returned by
__enter__and stored by the target variable.
example code Custom Context Manager for mirror
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""" A "mirroring" ``stdout`` context. While active, the context manager reverses text output to ``stdout``:: # tag::MIRROR_DEMO_1[] >>> from mirror import LookingGlass >>> with LookingGlass() as what: # <1> ... print('Alice, Kitty and Snowdrop') # <2> ... print(what) ... pordwonS dna yttiK ,ecilA YKCOWREBBAJ >>> what # <3> 'JABBERWOCKY' >>> print('Back to normal.') # <4> Back to normal. # end::MIRROR_DEMO_1[] This exposes the context manager operation:: # tag::MIRROR_DEMO_2[] >>> from mirror import LookingGlass >>> manager = LookingGlass() # <1> >>> manager # doctest: +ELLIPSIS <mirror.LookingGlass object at 0x...> >>> monster = manager.__enter__() # <2> >>> monster == 'JABBERWOCKY' # <3> eurT >>> monster 'YKCOWREBBAJ' >>> manager # doctest: +ELLIPSIS >... ta tcejbo ssalGgnikooL.rorrim< >>> manager.__exit__(None, None, None) # <4> >>> monster 'JABBERWOCKY' # end::MIRROR_DEMO_2[] The context manager can handle and "swallow" exceptions. # tag::MIRROR_DEMO_3[] >>> from mirror import LookingGlass >>> with LookingGlass(): ... print('Humpty Dumpty') ... x = 1/0 # <1> ... print('END') # <2> ... ytpmuD ytpmuH Please DO NOT divide by zero! >>> with LookingGlass(): ... print('Humpty Dumpty') ... x = no_such_name # <1> ... print('END') # <2> ... Traceback (most recent call last): ... NameError: name 'no_such_name' is not defined # end::MIRROR_DEMO_3[] """ # tag::MIRROR_EX[] import sys class LookingGlass: def __enter__(self): # <1> self.original_write = sys.stdout.write # <2> sys.stdout.write = self.reverse_write # <3> return 'JABBERWOCKY' # <4> def reverse_write(self, text): # <5> self.original_write(text[::-1]) def __exit__(self, exc_type, exc_value, traceback): # <6> sys.stdout.write = self.original_write # <7> if exc_type is ZeroDivisionError: # <8> print('Please DO NOT divide by zero!') return True # <9> # <10> NOTE: if exit returns None or any falsy value, any exception raised in the =with= block will be propagated. # end::MIRROR_EX[]enter and exit:
__enter__is called without any arguments other than the implicitself(which is the context manager instance)__exit__is called with 3 arguments:exc_typeexc_valuethe actual exception instancetraceback
these 3 args received by
selfare the same as what happens if we callsys.exc_info()in thefinallyblock of atry/finally. in the past, calling that was necessary to determine how to do the cleanup.
we can now do parenthesized context managers:
1 2 3 4 5 6with ( CtxManager1() as example1, CtxManager2() as example2, CtxManager3() as example3, ): ...thanks to a new parser from python 3.10 onwards
The contextlib Utilities #
- first-reach before writing custom context managers
- things that look useful:
using
@contextmanagerto build a context manager from a generator functionContextDecoratorto define class-based context managersthe async versions of all of them
Using @contextmanager #
just implement a generator with a single
yieldstatement that should produce whatever you want the__enter__method to returnthe
yieldsplits the function body into two parts:before
yield: gets executed at the beginning of thewithblock when interpreter calls__enter__after
yield: gets executed when__exit__is called at the end of the block
correct example:
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""" A "mirroring" ``stdout`` context manager. While active, the context manager reverses text output to ``stdout``:: # tag::MIRROR_GEN_DEMO_1[] >>> from mirror_gen import looking_glass >>> with looking_glass() as what: # <1> ... print('Alice, Kitty and Snowdrop') ... print(what) ... pordwonS dna yttiK ,ecilA YKCOWREBBAJ >>> what 'JABBERWOCKY' # end::MIRROR_GEN_DEMO_1[] This exposes the context manager operation:: # tag::MIRROR_GEN_DEMO_2[] >>> from mirror_gen import looking_glass >>> manager = looking_glass() # <1> >>> manager # doctest: +ELLIPSIS <contextlib._GeneratorContextManager object at 0x...> >>> monster = manager.__enter__() # <2> >>> monster == 'JABBERWOCKY' # <3> eurT >>> monster 'YKCOWREBBAJ' >>> manager # doctest: +ELLIPSIS >...x0 ta tcejbo reganaMtxetnoCrotareneG_.biltxetnoc< >>> manager.__exit__(None, None, None) # <4> False >>> monster 'JABBERWOCKY' # end::MIRROR_GEN_DEMO_2[] The context manager can handle and "swallow" exceptions. The following test does not pass under doctest (a ZeroDivisionError is reported by doctest) but passes if executed by hand in the Python 3 console (the exception is handled by the context manager): # tag::MIRROR_GEN_DEMO_3[] >>> from mirror_gen_exc import looking_glass >>> with looking_glass(): ... print('Humpty Dumpty') ... x = 1/0 # <1> ... print('END') # <2> ... ytpmuD ytpmuH Please DO NOT divide by zero! # end::MIRROR_GEN_DEMO_3[] >>> with looking_glass(): ... print('Humpty Dumpty') ... x = no_such_name # <1> ... print('END') # <2> ... Traceback (most recent call last): ... NameError: name 'no_such_name' is not defined """ # tag::MIRROR_GEN_EXC[] import contextlib import sys @contextlib.contextmanager def looking_glass(): original_write = sys.stdout.write def reverse_write(text): original_write(text[::-1]) sys.stdout.write = reverse_write msg = '' # <1> try: yield 'JABBERWOCKY' except ZeroDivisionError: # <2> msg = 'Please DO NOT divide by zero!' finally: sys.stdout.write = original_write # <3> if msg: print(msg) # <4> # end::MIRROR_GEN_EXC[]it’s unavoiadable to use the
try/exceptwhen using theyieldwhen using@contextmanagersince we never know what the users of the context managers will do.GOTCHA:
Generally,
if
__exit__returns truthy even if there’s an exception, then the exception is suppressed. If it’s not truthy, then the exception is propagated outHOWEVER, with
@contextmanager, the default behaviour is inverted. the__exit__method provided by the decorator assumes any exception sent into the generator is handled and should be suppressed.
- (flawed) example
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""" A "mirroring" ``stdout`` context manager. While active, the context manager reverses text output to ``stdout``:: # tag::MIRROR_GEN_DEMO_1[] >>> from mirror_gen import looking_glass >>> with looking_glass() as what: # <1> ... print('Alice, Kitty and Snowdrop') ... print(what) ... pordwonS dna yttiK ,ecilA YKCOWREBBAJ >>> what 'JABBERWOCKY' >>> print('back to normal') back to normal # end::MIRROR_GEN_DEMO_1[] This exposes the context manager operation:: # tag::MIRROR_GEN_DEMO_2[] >>> from mirror_gen import looking_glass >>> manager = looking_glass() # <1> >>> manager # doctest: +ELLIPSIS <contextlib._GeneratorContextManager object at 0x...> >>> monster = manager.__enter__() # <2> >>> monster == 'JABBERWOCKY' # <3> eurT >>> monster 'YKCOWREBBAJ' >>> manager # doctest: +ELLIPSIS >...x0 ta tcejbo reganaMtxetnoCrotareneG_.biltxetnoc< >>> manager.__exit__(None, None, None) # <4> False >>> monster 'JABBERWOCKY' # end::MIRROR_GEN_DEMO_2[] The decorated generator also works as a decorator: # tag::MIRROR_GEN_DECO[] >>> @looking_glass() ... def verse(): ... print('The time has come') ... >>> verse() # <1> emoc sah emit ehT >>> print('back to normal') # <2> back to normal # end::MIRROR_GEN_DECO[] """ # tag::MIRROR_GEN_EX[] import contextlib import sys @contextlib.contextmanager # <1> def looking_glass(): original_write = sys.stdout.write # <2> def reverse_write(text): # <3> original_write(text[::-1]) sys.stdout.write = reverse_write # <4> yield 'JABBERWOCKY' # <5> sys.stdout.write = original_write # <6> # end::MIRROR_GEN_EX[]
this is flawed because if an exception is raised in the body of the with block, the Python interpreter will catch it and raise it again in the yield expression inside looking_glass. But there is no error handling there, so the looking_glass generator will terminate without ever restoring the original sys.stdout.write method, leaving the system in an invalid state.
Cleanup not done if there’s an exception raised within the with block.
TRICK: generators decorated with it can also be used as decorators themselves.
happens because
@contextmanageris implemented with thecontextlib.ContextDecoratorclass.1 2 3 4 5@looking_glass() def verse(): print("the time has come") verse() # returns in reverseHere,
looking_glassdoes its job before and after the body of verse runs.
Pattern Matching in lis.py: A Case Study #
Scheme Syntax #
Imports and Types #
The Parser #
The Environment #
The REPL #
The Evaluator #
Procedure: A Class Implementing a Closure #
Using OR-patterns #
Do This, Then That: else Blocks Beyond if #
use cases: avoids the need to setup extra control flags or coding extra
ifstatementsraising pattern:
1 2 3 4 5for item in my_list: if item.flavor == 'banana': break else: raise ValueError('No banana flavor found!')keep the try blocks lean in
try/exceptThe body of the
tryblock should only have the statements that generate the expected exceptions.Instead of doing this:
1 2 3 4 5try: dangerous_call() after_call() except OSError: log('OSError...')We should do:
1 2 3 4 5 6try: dangerous_call() except OSError: log('OSError...') else: after_call()
This gives clarity.
tryblock is guarding against possible errors indangerous_call()and not inafter_call(). It’s also explicit thatafter_call()will only execute if no exceptions are raised in thetryblock.IDIOM:
try/exceptis NOT only for error handling, it can be used for control flow as well. E.g. duck typing type checks.Follows EAFP
EAFP
Easier to ask for forgiveness than permission. This common Python coding style assumes the existence of valid keys or attributes and catches exceptions if the assumption proves false. This clean and fast style is characterized by the presence of many try and except statements. The technique contrasts with the LBYL style common to many other languages such as C.
this contrasts LBYL:
LBYL
Look before you leap. This coding style explicitly tests for pre-conditions before making calls or lookups. This style contrasts with the EAFP approach and is characterized by the presence of many if statements. In a multi-threaded environment, the LBYL approach can risk introducing a race condition between “the looking” and “the leaping.” For example, the code, if key in mapping: return mapping[key] can fail if another thread removes key from mapping after the test, but before the lookup. This issue can be solved with locks or by using the EAFP approach.
elseblocks apply to most control flow constructs, they are closely related to each other but very different fromif/elseLANGUAGE_LIMITIATION: GOTCHA: the
elsekeyword is more of more of a “run this loop, then do that” instead of “Run this loop, otherwise do that”for:elseblock will run only if and when theforloop runs to completion (i.e.,notif the for is aborted with abreak).while:elseblock will run only if and when thewhileloop exits because the condition became falsy (i.e., not if thewhileis aborted with a break).try: Theelseblock will run only if no exception is raised in thetryblock.NOTE: “Exceptions in the else clause are not handled by the preceding except clauses.”
the
elseclause is also skipped if an exception or areturn,break, orcontinuestatement causes control to jump out of the main block of the compound statement.
Chapter Summary #
an insight:
subroutines are the most important invention in the history of computer languages. If you have sequences of operations like A;B;C and P;B;Q, you can factor out B in a subroutine. It’s like factoring out the filling in a sandwich: using tuna with different breads. But what if you want to factor out the bread, to make sandwiches with wheat bread, using a different filling each time? That’s what the with statement offers. It’s the complement of the subroutine.