Fluent Python
Table of Contents
1 Philosophy
- EAFP(python way)
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.
- LBYL(contrast with EAFP)
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.
2 Data Model
2.1 Special Methods
| Category | Method names |
|---|---|
| String/bytes representation | __repr__ , __str__ , __format__ , __bytes__ |
| Conversion to number | __abs__, __bool__, __complex__, __int__, __float__, __hash__, __index__ |
| Emulating collections | __len__, __getitem__, __setitem__, __delitem__, __contains__ |
| Iteration | __iter__, __reversed__, __next__ |
| Emulating callables | __call__ |
| Context management | __enter__, __exit__ |
| Instance creation and destruction | __new__, __init__, __del__ |
| Attribute management | __getattr__, __getattribute__, __setattr__, __delattr__, __dir__ |
| Attribute descriptors | __get__, __set__, __delete__ |
| Class services | __prepare__, __instancecheck__, __subclasscheck__ |
- operators
| Category | Method names |
|---|---|
| Unary numeric operators | __neg__ -, __pos__ +, __abs__ abs() |
| Rich comparison operators | __lt__ >, __le__ <=, __eq__ ==, __ne__ !=, __gt__ >, __ge__ >= |
| Arithmetic operators | __add__ +, __sub__ -, __mul__ =, =__truediv__ /, __floordiv__ //, __mod__\n%, __divmod__ divmod(), __pow__ ** or pow(), __round__ round() |
| Reversed arithmetic operators | __radd__, __rsub__, __rmul__, __rtruediv__, __rfloordiv__, __rmod__, __rdivmod__, __rpow__ |
| Augmented assignment arithmetic operators | __iadd__, __isub__, __imul__, __itruediv__, __ifloordiv__, __imod__, __ipow__ |
| Bitwise operators | __invert__ ~, __lshift__ <<, __rshift__ >>, __and__ &, __or__, __xor__ ^ |
| Reversed bitwise operators | __rlshift__, __rrshift__, __rand__, __rxor__, __ror__ |
| Augmented assignment bitwise operators | __ilshift__, __irshift__, __iand__, __ixor__, __ior__ |
2.2 numeric types
- decimal.Decimal
- fractions.Fraction
2.3 Sequence categories
2.3.1 Flat(hold one type)
str, bytes, bytearray, memoryview, array.array
- If the
listwill only contain numbers,array.arrayis more efficient than alist
2.3.2 Mutable
list, bytearray, array.array, collections.deque, memoryview
2.3.3 Immutable
tuple, str, bytes
2.3.4 abcs
2.4 Collection Hierarchy
interface Container { {method} __contains__ } interface Iterable { {method} __iter__ } interface Sized { {method} __len__ } interface Sequence { {method} __getitem__ {method} __reversed__ {method} index {method} count } Container <|-- Sequence Iterable <|-- Sequence Sized <|-- Sequence
3 Metaprogramming
3.1 Monkey Patching
Changing a class or module at runtime, without touching the source code.
some_obj.__getitem__ = some_get_function(some_class, position)
4 Iterator
4.1 Compare with Iterable
- Iterator is a Iterable, they both have a
__iter__method - In addition, Iterator has a
__next__method - Iterator
__iter__often return itself - To support multiple traversals of aggregate objects
5 Functional Programming
5.1 Filtering
- built-in
filter filterfalse(predicate, it)islice(it, stop) or islice(it, start, stop, step=1)compress(it, selector_it): Consumes two iterables in parallel; yields items from it whenever the corresponding item in selector_it is truthydropwhile(predicate, it): Consumes it skipping items while predicate computes truthy, then yields every remaining itemtakewhile(predicate, it): Yields items while predicate computes truthy, then stops
5.2 Mapping
- built-in
enumerate(iterable, start=0) - built-in
map(func, it1, [it2, …, itN]) accumulate(it, [func]): Yields accumulated sums(default); if func is provided, yields the result of applying it to the first pair of items, then to the first result and next itemstarmap(func, it): Applies func to each item of it, yielding the result; the input iterable should yield iterable items iit, and func is applied as func(*iit)
5.3 Merging Multiple Inputs
chain(it1, …, itN)chain.from_iterable(it): Yield all items from each iterable produced by it, one after the other, seamlesslyproduct(it1, …, itN, repeat=1): Cartesian product; yields N-tuples made by combining items from each input iterable like nested for loops could produce; repeat allows the input iterables to be consumed more than once.- (built-in)
zip(it1, …, itN)~: Silently stopping when the first iterable is exhausted zip_longest(it1, …, itN, fillvalue=None): Stopping only when the last iterable is exhausted, filling the blanks with the fillvalue
5.4 Rearranging
groupby(it, key=None): Yields 2-tuples of the form (key, group)reversed(seq)tee(it, n=2): Yields multiple generators from a single input iterable
5.5 Reducing
- built-in
all(it) - built-in
any(it) - built-in
max(it, [key=,] [default=]) - built-in
min(it, [key=,] [default=]) - functools
reduce(func, it, [initial]) - built-in
sum(it, start=0)
5.6 Others
count(start=0, step=1): indefinitelydef aritprog_gen(begin, step, end=None): first = type(begin + step)(begin) ap_gen = itertools.count(first, step) if end is not None: ap_gen = itertools.takewhile(lambda n: n < end, ap_gen) return ap_gen
cycle(it): indefinitelyrepeat(item, [times]): Yield the given item repeadedly, indefinetly unless a number of times is givencombinations(it, out_len): Yield combinations of out_len items from the items yielded by itcombinations_with_replacement(it, out_len)permutations(it, out_len=None)
5.7 iter Tricks
iter(callable, sentinel)
def d6(): return randint(1, 6) d6_iter = iter(d6, 1) # the second argument is a sentinel # another useful example with open('mydata.txt') as fp: for line in iter(fp.readline, ''): process_line(line)
- when a sentinel returned by the callable, causes the iterator to raise StopIteration instead of yielding the sentinel.
6 else keyword
6.1 with for/while
for item in my_list: if item.flavor == 'banana': break else: raise ValueError('No banana flavor found!')
6.2 with try
try: dangerous_call() except OSError: log('OSError...') else: after_call()
7 Context Manager
- The
withstatement was designed to simplify the try/finally pattern __enter__()~__exit__(self, exc_type, exc_value, traceback): arguments are None, None, None if all went well- return True to tell the interpreter that the exception was handled.
- If returns None or anything but True, any exception raised in the with block will be propagated.
7.1 Novel Usages
- Managing transactions in the sqlite3 module
- Holding locks, conditions, and semaphores in threading code(RAII)
- Setting up environments for arithmetic operations with Decimal objects
- Applying temporary patches to objects for testing
7.2 contextlib
7.2.1 @contextmanager & @asynccontextmanager
A decorator that lets you build a context manager from a simple generator function, instead of creating a class and implementing the protocol.
- Use
yieldto produce whatever you want the__enter__method to return - Essentially the contextlib.contextmanager decorator wraps the function in a class that implements the
__enter__and__exit__methods. __enter__- Invokes the generator function and holds on to the generator object—let's call it gen.
- Calls
next(gen)to make it run to theyieldkeyword. - Returns the value yielded by
next(gen), so it can be bound to a target variable in thewith/asform.
__exit__- Checks an exception was passed as
exc_type; if so,gen.throw(exception)is invoked, causing the exception to be raised in the yield line inside the generator function body. - Otherwise,
next(gen)is called, resuming the execution of the generator function body after the yield.
- Checks an exception was passed as
- Tips
- The
__exit__method provided by the decorator assumes any exception sent into the generator is handled and should be suppressed.
You must explicitly re-raise an exception in the decorated function if you don't want
@contextmanagerto suppress it.- Having a
try/finally(or awithblock) around theyieldis an unavoidable price of using@contextmanager,
because you never know what the users of your context manager are going to do inside their with block.
- The
- Example
A context manager for rewriting files in place
import csv with inplace(csvfilename, 'r', newline='') as (infh, outfh): reader = csv.reader(infh) writer = csv.writer(outfh) for row in reader: row += ['new', 'columns'] writer.writerow(row)
7.2.2 closing
A function to build context managers out of objects that provide a close() method but don’t implement the __enter__/__exit__ protocol.
from contextlib import closing from urllib.request import urlopen with closing(urlopen('http://www.python.org')) as page: for line in page: print(line)
7.2.3 suppress
A context manager to temporarily ignore specified exceptions.
from contextlib import suppress with suppress(FileNotFoundError): os.remove('somefile.tmp')
7.2.4 ContextDecorator
A base class for defining class-based context managers that can also be used as function decorators, running the entire function within a managed context.
from contextlib import ContextDecorator class mycontext(ContextDecorator): def __enter__(self): print('Starting') return self def __exit__(self, *exc): print('Finishing') return False @mycontext() def function(): print('The bit in the middle')
7.2.5 ExitStack & AsyncExitStack
A context manager that lets you enter a variable number of context managers. When the with block ends, ExitStack calls the stacked context managers' __exit__ methods in LIFO order
with ExitStack() as stack: files = [stack.enter_context(open(fname)) for fname in filenames] # All opened files will automatically be closed at the end of # the with statement, even if attempts to open files later # in the list raise an exception
7.2.6 others
redirect_stdout,redirect_stderr
8 Coroutine
- Generators produce data for iteration
- Coroutines are consumers of data
- To keep your brain from exploding, you don't mix the two concepts together
- Coroutines are not related to iteration
.send()allows two-way data exchange between the client code and the generator in contrast with.__next__()
def simple_coroutine(): print('-> coroutine started') x = yield # received by send(x) print('-> coroutine received:', x) coro = simple_coroutine() # GEN_CREATED next(coro) # equivalent to coro.send(None). GEN_CREATED->GEN_RUNNING->GEN_SUSPENDED #-> coroutine started coro.send(42) # GEN_SUSPENDED->GEN_RUNNING->GEN_CLOSED #-> coroutine received: 42
next(coro)is often describe as "priming" the coroutine(advancing it to the firstyield, the EXPR after first yield will be executed)
8.1 states
use inspect.getgeneratorstate() to get state
- GEN_CREATED: Waiting to start execution
- GEN_RUNNING: Currently being executed by the interpreter
- GEN_SUSPENDED: Currently suspended at a yield expression
- GEN_CLOSED
8.2 Termination & Exception
The coroutine will only terminate when the caller calls .close() on it, or when it's garbage collected
8.2.1 Return Value
The value attribute of the StopIteration carries the value returned
8.2.2 Exception From Generator
An exception within a coroutine propagates to the caller that triggered it.
8.2.3 Terminate Generator
throw(exc_type[, exc_value[, traceback]]): Causes the yield expression where the generator was paused to raise the exception given.close: Causes the yield expression where the generator was paused to raise aGeneratorExitexception.
8.2.4 Sentinel Values
None, ...
8.3 yield from
The main feature of yield from is to open a bidirectional channel from the outermost caller to the innermost subgenerator.
- Any values that the subgenerator yields are passed directly to the caller of the delegating generator
- Any values sent to the delegating generator using
send()are passed directly to the subgenerator. - return expr in a generator (or subgenerator) causes
StopIteration(expr)to be raised upon exit from the generator. - The value of the
yield fromexpression is the first argument to theStopIterationexception raised by the subgenerator when it terminates. - Exception Handling: Exceptions other than
GeneratorExitthrown into the delegating generator are passed to thethrow()method of
the subgenerator. If the call raises StopIteration, the delegating generator is resumed. Any other exception is propagated to the
delegating generator.
- If a
GeneratorExitexception is thrown into the delegating generator, or theclose()method of the delegating generator is called,
then the close() method of the subgenerator is called if it has one. If this call results in an exception, it is propagated to the
delegating generator. Otherwise, GeneratorExit is raised in the delegating generator.
8.3.1 Pseudocode
- Simple Version
_i = iter(EXPR) try: _y = next(_i) except StopIteration as _e: _r = _e.value # the RESULT in the simplest case else: # channel between the caller and the subgenerator while 1: _s = yield _y try: _y = _i.send(_s) except StopIteration as _e: _r = _e.value break RESULT = _r
- _i(iterator): The subgenerator
- _y(yielded): A value yielded from the subgenerator
- _r(result): The eventual result
- _s(sent): A value sent by the caller to the delegating generator, which is forwarded to the subgenerator
- _e(exception): An exception
- Complete Version
_i = iter(EXPR) try: _y = next(_i) except StopIteration as _e: _r = _e.value else: while 1: try: _s = yield _y except GeneratorExit as _e: try: _m = _i.close except AttributeError: pass else: _m() raise _e except BaseException as _e: _x = sys.exc_info() try: _m = _i.throw except AttributeError: raise _e else: try: _y = _m(*_x) except StopIteration as _e: _r = _e.value break else: try: if _s is None: _y = next(_i) else: _y = _i.send(_s) except StopIteration as _e: _r = _e.value break RESULT = _r
8.4 Discrete Event Simulations(DES)
SimPy
9 Currency
9.1 Threads & Processes
9.1.1 Executor
from concurrent import futures with futures.ThreadPoolExecutor(4) as executor: results = executor.map(process_one, sorted(args)) # map: 1. create and schedule futures 2. retrive the future.result() one by one print(results) # equivalent to with futures.ThreadPoolExecutor(4) as executor: fs = [executor.submit(process_one, arg) for arg in args] for completed_future in futures.as_completed(fs): print(completed_future.result()) return len(list(results))
- The combination of
executor.submitandfutures.as_completedis more flexible thanexecutor.map
because you can submit different callables and arguments, while executor.map is designed to run the
same callable on the different arguments.
- Future is hashable
9.1.2 GIL
Restricts only one thread at a time to execute Python bytecodes. However, all standard library functions that perform blocking I/O release the GIL when waiting for a result from the OS.
- All standard library functions that perform blocking I/O release the GIL when waiting for a result from the OS. I/O bound program can benefit from using threads.
9.1.3 Spin Example
class Signal: go = True def spin(msg, signal): write, flush = sys.stdout.write, sys.stdout.flush for char in itertools.cycle('|/-\\'): status = char + ' ' + msg write(status) flush() write('\x08' * len(status)) time.sleep(.1) if not signal.go: break write(' ' * len(status) + '\x08' * len(status)) def supervisor(): signal = Signal() spinner = threading.Thread(target=spin, args=('thinking!', signal)) print('spinner object:', spinner) spinner.start() result = slow_function() signal.go = False spinner.join() return result
9.2 Future
Interface: cancel, cancelled, running, done, result(timeout=None), exception(timeout=None), add_done_callback(fn)
9.3 asyncio + executor
import asyncio import time from concurrent.futures import ThreadPoolExecutor as Executor def blocking_task(): time.sleep(2) return 42 async def run_tasks(executor): loop = asyncio.get_event_loop() blocking_tasks = [] for _ in range(40): task = loop.run_in_executor(executor, blocking_task) blocking_tasks.append(task) completed, pending = await asyncio.wait(blocking_tasks) for t in completed: print(t.result()) if __name__ == '__main__': event_loop = asyncio.get_event_loop() executor = Executor(20) event_loop.run_until_complete(run_tasks(executor))
9.4 asyncio + aioXXX
import asyncio async def aiotask(): await asyncio.sleep(2) return 42 event_loop = asyncio.get_event_loop() tasks = [aiotask() for _ in range(1000)] results = event_loop.run_until_complete(asyncio.gather(*tasks)) for result in results: print(result)
10 asyncio
10.1 loop.create_future
This is a preferred way to create futures in asyncio, as event loop implementations can provide alternative implementations of the Future class
10.2 loop.create_task vs. asyncio.ensure_future
Always use loop.create_task to schedule coroutines, however use asyncio.ensure_future for
other awaitable objects(like Futures, Tasks)
11 Doctest
12 Utilities
math.hypot: Return the Euclidean distance,sqrt(x*x + y*y)b, a = a, b: swap values without using a temporary variable- Vaurien: chaos monkey, the Chaos TCP Proxy
- requests resp:
resp.raise_for_status() httpbinSimPy: DES simulationlelopython-parallelizeCelery