Иногда нужно выполнить блок кода с несколькими менеджерами контекста:
Начиная с Python 2.7 и 3.1, это можно записать в одной конструкции
Раньше для этого использовали функцию
Если же число менеджеров контекста заранее неизвестно, лучше подойдёт более продвинутый инструмент.
👉@BookPython
with open('f') as f:
with open('g') as g:
with open('h') as h:
pass
Начиная с Python 2.7 и 3.1, это можно записать в одной конструкции
with
:
o = open
with o('f') as f, o('g') as g, o('h') as h:
pass
Раньше для этого использовали функцию
contextlib.nested
:
with nested(o('f'), o('g'), o('h')) as (f, g, h):
pass
Если же число менеджеров контекста заранее неизвестно, лучше подойдёт более продвинутый инструмент.
contextlib.ExitStack
позволяет открывать любое число контекстов в произвольный момент, но гарантирует корректный выход из них в конце:
with ExitStack() as stack:
f = stack.enter_context(o('f'))
g = stack.enter_context(o('g'))
other = [
stack.enter_context(o(filename))
for filename in filenames
]
👉@BookPython
Все объекты, которые в настоящий момент существуют в памяти интерпретатора, можно получить с помощью
👉@BookPython
gc.get_objects()
:
In [1]: class A:
...: def __init__(self, x):
...: self._x = x
...:
...: def __repr__(self):
...: class_name = type(self).__name__
...: x = self._x
...: return f'{class_name}({x!r})'
...:
In [2]: A(1)
Out[2]: A(1)
In [3]: A(2)
Out[3]: A(2)
In [4]: A(3)
Out[4]: A(3)
In [5]: [x for x in gc.get_objects() if isinstance(x, A)]
Out[5]: [A(1), A(2), A(3)]
👉@BookPython
Начиная с Python 3.0, при возникновении нового исключения внутри блока
Кроме того, вы можете явно указать причинное исключение, использовав конструкцию
👉@BookPython
except
перехваченное исключение автоматически сохраняется в атрибуте __context__
создаваемого исключения. В результате при выводе будут показаны оба исключения:
try:
1 / 0
except ZeroDivisionError:
raise ValueError('Zero!')
(most recent call last):
File "test.py", line 2, in <module>
1 / 0
ZeroDivisionError: division by zero
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "test.py", line 4, in <module>
raise ValueError('Zero!')
ValueError: Zero!
Кроме того, вы можете явно указать причинное исключение, использовав конструкцию
raise … from
. Тогда в атрибут __cause__
нового исключения будет помещено исходное:
division_error = None
try:
1 / 0
except ZeroDivisionError as e:
division_error = e
raise ValueError('Zero!') from division_error
(most recent call last):
File "test.py", line 4, in <module>
1 / 0
ZeroDivisionError: division by zero
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "test.py", line 8, in <module>
raise ValueError('Zero!') from division_error
ValueError: Zero!
👉@BookPython
In:
Out:
Цифры 0 1 2 3 4 5 6 7 8 9 — это не единственные символы, которые считаются цифрами. Python следует правилам Unicode и считает «цифрами» сотни различных символов. Вот полный список таких символов.
Это влияет на такие функции, как
👉@BookPython
int('୧৬𝟙༣')
Out:
1613
Цифры 0 1 2 3 4 5 6 7 8 9 — это не единственные символы, которые считаются цифрами. Python следует правилам Unicode и считает «цифрами» сотни различных символов. Вот полный список таких символов.
Это влияет на такие функции, как
int
, unicode.isdecimal
и даже re.match
:
# Пример 1
int('෯')
# Вывод:
9
# Пример 2
'٢'.isdecimal()
# Вывод:
True
# Пример 3
import re
bool(re.match(r'\d', '౫'))
# Вывод:
True
👉@BookPython
>>> bool(datetime(2018, 1, 1).time())
False
>>> bool(datetime(2018, 1, 1, 13, 12, 11).time())
True
До Python 3.5 объекты
datetime.time()
считались ложными, если они представляли полночь по UTC. Это могло приводить к трудноуловимым ошибкам. В следующих примерах if not
может выполниться не потому, что created_time
равен None
, а потому, что время — полночь.
def create(created_time=None) -> None:
if not created_time:
created_time = datetime.now().time()
Можно исправить это, явно проверяя на
None
:
def create(created_time=None) -> None:
if created_time is None:
created_time = datetime.now().time()
👉@BookPython
В Python нет поддержки асинхронных операций с файлами. Чтобы сделать их неблокирующими, нужно использовать отдельные потоки.
Для асинхронного выполнения кода в потоке следует использовать метод
Сторонний модуль aiofiles делает всё это за тебя, предоставляя простой и удобный интерфейс:
👉@BookPython
Для асинхронного выполнения кода в потоке следует использовать метод
loop.run_in_executor
.Сторонний модуль aiofiles делает всё это за тебя, предоставляя простой и удобный интерфейс:
async with aiofiles.open('filename', mode='r') as f:
contents = await f.read()
👉@BookPython
Иногда нужно создать функцию на основе более универсальной.
Например, у функции
Это полезно, когда нужно передать функцию как аргумент другой, более общей функции, но при этом некоторые аргументы должны быть зафиксированы:
Без
👉@BookPython
Например, у функции
int()
есть параметр base
, который мы хотим зафиксировать, чтобы получить новую функцию base2
:
>>> int("10")
10
>>> int("10", 2)
2
>>> def base2(x):
... return int(x, 2)
...
>>> base2("10")
2
functools.partial
позволяет сделать то же самое, но точнее и семантически понятнее:
base2 = partial(int, base=2)
Это полезно, когда нужно передать функцию как аргумент другой, более общей функции, но при этом некоторые аргументы должны быть зафиксированы:
>>> map(partial(int, base=2), ["1", "10", "100"])
[1, 2, 4]
Без
partial
пришлось бы делать так:
>>> map(lambda x: int(x, base=2), ["1", "10", "100"])
[1, 2, 4]
👉@BookPython
Существует две встроенные функции, которые позволяют анализировать итерируемые объекты без необходимости писать тривиальные и избыточные циклы
Обе функции особенно полезны в сочетании со списковыми включениями (list comprehensions):
Функции
👉@BookPython
for
. Это all
и any
.any
возвращает True
, если хотя бы одно значение истинно; all
возвращает True
, только если все значения истинны. Для пустого итерируемого объекта all
возвращает True
, а any
— False
.Обе функции особенно полезны в сочетании со списковыми включениями (list comprehensions):
package_broken = any(
part.is_broken() for part in package.get_parts()
)
package_ok = all(
part.ok() for part in package.get_parts()
)
Функции
any
и all
зачастую взаимозаменяемы благодаря законам де Моргана. Используй ту, с которой код будет понятнее.👉@BookPython
Если ты хочешь, чтобы у объектов класса автоматически увеличивался ID, это можно реализовать, отслеживая текущий ID в атрибуте класса:
Обрати внимание, что нельзя использовать
Лучше использовать фабричный метод вместо
Эта версия также проще для тестирования, так как можно легко задать любой ID вручную.
👉@BookPython
class Task:
_task_id = 0
def __init__(self):
self._id = self._task_id
type(self)._task_id += 1
Обрати внимание, что нельзя использовать
self._task_id += 1
— это создаст атрибут _task_id
внутри экземпляра, а не изменит значение на уровне класса.Лучше использовать фабричный метод вместо
__init__
, чтобы код выглядел аккуратнее:
class Task:
_task_id = 0
def __init__(self, task_id):
self._id = task_id
@classmethod
def create(cls):
obj = cls(cls._task_id)
cls._task_id += 1
return obj
Эта версия также проще для тестирования, так как можно легко задать любой ID вручную.
👉@BookPython
17 июня(уже завтра!) в 19:00 по мск приходи онлайн на открытое собеседование, чтобы посмотреть на настоящее интервью на Middle Python-разработчика.
Как это будет:
Это бесплатно. Эфир проходит в рамках менторской программы от ШОРТКАТ для Java-разработчиков, которые хотят повысить свой грейд, ЗП и прокачать скиллы.
Переходи в нашего бота, чтобы получить ссылку на эфир → @shortcut_py_bot
Реклама. ООО "ШОРТКАТ", ИНН: 9731139396, erid: 2VtzqubLfbk
Please open Telegram to view this post
VIEW IN TELEGRAM
Если ты хочешь запустить код с изменённой глобальной переменной, лучше использовать контекстный менеджер, а не менять её напрямую:
👉@BookPython
from contextlib import contextmanager
QUIT_MESSAGE = 'Bye'
def print_quit_mesage():
global QUIT_MESSAGE
print(QUIT_MESSAGE)
@contextmanager
def global_variable_changed(name, value):
orig_value = globals()[name]
globals()[name] = value
yield
globals()[name] = orig_value
with global_variable_changed(
'QUIT_MESSAGE',
'Tschüss'
):
print_quit_mesage()
👉@BookPython
Ты можешь создавать словари двумя способами: с помощью литералов или функции
Литералы работают быстрее, чем
Во-первых, не нужно ставить дополнительные кавычки. Однако это работает только в том случае, если все ключи — допустимые идентификаторы Python:
Во-вторых, ты не сможешь случайно указать один и тот же ключ дважды:
В-третьих, легко создать новый словарь на основе уже существующего:
Но учти, что ключи нельзя переопределять при таком способе:
👉@BookPython
dict
:
>>> dict(a=1, b=2)
{'a': 1, 'b': 2}
>>> {'a': 1, 'b': 2}
{'a': 1, 'b': 2}
Литералы работают быстрее, чем
dict
, но у функции есть свои преимущества.Во-первых, не нужно ставить дополнительные кавычки. Однако это работает только в том случае, если все ключи — допустимые идентификаторы Python:
>>> dict(a=1)
{'a': 1}
>>> dict(1='a')
File "<stdin>", line 1
SyntaxError: keyword can't be an expression
Во-вторых, ты не сможешь случайно указать один и тот же ключ дважды:
>>> {'a': 1, 'a': 1}
{'a': 1}
>>> dict(a=1, a=1)
File "<stdin>", line 1
SyntaxError: keyword argument repeated
В-третьих, легко создать новый словарь на основе уже существующего:
>>> d = dict(b=2)
>>> dict(a=1, **d)
{'a': 1, 'b': 2}
Но учти, что ключи нельзя переопределять при таком способе:
>>> dict(b=3, **d)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: type object got multiple values for keyword argument
👉@BookPython
Начиная с Python 3.5, стало возможно использовать распаковку в литералах словарей и списков.
Пример со словарём:
Пример со списком:
Для словарей такая форма даже мощнее, чем функция
👉@BookPython
Пример со словарём:
{**{'a': 1}, 'b': 2, **{'c': 3}}
# Результат: {'a': 1, 'b': 2, 'c': 3}
Пример со списком:
[1, 2, *[3, 4]]
# Результат: [1, 2, 3, 4]
Для словарей такая форма даже мощнее, чем функция
dict
, потому что позволяет переопределять значения:
{**{'a': 1, 'b': 1}, 'a': 2, **{'b': 3}}
# Результат: {'a': 2, 'b': 3}
👉@BookPython
Если ты хочешь, чтобы контекстный менеджер при входе или выходе из контекста приостанавливал выполнение корутины, следует использовать асинхронные контекстные менеджеры. Вместо вызова
Асинхронные контекстные менеджеры нужно использовать с синтаксисом
В этом примере класс
👉@BookPython
m.__enter__()
и m.__exit__()
Python в этом случае выполняет await m.__aenter__()
и await m.__aexit__()
соответственно.Асинхронные контекстные менеджеры нужно использовать с синтаксисом
async with
:
import asyncio
class Slow:
def __init__(self, delay):
self._delay = delay
async def __aenter__(self):
await asyncio.sleep(self._delay / 2)
async def __aexit__(self, *exception):
await asyncio.sleep(self._delay / 2)
async def main():
async with Slow(1):
print('slow')
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
В этом примере класс
Slow
симулирует задержку при входе и выходе из контекста.👉@BookPython
Начиная с Python 3.7, модуль
Для более старых версий Python можно использовать
👉@BookPython
contextlib
предоставляет декоратор asynccontextmanager
, который позволяет определять асинхронные контекстные менеджеры точно так же, как contextmanager
:
import asyncio
from contextlib import asynccontextmanager
@asynccontextmanager
async def slow(delay):
half = delay / 2
await asyncio.sleep(half)
yield
await asyncio.sleep(half)
async def main():
async with slow(1):
print('slow')
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
Для более старых версий Python можно использовать
@asyncio_extras.async_contextmanager
.👉@BookPython
В Python нет оператора
Суть в том, что в Python есть унарный плюс, и выражение
Здесь
👉@BookPython
++
, вместо него используется x += 1
. Однако синтаксис ++x
всё ещё допустим (в отличие от x++
, который вызывает ошибку).Суть в том, что в Python есть унарный плюс, и выражение
++x
на самом деле интерпретируется как x.__pos__().__pos__()
. Этим можно злоупотребить и заставить ++
работать как инкремент (хотя так делать не рекомендуется):
class Number:
def __init__(self, value):
self._value = value
def __pos__(self):
return self._Incrementer(self)
def inc(self):
self._value += 1
def __str__(self):
return str(self._value)
class _Incrementer:
def __init__(self, number):
self._number = number
def __pos__(self):
self._number.inc()
x = Number(4)
print(x) # 4
++x
print(x) # 5
Здесь
++x
вызывает дважды __pos__()
: сначала на x
, затем на возвращённом объекте _Incrementer
, в котором второй __pos__()
вызывает inc()
, увеличивая значение.👉@BookPython
Иногда в тестах нужно сравнивать сложные структуры, игнорируя некоторые значения. Обычно это делается путем сравнения отдельных значений внутри структуры:
Однако можно создать специальное значение, которое будет считаться равным любому другому значению:
Это легко реализовать, определив метод
👉@BookPython
>>> d = dict(a=1, b=2, c=3)
>>> assert d['a'] == 1
>>> assert d['c'] == 3
Однако можно создать специальное значение, которое будет считаться равным любому другому значению:
>>> assert d == dict(a=1, b=ANY, c=3)
Это легко реализовать, определив метод
__eq__
:
>>> class AnyClass:
... def __eq__(self, another):
... return True
...
>>> ANY = AnyClass()
👉@BookPython
Если вы хотите итерироваться одновременно по нескольким итерируемым объектам, функция
Вывод:
Обратите внимание, что
👉@BookPython
zip
может быть хорошим выбором. Она возвращает генератор, который выдаёт кортежи, содержащие по одному элементу из каждого исходного итерируемого объекта:
In : eng = ['one', 'two', 'three']
In : ger = ['eins', 'zwei', 'drei']
In : for e, g in zip(eng, ger):
...: print('{e} = {g}'.format(e=e, g=g))
...:
Вывод:
one = eins
two = zwei
three = drei
Обратите внимание, что
zip
принимает итерируемые объекты как отдельные аргументы, а не в виде списка аргументов. Чтобы «развернуть» значения (unzip), можно использовать оператор *
:
In : list(zip(*zip(eng, ger)))
Out: [('one', 'two', 'three'), ('eins', 'zwei', 'drei')]
👉@BookPython
Please open Telegram to view this post
VIEW IN TELEGRAM
💡10 функций, для продвинутых Python-разработчиков
1. Разворачиваем вложенных списков любой глубины
2. Декоратор для мемоизации результатов функции
3. Разбиение списка на куски длины n
4. Уникализация последовательности с сохранением порядка
5. Глубокий доступ к вложенным ключам словаря
6. Преобразование Python-объекта в читаемый JSON
7. Чтение последних n строк файла (аналог tail)
8. Выполнение shell-команды и возврат вывода
9. Быстрое объединение путей
10. Группировка списка словарей по значению ключа
👉@BookPython
1. Разворачиваем вложенных списков любой глубины
flatten = lambda lst: [x for sub in lst for x in (flatten(sub) if isinstance(sub, list) else [sub])]
2. Декоратор для мемоизации результатов функции
memoize = lambda f: (lambda *args, _cache={}, **kwargs: _cache.setdefault((args, tuple(kwargs.items())), f(*args, **kwargs)))
3. Разбиение списка на куски длины n
chunked = lambda lst, n: [lst[i:i+n] for i in range(0, len(lst), n)]
4. Уникализация последовательности с сохранением порядка
uniq = lambda seq: list(dict.fromkeys(seq))
5. Глубокий доступ к вложенным ключам словаря
deep_get = lambda d, *keys: __import__('functools').reduce(lambda a, k: a.get(k) if isinstance(a, dict) else None, keys, d)
6. Преобразование Python-объекта в читаемый JSON
pretty_json = lambda obj: __import__('json').dumps(obj, ensure_ascii=False, indent=2)
7. Чтение последних n строк файла (аналог tail)
tail = lambda f, n=10: list(__import__('collections').deque(open(f), maxlen=n))
8. Выполнение shell-команды и возврат вывода
sh = lambda cmd: __import__('subprocess').run(cmd, shell=True, check=True, capture_output=True).stdout.decode().strip()
9. Быстрое объединение путей
path_join = lambda *p: __import__('os').path.join(*p)
10. Группировка списка словарей по значению ключа
group_by = lambda seq, key: {k: [d for d in seq if d.get(key) == k] for k in set(d.get(key) for d in seq)}
👉@BookPython