Подборка @pythonetc, сентябрь 2019

qnjflgykxufbpp0_ggzzn1sr-pc.jpeg

Новая подборка советов про Python и программирование из моего авторского канала @pythonetc.

← Предыдущие подборки

a8a6a2973bc80d523fcb0f0698202a88.png


В asyncio цикл (loop) не обязан быть запущенным, чтобы содержать задачи (tasks). Вы можете создавать и останавливать задачи даже при остановленном цикле. Если он остановлен, некоторые задачи могут так и остаться незавершёнными.

import asyncio


async def printer():
    try:
        try:
            while True:
                print('*')
                await asyncio.sleep(1)
        except asyncio.CancelledError:
            print('х')
    finally:
        await asyncio.sleep(2)
        print('о')  # never happens


loop = asyncio.get_event_loop()
run = loop.run_until_complete
task = loop.create_task(printer())

run(asyncio.sleep(1))  # printer works here
print('||')

run(asyncio.sleep(1))  # printer works here
task.cancel()  # nothing happens
run(asyncio.sleep(1))  # х printed


Результат:

*
*
||
*
х


Удостоверьтесь, что вы дождались завершения всех задач, прежде чем остановить цикл. Если этого не сделать, то вы можете пропустить какие-то блоки finally и некоторые контекстные менеджеры не будут отключены.

2081cc1fa6dbebbbb6ddd4108512ff5b.png


Python позволяет переопределять многие операторы, в том числе оператор побитового сдвига. Вот пример создания композиции функций с помощью этого оператор. Стрелки обозначают направление передачи данных:

from collections import deque
from math import sqrt


class Compose:
    def __init__(self):
        self._functions = deque()

    def __call__(self, *args, **kwargs):
        result = None
        for f in self._functions:
            result = f(*args, **kwargs)
            args = [result]
            kwargs = dict()
        return result

    def __rshift__(self, f):
        self._functions.append(f)
        return self

    def __lshift__(self, f):
        self._functions.appendleft(f)
        return self


compose = Compose


sqrt_abs = (compose() << sqrt << abs)
sqrt_abs2 = (compose() >> abs >> sqrt)

print(sqrt_abs(-4))  # 2.0
print(sqrt_abs2(-4))  # 2.0


2081cc1fa6dbebbbb6ddd4108512ff5b.png


При определении класса можно передавать аргументы в его метакласс. Нотация class поддерживает ключевые слова в качестве аргументов: class Klass(Parent, arg='arg'). Ключевое слово metaclass зарезервировано для выбора метакласса, а другие вы можете использовать по своему усмотрению.

Вот пример метакласса, создающего класс без одного из атрибутов. Название атрибута предоставлено в аргументе remove:

class FilterMeta(type):
    def __new__(mcs, name, bases, namespace, remove=None, **kwargs):
        if remove is not None and remove in namespace:
            del namespace[remove]

        return super().__new__(mcs, name, bases, namespace)


class A(metaclass=FilterMeta, remove='half'):
    def half(x):
        return x // 2

    half_of_4 = half(4)
    half_of_100 = half(100)


a = A()
print(a.half_of_4)  # 2
print(a.half_of_100)  # 50
a.half  # AttributeError


2081cc1fa6dbebbbb6ddd4108512ff5b.png


Иногда нужно исчерпать генератор, но при этом вам интересуют не создаваемые им значения, а какие-то побочные эффекты. Например, исключение, запись в файл, изменение глобальной переменной и т.д.

Для этого есть удобный и популярный способ list(gen()). Однако этот способ сохраняет все значения в память, а потом сразу их удаляет. Это может быть излишним. Если хотите избежать такого поведения, можете использовать deque с ограничением размера:

from collections import deque

def inversed(nums):
    for num in nums:
        yield 1 / num

try:
    deque(inversed([1, 2, 0]), maxlen=0)
except ZeroDivisionError:
    print('E')
Ради семантической точности можно определить собственную функцию exhaust:


def exhaust(iterable):
    for _ in iterable:
        pass


2081cc1fa6dbebbbb6ddd4108512ff5b.png


Допустим, у вас есть пара классов — родительский дочерний, User и Admin. И ещё у вас есть функция, берущая в качестве аргумента список пользователей. Вы можете предоставить список админов? Нет: функция может добавить ещё одного пользователя в список админов, который является ошибочным и нарушает предоставляемые списком гарантии.

Однако вы можете предоставить тип Sequence, потому что он доступен только для чтения. Точнее, в данном случае Sequence является ковариантным по типу участников.

Можно определять ковариантные типы, предоставляя covariant=True в качестве аргумента для TypeVar:

from typing import TypeVar, Generic

T = TypeVar('T', covariant=True)


class Holder(Generic[T]):
    def __init__(self, var: T):
        self._var: T = var

    def get(self) -> T:
        return self._var


class User:
    pass


class Admin(User):
    pass


def print_user_from_holder(holder: Holder[User]) -> None:
    print(holder.get())


h: Holder[Admin] = Holder(Admin())
print_user_from_holder(h)


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

from typing import TypeVar, Generic

T = TypeVar('T', contravariant=True)


class Holder(Generic[T]):
    def __init__(self, var: T):
        self._var: T = var

    def change(self, x: T):
        self._var = x


class User:
    pass


class Admin(User):
    pass


def place_admin_to_holder(holder: Holder[Admin]) -> None:
   holder.change(Admin())


h: Holder[User] = Holder(User())
place_admin_to_holder(h)


Классы, не являющиеся ни ковариантными, ни контравариантными, называются инвариантными.

© Habrahabr.ru