Если вы хотите, чтобы объекты класса имели автоинкрементируемый 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
в экземпляре, а не в классе. Вместо этого стоит использовать фабричный метод, чтобы сделать код красивее:
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
👍3🤔1
Можно добавлять символы Unicode в строковый литерал не только по их номеру, но и по имени.
Это также совместимо с f-строками:
👉@BookPython
>>> '\N{EM DASH}'
'—'
>>> '\u2014'
'—'
Это также совместимо с f-строками:
>>> width = 800
>>> f'Width \N{EM DASH} {width}'
'Width — 800'
👉@BookPython
👍4❤1
Python предоставляет мощную библиотеку для работы с датой и временем — datetime.
Интересная особенность в том, что объекты
Наиболее популярный модуль для этой задачи — pytz.
Хитрость в том, что
Вы не можете использовать объекты часовых поясов
Обратите внимание на смещение
Правильное использование
Также после любых арифметических операций с датами рекомендуется нормализовать объект
Начиная с Python 3.6, рекомендуется использовать dateutil.tz вместо
Он полностью совместим с
👉@BookPython
Интересная особенность в том, что объекты
datetime
имеют специальный интерфейс для поддержки часовых поясов (а именно атрибут tzinfo
), но сам модуль реализует этот интерфейс лишь частично, оставляя остальную работу другим модулям.Наиболее популярный модуль для этой задачи — pytz.
Хитрость в том, что
pytz
не полностью соответствует интерфейсу tzinfo
. В документации pytz
это указано уже в первых строках:«Эта библиотека отличается от задокументированного Python API для реализаций tzinfo».
Вы не можете использовать объекты часовых поясов
pytz
напрямую в качестве tzinfo
. Если попробовать, можно получить совершенно неожиданные результаты:
In : paris = pytz.timezone('Europe/Paris')
In : str(datetime(2017, 1, 1, tzinfo=paris))
Out: '2017-01-01 00:00:00+00:09'
Обратите внимание на смещение
+00:09
.Правильное использование
pytz
выглядит так:
In : str(paris.localize(datetime(2017, 1, 1)))
Out: '2017-01-01 00:00:00+01:00'
Также после любых арифметических операций с датами рекомендуется нормализовать объект
datetime
на случай изменения смещения (например, на границе перехода на летнее время):
In : new_time = time + timedelta(days=2)
In : str(new_time)
Out: '2018-03-27 00:00:00+01:00'
In : str(paris.normalize(new_time))
Out: '2018-03-27 01:00:00+02:00'
Начиная с Python 3.6, рекомендуется использовать dateutil.tz вместо
pytz
.Он полностью совместим с
tzinfo
, может напрямую передаваться в атрибут tzinfo
, не требует нормализации, хотя работает немного медленнее.👉@BookPython
👍4
Есть шесть магических методов Python, которые определяют правила сравнения объектов:
‣
‣
‣
‣
‣
‣
Если некоторые из этих методов не определены или возвращают
‣
‣
‣
Однако, из
Декоратор
👉@BookPython
‣
__lt__
для <
‣
__gt__
для >
‣
__le__
для <=
‣
__ge__
для >=
‣
__eq__
для ==
‣
__ne__
для !=
Если некоторые из этих методов не определены или возвращают
NotImplemented
, применяются следующие правила:‣
a.__lt__(b)
эквивалентно b.__gt__(a)
‣
a.__le__(b)
эквивалентно b.__ge__(a)
‣
a.__eq__(b)
эквивалентно not a.__ne__(b)
(обратите внимание, что в этом случае a
и b
не меняются местами)Однако, из
a >= b
и a != b
не следует автоматически, что a > b
.Декоратор
functools.total_ordering
создает все шесть методов на основе __eq__
и одного из следующих: __lt__
, __gt__
, __le__
или __ge__
.
from functools import total_ordering
@total_ordering
class User:
def __init__(self, pk, name):
self.pk = pk
self.name = name
def __le__(self, other):
return self.pk <= other.pk
def __eq__(self, other):
return self.pk == other.pk
assert User(2, 'Vadim') < User(13, 'Catherine')
👉@BookPython
👍4
В Python можно переопределить оператор квадратных скобок (
Вот так можно создать объект, который виртуально содержит бесконечное количество повторяющихся элементов:
Необычность здесь в том, что оператор
Обычно это интерпретируется как
Что же передаётся в параметр
Можно даже комбинировать кортежи и срезы:
Объект
👉@BookPython
[]
), реализовав магический метод __getitem__
.Вот так можно создать объект, который виртуально содержит бесконечное количество повторяющихся элементов:
class Cycle:
def __init__(self, lst):
self._lst = lst
def __getitem__(self, index):
return self._lst[index % len(self._lst)]
print(Cycle(['a', 'b', 'c'])[100]) # 'b'
Необычность здесь в том, что оператор
[]
поддерживает особый синтаксис. Его можно использовать не только так — [2]
, но и так — [2:10]
, [2:10:2]
, [2::2]
или даже [:]
.Обычно это интерпретируется как
[start:stop:step]
, но вы можете задать любую логику для своих объектов.Что же передаётся в параметр
index
метода __getitem__
, если использовать такой синтаксис? Для этого в Python существуют объекты slice
.
class Inspector:
def __getitem__(self, index):
print(index)
Inspector()[1]
# 1
Inspector()[1:2]
# slice(1, 2, None)
Inspector()[1:2:3]
# slice(1, 2, 3)
Inspector()[:]
# slice(None, None, None)
Можно даже комбинировать кортежи и срезы:
Inspector()[:, 0, :]
# (slice(None, None, None), 0, slice(None, None, None))
Объект
slice
сам по себе ничего не делает — он просто хранит атрибуты start
, stop
и step
:
s = slice(1, 2, 3)
s.start # 1
s.stop # 2
s.step # 3
👉@BookPython
👍5
Оператор
Python будет перебирать элементы генератора
Однако
У него переопределён магический метод
Имейте в виду, что это не работает для функции
👉@BookPython
in
может использоваться с генераторами: x in g
.Python будет перебирать элементы генератора
g
, пока не найдёт x
или пока генератор не исчерпает элементы.
>>> def g():
... print(1)
... yield 1
... print(2)
... yield 2
... print(3)
... yield 3
...
>>> 2 in g()
1
2
True
Однако
range()
делает для вас больше.У него переопределён магический метод
__contains__
, что позволяет оператору in
работать за O(1):
In [1]: %timeit 10**20 in range(10**30)
375 ns ± 10.7 ns per loop
Имейте в виду, что это не работает для функции
xrange()
в Python 2.👉@BookPython
👍1
Оператор in можно использовать с генераторами:
Python будет итерироваться по
Однако
У него переопределён магический метод
Имейте в виду, что это не работает для функции
👉@BookPython
x in g
.Python будет итерироваться по
g
, пока не найдёт x
или пока генератор не закончится.
>>> def g():
... print(1)
... yield 1
... print(2)
... yield 2
... print(3)
... yield 3
...
>>> 2 in g()
1
2
True
Однако
range()
делает для вас больше.У него переопределён магический метод
__contains__
, который позволяет оператору in
работать с O(1) сложностью:
In [1]: %timeit 10**20 in range(10**30)
375 ns ± 10.7 ns per loop
Имейте в виду, что это не работает для функции
xrange()
в Python 2.👉@BookPython
👍4❤1
Некоторый код, который вы используете, может выводить данные, представляющие для вас интерес, в
Вместо того чтобы рефакторить такой код, вы можете использовать менеджер контекста
Также доступен
👉@BookPython
stdout
, вместо того чтобы предоставлять какой-то API, пригодный для использования в программе (например, возвращать строку).Вместо того чтобы рефакторить такой код, вы можете использовать менеджер контекста
contextlib.redirect_stdout
, который позволяет временно перенаправить stdout
в любой объект, похожий на файл. В связке с io.StringIO
это позволяет захватывать вывод в переменную.
from contextlib import redirect_stdout
from io import StringIO
s = StringIO()
with redirect_stdout(s):
print(42)
print(s.getvalue())
Также доступен
contextlib.redirect_stderr
для перенаправления sys.stderr
.👉@BookPython
👍2🤔2
Символ
Можно использовать сырые строки (raw-strings), чтобы отключить это поведение.
Очевидно, что нельзя использовать
👉@BookPython
\
в обычной строке имеет специальное значение.\t
— это символ табуляции, \r
— возврат каретки и так далее.Можно использовать сырые строки (raw-strings), чтобы отключить это поведение.
r'\t'
— это просто обратный слэш и буква t
.Очевидно, что нельзя использовать
'
внутри r'...'
. Однако его всё ещё можно экранировать через \
, но при этом сам \
сохраняется в строке:
>>> print(r'It\'s insane!')
It\'s insane!
👉@BookPython
👍1
В списковых включениях (list comprehensions) может быть больше одного цикла
Кроме того, любое выражение внутри
Можно смешивать
👉@BookPython
for
и условия if
:
In : [(x, y) for x in range(3) for y in range(3)]
Out: [
(0, 0), (0, 1), (0, 2),
(1, 0), (1, 1), (1, 2),
(2, 0), (2, 1), (2, 2)
]
In : [
(x, y)
for x in range(3)
for y in range(3)
if x != 0
if y != 0
]
Out: [(1, 1), (1, 2), (2, 1), (2, 2)]
Кроме того, любое выражение внутри
for
и if
может использовать все переменные, которые были определены ранее:
In : [
(x, y)
for x in range(3)
for y in range(x + 2)
if x != y
]
Out: [
(0, 1),
(1, 0), (1, 2),
(2, 0), (2, 1), (2, 3)
]
Можно смешивать
if
и for
в любом порядке:
In : [
(x, y)
for x in range(5)
if x % 2
for y in range(x + 2)
if x != y
]
Out: [
(1, 0), (1, 2),
(3, 0), (3, 1), (3, 2), (3, 4)
]
👉@BookPython
👍6👎2✍1
Начиная с Python 3.5, стало возможно использовать распаковку в литералах словарей и списков.
Пример:
Для словарей эта форма ещё более мощная, чем функция
👉@BookPython
Пример:
In : {**{'a': 1}, 'b': 2, **{'c': 3}}
Out: {'a': 1, 'b': 2, 'c': 3}
In : [1, 2, *[3, 4]]
Out: [1, 2, 3, 4]
Для словарей эта форма ещё более мощная, чем функция
dict
, так как позволяет переопределять значения:
In : {**{'a': 1, 'b': 1}, 'a': 2, **{'b': 3}}
Out: {'a': 2, 'b': 3}
👉@BookPython
👍3✍1
В Python функция range() определяет все целые числа в полуоткрытом интервале.
То есть
или, говоря на языке Python: [2, 3, 4, 5, 6, 7, 8, 9].
Несмотря на асимметрию, это не ошибка и не случайность.
В этом есть логика: такой подход позволяет "склеивать" два соседних интервала без риска ошибиться на единицу:
[a, c) = [a, b) + [b, c)
Для сравнения, если бы использовались закрытые интервалы, получалось бы так:
[a, c] = [a, b] + [b+1, c]
Эта же идея объясняет, почему индексация начинается с нуля:
[0, N) содержит ровно N элементов.
Эдсгер Дейкстра написал на эту тему отличную статью ещё в 1982 году.
👉@BookPython
То есть
range(2, 10)
математически означает [2, 10),или, говоря на языке Python: [2, 3, 4, 5, 6, 7, 8, 9].
Несмотря на асимметрию, это не ошибка и не случайность.
В этом есть логика: такой подход позволяет "склеивать" два соседних интервала без риска ошибиться на единицу:
[a, c) = [a, b) + [b, c)
Для сравнения, если бы использовались закрытые интервалы, получалось бы так:
[a, c] = [a, b] + [b+1, c]
Эта же идея объясняет, почему индексация начинается с нуля:
[0, N) содержит ровно N элементов.
Эдсгер Дейкстра написал на эту тему отличную статью ещё в 1982 году.
👉@BookPython
👍13❤2✍1
itertools.tee()
создаёт несколько итераторов из одного. Это может быть полезно, если нескольким потребителям нужно читать один и тот же поток.Пример:
In : a, b, c = tee(iter(input, ''), 3)
In : next(a), next(c)
FIRST
Out: ('FIRST', 'FIRST')
In : next(a), next(b)
SECOND
Out: ('SECOND', 'FIRST')
In : next(a), next(b), next(c)
THIRD
Out: ('THIRD', 'SECOND', 'SECOND')
Данные, которые ещё не были использованы всеми итераторами, сохраняются в памяти. Если часть созданных итераторов ещё не была начата в тот момент, когда другой уже дошёл до конца, это означает, что все сгенерированные элементы будут храниться в памяти для будущего использования.
В таком случае проще и эффективнее использовать
list(iter(input, ''))
, чем tee
.👉@BookPython
👍2
Условное использование менеджеров контекста обычно доставляет неудобства: нельзя просто разместить
Способ борьбы с этой проблемой — использовать
Однако более очевидный способ достичь того же — использовать тривиальные менеджеры контекста, которые ничего не делают, когда они не нужны, вместо «настоящих». Начиная с Python 3.7, их можно получить с помощью
👉@BookPython
with
внутри блока if
, не заключив туда весь блок with
. Это часто приводит к дублированию кода:
def print_whole_file(
*,
path: Optional[str] = None,
file_obj: Optional[TextIO] = None
):
assert path or file_obj
if path:
with open(path) as f:
print(f.read(), end='')
else:
print(file_obj.read(), end='')
Способ борьбы с этой проблемой — использовать
ExitStack
и вызывать enter_context
внутри if
:
def print_whole_file(
*,
path: Optional[str] = None,
file_obj: Optional[TextIO] = None
):
assert path or file_obj
with ExitStack() as stack:
if path:
file_obj = stack.enter_context(
open(path)
)
print(file_obj.read(), end='')
Однако более очевидный способ достичь того же — использовать тривиальные менеджеры контекста, которые ничего не делают, когда они не нужны, вместо «настоящих». Начиная с Python 3.7, их можно получить с помощью
contextlib.nullcontext
:
def print_whole_file(
*,
path: Optional[str] = None,
file_obj: Optional[TextIO] = None
):
assert path or file_obj
if path:
context = open(path)
else:
context = nullcontext(file_obj)
with context as f:
print(f.read(), end='')
👉@BookPython
👍4❤2
Любая выполняющаяся корутина asyncio может быть отменена с помощью метода cancel(). В корутину будет выброшено исключение CancelledError, что приведёт к её завершению, а также завершению всех оборачивающих её корутин, если только ошибка не будет перехвачена и подавлена.
CancelledError является подклассом Exception, а значит, его можно случайно перехватить конструкцией
👉@BookPython
CancelledError является подклассом Exception, а значит, его можно случайно перехватить конструкцией
try ... except Exception
, которая предназначена для отлова «любых ошибок». Чтобы безопасно это обработать внутри корутины, приходится писать примерно так:
try:
await action()
except asyncio.CancelledError:
raise
except Exception:
logging.exception('action failed')
👉@BookPython
👍2❤1
Ты не можешь изменять переменные замыкания простым присваиванием.
Python рассматривает присваивание как определение локальной переменной внутри тела функции и вообще не делает замыкания.
Работает нормально, печатает
Вызывает
Чтобы это заработало, нужно использовать
Оно явно сообщает интерпретатору, что присваивание не создает новую локальную переменную, а работает с переменной из замыкания:
👉@BookPython
Python рассматривает присваивание как определение локальной переменной внутри тела функции и вообще не делает замыкания.
Работает нормально, печатает
2
:
def make_closure(x):
def closure():
print(x)
return closure
make_closure(2)()
Вызывает
UnboundLocalError: local variable 'x' referenced before assignment
:
def make_closure(x):
def closure():
print(x)
x *= 2
print(x)
return closure
make_closure(2)()
Чтобы это заработало, нужно использовать
nonlocal
.Оно явно сообщает интерпретатору, что присваивание не создает новую локальную переменную, а работает с переменной из замыкания:
def make_closure(x):
def closure():
nonlocal x
print(x)
x *= 2
print(x)
return closure
make_closure(2)()
👉@BookPython
👍3❤1✍1
Декоратор создаёт новый объект (обычно функцию), используя в качестве аргумента другую единственную функцию. Однако иногда хочется задать больше, чем одну функцию.
Сделать это напрямую невозможно из-за ограничений синтаксиса Python, но можно использовать небольшой трюк. Возвращаемая функция может содержать ещё один декоратор, который можно повторно применить к дополнительным функциям, добавляя новое поведение. Примерно так работает
Ниже приведён пример того, как можно определить функцию, которая использует дополнительные функции для особых случаев:
👉@BookPython
Сделать это напрямую невозможно из-за ограничений синтаксиса Python, но можно использовать небольшой трюк. Возвращаемая функция может содержать ещё один декоратор, который можно повторно применить к дополнительным функциям, добавляя новое поведение. Примерно так работает
@property
:
@property
def x(self):
return self._x
@x.setter
def x(self, value):
self._x = value
Ниже приведён пример того, как можно определить функцию, которая использует дополнительные функции для особых случаев:
from functools import wraps
def make_case_decorator(func):
def case_decorator(*case_decorator_args):
def decorator(special_case_func):
@wraps(func)
def decorated(*args):
if case_decorator_args == args:
return special_case_func(*args)
return func(*args)
decorated.case = make_case_decorator(decorated)
return decorated
return decorator
return case_decorator
def special_cases(func):
@wraps(func)
def decorated(*args):
return func(*args)
decorated.case = make_case_decorator(decorated)
return decorated
@special_cases
def fact(x):
return x * fact(x - 1)
@fact.case(0)
def fact(x):
return 1
@fact.case(10)
def fact(x):
print(f'(сработала оптимизация для {x})')
return 3628800
👉@BookPython
👍3❤1
Если вы хотите измерить время между двумя событиями, следует использовать time.monotonic() вместо time.time().
👉@BookPython
time.monotonic()
никогда не идёт назад, даже если системные часы были изменены:
from contextlib import contextmanager
import time
@contextmanager
def timeit():
start = time.monotonic()
yield
print(time.monotonic() - start)
def main():
with timeit():
time.sleep(2)
main()
👉@BookPython
👍5❤1
Проблема при вызове
Чтобы легко решить эту проблему, можно использовать декоратор
Теперь всё работает корректно:
👉@BookPython
repr
для других объектов внутри собственного метода __repr__
заключается в том, что нельзя гарантировать, что ни один из этих объектов не равен self
, и вызов не окажется рекурсивным:
In : p = Pair(1, 2)
In : p
Out: Pair(1, 2)
In : p.right = p
In : p
Out: [...]
RecursionError: maximum recursion depth exceeded while calling a Python object
Чтобы легко решить эту проблему, можно использовать декоратор
reprlib.recursive_repr
:
@reprlib.recursive_repr()
def __repr__(self):
class_name = type(self).__name__
return f'{class_name}({self.left!r}, {self.right!r})'
Теперь всё работает корректно:
In : p = Pair(1, 2)
In : p.right = p
In : p
Out: Pair(1, ...)
👉@BookPython
👍1
Словари, которые используются для хранения атрибутов объектов, не такие же, как те, что вы создаёте с помощью
Чтобы уменьшить потребление памяти, словари для
👉@BookPython
dict
, хотя выглядят они абсолютно одинаково:
>>> from sys import getsizeof
>>> class A:
... pass
...
>>> a = dict()
>>> b = A().__dict__
>>> type(a)
<class 'dict'>
>>> type(b)
<class 'dict'>
>>> a
{}
>>> b
{}
>>> getsizeof(a)
240
>>> getsizeof(b)
112
Чтобы уменьшить потребление памяти, словари для
__dict__
реализованы иначе. Они разделяют ключи между всеми экземплярами класса A
. Однако важно понимать, что b
на самом деле не меньше, чем a
, - это просто особенность работы getsizeof
.👉@BookPython
✍4👍1