Exceptions · context managers · iterators

By on

🐍 Exceptions · context managers · iterators

SPbPU, Viktor Tiulpin, 2021

In the previous episodes

  • basic stuff, syntax
  • functions (wrote our cool min function) + functional stuff
  • scopes
  • PEP-8
  • strings
  • bytes
  • collections
  • classes basics
  • decorators – try more here
  • functools

‼️ Exceptions


Exceptions are for exceptional situations.

>>> [0] * int(1e16)
Traceback (most recent call last):
	File "<stdin>", line 1, in <module>
MemoryError
>>> import foobar
Traceback (most recent call last):
	File "<stdin>", line 1, in <module>
ImportError: No module named 'foobar'
>>> [1, 2, 3] + 4
Traceback (most recent call last):
	File "<stdin>", line 1, in <module>
TypeError: can only concatenate list to list

They are errors, which we can process.


👇 How to process

>>> try:
... 	something_dangerous()
... except (ValueError, ArithmeticError): 
...		pass
... except TypeError as e:
... 	pass

Variable stays inside

>>> try:
...     1 + "42"
... except TypeError as e:
... 	pass
>>> e
Traceback (most recent call last):
	File "<stdin>", line 1, in <module>
NameError: name 'e' is not defined

BaseException

>>> BaseException.__subclasses__()
[<class 'Exception'>, <class 'GeneratorExit'>,
   <class 'KeyboardInterrupt'>, <class 'SystemExit'>]
>>> try:
... 	something_dangerous()
... except Exception: 
... 	pass

Finally, assert

>>> assert 2 + 2 == 5, ("Math", "still", "works") 
Traceback (most recent call last):
	File "<stdin>", line 1, in <module>
AssertionError: ('Math', 'still', 'works')

do not except AssertionError


More exceptions…

>>> import foobar
Traceback (most recent call last):
	File "<stdin>", line 1, in <module>
ImportError: No module named 'foobar'
>>> foobar
Traceback (most recent call last):
	File "<stdin>", line 1, in <module>
NameError: name 'foobar' is not defined

And more…

>>> object().foobar
Traceback (most recent call last):
	File "<stdin>", line 1, in <module>
AttributeError: 'object' object has no attribute 'foobar'
>>> {}["foobar"]
Traceback (most recent call last):
File "<stdin>", line 1, in <module> KeyError: 'foobar'
>>> [][0]
Traceback (most recent call last):
	File "<stdin>", line 1, in <module>
IndexError: list index out of range

And even more…

>>> "foobar".split("")
Traceback (most recent call last):
	File "<stdin>", line 1, in <module>
ValueError: empty separator
>>> b"foo" + "bar"
Traceback (most recent call last):
	File "<stdin>", line 1, in <module>
TypeError: can't concat bytes to str

see https://docs.python.org/3/library/exceptions.html


Good practice: make your own base class

>>> class OurException(Exception):
... 	pass
...
>>> class TestFailure(OurException): 
... 	def __str__(self):
... 		return "lecture test failed"

Why?


This will allow you to

>>> try:
... 	do_something() 
... except OurException: 
...     # ...

The exception interface is easy

>>> try:
... 	1 + "42"
... except Exception as e:
... 	caught = e
...
>>> caught.args
("unsupported operand type(s) for +: 'int' and 'str'",) 
>>> caught.__traceback__
<traceback object at 0x10208d148>
>>> import traceback
>>> traceback.print_tb(caught.__traceback__)
File "<stdin>", line 2, in <module>

Rise and shine, new exception

>>> raise TypeError("type mismatch")
Traceback (most recent call last):
	File "<stdin>", line 1, in <module>
TypeError: type mismatch
>>> raise 42
Traceback (most recent call last):
	File "<stdin>", line 1, in <module>
TypeError: exceptions must derive from BaseException
>>> raise
Traceback (most recent call last):
	File "<stdin>", line 1, in <module>
RuntimeError: No active exception to reraise

raise from…

>>> try:
... 	{}["foobar"]
... except KeyError as e:
... 	raise RuntimeError("Ooops!") from e 
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
KeyError: 'foobar'
The [...] exception was the [...] cause of the following [...]:
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
RuntimeError: Ooops!

else!

>>> try:
... 	handle = open("example.txt", "wt")
... else:
... 	report_success(handle)
... except IOError as e:
... 	print(e, file=sys.stderr)

finally, finally:

>>> try:
... 	{}["foobar"]
... finally:
... 	"foobar".split("")
...
Traceback (most recent call last):
	File "<stdin>", line 2, in <module>
KeyError: 'foobar'
During handling of [...] exception, [...] exception occurred:
Traceback (most recent call last):
	File "<stdin>", line 4, in <module>
ValueError: empty separator

Not having any exception handler is slightly faster (but blows up in your face when the exception happens), and try/except is faster than an explicit if as long as the condition is not met. [^1](https://stackoverflow.com/questions/2522005/cost-of-exception-handlers-in-python)


Exceptions wrap-up

  • similar to C++/Java, but Python has else
  • raise is like throw in C++/Java
  • inherite from Exception
  • try to minimize size of try
  • try not to use too broad exceptions

🤔 Context managers


>>> r = acquire_resource()
... try:
... 	do_something(r)
... finally:
... 	release_resource(r)

to this

>>> with acquire_resource() as r:
... 	do_something(r)

Context manager implementation

  • implement __enter__
  • implement __exit__

with syntax

>>> with acquire_resource() as r:
...

==

>>> r = manager.__enter__()
>>> try:
... 	do_something(r)
... finally:
... 	exc_type, exc_value, tb = sys.exc_info()
... 	suppress = manager.__exit__(exc_type, exc_value, tb)
... if exc_value is not None and not suppress:
... 	raise exc_value

Toy example

>>> from functools import partial
>>> class opened:
... 	def __init__(self, path, *args, **kwargs):
... 		self.opener = partial(open, path, *args, **kwargs)
...
... 	def __enter__(self):
... 		self.handle = self.opener()
... 		return self.handle
...
... 	def __exit__(self, *exc_info):
... 		self.handle.close()
... 		del self.handle # where is return???
...
>>> with opened("./example.txt", mode="rt") as handle:
... 	pass

with tempfile

>>> import tempfile
>>> with tempfile.TemporaryFile() as handle:
... 	path = handle.name
... 	print(path)
...
/var/folders/nj/T/tmptpy6nn5y
>>> open(path)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory

contextlib: closing

>>> from contextlib import closing
>>> from urllib.request import urlopen
>>> url = "http://compscicenter.ru"
>>> with closing(urlopen(url)) as page:
... 	do_something(page)

contextlib: redirect_stdout

>>> from contextlib import redirect_stdout
>>> import io
>>> handle = io.StringIO()
>>> with redirect_stdout(handle):
... 	print("Hello, World!")
...
>>> handle.getvalue()
'Hello, World!\n'

contextlib: supress

>>> from contextlib import suppress
>>> with suppress(FileNotFoundError):
... 	os.remove("example.txt")

supress realization

>>> class supress:
... 	def __init__(self, *suppressed):
... 		self.suppressed = suppressed
...
... 	def __enter__(self):
... 		pass
...
... 	def __exit__(self, exc_type, exc_value, tb):
... 		return (exc_type is not None and
... 			issubclass(exc_type, suppressed))

contextlib: ContextDecorator

def f():
    with context():
	    # ...

to…

@context()
def f():
    # ...

Saves 4 spaces in every string!


So

>>> from contextlib import (suppress as _suppress,
... 	ContextDecorator)
>>> class suppressed(_suppress, ContextDecorator):
... 	pass
...
>>> @suppressed(IOError)
... def do_something():
... 	pass

many resources?

>>> def merge_logs(output_path, *logs):
... 	handles = open_files(logs)
... 	with open(output_path, "wt") as output:
... 		merge(output, handles)
... 	close_files(logs)


contextlib: ExitStack

>>> from contextlib import ExitStack
>>> def merge_logs(output_path, *logs):
... 	with ExitStack() as stack:
... 		handles = [stack.enter_context(open(log))
... 				   for log in logs]
... 	output = open(output_path, "wt")
... 	stack.enter_context(output)
... 	merge(output, handles)

ExitStack() is a context manager too

>>> with ExitStack() as stack:
... 	stack.enter_context(some_resource)
... 	stack.enter_context(other_resource)
... 	do_something(some_resource, other_resource)

Calls __exit__ in every context manager (reversed order)


Context managers wrap-up

  • context manager is a useful way to control resources lifecycle in Python
  • used with with, to implement in your object just create __enter_ and __exit_
  • some default types (e.g. files) already support with – they have context managers, use it
  • contextlib sometimes can make your life easier

🏃‍♀️ Iterators


for…

>>> import dis
>>> dis.dis("for x in xs: do_something(name)")
	0 SETUP_LOOP 		24 (to 27)
	3 LOAD_NAME 		0 (xs)
	6 GET_ITER
>> 	7 FOR_ITER 			16 (to 26)
	10 STORE_NAME 		1 (x)
	[...]
>> 	26 POP_BLOCK
>> 	27 LOAD_CONST 		0 (None)
	30 RETURN_VALUE

Iterator protocol

  • GET_ITER calls __iter__ which returns an iterator
  • FOR_ITER calls __next__ which returns an element from iterable, until StopIteration exception.

iter() behaviour

  • gets an iterator and calls __iter__
  • gets a function and calls it until the needed value
from functools import partial
with open(path, "rb") as handle:
	read_block = partial(handle.read, 64)
	for block in iter(read_block, ""):
		do_something(block)

next() behaviour

  • gets an iterator and calls __next__
>>> next(iter([1, 2, 3]))
1
>>> next(iter([]), 42)
42

so

for x in xs:
	do_something(x)

is like

it = iter(xs)
while True:
    try:
        x = next(it)
    except StopIteration:
        break
 do_something(x)

in, not in

Default implementation:

class object:
    # ...
    def __contains__(self, target):
        for item in self:
            if item == target:
                return True
        return False

>>> id = Identity()
>>> 5 in id # ≡ id.__contains__(5)
True
>>> 42 not in id # ≡ not id.__contains__(42)
True

Simpler iterator protocol implementation

__getitem__ gets 1 argument – an index

  1. returns element from given index
  2. raises IndexError otherwise
>>> class Identity:
...     def __getitem__(self, idx):
...         if idx > 5:
...             raise IndexError(idx)
...         return idx
...
>>> list(Identity())
[0, 1, 2, 3, 4, 5]

Iterators wrap-up

  • iterator is an object of a class which has __init__ and __next__ methods.
  • alternatively, it’s an object of a class which has __getitem__ (to get default implementation of next and iter)
  • for/in/not in

🏁 Any questions?