Upload
cs-center
View
445
Download
2
Embed Size (px)
DESCRIPTION
Немного про систему импорта и операторы `import` и `from...import`. Исключения, зачем они нужны и как их обрабатывать. Встроенные исключения и базовые классы `BaseException` и `Exception`. Операторы `try...except..else..finally`. Менеджеры контекста и модуль `contextlib`.
Citation preview
Лекция 7: Модули, исключения, менеджеры
контекста
Сергей Лебедев
5 ноября 2014 г.
Модули
example.py
"""I'm an example.module."""
def f():
return 42
some_variable = "foobar"
1 / 41
Файл == модуль
• Модулем называется файл с расширением .py.
• Каждый модуль задаёт новое пространство имён, атрибуты
которого соответствуют именам, определённым в файле:
>>> import example
>>> dir(example)
[..., '__cached__', '__doc__', '__file__', '__name__',
'f', 'some_variable']
• Кроме явно определённых имён в модуле также
содержатся:
>>> example.__name__
'example'
>>> example.__doc__
"I'm an example module."
>>> example.__file__
'./example.py'
>>> example.__cached__
'./__pycache__/example.cpython-34.pyc'
2 / 41
Оператор import
• Оператор import “импортирует” модуль с указанным
именем и создаёт на него ссылку в текущей области
видимости:
>>> import example
>>> example
<module 'example' from './example.py'>
• С помощью оператора as можно явно указать имя
переменной, в которую будет записана ссылка на модуль:
>>> import example as alias
>>> alias
<module 'example' from './example.py'>
• Внутреннее устройство системы импорта в Python 3
выходит за рамки сегодняшней лекции. Интересующиеся
могут обратиться к PEP-3021 и выяснить все детали из
первоисточника.
1http://python.org/dev/peps/pep-03023 / 41
Подробнее об операторе import
• В момент импорта байт-код модуля выполняется
интерпретатором сверху вниз.
• Полученный в результате импорта модуль попадает в
специальный словарь sys.modules.
• Ключом в словаре является имя модуля, то есть значение
атрибута __name__:
>>> import sys
>>> ("example" in sys.modules, "alias" in sys.modules)
(True, False)
• Повторный импорт уже загруженного модуля не приводит к
его перезагрузке:
>>> import example
>>> id(sys.modules["example"])
4329007128
>>> import example
>>> id(sys.modules["example"])
4329007128
4 / 41
__name__ == "__main__"
• Чтобы выполнить модуль на Python, достаточно указать путь
к нему в качестве аргумента интерпретатора:
python ./example.py.
• В момент исполнения имя переменной __name__ внутри
модуля example будет иметь специальное значение
"__main__".
• Это можно использовать для того, чтобы добавить в модуль
код, который будет исполняться, только если модуль был
запущен из командной строки, а не импортирован из
другого модуля:
def test():
assert 2 + 2 == 4
if __name__ == "__main__":
print("Running tests.")
test()
5 / 41
Оператор from...import
• Оператор from...import позволяет импортировать имя из
другого модуля в текущую область видимости:
>>> from example import f
>>> f()
42
• Синтаксис оператора позволяет перечислить несколько
имен через запятую и, возможно, переименовать некоторые
из них:
>>> from example import f as g, some_variable
>>> g()
42
>>> some_variable
'foobar'
6 / 41
“Семантика” оператора from...import
• Оператор from...import можно однозначно переписать
через оператор import:
>>> from module import foo as bar, boo
# HARDCORE REWRITING MAGIC
>>> import module
>>> bar = module.foo
>>> boo = module.boo
>>> del module # Зачем это нужно?
• Таким образом, всё сказанное про оператор import
релевантно также и для оператора from...import.
7 / 41
Модули: резюме
• Модуль в Python — это просто файл с расширением .py.
• Модуль можно импортировать целиком или выборочно с
помощью операторов import и from...import.
• Три важных правила импортирования модулей:
• размещайте все импорты в начале модуля,
• сортируйте их в лексикографическом порядке,
• не смешивайте import и from...import.
• Пример:
import os
import sys
from collections import deque
from itertools import groupby
8 / 41
Исключения
Зачем нужны исключения?
• Исключения нужны для исключительных ситуаций,например:
• не удалось выделить память для объекта,
>>> [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
• Исключения — это ошибки, которые можно обрабатывать. В
этом их прелесть.
9 / 41
Обработка исключений: try...except
• Для обработки исключений в Python используются
операторы try и except:
>>> try:
... something_dangerous()
... except (ValueError, ArithmeticError):
... pass
... except TypeError as e:
... pass
• Ветка except принимает два аргумента:
1. выражение, возвращающее тип или кортеж типов,
2. опциональное имя для перехваченного исключения.
• Исключение e обрабатывается веткой except, если её
первый аргумент expr можно сопоставить с исключением:
isinstance(e, expr)
• При наличии нескольких веток except интерпретатор
сверху вниз ищет подходящую.
10 / 41
Обработка исключений: подробнее о try...except
• На месте выражения в ветке except может стоять любое
выражение, например, вызов функции или обращение к
переменной:>>> try:
... something_dangerous()
... except Exception as e:
... try:
... something_else()
... except type(e): # Какое исключение мы
... pass # перехватим?
• Время жизни переменной e ограничивается веткой except:>>> try:
... 1 + "42"
... except TypeError as e:
... pass # Что делать, если нам нужно e?
...
>>> e
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'e' is not defined
11 / 41
Встроенные исключения
• BaseException — базовый класс для встроенных
исключений в Python.
>>> BaseException.__subclasses__()
[<class 'Exception'>, <class 'GeneratorExit'>,
<class 'KeyboardInterrupt'>, <class 'SystemExit'>]
• Напрямую от класса BaseException наследуются только
системные исключения и исключения, приводящие к
завершению работы интерпретатора.
• Все остальные встроенные исключения, а также исключения,
объявленные пользователем, должны наследоваться от
класса Exception.
• Отсюда следует, что, чтобы обработать любое исключение,
достаточно написать:
>>> try:
... something_dangerous()
... except Exception: # Почему не BaseException?
... pass
12 / 41
Встроенные исключения: AssertionError
• Исключение AssertionError поднимается, когда условие
оператора assert не выполняется:
>>> assert 2 + 2 == 5, ("Math", "still", "works")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError: ('Math', 'still', 'works')
• Оператор assert используется для ошибок, которые могут
возникнуть только в результате ошибки программиста,
поэтому перехватывать AssertionError считается дурным
тоном.
13 / 41
Встроенные исключения: ImportError и NameError
• Если оператор import не смог найти модуль с указанным
именем, поднимается исключение ImportError:
>>> import foobar
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named 'foobar'
• NameError поднимается, если не была найдена локальная
или глобальная переменная:
>>> foobar
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'foobar' is not defined
14 / 41
Встроенные исключения: AttributeError и LookupError
• Исключение AttributeError поднимается при попытке
прочитать или (в случае __slots__) записать значение в
несуществующий атрибут:
>>> object().foobar
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'object' object has no attribute 'foobar'
• Исключения KeyError и IndexError наследуются от
базового класса LookupError и поднимаются, если в
контейнере нет элемента по указанному ключу или индексу:
>>> {}["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
15 / 41
Встроенные исключения: ValueError и TypeError
• Исключение ValueError используется в случаях, когда
другие более информативные исключения, например,
KeyError, не применимы:
>>> "foobar".split("")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: empty separator
• Исключение TypeError поднимается, когда оператор,
функция или метод вызываются с аргументом
несоответствующего типа:
>>> b"foo" + "bar"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't concat bytes to str
• Полный список исключений можно найти в документации
языка2.
2https://docs.python.org/3/library/exceptions.html16 / 41
Исключения, объявленные пользователем
• Для объявления нового типа исключения достаточно
объявить класс, наследующийся от базового класса
Exception.
• Хорошая практика при написании библиотек на Python —
объявлять свой базовый класс исключений, например:
>>> class CSCException(Exception):
... pass
...
>>> class TestFailure(CSCException):
... def __str__(self):
... return "lecture test failed"
• Наличие базового класса позволяет пользователю
обработать любое исключение, специфичное для
библиотеки в одной ветке except:
>>> try:
... do_something()
... except CSCException:
... # ...
17 / 41
Интерфейс исключений
Интерфейс исключений в Python довольно нехитрый:
• атрибут args хранит кортеж аргументов, переданных
конструктору исключения,
• атрибут __traceback__ содержит информацию о стеке
вызовов на момент возникновения исключения.
>>> 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>
18 / 41
Оператор raise
• Поднять исключение можно с помощью оператора raise:
>>> raise TypeError("type mismatch")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: type mismatch
• Аргумент оператора raise должен наследоваться от
базового класса BaseException:
>>> raise 42
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: exceptions must derive from BaseException
• Если вызвать оператор raise без аргумента, то он поднимет
последнее пойманное исключение или, если такого
исключения нет, RuntimeError.
>>> raise
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: No active exception to reraise
19 / 41
Обработка исключений: try...finally
• Иногда требуется выполнить какое-то действие вне
зависимости от того, произошло исключение или нет,
например, закрыть файл:
>>> try:
... handle = open("example.txt", "wt")
... try:
... do_something(handle)
... finally:
... handle.close()
... except IOError as e:
... print(e, file=sys.stderr)
• Аналогичным образом нужно работать с любыми другими
ресурсами: сетевыми соединениями, примитивами
синхронизации.
20 / 41
Обработка исключений: try...else
• С помощью ветки else можно выполнить какое-то действие
в ситуации, когда внутри try блока не возникло
исключения:
>>> try:
... handle = open("example.txt", "wt")
... else:
... report_success(handle)
... except IOError as e:
... print(e, file=sys.stderr)
• Чем использование else лучше следующего варианта?
>>> try:
... handle = open("example.txt", "wt")
... report_success(handle)
... except IOError as e:
... print(e, file=sys.stderr)
21 / 41
Исключения: резюме
• Механизм обработки исключений в Python похож на
аналогичные конструкции в С++ и Java, но Python расширяет
привычную пару try...except с помощью веток else и
finally.
• Поднять исключение можно с помощью оператора raise,
его семантика эквивалентна throw в C++ и Java.
• В Python много встроенных типов исключений, которые
можно и нужно использовать при написании функций и
методов.
• Для объявления нового типа исключения достаточно
унаследоваться от базового класса Exception.
• Два важных правила при работе с исключениями:
• минимизируйте размер ветки try,
• всегда старайтесь использовать наиболее специфичный тип
исключения в ветке except.
22 / 41
Менеджерыконтекста
Зачем нужны менеджеры контекста?
• Менеджеры контекста позволяют компактно выразить уже
знакомый нам паттерн управления ресурсами:
>>> r = acquire_resource()
... try:
... do_something(r)
... finally:
... release_resource(r)
• С помощью менеджера контекста пример выше можно
записать так:
>>> with acquire_resource() as r:
... do_something(r)
Действие release_resource будет выполнено
автоматически, вызывать его явно не нужно.
23 / 41
Протокол менеджеров контекста
• Протокол менеджеров контекста состоит из двух методов.
• Метод __enter__ инициализирует контекст, например,
открывает файл или захватывает мьютекс. Значение,
возвращаемое методом __enter__, записывается по имени,
указанному после оператора as.• Метод __exit__ вызывается после выполнения телаоператора with. Метод принимает три аргумента:
1. тип исключения,
2. само исключение и
3. объект типа traceback.
Если в процессе исполнения тела оператора with было
поднятно исключение, метод __exit__ может подавить его,
вернув True.
• Экземпляр любого класса, реализующего эти два метода,
является менеджером контекста.
24 / 41
Duck Typing3
3http://theregister.co.uk/Print/2007/05/06/fables25 / 41
“Семантика” оператора with
• Напоминание:
>>> with acquire_resource() as r:
... do_something(r)
• Процесс исполнение оператора with можно концептуально
записать так:
>>> manager = acquire_resource()
>>> 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
26 / 41
Расширенные возможности оператора with
• Оператор with позволяет работать с несколькими
контекстными менеджерами одновременно:
>>> with acquire_resource() as r, \
... acquire_other_resource() as other:
... do_something(r, other)
Такая запись эквивалентна двум вложенным менеджерам
контекста:
>>> with acquire_resource() as r:
... with acquire_other_resource() as other:
... do_something(r, other)
• Можно также использовать оператор with без указания
имени переменной:
>>> with acquire_resource():
... do_something()
27 / 41
Примеры менеджеров контекста: opened
>>> 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 # без return?
...
>>> with opened("./example.txt", mode="rt") as handle:
... pass
Капитан сообщает
opened интересен только в качестве примера, потому что файлы
в Python уже поддерживают протокол менеджеров контекста.
28 / 41
Примеры менеджеров контекста: модуль tempfile
• Модуль tempfile реализует классы для работы с
временными файлами.
• Все классы реализуют протокол менеджеров контекста,
которые работают так же, как и для обычных файлов.
• Интересный пример — класс NamedTemporaryFile, который
автоматически удаляет временный файл при выходе из
менеджера контекста:
>>> import tempfile
>>> with tempfile.NamedTemporaryFile() 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
29 / 41
Примеры менеджеров контекста: syncronized
>>> class synchronized:
... def __init__(self):
... self.lock = threading.Lock()
...
... def __enter__(self):
... self.lock.acquire()
...
... def __exit__(self, *exc_info):
... self.lock.release()
...
>>> with synchronized():
... do_something()
Капитан сообщает
Большая часть примитивов синхронизации в Python, включая
класс Lock, реализует протокол менеджера контекста.
Использовать менеджер synchronized не нужно — он интересен
только в качестве примера.
30 / 41
Примеры контекстных менеджеров: cd
>>> import os
>>> class cd:
... def __init__(self, path):
... self.path = path
...
... def __enter__(self):
... self.saved_cwd = os.getcwd()
... os.chdir(self.path)
...
... def __exit__(self, *exc_info):
... os.chdir(self.saved_cwd)
...
>>> print(os.getcwd())
./csc/python
>>> with cd("/tmp"):
... print(os.getcwd())
...
/tmp
31 / 41
Менеджеры контекста: резюме
• Менеджеры контекста — удобный способ управлять
жизненным циклом ресурсов в Python.
• Для работы с менеджером контекста используется оператор
with.
• Менеджером контекста является экземпляр любого класса,
реализующего методы __enter__ и __exit__.
• Некоторые встроенные типы, например, файлы и
примитивы синхронизации уже поддерживают протокол
менеджеров контекста — этим можно и нужно пользоваться
при написании кода.
32 / 41
Модуль
contextlib
Модуль contextlib: closing
• Менеджер контекста closing обобщает логику уже
известного нам opened на экземпляр любого класса,
реализующего метод close.
• Реализовать closing самому несложно, но приятно, когда в
стандартной библиотеке языка есть и такие мелочи.
• С помощью closing можно, например, безопасно работать с
HTTP ресурсами:
>>> from contextlib import closing
>>> from urllib.request import urlopen
>>> url = "http://compscicenter.ru"
>>> with closing(urlopen(url)) as page:
... do_something(page)
33 / 41
Модуль contextlib: redirect_stdout
• Менеджер контекста redirect_stdout позволяет локально
перехватывать вывод в стандартный поток.
• Пример использования:
>>> from contextlib import redirect_stdout
>>> import io
>>> handle = io.StringIO()
>>> with redirect_stdout(handle):
... print("Hello, World!")
...
>>> handle.getvalue()
'Hello, World!\n'
Вопрос
Как можно было бы реализовать redirect_stdout?
34 / 41
Модуль contextlib: suppress
• С помощью менеджера контекста suppress можно
локального подавить исключения указанных типов:>>> from contextlib import suppress
>>> with suppress(FileNotFoundError):
... os.remove("example.txt")
• Реализация менеджера не хитра:>>> 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))
• При использовании suppress, как и в целом при работе с
исключениями, стоит указывать ниболее специфичный тип
исключения.35 / 41
Модуль contextlib: ContextDecorator
• Базовый класс ContextDecorator позволяет объявлять
менеджеры контекста, которые можно использовать как
декораторы.
• Зачем это нужно?
def f():
with context():
# ...
@context()
def f():
# ...
• Переход к синтаксису декораторов:
• подчеркивает, что менеджер контекста применяется ко
всему телу функции,
• позволяет сэкономить 4 пробела :)
Вопрос
Как должен быть реализован менеджер контекста, чтобы его
можно было использовать в качестве декоратора?
36 / 41
Модуль contextlib: менеджеры контекста и декораторы
• Для того, чтобы менеджер контекста можно было
использовать как декоратор, достаточно унаследовать его
от ContextDecorator.
• Модифицируем менеджер suppress из модуля contextlib,
чтобы с помощью него можно было подавлять исключения
во всей функции:
>>> from contextlib import (suppress as _suppress,
... ContextDecorator)
>>> class suppressed(_suppress, ContextDecorator):
... pass
...
>>> @suppressed(IOError)
... def do_something():
... pass
37 / 41
Модуль contextlib: ExitStack
• Что делать, если количество ресурсов может быть
произвольным? Например:
>>> def merge_logs(output_path, *logs):
... handles = open_files(logs)
... with open(output_path, "wt") as output:
... merge(output, handles)
... close_files(logs)
• Правильный ответ: ExitStack. Менеджер 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)
38 / 41
“Семантика” менеджера ExitStack
• Менеджер ExitStack поддерживает стек вложенных
менеджеров контекста:
>>> with ExitStack() as stack:
... stack.enter_context(some_resource)
... stack.enter_context(other_resource)
... do_something(some_resource, other_resource)
• При выходе из контекста, ExitStack обходит список
вложенных менеджеров контекста в обратном порядке и
вызывает у каждого менеджера метод __exit__.
• Менеджер ExitStack корректно обрабатывает ситуации,
• когда метод __exit__ подавил исключение
• или, когда в процессе работы метода __exit__ возникло
новое исключение.
39 / 41
Менеджер ExitStack и вызываемые функции
Полезная особенность менеджера ExitStack — возможность
зарегистрировать произвольную функцию на выполнение в
момент выхода из контекста:
>>> with ExitStack() as stack:
... @stack.callback
... def cleanup():
... print("Cleaning up.")
...
... def more_cleanup(arg):
... print("More cleaning up: {}.".format(arg))
... stack.callback(more_cleanup,
... "important resource")
...
More cleaning up: important resource.
Cleaning up. # LIFO again.
Вопрос
Для чего это может быть полезно?
40 / 41
Модуль contextlib: резюме
• Модуль contextlib содержит функции и классы,
украшающие жизнь любителя менеджеров контекста.
• Мы поговорили про:
• closing,
• redirect_stdout,
• suppress,
• ContextDecorator,
• ExitStack.
41 / 41