Библиотека Python разработчика | Книги по питону – Telegram
Библиотека Python разработчика | Книги по питону
18.7K subscribers
1.07K photos
403 videos
82 files
1.09K links
Погружение в CPython и архитектуру. Разбираем неочевидное поведение (GIL, Memory), Best Practices (SOLID, DDD) и тонкости Django/FastAPI. Решаем задачи с подвохом и оптимизируем алгоритмы. 🐍

По всем вопросам @evgenycarter

РКН clck.ru/3Ko7Hq
Download Telegram
В Python очень короткий список встроенных констант. Одна из них — Ellipsis, которую также можно записать как .... Эта константа не имеет особого значения для интерпретатора, но используется в местах, где такой синтаксис выглядит уместно.

Библиотека NumPy поддерживает Ellipsis в качестве аргумента __getitem__, например: x[...] возвращает все элементы массива x.

PEP 484 придаёт Ellipsis дополнительное значение: Callable[..., type] — это способ определить тип вызываемых объектов без указания типов аргументов.

Наконец, ... можно использовать, чтобы обозначить, что функция ещё не реализована. Это абсолютно валидный код на Python:


def x():
...


📲 Мы в MAX

👉@BookPython
👍4
Чтобы сохранить любую информацию в памяти или на устройстве хранения, её необходимо представить в виде байтов. Python, как правило, предоставляет уровень абстракции, при котором вы оперируете непосредственно данными, а не их байтовым представлением.

Тем не менее, когда вы, скажем, записываете строку в файл, вы работаете с физической структурой данных. Чтобы поместить символы в файл, их нужно преобразовать в байты — это называется кодированием. Когда вы получаете байты из файла, скорее всего, вы захотите превратить их в осмысленные символы — это называется декодированием.

Существует сотни методов кодирования. Самый популярный — вероятно, Unicode, но он сам по себе не может преобразовывать данные в байты. В смысле байтового представления Unicode вообще не является кодировкой. Unicode определяет соответствие между символами и их числовыми кодами. Например, 🐍 имеет код 128013.

Но чтобы записать числа в файл, нужна настоящая кодировка. Unicode обычно используется вместе с utf-8, которая (в большинстве случаев) является стандартной в Python. При чтении из файла Python автоматически декодирует utf-8. Вы можете выбрать любую другую кодировку с помощью параметра encoding= функции open, либо читать «сырые» байты, добавив b к режиму открытия файла.

📲 Мы в MAX

👉@BookPython
👍3
Если вы создаёте новые объекты внутри метода __init__, возможно, будет лучше передавать их как аргументы и использовать фабричный метод. Это позволяет разделить бизнес-логику и технические детали создания объектов.

В этом примере __init__ принимает host и port для создания подключения к базе данных:


class Query:
def __init__(self, host, port):
self._connection = Connection(host, port)


Возможный рефакторинг:


class Query:
def __init__(self, connection):
self._connection = connection

@classmethod
def create(cls, host, port):
return cls(Connection(host, port))


Такой подход имеет как минимум следующие преимущества:

• Упрощает внедрение зависимостей. В тестах можно использовать Query(FakeConnection()).
• Класс может иметь столько фабричных методов, сколько нужно; подключение может создаваться не только по host и port, но и путём клонирования другого подключения, чтения конфигурационного файла или объекта, использования значения по умолчанию и т.д.
• Такие фабричные методы можно сделать асинхронными; это невозможно для __init__.

📲 Мы в MAX

👉@BookPython
👍5
Сегодня я покажу вам простой, но очень полезный приём, который часто выручает при работе с Python-скриптами — автоматическое логирование вызовов функций с помощью декоратора.

Иногда, особенно в отладке, хочется видеть, какие функции вызываются, с какими аргументами и что они возвращают. Не писать же в каждую вручную print()? Вот тут и приходит на помощь наш герой — универсальный логгер-декоратор:


import functools

def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"[CALL] {func.__name__} args={args} kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"[RETURN] {func.__name__} -> {result}")
return result
return wrapper


Пример использования:


@log_calls
def multiply(a, b):
return a * b

multiply(3, 5)


📌 Вывод:

[CALL] multiply args=(3, 5) kwargs={}
[RETURN] multiply -> 15


Такой декоратор можно подключить временно на любую функцию — и сразу видеть, что происходит у вас в коде. Это особенно удобно при работе со сторонними библиотеками или когда вы разбираетесь в чужом проекте.

Кстати, с небольшими изменениями можно направить вывод не в print(), а в logging, или даже сохранять в файл — по вкусу.

Пользуетесь такими декораторами? Или у вас свой лайфхак?

📲 Мы в MAX

👉@BookPython
👍1
Одна и та же строка может быть представлена по-разному в Unicode, и стандарт это учитывает. Он определяет два типа эквивалентности: последовательности могут быть канонически эквивалентными или совместимыми.

Канонически эквивалентные последовательности выглядят одинаково, но содержат разные кодовые точки. Например, символ ö может быть представлен как LATIN SMALL LETTER O WITH DIAERESIS (U+00F6) или как комбинация из o и диакритического знака: LATIN SMALL LETTER O (U+006F) + COMBINING DIAERESIS (U+0308).

Совместимые последовательности выглядят по-разному, но могут трактоваться одинаково с точки зрения смысла, например, ff и ff.

Для каждого из этих типов эквивалентности можно нормализовать строку в Unicode, сжимая или расширяя последовательности. В Python для этого используется модуль unicodedata:


import unicodedata

modes = [
# Сжать канонически эквивалентные
'NFC',
# Расширить канонически эквивалентные
'NFD',
# Сжать совместимые
'NFKC',
# Расширить совместимые
'NFKD',
]

s = 'ff + ö'

for mode in modes:
norm = unicodedata.normalize(mode, s)
print('\t'.join([
mode,
norm,
str(len(norm.encode('utf8'))),
]))


Результат:

NFC ff + ö 8
NFD ff + ö 9
NFKC ff + ö 7
NFKD ff + ö 8


📲 Мы в MAX

👉@BookPython
👍2🤯1
Обычно вы взаимодействуете с генератором, запрашивая данные с помощью next(gen). В Python 3 вы также можете отправлять значения обратно в генератор с помощью g.send(x). Но существует техника, которой вы, вероятно, не пользуетесь каждый день, а возможно, и вовсе не знаете: выбрасывание исключений внутри генератора.

С помощью gen.throw(e) можно выбросить исключение в той точке, где генератор gen приостановлен — то есть на инструкции yield. Если генератор обрабатывает это исключение, gen.throw(e) возвращает следующее значение, полученное через yield (или выбрасывает StopIteration, если генератор завершён). Если генератор не перехватывает исключение, оно пробрасывается обратно к вызывающему коду.


def gen():
try:
yield 1
except ValueError:
yield 2

g = gen()

next(g)
# Out: 1

g.throw(ValueError)
# Out: 2

g.throw(RuntimeError('TEST'))
# RuntimeError: TEST


Эта техника позволяет более точно управлять поведением генератора — не только передавать данные внутрь, но и, например, сообщать о проблемах со значениями, полученными через yield. Однако такие случаи бывают редко, и встретить g.throw в дикой природе почти невозможно.

Тем не менее, декоратор @contextmanager из модуля contextlib использует именно такую технику, позволяя коду внутри контекста перехватывать исключения.


from contextlib import contextmanager

@contextmanager
def atomic():
print('BEGIN')

try:
yield
except Exception:
print('ROLLBACK')
else:
print('COMMIT')

with atomic():
print('ERROR')
raise RuntimeError()



BEGIN
ERROR
ROLLBACK


📲 Мы в MAX

👉@BookPython
2👍1
Стандартный механизм расширения путей в оболочке называется globbing. Шаблоны, которые вы используете для сопоставления путей, называются globs.


$ echo /li*
/lib /lib64


Python поддерживает globbing с помощью модуля glob. Однако есть важное замечание: оболочка возвращает сам шаблон, если файлы не найдены, а Python — нет:


$ echo /zz**
/zz**
$ python -c 'from glob import glob; print(glob("/zz**"))'
[]


📲 Мы в MAX

👉@BookPython
3👍1
Функция super() позволяет обращаться к родительскому (базовому) классу. Это может быть очень полезно в случаях, когда производный класс хочет добавить что-то к реализации метода, а не полностью переопределять его:


class BaseTestCase(TestCase):
def setUp(self):
self._db = create_db()

class UserTestCase(BaseTestCase):
def setUp(self):
super().setUp()
self._user = create_user()


Имя функции super не означает "отличный" или "очень хороший". В данном контексте слово super означает "выше" (как, например, в слове superintendent — заведующий). Несмотря на это, super() не всегда ссылается на базовый класс — он может вернуть и "соседний" класс. Более точным названием была бы, возможно, функция next(), так как возвращается следующий класс согласно цепочке разрешения методов (MRO — Method Resolution Order).

Пример:


class Top:
def foo(self):
return 'top'

class Left(Top):
def foo(self):
return super().foo()

class Right(Top):
def foo(self):
return 'right'

class Bottom(Left, Right):
pass

# выводит 'right'
print(Bottom().foo())


Обрати внимание: результат работы super() может отличаться в зависимости от MRO вызвавшего объекта.


>>> Bottom().foo()
'right'
>>> Left().foo()
'top'


📲 Мы в MAX

👉@BookPython
Please open Telegram to view this post
VIEW IN TELEGRAM
👍32
Функция map вызывает другую функцию для каждого элемента итерируемого объекта. Это значит, что функция должна принимать одно значение в качестве аргумента:


In : list(map(lambda x: x ** 2, [1, 2, 3]))
Out: [1, 4, 9]


Однако если каждый элемент итерируемого объекта — это кортеж, было бы удобно передавать каждый элемент кортежа как отдельный аргумент. В Python 2 это было возможно благодаря распаковке параметров кортежа (обратите внимание на скобки):


>>> map(lambda (a, b): a + b, [(1, 2), (3, 4)])
[3, 7]


В Python 3 эта возможность исчезла, но есть другое решение — itertools.starmap. Она распаковывает кортежи за вас, будто функция вызывается со звёздочкой: f(*arg) (отсюда и название функции):


from itertools import starmap

In [3]: list(starmap(lambda a, b: a + b, [(1, 2), (3, 4)]))
Out[3]: [3, 7]


📲 Мы в MAX

👉@BookPython
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Когда вы используете модуль multiprocessing, и в одном из процессов происходит исключение, оно передаётся в основную программу с помощью механизма сериализации (pickling). Исключение сериализуется, передаётся в другой процесс и там десериализуется обратно.

Однако сериализация исключений может быть непростой задачей. Исключение создаётся с любым количеством аргументов, которые сохраняются в атрибуте args. Эти же аргументы используются при десериализации для воссоздания объекта исключения.

Но это может не сработать так, как вы ожидаете, особенно если используется наследование. Посмотрите на пример:


import pickle

class TooMuchWeightError(Exception):
def __init__(self, weight):
super().__init__()
self._weight = weight

pickled = pickle.dumps(TooMuchWeightError(42))
pickle.loads(pickled)


Вызов TooMuchWeightError.__init__ приводит к вызову Exception.__init__, который устанавливает args как пустой кортеж. Этот пустой кортеж затем используется в качестве аргументов при десериализации, что, очевидно, приводит к ошибке:


TypeError: __init__() missing 1 required positional argument: 'weight'


Обходное решение — либо вообще не вызывать super().__init__() (что обычно считается плохой практикой при наследовании), либо передавать все аргументы явно в конструктор родительского класса:


class TooMuchWeightError(Exception):
def __init__(self, weight):
super().__init__(weight)
self._weight = weight


📲 Мы в MAX

👉@BookPython
Please open Telegram to view this post
VIEW IN TELEGRAM
4👍2
Python предоставляет мощную библиотеку для работы с датой и временем — datetime. Интересный момент: объекты datetime имеют специальный интерфейс для поддержки часовых поясов (атрибут tzinfo), однако сама библиотека `datetime реализует его лишь частично, оставляя остальную работу сторонним модулям.

Самым популярным модулем для этой задачи является pytz. Хитрость в том, что pytz не полностью соответствует интерфейсу tzinfo. В документации pytz прямо указано с самого начала: «Эта библиотека отличается от задокументированного API Python для реализаций 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, может использоваться напрямую, не требует normalize, хотя и работает немного медленнее.

Если вам интересно, почему pytz не поддерживает API datetime, или вы хотите увидеть больше примеров, обязательно почитайте хорошую статью на эту тему.

📲 Мы в MAX

👉@BookPython
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1