Я люблю питон, и вот почему он меня бесит

в 5:36, , рубрики: python, господи за что, КодоБред, недостатки, ненормальное программирование, Питон, погромист, Программирование, Совершенный код, язык программирования, язык программирования python
Я люблю питон, и вот почему он меня бесит - 1

Вас приветствует ваш зануда!

Если вы следите за моей ленивой активностью, то заметили бы, что у меня много от чего пригорает. Вот, например:

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

GIL и скорость

А вот и нет! Здесь я не буду говорить про GIL и скорость. Шах и мат, хейтеры питона 😎 Потому что - серьёзно - для меня это никогда не было проблемой и к тому же можно перекладывать вину на медленный питон. У меня уже лет 5 под боком лежит nim, в котором я могу за минуты накидать и скомпилировать нативный супер-быстрый по-настоящему многопоточный код. И я даже могу запустить его из питона и наоборот. Сколько раз я этим воспользовался? Правильно, 0. Такое вот сугубо личное оправдание питона. Ну и nogil когда-нибудь прилетит в наше царство. Но мы же собрались сегодня не за этим, да?

Генераторы

Ну почти такие, да
Ну почти такие, да

Кто не использует генераторы в питоне, тот ещё юнец! Генераторы были придуманы, чтобы не писать скобки в вызовах функций:

# Красиво? Красиво!
sorted(element.value for element in elements)

# ... хотя стоит добавить аргумент, и скобки становятся обязательными. Какого хрена?!
sorted((element.value for element in elemens), key=attrgetter('attr'))

А, чуть не забыл - генераторы ещё экономят память, потому что не вываливают все результаты целиком, а генерируют элементы на лету. Поэтому в памяти хранится не вся коллекция, а только текущее состояние генератора.

Теперь из неприятного...

Генераторы откладывают выполнение кода

Поэтому заранее знать, где код будет выполнен - та ещё задача. Вот пример из прошлой статьи:

try:
	quota_chunks = quota_cache.apply(quota_chunks)
except InconsistentQuotaCache:
	log.error('Something went wrong')
	raise

# А InconsistentQuotaCache выкинулось на вот этой строчке! Ха-ха!
first_quota_chunks, quota_chunks = spy(quota_chunks, 1)

Очень мало контекста

В одном проекте у меня было два бесконечных потока данных, и их нужно было слить в один по возрастанию номеров. Типа a1, a2, a5, a6, ... + b2, b4, b5, b8, ... -> a1, a2, b2, b4, a5, b5, a6, b8, .... Генератор A и генератор B подаются на вход генератору-сливатору, и он сам куда-то там дальше передаётся. Это я вам расписал a1, a2, b2 и вроде всё понятно, но в реальности видно только одно значение, и на вопросы "откуда оно пришло", "что было раньше" и "что будет дальше" не так уж легко ответить. Сравните:

A = ['a1', 'a2']
B = ['b2', 'b4']
merged = sorted(A + B, key=lambda item: int(item[1:]))

print('Вот всё что есть:', merged)

for i, item in enumerate(merged):
	print('Было:', merged[:i], 'Щас:', item, 'Будет:', merged[i+1:])

print('Давай ещё раз Настя!')
for i, item in enumerate(merged):
	print('Было:', merged[:i], 'Щас:', item, 'Будет:', merged[i+1:])

# Вот всё что есть: ['a1', 'a2', 'b2', 'b4']
# Было: [] Щас: a1 Будет: ['a2', 'b2', 'b4']
# Было: ['a1'] Щас: a2 Будет: ['b2', 'b4']
# Было: ['a1', 'a2'] Щас: b2 Будет: ['b4']
# Было: ['a1', 'a2', 'b2'] Щас: b4 Будет: []
# Давай ещё раз Настя!
# Было: [] Щас: a1 Будет: ['a2', 'b2', 'b4']
# Было: ['a1'] Щас: a2 Будет: ['b2', 'b4']
# Было: ['a1', 'a2'] Щас: b2 Будет: ['b4']
# Было: ['a1', 'a2', 'b2'] Щас: b4 Будет: []

vs

A = ('a1', 'a2')
B = ('b2', 'b4')
            
merged = merge_iter(A, B, key=lambda item: int(item[1:]))

print('Вот всё что есть:', merged)

for i, item in enumerate(merged):
    print('Было:', 'хз', 'Щас:', item, 'Будет:', 'хз')

print('Давай ещё раз Настя!')
for i, item in enumerate(merged):
    print('Было:', 'хз', 'Щас:', item, 'Будет:', 'хз')

# Вот всё что есть: <generator object merge_iter at 0x7f22c81cac70>
# Было: хз Щас: a1 Будет: хз
# Было: хз Щас: a2 Будет: хз
# Было: хз Щас: b2 Будет: хз
# Было: хз Щас: b4 Будет: хз
# Давай ещё раз Настя!

Как отлаживать

Можно только один раз войти в ту же реку в тот же генератор. В примере выше генератор успешно исчерпал себя, а при повторном проходе не упал, а просто не вернул ничего. Разве это не эпично?

Я несколько раз попадался на ошибке, когда пытался пройтись по генератору второй раз. Иногда это были мои генераторы, а иногда какой-то умник решал, что в библиотеке всё должно быть memory-efficient, и там, где я ожидал функцию, меня ждал генератор. Они, блин, внешне никак не отличаются!

Ну и отлаживать это так лихо не получится. Смотрите в отладчик, а там у переменной значение <generator object fuck_you at 0x7f22c81cac70> - и чо с этим делать? Если просто посмотреть содержимое, то он исчерпается, и нужно будет его восстанавливать.

В общем, неудобно.

Никто не знает, что происходит

Питон слишком... динамический, что ли.

Например, я могу импортировать модуль прям в теле функции:

def foo():
    from bar import baz
	baz()

С одной стороны, это зачастую позволяет избежать рекурсии при импорте модулей, с другой - можно легко отложить импорт в рантайм. Поэтому если в модуле bar что-то плохо, то узнаем мы это только в момент импорта, то есть в момент вызова foo(), когда приложение уже работает. Получать ошибки в рантайме - это последнее, что вы хотите.

Ещё можно импортировать что угодно когда угодно. В примере ниже ползём по файловой системе и импортируем всё, что попадается под руку, в глобальную область видимости. Очень гибко, но и очень неочевидно. IDE в шоке!

from importlib import import_module
from pathlib import Path

for file in Path(__file__).parent.iterdir():
    if not (name := file.stem).startswith('_'):
        module = import_module(f'.{name}', __package__)
        symbols = [symbol for symbol in module.__dict__ if not symbol.startswith('_')]
        globals().update({symbol: getattr(module, symbol) for symbol in symbols})

Ещё я при импорте модуля могу выполнять какой-то код - например, если в модуле есть глобальная переменная:

def load_huge_table() -> pd.DataFrame:
    return pd.read_excel('data_1000000000_rows.xlsx')

HUGE_TABLE = load_huge_table()

Получается, что у импорта модулей может быть side effect, и мне, как любителю всего простого, это совсем не по душе.

А ещё я в любой момент времени могу почти любому объекту добавить атрибут. Я помню пару раз, когда я добавлял объекту или классу "несуществующий" атрибут. Рубрика "найди ошибку":

@app.task
def terminate_stale_membership():
    """ If there's no membership payment for a while, stop trying to charge. """

    stale_period = timedelta(days=60)
    for premium in Premium.objects.active().not_paid():
        log.debug(premium)
        last_payment = premium.payments.filter(paid__isnull=False).latest('end')
        now_ = now()
        if not last_payment or last_payment.end <= now_ - stale_period:
            premium.ends = now_
            premium.notes = f'[auto-disabled as stale @ {now_}]'
            premium.save()
            log.info('Disabled premium renewal for stale membership intent: %s', premium)

Тут вместо premium.ends = now_ должно было быть premium.end = now_. В статически типизированном языке компилятор мне бы всыпал за такие выкрутасы, а в питоне - пожалуйста, встретимся в рантайме!

Можно переопределить присвоение атрибута:

class MyClass:
    def __setattr__(self, attr, value):
        print('Nope!')

obj = MyClass()
obj.a = 1  # Nope!
obj.a  # AttributeError: 'MyClass' object has no attribute 'a'

Смотрите, теперь вы не знаете, что произойдёт при присваивании!

Что ещё можно сделать в питоне? Какие-то динамические фабрики. Или просто непознаваемые разумом ващи.

В общем, питон - динамический. Очень динамический. Даже слишком. По моему опыту, в 99% случаев мне эта динамика вообще не впёрлась - я знаю, какие где типы ожидаются и какие атрибуты у моих классов, но я всё равно плачу за "гибкость" питона. Плачу скоростью выполнения кода и количеством ошибок.

Питон что-то пытается. Есть type hints, которые добавляют подсказки IDE и разработчку (но на этом всё), если @dataclass(slots=True), который сделает класс немного "строже" (но типы всё равно не валидирует, ха-ха), есть pydantic, который старается проверять типы, но в целом разница такая: есть "строгие" языки, в которых чтобы сделать хрень - надо постараться, а есть "ХХиВП-языки", где нужно постараться, чтобы не сделать хрень. Питон, к сожалению, из последних.

Если вам интересны языки, которые и строгие, и гибкие, то поглядите хотя бы на nim - хотя у него тоже есть проблемы, правда, другого рода.

Mutable defaults

У меня горит! Нельзя просто взять и задать, скажем, пустой список как значение по умолчанию для аргумента:

def foo(items: list[str] = []):
    items.append(1)
    print(items)

foo()  # [1]
foo()  # [1, 1]
foo()  # [1, 1, 1]

Значение по умолчанию создаётся в единственном экземпляре, так что если вы планируете модифицировать аргумент, то добро пожаловать в паттерн "я хочу mutable default":

def foo(items: list[str] | None = None):
    items = items or []  # или if items is None: items = []
    items.append(1)
    print(items)

foo()  # [1]
foo()  # [1]
foo()  # [1]

В датаклассах это выглядит ещё ужаснее, только взгляните:

from dataclasses import dataclass, field

@dataclass
class Foo:
    items: list[str] = field(default_factory=list)  # <-- и всё это - чтобы по умолчанию был пустой список

Сравните это с пидантиком, в котором, кажется, думают о людях:

from pydantic import BaseModel

class Foo(BaseModel):
    items: list[str] = []

Недавно я осознал, что если функция "чистая", то есть не модифицирует входные аргументы, то такой код абсолютно нормальный:

def foo(var: int, checks: list[Callable] = []):
	for check in checks:
		check(var)

Но есть несколько "но":

  1. Линтеру это может не понравиться

  2. Кто-нибудь решит, что ему хочется изменять список в момент выполнения функции, и всё сломается

  3. Другой разраб может это увидеть и не понять, прям как в меме:

Я люблю питон, и вот почему он меня бесит - 3

Нет const

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

Аргументы

Я люблю, когда аргументы задают по имени:

call(me='maybe')

Сразу понятно, что, кого и как.

Я также не против позиционных аргументов, когда это просто, ну например:

max([1, 2, 3, 4, 5])

Но в целом именные аргументы (kwargs) всегда лучше позиционных (args):

  • При рефакторинге ничего не поломается: я могу менять местами и добавлять аргументы, и всё будет работать

  • Лучше читается, даже если вы называете ваши переменные как гоблин: сравните display(hehe, trololo) vs display(num_per_page=hehehe, items=trololo) - всё равно второй вариант понятнее

Но теперь в питоне придумали * и /, чтобы запретить юзать args или kwargs! Я постарался проследить логику, но не смог:

lst = [{'name': 'Bob'}, {'name': 'Alice'}]

lst.sort(itemgetter('name'))  # TypeError: sort() takes no positional arguments
lst.sort(key=itemgetter('name'))  # works

sorted(lst, itemgetter('name'))  # TypeError: sorted expected 1 argument, got 2
sorted(lst, key=itemgetter('name'))  # works
sorted(iterable=lst, key=itemgetter('name'))  # TypeError: sorted expected 1 argument, got 0

list(map(str.upper, 'abc'))  # works
list(map(function=str.upper, iterable='abc'))  # TypeError: map() takes no keyword arguments

open('docker-compose.yml')  # works
open(file='docker-compose.yml')  # works

Я не понимаю, почему где-то мне можно использовать имена аргументов, а где-то нельзя, и почему в разных случаях по-разному. Можно, пожалуйста, я буду писать так, как считаю нужным?

Странное legacy

Multiprocessing thread pool

Как запустить что-то в потоке и вывести результат?

  1. Ну, есть ThreadPoolExecutor - красиво:

from concurrent.futures import ThreadPoolExecutor

fn = lambda: 5
with ThreadPoolExecutor() as pool:
    future = pool.submit(fn)
    print(future.result())
  1. Но тут вы можете сказать: это слишком просто! Давай, напиши что-нибудь по-джуновски! Вот, получайте - куча бойлерплата, чтобы окостылить класс Thread возвращаемым значением:

from threading import Thread

class ThreadWithReturnValue(Thread):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._return = None
 
    def run(self):
        if self._target is not None:
            self._return = self._target(*self._args, **self._kwargs)
             
    def join(self, *args):
        super().join(*args)
        return self._return
        

fn = lambda: 5
thread = ThreadWithReturnValue(target=fn)
thread.start()
result = thread.join()
print(result)
  1. Казалось бы, вот вам low-level Thread, вот вам high-level ThreadPoolExecutor - юзай что нравится. Но вдруг вы фанат процессов и жить без них не можете? Ха, питон позаботился о вас, ведь вы можете работать с потоками при помощи своего любимого модуля multiprocessing:

from multiprocessing.pool import ThreadPool  # чего бл***?!
pool = ThreadPool(processes=2)  # ну реально, пацаны, это чо?

fn = lambda: 5
async_result_f = pool.apply_async(fn)  # ну и async можно добавить, чё уж там
print(async_result_f.get())  # и ещё get(), как будто это словарь :D
"Сударь, этот код кажется мне удивительным!"

"Сударь, этот код кажется мне удивительным!"

Вроде это типа deprecated, но работает и никаких warnings...

Названия

У нас тут питон! Поэтому классы пишем так: class MySuperClass. А переменные пишем так: my_super_variable = 1.

Но есть ещё, например, модуль logging, ему всё можно:

import logging

logger = logging.getLogger(__name__)  # почему не get_logger?!
logging.basicConfig()  # почему не basic_config?
logging.setLevel(level)  # почему не set_level...

Ладно, с logging всё понятно, решили просто делать всё в camelCase, чтобы программировать было интересней. А вот с csv не определились:

import csv

reader = csv.DictReader(file)  # название класса, поэтому с большой буквы
reader = csv.reader(file)  # этот просто функция, поэтому с маленькой буквы, но назовём её как будто это класс

Ещё раз, именование::

get_dialect  # эта функция возвращает диалект
list_dialects  # эта функция возвращает список диалектов
reader  # эта функция возвращает читалку csv файлов ¯_(ツ)_/¯

Порядок аргументов

Курица или яйцо? В смысле, функция или коллекция?

map(lambda x: x+1, [1, 2, 3])  # function, collection
sorted([1, 2, 3], key=lambda x: x+1)  # collection, function
filter(lambda x: x > 0, [0, 1, 2])  # function, collection
max([0, 1, 2], key=lambda x: x = 1)  # collection, function

Calendar

Этот модуль точно был вдохновлён php с его функциями на все случаи жизни:

(https://docs.python.org/3/library/calendar.html)

itermonthdates(year, month)¶
Return an iterator for the month month (1–12) in the year year. This iterator will return all days (as datetime.date objects) for the month and all days before the start of the month or after the end of the month that are required to get a complete week.

itermonthdays(year, month)
Return an iterator for the month month in the year year similar to itermonthdates(), but not restricted by the datetime.date range. Days returned will simply be day of the month numbers. For the days outside of the specified month, the day number is 0.

itermonthdays2(year, month)
Return an iterator for the month month in the year year similar to itermonthdates(), but not restricted by the datetime.date range. Days returned will be tuples consisting of a day of the month number and a week day number.

itermonthdays3(year, month)
Return an iterator for the month month in the year year similar to itermonthdates(), but not restricted by the datetime.date range. Days returned will be tuples consisting of a year, a month and a day of the month numbers.

itermonthdays4(year, month)
Return an iterator for the month month in the year year similar to itermonthdates(), but not restricted by the datetime.date range. Days returned will be tuples consisting of a year, a month, a day of the month, and a day of the week numbers.

Жду itermonthdays5.

Async

У меня прям горит синим пламенем!

Я как-то достаточно долго обходился без асинхронного кода и жил счастливо, пока не решил написать своего телеграм-бота. Синхронный код большого количества пользователей не вывозит, потоков не насоздаёшься вдоволь, а вот асинхронщина - то, что доктор прописал. И вот каждый раз, когда я пишу на async, у меня одни и те же проблемы:

Это другой язык

Это как будто пересесть на совершенно другой язык! Весь мой совершенный код превращается в совершенную помойку, как только я берусь за async.

Вот, например, я решил канонично создать сессию, чтобы не открывать соединение каждый раз заново:

from dataclasses import dataclass
import requests

@dataclass
class Parser:
    def __post_init__(self):
        self.session = requests.Session()  # и всё!

parser = Parser()

А вот я приехал в этой идеей а async:

import aiohttp

@dataclass
class Parser:
    def __post_init__(self):
        self.session = aiohttp.ClientSession()

parser = Parser()  # DeprecationWarning: The object should be created within an async function

Может, так?

import aiohttp

@dataclass
class Parser:
    async def __post_init__(self):
        self.session = aiohttp.ClientSession()

parser = Parser()  # RuntimeWarning: coroutine 'Parser.__post_init__' was never awaited

Или так?

import aiohttp

@dataclass
class Parser:
    async def __post_init__(self):
        self.session = aiohttp.ClientSession()

parser = await Parser()  # TypeError: object Parser can't be used in 'await' expression

Ах, да... Магические методы не предусматривают работы с async, поэтому хрен вам, а не ваша красивая инициализация. Если хотите что-то иницилизировать, то используйте __aenter__:

import aiohttp

class Parser:
    async def __aenter__(self):
        self.session = aiohttp.ClientSession()

async with Parser() as parser:  # AttributeError: __aexit__
    ...

Но теперь нужно определить __aexit__:

import aiohttp

class Parser:
    async def __aenter__(self):
        self.session = aiohttp.ClientSession()
        return self

    async def __aexit__(self, *args, **kwargs):
        pass

async with Parser() as parser:
    ...

Тот ли это лаконичный питон, на котором я привык в несколько строк писать крутые вещи? Почему мои классы раздуваются, а код сдвинут разными асинхронными контекстными менеджерами? Почему я должен помнить про event loop? Почему я должен бросать свои любимые библиотеки и фреймворки и учить новые? А главное, почему нельзя было, чтобы оно работало и писалось так же, как простой синхронный код? Ну там знаете, чтобы вместо

pool = ThreadPoolExecutor()
result = await some_fn()
async with some_weird_stuff() as stuff:
    tasks = []
    async for item in stuff.iter_elements():
        if not item:
            break
        tasks += [
	        loop.create_task(async_process(item))
		    loop.run_in_executor(pool, sync_process(item)), 
	    ]

results = await asyncio.gather(*tasks)

я писал чё-нить типа

result = some_fn()
stuff = some_weird_stuff()
tasks = []
for item in stuff.iter_elements():
    if not item:
        break
    tasks.append(
        loop.submit(async_process, item),
        loop.submit(sync_process, item),
    )
results = futures.wait(tasks)

а дальше Гвидо и python core team сами читали мой код, смотрели, что там асинхронное, а что нет, и сами дописывали весь этот синтаксический ад?

Я могу назвать себя "генератор-мастером", но асинхронный питон говорит мне, что я никто. Вот простой генератор:

import asyncio

async def generator():
    for i in range(10):
        yield i
        await asyncio.sleep(0.1)

async def main():
    async for value in generator():
        print(value)

if __name__ == '__main__':
    asyncio.run(main())

Теперь - внимание - я хочу два генератора! Вспомним про itertools.chain:

import asyncio
from itertools import chain

async def generator():
    for i in range(10):
        yield i
        await asyncio.sleep(0.1)

async def main():
    async for value in chain(generator(), generator()):
        print(value)

if __name__ == '__main__':
    asyncio.run(main())

Это прям я, когда пишу асинхронный код

Это прям я, когда пишу асинхронный код

Ну и так постоянно, все мои знания бесполезны, и мне приходится заново изучать, как делать привычные вещи в асинк-мире.

Разумеется, щас кто-нибудь в комментах напишет, почему async такой, какой он есть, и что иначе и быть не могло, но я, как человек с незамыленным взглядом, сейчас гляжу на всё это и офигеваю.

И треснул мир напополам

Тут уже написали за меня в статье про красные/синие функции - она прям мои мысли повторяет. У меня теперь где-то красные функции, где-то синие, синие в красных работают (но тогда всё тормозит), красные в синих не работают. Появляются какие-то дикие треды о том, как запустить красное в синем, джанго вроде покраснело, но многие части всё равно синие, requests синие и не краснеют, в IDE появляются какие-то красные клоны синих методов, и так далее, и тому подобное.

асобрать, амассово_создать, асодержит, асоздать, ане_поехать_ли_мне_в_дурку

асобрать, амассово_создать, асодержит, асоздать, ане_поехать_ли_мне_в_дурку

Как это отлаживать

Оказывется, у asyncio есть два режима: product и debug. Если вы хотите отлаживать async код, то у вас 4 способа (ЧЕТЫРЕ) включить отладочный режим:

* Setting the PYTHONASYNCIODEBUG environment variable to 1
* Using the Python Development Mode
* Passing debug=True to asyncio.run()
* Calling loop.set_debug()
(https://docs.python.org/3/library/asyncio-dev.html)

Ну и вообще отлаживать асинхронщину сложнее, потому что поток выполнения сначала здесь, потом там, потом тут, потом ещё где-то. В обычном питоне у меня может, конечно, быть многопоточность, но я просто ставлю количество потоков в 1, и у меня снова почти что синхронный код, где я даже что-то понимаю. В асинке у меня работает только "метод пристального взгляда", когда я смотрю на код и пытаюсь представить в уме, что там на самом деле происходит. Появляются всякие мониторы, чтобы хоть как-то понять текущее состояние.

А текущее состояние может быть странным. Вот, например, Гвидо вам говорит, что сорян, но если asyncio.as_completed падает по таймауту, то оставшиеся таски всё равно продолжают работать. С другой стороны, если вы запустите asyncio.create_task и случайно не сохраните где-нибудь эту задачу, то придёт сборщик мусора и убъёт вашу задачу, прям даже в середине выполнения. Asyncio - это весело.

Ладно, всё. Давайте просто забудем.

Нет "пустого элемента"

Из раздела синтаксического сахара: нет пустого элемента. Хотелось бы, чтобы был какой-нибудь токен, означающий "пропусти меня":

first_name, middle_name, last_name = 'Иван', None, 'Иваныч'
# хочу так:
name = ' '.join([first_name, middle_name or pass, last_name])  # -> 'Иван Иваныч'
# приходится так:
name = ' '.join([first_name, *([middle_name] if middle_name else []), last_name])  # -> 'Иван Иваныч'
# потому что так нельзя:
name = ' '.join([first_name, middle_name, last_name])  # -> 'Иван  Иваныч' - два пробела

Символьный ад

Уберите детей от экрана.

{'a': 1}  # это словарь
dict(a=1)  # это то же самое
{'a'}  # это множество (set)
{}  # это не пустое множество, это пустой словарь
set()  # а вот это пустое множество

1  # это просто число (int)
(1)  # это тоже просто число
(1, 2)  # это кортеж (tuple)
1,  # это тоже кортеж
()  # это пустой кортеж
(1+2)  # это число
tuple()  # а это тоже пустой кортеж

[]  # это пустой список
[1]  # это список с одним элементом
[1, 2, 3]  # это список с 3 элементами
[i for i in range(3)]  # это тоже список с 3 элементами
()  # это пустой кортеж
(1)  # это просто число
(1, 2, 3)  # это (ха-ха) кортеж
(i for i in range(3))  # это не кортеж, это генератор :D

Если нельзя, но очень хочется

Вот так писать нельзя:

foo(a=1, b.c=2, whatever-you-f***ing-want=3)`

Но если очень хочется, то можно:

foo(**{'a': 1, 'b.c': 2, 'whatever-you-f***ing-want': 3})

Декораторы

@cached_method

Есть объект с методом и свойством:

def super_expensive_function(a: int, b: int) -> int:
	print('calculation...')
    return a ** b

@dataclass
class Object:
    value: int

    def method(self, b: int) -> int:
        return super_expensive_function(self.value, b)

    @property
    def prop(self) -> int:
        return super_expensive_function(self.value, 2)

obj = Object(value=5)
obj.method(2)  # calculation...25
obj.method(2)  # calculation...25
obj.prop  # calculation...25
obj.prop  # calculation...25

Мы не хотим каждый раз считать super_expensive_function заново, поэтому для конкретного объекта хотим эти значения закэшировать. Да, можно закэшировать саму функцию, но нам бы хотелось, чтобы кэш хранился с самими объектами и удалялся вместе с ними.

Окей, есть @cached_property, который "запомнит" значение свойства:

from functools import cached_property

@dataclass
class Object:
    # ...

    @cached_property
    def prop(self) -> int:
        return super_expensive_function(self.value, 2)

obj.prop  # calculation...25
obj.prop  # 25

А вот для метода никакого @cached_method нет - будьте добры запилить самостоятельно в каждом проекте! @cache не подходит, потому что он сохранит кэш даже после удаления объекта.

@classmethod

Классметод возвращает что-то настолько чуждое, что после этого декорировать уже нельзя!

# NO:
@my_decorator
@classmethod
def method(cls, ...):

# YES:
@classmethod
@my_decorator
def method(cls, ...):

Индексы

range задаётся как [начало, конец):

  • range(0, 3) - это [0, 1, 2], и если вам нужны числа от 1 до 10, то выпишете range(1, 11)

  • По этой же причине если вам нужно посчитать с 10 до 1 включительно, то вы пишете range(10, 0, -1) Как только нужно работать с индексами (что в питоне, к счастью, нечасто), то везде появляются i+1 или i-1, потому что начало включается,а конец - нет. Это бесит.

Open

open открывает файлы не в utf8, как вы могли думать, а в какой-то кодировке - выбор зависит от среды выполнения. В windows это будет, скорее всего, Windows-1252. Признаюсь, попадался на это пару раз. Вообще обожаю функции, которые зависят от окружения, это так загадочно.

NotImplemented и len

  • Есть NotImplemented - вроде как нужно возвращать, если у функции нет имплементации того, что у неё просят.

  • Есть NotImplementedError - вроде как нужно выбрасывать, если у функции нет имплементации того, что у неё просят.

Вот тут люди пишут оправдания, почему так надо, а я молчу. Это питон, просто оно так, как есть. Смирись.

  • Есть 'my string'.split() - вроде как у строки есть метод "разбей на несколько строк", и это логично.

  • Есть len('my string') - вроде как есть глобальная функция, которая вернёт длину этой строки.

Вот тут люди пишут оправдания, почему так надо, а я молчу. Это питон, просто оно так, как есть. Смирись.

map и filter

В питоне есть map() и filter():

filtered = filter(lambda x: x>2, [1, 2, 3])
multiplied = map(lambda x: x*2, [1, 2, 3])

filtered_and_multiplied = map(lambda x: x*2, filter(lambda x: x>2, [1, 2, 3]))
multiplied_and_filtered = filter(lambda x: x>2, map(lambda x: x*2, [1, 2, 3]))

При этом в питоне же есть "comprehensions", которые всегда лучше читаются:

filtered = (x for x in [1, 2, 3] if x>2)
multiplied = (x*2 for x in [1, 2, 3])

filtered_and_multiplied = (x*2 for x in [1, 2, 3] if x>2)
multiplied_and_filtered = (y for x in [1, 2, 3] if (y := x*2) > 2)

Когда кто-то начинает комбинировать map и filter в одном выражении, где-то на другом конце света у kesnа умирает нервная клетка. Пожалуйста, не надо так.

Неудобно "сцеплять" функции

Примеры - вместо тысячи слов.

nim-style:

items.sorted(key=...).groupby(key=...)

python-style:

groupby(sorted(items, key=...), key=...)

nim-style:

items.filter(lambda x: x>2).map(lambda x: x*2)

python-style:

map(lambda x: x*2, filter(lambda x: x>2, items))

Starmap

Пусть у вас есть парочки:

pairs = [
    (1, 2),
    (3, 6),
    (5, 6),
    ...
]

и функция, которая принимает больше одного параметра:

def is_subsequent(a: int, b: int) -> bool:
	return abs(a-b) == 1

Почему-то у нас есть starmap:

from itertools import starmap
results = starmap(is_subsequent, pairs)

... но нет ThreadPoolExecutor.starmap:

from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor()
# results = pool.starmap(is_subsequent, pairs)  <-- fuck you kesn! 'ThreadPoolExecutor' object has no attribute 'starmap'
results = pool.map(lambda pair: is_subsequent(*pair), pairs)

Нет break(2)

Реально, вы не можете просто взять и выбраться из вложенного цикла for:

# ... some code here

for item in items:
    # ... more code here
    for element in item.elements:
	    # ... more code here
        if is_ok:
			break(2)

# ... some code there

Такое не сработает, и приходится придумывать обходные пути - например, выносить в отдельную функцию, где вместо break(2) делать return. Ещё одна маленькая вещь, которая подбешивает.

Нет аннотации исключений

Какие исключения может выбросить функция? Да любые. Поэтому и появляется код типа такого:

try:
	incoming_object = yaml.safe_load(body.decode())
	request_data = PostFlopRequestData.parse_obj(incoming_object)
except Exception as exc:
	# ловим всё, потому что хрен знает, что выбросится

В более строгих языках, наверно, можно просто пройтись какой-нибудь тулзой по определениям всех функций и собрать, какие исключения откуда могут прийти, но в питоне что угодно может произойти когда угодно. Может, тогда аннотировать функции?

def fn(a: int) -> int, ValueError | ZeroDivizionError:
    # ...

Глупости какие. Это никому не нужно. Давайте лучше введём какие-нибудь ParamSpec, чтоб жилось веселее:

F_Spec = ParamSpec("F_Spec")
F_Return = TypeVar("F_Return")

def decorator(
    call: Callable[
        F_Spec,  # функция с произвольными входными аргументами
        F_Return
    ]
) -> Callable[
    F_Spec,      # функция с теми же входными аргументами
    F_Return
]:
    @wraps(func)
    def wrapper(
        *args: F_Spec.args,      # эти аргументы
        **kwargs: F_Spec.kwargs  # эти аргументы
    ) -> F_Return:
        return call(*args, **kwargs):
    return wrapper

(кстати, этот пример - из статьи про декораторы, она классная)

Да и вообще аннотации ничего не делают

Бездумная машина не смотрит на наши аннотации типов, поэтому каждый питонист хоть раз в своей жизни писал такой костыль:

def spend_coins(cls, client: Client, uid: str, num_coins: int):
    assert isinstance(num_coins, int), "А вот и нет!"

Я написал num_coins: int, но вообще туда можно передавать что угодно, хе-хе, поэтому пришлось ловить нарушителей при помощи assert. Кстати, assert можно тоже отключить, если запустить python с флагом -O...

Гвидо

Чего уж мелочиться, сам Гвидо Ван Россум (Guido Van Rossum) - создатель языка питон - какой-то забагованный. Ведь пишется "Гвидо", а читается - "Хидо". Живите теперь с этим :D


В черновике у меня ещё много всяких нелепостей про питон, но я пожалел себя и вас. Да и к чему это? Ведь, несмотря ни на что, я всё равно считаю питон очень лаконичным и красивым языком - и как раз об этом я и хотел написать в одной из следующих статей. Прыгайте в телегу, если не хотите ничего пропустить, а если хотите почитать мои гиенистые статьи - то добро пожаловать в блог Погромиста!

Автор:
kesn

Источник

  1. Serый:

    и я, и я такого мнения! )))

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js