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

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

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

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. Другой разраб может это увидеть и не понять, прям как в меме:

Нет 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


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

Комментарии (114)


  1. iamkisly
    10.08.2023 06:16
    +7

    Это был интересный, хорошо оформленный пост! Держи плюсик в карму)

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

    А я не люблю питон, но пишу на нем потому что "нам еще нужно на что-то кушац".. шутки шутками, но мне правда интересно было бы почитать исследование, а как много разработчиков занимаются тем, что искренне не любят и местами ненавидят.


  1. ivankudryavtsev
    10.08.2023 06:16
    +4

    А меня бесит, что нельзя понять стандартным образом сколько я прождал GIL. Да и вообще, профайлинг GIL отстойный. Да, ничего не делающие аннотации типов - тоже такое себе.

    Прям интересно услышать мнения минусующих данный опус. От чего у вас подгорает?


    1. Abobcum
      10.08.2023 06:16
      +4

      Аннотации типов позволяют использовать статический анализатор. А вот включить его или нет остаётся за пользователем, как и все в питоне - полная свобода.


  1. Kanut
    10.08.2023 06:16
    +14

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

    Так может вы просто выбрали для своих задач не тот язык программирования? :)


    1. syrslava
      10.08.2023 06:16
      +16

      Тогда мне искренне интересно, для каких задач нужна такая динамика, и почему Python не остаётся узкоспециализированным языком?


      1. Kanut
        10.08.2023 06:16
        +7

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


        Я на нём пишу всякие скрипты для генерации всего подряд: wrapper'ов, конфигураций, упаковки пакетов и т.д. и т.п. Тоже заметно быстрее и удобнее чем использовать для этого те же C#/Java на которых у нас собственно пишется весь "продуктивный" код.


        1. RH215
          10.08.2023 06:16

          Оно удобно, конечно, но можно динамики будет немного меньше? :)


          1. Kanut
            10.08.2023 06:16
            +7

            А нет других языков где её "немного меньше"? Ну то есть зачем её из питона убирать если куча людей выбирает питон именно из-за неё?


        1. sshikov
          10.08.2023 06:16

          А нахрена вы "прототипы" пишете на Java, когда много-много лет есть отличные котлин и груви? Причем берете и ставите типы — и получаете меньше динамики. Что-то мешает? Я понимаю когда мешает отсутсвие под рукой JVM, но у вас же есть?


          1. Kanut
            10.08.2023 06:16
            +5

            Вы не правильно поняли. На Java я их как раз таки не пишу. Их я пишу на Питоне.

            А на Java у нас всё ещё есть ряд легаси-проектов которые не особо имеет смысл целиком переписывать заново на данный момент.


            1. sshikov
              10.08.2023 06:16

              Ну хорошо, на Java не пишете, я неправильно сформулировал вопрос. Я тоже не предлагаю вам легаси приложение на груви или котлин переписывать.


              Тоже заметно быстрее и удобнее чем использовать для этого те же C#/Java

              Речь вот об этом. Вам питон удобнее Java. Для скриптов — верю. Так вот груви или котлин — удобнее питона. Их недостаток — что нужно JVM. Это не всем нравится. Но у вас же это есть.


              Почему вы прототипы не пишете на условном груви, при наличии под рукой JVM? Вот просто пример — я пытался скажем на питоне писать некий скрипт для мониторинга приложения. Если приложение на Java — то его метрики зачастую уже лежат в JMX. А добраться до JMX из скрипта на груви в сто раз проще, чем из скрипта на питоне. И таких примеров я мог бы вспомнить десятки. Это не значит, что груви всегда выигрывает у питона для таких проектов — но очень часто таки да (при том что синтаксис у груви и котлина, как по мне, местами сильно приятнее питоновского).


              1. Kanut
                10.08.2023 06:16
                +4

                Потому что мне так проще. Вот как-то так получается :)

                Плюс ещё пожалуй потому что наши инженеры или эмбеддеры в питон тоже умеют, а в джаву/котлин/груви нет.


                1. sshikov
                  10.08.2023 06:16

                  Ну "я так не умею" — это тема, несомненно. Мы учим :)


                  У нас девопс весь на дженкинсе, там так или иначе либо "а ля груви", либо прямо чистый груви. Так что многие умеют.


                  1. Kanut
                    10.08.2023 06:16
                    +1

                    Вот только вопрос зачем их именно этому учить? Чтобы лично мне было удобнее? Так себе аргумент. Особенно учитывая что у них то JVM не стоит.

                    И наши инженеры это не девопс. Это именно что инженеры: электротехника, мехатроника, машиностроение. Хорошо что хоть на чём-то умеют :)


                    1. sshikov
                      10.08.2023 06:16

                      Чтобы лично мне было удобнее?

                      Ну как вы догадываетесь, я вряд ли смогу объективно измерить, что мне дает груви вместо питона. Субъективно — как правило объем кода сокращается (не на порядок, а скажем в пару раз), а сопровождение упрощается. Функциональность при этом либо не страдает, либо больше. Качество и функциональность доступных библиотек — как правило в моем случае сильно выше. Скажем, не так просто будет найти аналог условного Apache POI, если вдруг придется поработать с файлами Office. Ну это тема неисчерпаемая, не буду углубляться.


                      что у них то JVM не стоит.

                      Ну, у нас вот Java применяется, и я с трудом могу представить себе аргумент такого вида "у нас тут на хосте оно не стоит".


                      1. Kanut
                        10.08.2023 06:16

                        Ну, у нас вот Java применяется, и я с трудом могу представить себе аргумент такого вида "у нас тут на хосте оно не стоит".

                        Ну так Java она у нас применяется. Не у них :)


              1. funca
                10.08.2023 06:16
                +3

                груви

                Шикарная тема для цикла статей на тему "почему оно меня бесит".


                1. sshikov
                  10.08.2023 06:16

                  Ну, меня он не бесит. Могу на противоположную тему накатать описание практического опыта.


        1. 0xd34df00d
          10.08.2023 06:16
          +5

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

          Смотря с чем сравнивать. Если с


          те же C#/Java

          где языки тяжеловесные по синтаксису/необходимости аннотаций типов/етц, то да, конечно. Но динамика просто скрывает некоторые из этих вопросов.


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


          Поэтому питон так любят научные работники

          Исторические и культурные причины.


          Олсо, другие научные работники любят матлаб, третьи — идрис. Зависит от того, в какой ветви науки эти работники работают.


        1. Zara6502
          10.08.2023 06:16
          +2

          как ни странно, но мне проще на C# написать в первую очередь из-за удобства отладки кода, а с питоном как-то странно - кидаешь в черный ящик и непонятно почему оно не работает.


          1. Zara6502
            10.08.2023 06:16

            из плюсов только не нужна IDE и создание проекта (но в C# внезапно тоже есть консольный компилятор и писать можно в блокноте, но по понятным причинам я так никогда не делаю.


            1. Kanut
              10.08.2023 06:16

              C# требует слишком много телодвижений. Питон можно просто в консоли быстро набросать и запустить.


              1. Zara6502
                10.08.2023 06:16
                +2

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

                а работа в самой консоли для меня совершенно неудобна, мне проще запустить VS и там же набрать код для питона и запустить - и синтаксис подсвечивается и дебажить проще. а сразу с нуля что-то без ошибок написать это уровня Hello World? Тем более в структуре кода постоянно то туда то сюда бегаешь пока пишешь, как это вменяемо делать в консоли?


  1. economist75
    10.08.2023 06:16
    +4

    Все верно написано, но доставляют трудности ~1% из этого. Ясность синтаксиса позволяет прощать Питону любые несуразности. И потом, они чем-то даже приятны, дают бронзовый налет эрудированности и поводы для душевных разговоров.

    Нет ЯП без недостатков, питонисты их не скрывают вообще. Надо отдать должное - все статьи подобного рода (даже иностранные) - пропитаны здоровой самоиронией, читать приятно. Стоит ли исправлять - да. Но именно неспешно, как было сделано с unicode в Python 2->3. Тогда было жестко, но язык никто не бросил.


    1. HemulGM
      10.08.2023 06:16
      +21

      Ясность синтаксиса позволяет прощать Питону любые несуразности

      По-моему в статье как раз идет речь о том, что синтаксис и приемы не совсем ясные


      1. economist75
        10.08.2023 06:16

        Статья про несуразности в синтаксисе. Их в нем немного, иначе бы таких статей была на одна в год, а больше, а язык ни за что не стал бы Top-3 на десятилетие.

        Ясность синтаксиса - это про среднюю длину ключевого слова, объем кода в символах и строках, число скобок, спецсимволов итд. Код на Python лаконичен и читается с двух метров. Да, это заслуга значащих отступов и удобного Py-синтаксиса без ";" и т.н. закрывающих (циклы, условия, функции, классы итд) команд.


        1. HemulGM
          10.08.2023 06:16

          Я совершенно не вижу отличий в ясности кода между Питоном и, например, Паскалем.

          Единственное - отступы - это сразу же и форматирование. А форматирование - это залог удобности чтения. Т.е. если в Паскале форматирование - доп. опция, в Питоне - необходимость изначально.

          Питон

          number = 1
           
          while number < 5:
              print(f"number = {number}")
              number += 1
          print("Работа программы завершена")

          Паскаль (речь о современной версии языка - Делфи)

          var number := 1;
          
          while number < 5 do 
          begin
            writeln('number = ', number);
            Inc(number);
          end;
          writeln('Работа программы завершена');

          Питон

          message = "Hello"
           
          for c in message:
              print(c)
          
          for i in range(1, 10, 2):
              print(i)

          Паскаль

          var message := 'Hello';
          
          for var c in message do
            writeln(c)
          
          for var i in range(1, 10, 2) do
              print(i)

          Питон

          def say_hello():    # определение функции say_hello
              print("Hello")
          
          
          say_hello()         # вызов функции say_hello
          say_hello()
          say_hello()

          Паскаль

          procedure say_hello(); // определение функции say_hello
          begin
            writeln('Hello');
          end;
          
          
          say_hello();           // вызов функции say_hello
          say_hello();
          say_hello();

          Питон

          def get_message():
              return "Hello"
          
          message = get_message()  # получаем результат функции get_message в переменную message
          print(message)           # Hello 
           
          # можно напрямую передать результат функции get_message
          print(get_message())     # Hello 

          Паскаль

          function get_message: string;
          begin
            Exit('Hello');
          end;
          
          var message := get_message();  // получаем результат функции get_message в переменную message
          writeln(message);              // Hello 
          
          // можно напрямую передать результат функции get_message
          writeln(get_message);          // Hello 

          Питон

          def multiply(n):
              def inner(m): return n * m
          
              return inner
          
          
          fn = multiply(5)
          print(fn(5))        # 25
          print(fn(6))        # 30
          print(fn(7))        # 35

          Паскаль

          function multiply(n: integer): TFunc<integer, integer>;
          begin
            Result := function(m: integer): integer begin Exit(n * m); end;
          end;
          
          
          var fn := multiply(5);
          writeln(fn(5));        //25
          writeln(fn(6));        //30
          writeln(fn(7));        //35

          Отличие - типы, которые требуется указать, а не просто аннотации. Точки с запятой, которые читаемость не ухудшают. Операторные скобки - тоже не ухудшают. В Питоне тоже, если будет множество вложенных блоков появляется проблема определения в каком блоке мы находимся и без средств разработки это бОльшая проблема, чем при множестве вложенных begin/end;


          1. economist75
            10.08.2023 06:16

            Паскаль - тоже хороший и довольно ясный язык. Но убедить в одинаковой читаемости разных ЯП еще никогда никому не удавалось, это ведь вкусовщина. Хотя...

            Без пробелов и комментов код на Питон в ~2 раза меньше занимает в байтах (из ваших же примеров). Значит он вдвое быстрее набирается и исправляется, содержит вдвое меньше опечаток.

            Есть несколько общепризнанных критериев существенности отличий, p=0.05, 10%, 20%. Различия на +100% - точно существенны. Думаю что многие пользователи видят эти различия и выбирают то, что им важнее. Метрики популярности тому свидетели.

            Такие архитектурные изыски сабжа как кавычки 3-х разных типов, отступы, срезы, списковые включения, итераторы, генераторы - оказались очень удачными, кмк. Я почти уверен что все это мелькало в других ЯП раньше, но собрать все подобные трюки в одном языке - это было смелое решение, и "ставка сыграла".


            1. HemulGM
              10.08.2023 06:16

              С длинной вы не совсем правы.
              1. Чем длиннее ключевое слово, тем сложнее сделать опечатку. Либо эта опечатка приведет лишь к синтаксической и самой простой ошибке во время компиляции или анализа кода, а не исполнения.
              2. Редакторы кода сами помогают написать ключевое слово. В том числе и операторные скобки.
              3. Время, которое тратится на написание кода увеличивается минимально из-за длинных ключевых слов.
              4. Возьмём, например, ключевое слово var, которое позволяет объявить переменную. Это ключевое слово оберегает ещё и от того, что переменная может быть использована из другой области видимости (про глобальные переменные в Питоне я знаю, и это только подтверждает мои слова, наличием слова global)


          1. Andrey_Solomatin
            10.08.2023 06:16
            -1

            Добавьте парочку примеров.

            Открыть файл прочитать строку, закрыть, в случае исключения закрыть.

            with open(path) as f:
               print(f.read())

            Создать список четных чисел от одного до 20

            print([n for n in range(21) if n %2 == 0])


            Что у Паскаля с литералами для коллекций (списки, словари, множества)?

            Как обстоят дела с распаковкой?

            for k, v in {1:1, 2:2}.items():
                print(k, v)


            1. HemulGM
              10.08.2023 06:16
              -1

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

              Читаем первую строку файла. Закроется сам

              Writeln(TFile.ReadAllLines(path)[0]);

              Создать список четных чисел от одного до 20

                  for var i in range(1, 21, 1,
                    function(n: Integer): Boolean
                    begin
                      Result := n mod 2 = 0;
                    end)
                  do
                    WriteLn(i);

              Генератор range, он же енумератор с функцией для проверки числа. Точно такое же range как в питоне, но ещё с возможностью указать "фильтрацию". Не генерирует массив, а вычисляет по мере цикла. Я уверен, что вы даже так всё тут легко прочитали, не смотря на то, что конструкция очевидно больше, чем в питоне. Были б ещё лямбды, было бы ещё короче. Но они будут потом.

              for k, v in {1:1, 2:2}.items():
                  print(k, v)

              Этот код лишен смысла, но если мы имеем дело со каким-то созданным словарем, а не с создаваемым на ходу, то

              for var Item in Dic do
                writeln(Item.Key, Item.Value);


              1. ValeryIvanov
                10.08.2023 06:16

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

                1. Просто мимо. На каждую манипуляцию с файлом нужно искать или писать утилитарный метод? Неужели, в паскале нет менеджера контекста? Или какого-нибудь оператора, который закрывал ресурс при выходе из блока. Видел недавно язык(zig, кажется), где такой оператор назывался defer.

                2. Создание списка(хотя у вас почему-то числа просто выводятся на экран) занимает на порядок больше строк чем в питоне, что уже является большим минусом, так как ухудшает читаемость кода. Анонимная функция, кстати, тоже читабельности не способствует. Даже в жабе, которая славится своей многословностью, завезли Stream API с лямбдами и если чего-то подобного нет в паскале, то это явно минус.

                3. Опять же, распаковка выглядит удобнее нежели постоянное обращение к Item. Такая же болячка есть у жабы, а вот C# удалось её побороть, добавив возможность деконструкции у KeyValuePair.


                1. HemulGM
                  10.08.2023 06:16
                  +1

                  Все, о чем вы говорите - лишь синтаксический сахар.

                  Можно десятки способов реализовать для чтения файла. Вплоть до освобождения при выходе из блока. Это не проблема и не сложность.

                  Range - всего лишь функция, которая создаёт генератор. Добавить в него функцию ToArray дело 20 секунд.

                  Опять же, это просто сахар.

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

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


                  1. ValeryIvanov
                    10.08.2023 06:16

                    Все, о чем вы говорите - лишь синтаксический сахар.

                    И этот синтаксический сахар кратно уменьшает сложность кода.

                    Можно десятки способов реализовать для чтения файла. Вплоть до освобождения при выходе из блока. Это не проблема и не сложность.

                    Было бы славно, если бы в ответ на примеры кода @Andrey_Solomatin вы бы привели идентичные примеры на паскале. Тогда бы уже и можно было бы рассуждать о том, избыточен ли синтаксис питона или нет.

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

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

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

                    Ручное управление памятью, как правило не способствует читаемости кода. Наверное, только Rust сумел выделиться на этом поприще.


              1. Andrey_Solomatin
                10.08.2023 06:16
                -1

                Речь не о возможностях языка, а о читаемости.

                Говорить о читаемости не используя всех возможностей языка смысла нет.

                Явное открытие и автоматическое закрытие с контекстным менеджером это как раз о читаемости. Прочитав первую строчку сразу понятно, что не надо дочитав секцию до конца проверять, закрыли ли объект.

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

                С темы литералов для коллекций в плавно съехали. Вот например в джаве это целый цирк с конями. https://stackoverflow.com/a/6802502/1310066



  1. vba
    10.08.2023 06:16
    +1

    Автору спасибо,
    А что за линтер такой, что проверяет на неизменяемость(immutability)?
    Мне тоже не нравится система типов, типа :D :

    class Class1:
      @staticmethod
      def build() -> "Class1":  # Господи, за что ?
          pass
    


    1. kesn Автор
      10.08.2023 06:16
      +3

      Так это, Self вроде уже завезли, не? https://peps.python.org/pep-0673/

      class Class1:
          @staticmethod
          def build() -> Self:
      

      Ну и статикметоды были ошибкой (по мненю Гвидо), классметоды рулят :)

      We all know how limited static methods are. (They’re basically an accident — back in the Python 2.2 days when I was inventing new-style classes and descriptors, I meant to implement class methods but at first I didn’t understand them and accidentally implemented static methods first. Then it was too late to remove them and only provide class methods.


      1. vba
        10.08.2023 06:16

        Так это, Self вроде уже завезли

        Так то в 3.11, А у нас часть продакшена на 3.7 ешшо ;( . Ну и тоже, почему не сделать просто имя класса без кавычек, зачем вводить Self?


        1. whoisking
          10.08.2023 06:16
          +5

          Self как минимум лучше тем, что уменьшает вероятность багов при изменении названия класса. Как бы вы его не переименовали в будущем, аннотация останется всегда верной.


          1. vba
            10.08.2023 06:16
            +1

            Аргумент так себе. Если вы меняете имя класса в ручную на свой страх и риск, то да.


            1. whoisking
              10.08.2023 06:16
              +4

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


        1. kivsiak
          10.08.2023 06:16
          +2

          Для вас есть typing_extensions


        1. Andy_U
          10.08.2023 06:16
          +2

          Ну и тоже, почему не сделать просто имя класса без кавычек, зачем вводить Self?

          Так добавьте первой строчкой

          from __future__ import annotations


          1. vba
            10.08.2023 06:16

            А что так можно было ? :palm_face: :D Спасибо, не знал


        1. Andy_U
          10.08.2023 06:16
          +1

          зачем вводить Self?

          На самом деле для использования в иерархии классов. До Self вот так приходилось.

          from __future__ import annotations
          
          from typing import TypeVar
          
          
          ClassType = TypeVar('ClassType', bound='A')
          
          class A:
          
              def __init__(self) -> None:
                  self._x = 0
          
              def func(self: ClassType, x: int) -> ClassType:
                  self._x = x
                  return self
          
          
          class B(A):
          
              def __init__(self) -> None:
                  super().__init__()
          
              def func(self: ClassType, x: int) -> ClassType:
                  return super().func(x)
          
          
          def main() -> None:
              b = B()
              print(type(b.func(1)))
          
          
          # Press the green button in the gutter to run the script.
          if __name__ == '__main__':
             main()
          


  1. Newbilius
    10.08.2023 06:16
    +6

    Плюсик посту за сломанную вёрстку)


  1. mini_nightingale
    10.08.2023 06:16
    +14

    TL;DR: "Мой простой Питон оказался нифига не таким простым, да как так то, а?"


  1. avshkol
    10.08.2023 06:16
    +4

    {} # это не пустое множество, это пустой словарь

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

    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
    - да, и это отличает его от списка (в том смысле, что список содержит все сразу, а генератор выдаёт по одному). А в кортеже по логике, не может быть цикла, ибо кортеж неизменяем, а цикл предполагает изменяемый список - создал первое значение, добавил второе и т.д.


  1. nivorbud
    10.08.2023 06:16
    +7

    Многие подобные вещи имхо надо рассматривать в историческом контексте. И питон и, например, джанго развивались параллельно и быстрыми темпами.

    Мой первый проект на джанго датируется 2007-м годом. Тогда джанго была еще в альфа версии, и там еще не было 90% современных возможностей и батареек.

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


    1. Fox_exe
      10.08.2023 06:16
      +1

      О, да, пробовал я както в Django добавить Websockets и Async, соответственно... Это был ад танцы с бубном, т.к. часть кода - синхронна, а часть - асинхронна. А для кеша вообще пришлось писать свой модуль, склеенный из двух других (синхронного и асинхронного), т.к. ни тот ни другой не могли нормально работать с sync_to_async / async_to_sync...


  1. Gadd
    10.08.2023 06:16
    +11

    А ещё, забытая запятая в конце строки с присваиванием (упомянутое в статье) может подарить тонны WTF??? при попытках разобраться, откуда тут кортеж? И только опыт лишает или сокращает это удовольствие.

    a = 42,
    

    Опциональные скобки - это зло. Хотя часто это удобно.


    1. Andrey_Solomatin
      10.08.2023 06:16

      У меня достаточно часто такие вещи ловятся статическим анализатором. Но для этого надо расставлять аннотации по проекту.


  1. ammo
    10.08.2023 06:16
    +1

    Мои пять копеек: в itertools нет эффективного итератора по части списка.
    Допустим, нужно посчитать сумму элементов с 10 по 20.
    1. sum(my_list[10:21]) - лучший вариант, но он тратит доп. память
    2. sum(itertools.islice(my_list, 10, 21)) - не эффективно по времени, т.к. начинает перебирать элементы с 0
    3. Эффективно, но уродливо:
    sum_ = 0
    for i in range(10, 21):
    sum_ += my_list[i]


    1. Aquahawk
      10.08.2023 06:16
      +8

      Вот объясните мне, почему п. 3 это уродливо? Я слышу это уже лет 10 в контексте разных языков программирования. Но этот код же элементарно читается, понятно что он делает, в большинстве ситуаций он будет самым эффективным. Что в нём такого плохого?


      1. ammo
        10.08.2023 06:16
        +2

        Очевидно же:

        • занимает 3 строчки вместо 1

        • читается дольше чем варианты 1 и 2

        А про лучший вариант я уже сказал - он почему-то отсутствует в стандартной библиотеке, хотя по сути очень прост.

        def list_iterator(list_, start=0, stop=None, step=1):
            if stop is None:
                stop = len(list_)
            for i in range(start, stop, step):
                yield list_[i]
        
        
        sum(list_iterator(my_list, 10, 21))
        


        1. farm
          10.08.2023 06:16
          +7

          занимает 3 строчки вместо 1

          тогда почему бы не записать в одну?

          sum(my_list[i] for i in range(10, 21))


        1. Aquahawk
          10.08.2023 06:16

          занимает 3 строчки вместо 1

          читается дольше чем варианты 1 и 2

          Т.е. стоит играть в code golf с с каждой функцией? Может и классы не писать, не закладываться под расширение. Большая часть паттернов приводит к увеличению количества кода. Ну и покажите мне того человека которому сложно прочитать 3 строчки кода? Я никогда этого не понимал и продолжаю не понимать.


          1. ammo
            10.08.2023 06:16
            -2

            Если б вы пытались понять так же усердно, как передергиваете мои аргументы, то может и получилось бы


    1. Sulerad
      10.08.2023 06:16
      +9

      А как же следующее?

      sum(my_list[i] for i in range(10, 21))

      itertools работает с итераторами, у которых далеко не всегда есть произвольный доступ. Если вдруг у вас он есть, то лучше использовать его явно (ну или что-то кроме itertools)


      1. ammo
        10.08.2023 06:16

        соглашусь, отличное решение


    1. Andrey_Solomatin
      10.08.2023 06:16

      Первый вариант может быть за гранью возможности измерений, разница будет слишком маленькая.

      Третий вариант норм, нет в нём ничего ужасного. Если нужно часто, напишите свою функцию.


  1. ShashkovS
    10.08.2023 06:16

    Справедливости ради mutable default, который определяется в момент создания функции, позволяет коротко писать функции, которым нужно что-то хранить между вызовами:

    def do_some(x, *, _counter=[0]):
        _counter[0] += 1
        return x, _counter[0]
    
    print(do_some('foo'))  # ('foo', 1)
    print(do_some('boo'))  # ('boo', 2)
    
    const do_some = (() => {
        let _counter = 0;
        return (x) => {
            _counter += 1;
            return [x, _counter];
        };
    })();
    
    console.log(do_some('foo'));  // ['foo', 1]
    console.log(do_some('boo'));  // ['boo', 2],
    

    Без этого нужно либо делать глобальную переменную, либо делать «фабрику», замыкание, класс или ещё что-то подобное.

    Мне вот в питоне больше всего не хватает удобных распаковок словарей. То, что в js коротко:

    const d = {foo: 1, boo: 2, zoo: 3};
    const {foo, zoo} = d;
    

    в питоне это громоздкое

    d = {'foo': 1, 'boo': 2, 'zoo': 3}
    foo, zoo = d['foo'], d['zoo']
    


    1. kardfox01
      10.08.2023 06:16

      Можно сделать вот так:

      d = {"a": 0, "b": 1}
      a, b = d.values()

      или

      d = {'foo': 1, 'boo': 2, 'zoo': 3}
      foo, zoo, _ = d.values()


      1. Vindicar
        10.08.2023 06:16
        +4

        Тут можно здорово налететь, если словарь приходит откуда-то ещё, и содержит ключи в другом порядке.


    1. isden
      10.08.2023 06:16

      Вот так можно:


      >>> d = {'foo': 1, 'boo': 2, 'zoo': 3}
      >>> foo, boo, zoo = d.values()
      >>> print(foo,boo,zoo)
      1 2 3

      Еще есть **.


  1. san-smith
    10.08.2023 06:16
    +4

    Есть ощущение, что примерно половина названных проблем характерна и для других языков программирования (в том числе — статически типизированных) или не является проблемой на самом деле (если человек уже знаком с этими концепциями). А вторая половина — наследие языка с долгой историей.


    Статья забавная, тут не поспоришь, но порой удивление автора вызывает не меньшее удивление.


    1. danilovmy
      10.08.2023 06:16

      это у Алекса стилистика такая. Тут не надо удивляться, просто наслаждайтесь.

      @kesn - спасибо в очередной раз, услада для мозга. Хотя ты знаешь, про генераторы я с тобой не согласен, тот же пропуск элемента или как рапараллелить асинк генератор. А про вложенный break - как останавливать внешние циклы, если они организованы генераторами тоже уже есть на habr.


  1. egaoharu_kensei
    10.08.2023 06:16

    Интересно было почитать, но добавлю пару слов в пользу питона.

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

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


    1. slonopotamus
      10.08.2023 06:16
      +3

      у него простой синтаксис

      По сравнению с чем?


      1. trinxery
        10.08.2023 06:16
        -3

        Он простой скорее в значении "ненагруженный"; обычно есть единственно правильный способ что-то записать, когда в других языках есть и несколько способов объявить функцию, ставить/не ставить фигурные скобки (и сами скобки нагружают), разные отступы между всем чем можно и прочее.


      1. shabelski89
        10.08.2023 06:16
        +1

        С php например


      1. egaoharu_kensei
        10.08.2023 06:16
        +1

        По сравнению с низкоуровневыми языками. Например, если сравнивать с С++, то не нужно контролировать утечки памяти, объявлять тип данных (хотя такое есть в питоне), писать кучу фигурных скобок и многое другое. В сумме такие вещи делают код проще для восприятия, а это многого стоит.


        1. Efrit
          10.08.2023 06:16
          +2

          Интересно, для меня одного

          код проще для восприятия

          в случае, если в нём явно объявлены типы данных?)


          1. egaoharu_kensei
            10.08.2023 06:16
            -1

            Хорошо подмечено). Дело в том, что в питоне также есть аннотация типов, но она в отличие от низкоуровневых ЯП на производительность не влияет и как раз нужна для лучшего восприятия.

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

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


            1. ris58h
              10.08.2023 06:16

              Как аннотации типов влияют на производительность в низкоуровневых ЯП? Раскройте мысль, пожалуйста.


              1. slonopotamus
                10.08.2023 06:16
                +3

                Положительно влияют. Сложить int + int процессор может одной ассемблерной инструкцией. Но для этого надо заранее знать что там именно int.


              1. egaoharu_kensei
                10.08.2023 06:16

                Указывая тип переменных, заранее известно сколько ресурсов требуется выделить оперативной памяти и в момент компиляции система подготовит необходимое кол-во памяти под наши переменные, а в том же самом питоне распределение памяти идет на момент выполнения каждой строки кода (грубо говоря).

                Например, эта разница хорошо видна при использовании разных интерпретаторов: если использовать Cython вместо Cpython, то код в основном будет отличаться только указанием типов переменных, а скорость увеличится в разы.


  1. Pastoral
    10.08.2023 06:16
    -6

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

    А то. Ведь у Вас замкнуло - Вы пытаетесь минимизировать энергетические затраты мозга на понимание вызывающими гормональное поощрение методами.

    Захотелось убрать под спойлер

    Классика: если я не понимаю, значит я глупый -> если я не понимаю, значит это неубедительно -> если я не понимаю, значит это неверно. Достигнуто: если я не понимаю, значит я самый умный.

    Это я не сам догадался, это профессор Савельев на YouTube объяснил, он знатный популяризатор, даже в книжки евойные можно не глядеть.

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

    Как я такое замечаю? Да по замене "я хочу достичь этого" на "я хочу сделать так".

    Кому не нравится пост - обломайтесь. Моя карма, как хочу так и трачу (за Интернет уплачено, развлекаюсь как моей душеньке угодно).


  1. sci_nov
    10.08.2023 06:16

    Чем вас облучили? :)


  1. sci_nov
    10.08.2023 06:16
    +4

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


    1. max851
      10.08.2023 06:16
      +2

      Скажите это тоннам легаси аля 2.2-2.7, которые работают уже лет пятнадцать (сам не верю этой цифре... как время то пролетело)
      PS пример с прода, чтобы не быть голословным https://github.com/SpriteLink/NIPAP


      1. sci_nov
        10.08.2023 06:16
        -2

        Что поделать, тогда видимо был хайп.


    1. egaoharu_kensei
      10.08.2023 06:16
      +1

      Согласен с вашими словами кроме продукта: Cython, Numba, библиотеки типа itertools, numpy, pandas, joblib, asyncio и расширения на других ЯП в совокупности позволяют добиться хорошей производительности, а этого хватит чтобы написать средних объемов сайт или неплохую рекомендательную систему.

      Да, для беспилотников и прочих задач, где нужна максимальная скорость, питона не хватит, но для задач попроще - с головой, например, большая часть кода ml-библиотеки sklearn написана на Cython и этого вполне хватает чтобы учить модели на данных конских размеров, учить нейросетки на питоне - одно удовольствие, тестеры и девопсеры тоже пишут на нём скрипты. Так что всё не так плохо)


      1. sci_nov
        10.08.2023 06:16

        Так да, тестеры и девопсы пишут вспомогательные скрипты. Учить нейросети - тоже вспомогательная задача (research). В продукте всё равно всё будет статическое и написано как правило на С. Продукт - я имею ввиду продаваемое устройство.


        1. egaoharu_kensei
          10.08.2023 06:16

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

          Я могу на плюсах написать любое нужное расширение и импортировать его из питона, получив такую же скорость. Можно с помощью Cython или numba прямо на питоне тащить high-load прод, единственное - это computer vision или очень большие сервисы: в этих задачах очень часто питон используется для прототипирования, а в прод всё или почти всё тащится на низкоуровневых языках, но это действительно интересные и сложные задачи, но таких от общего количества не очень много.

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


  1. anonymous
    10.08.2023 06:16

    НЛО прилетело и опубликовало эту надпись здесь


  1. bazilevichla
    10.08.2023 06:16

    Если честно, мне до опытного программиста далеко, так что какие-то вещи я пока не понял, НО
    Вот там, где я понял, не могу не согласиться. А примеры с Calendar и символьным адом, так вообще непроизвольно вызвали у меня смех.
    В общем, статья длинная, новичкам не вполне понятная, но определённо познавательная, спасибо!)


  1. event1
    10.08.2023 06:16
    +1

    Тут всё понятно, если обратится к истории. Питон создавался чтобы быстро и интересно слеплять крестовые библиотеки. Штуки, которые слишком сложны для баш-скрипта, но до полноценного проекта не дотягивают. А потом язык и Гвидо были взяты на вооружение гуглом и там стали делать из питона джаву. Добавили ABC, исключения стали исключениями (раньше можно было кидать что угодно), аннотации типов. Всё что нужно для "настоящего" проекта ПО. Результат — текущее состояние, где соседствуют старый и новый подходы.


    1. funca
      10.08.2023 06:16

      Гугл ставили опыты над питоном буквально несколько лет. Пока не поняли, что сделать из уже ежа не выйдет. Наработки были выброшены на всеобщее обозрение в виде патчей, из которых были приняты лишь несколько штук. А Гугл потом сделали по мотивам Go и взялись за JavaScript, получив V8.


  1. bikeroleg
    10.08.2023 06:16
    +1

    Очень рад видеть очередной кайфовый пост от @kesn — и и покачать головой, и посмеяться, и что-то новое узнать. Спасибо, не болейте!


  1. funca
    10.08.2023 06:16
    +2

    Хорошая работа. Из свежего можно было добавить pattern matching, но здесь поводов для глумления хватит на отдельный пост.

    Основная проблема питона в том, что они тащут в язык и стандартную библиотеку абсолютно сырые вещи. Вероятно чтобы потом, с помощью сообщества, доделать как надо. Но это 'как надо' никогда не наступает потому, что сообществу в первую очередь нужны не эти ваши эксперименты, а чтобы при обновлении ни чего не ломалось. Иными словами - обратная совместимость. Поэтому все эти уродцы, единожды попав в питон, остаются в практически неизменном виде там на многие годы.


    1. Alpensin
      10.08.2023 06:16

      А было бы интересно про pattern matching. По-моему крутая вещь. Отлично что ее взяли из раста. И там она очень даже к месту.


  1. Lamaster
    10.08.2023 06:16

    Язык старше Java, что вы от него хотите. И даже при всей своей многолетней истории даже не пытается избавиться от родовых болячек.

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


    1. Andy_U
      10.08.2023 06:16

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

      Вы могли бы пример кода привести?


      1. Lamaster
        10.08.2023 06:16

        Кажется я перепутал с поведением поля экземпляра. Но всё равно для меня это выглядит очень странно

        class What:
            field = 1
        
        a = What()
        print(a.field) // 1
        a.field = 2
        print(a.field) // 2
        del(a.field)
        print(a.field) // 1

        PS: Сейчас прочитал, что это переменная класса, а не экземпляра. Ладно, я не знаю питон)


        1. Andy_U
          10.08.2023 06:16

          В вашем коде изначально и в конце - она. А после присваивания двойки - таки экземпляра. Поменяйте print() на

          print(a.field, id(a.field), What.field, id(What.field))

          и все увидите.


        1. Andy_U
          10.08.2023 06:16

          Вдогонку, а последний PyCharm 2023.2 врет. Но issue лень оформлять.


    1. slonopotamus
      10.08.2023 06:16

      Язык старше Java, что вы от него хотите.

      В отличие от Java, у питона случился переход 2->3 с довольно значительными поломами обратной совместимости, тут нет непрерывности.


      1. khajiit
        10.08.2023 06:16

        Зато сейчас требуется устанавливать аж 3 jvm, если вы используете какое-нибудь копроративное поделие вроде апплета для BMC: актуальную, с поддержкой современного софта, устаревшую, в которой работает несовременный, и еще одну для старых извращенцев, не осиливших за охреневшие бабки и 17 лет переехать на что-нибудь посвежее..


  1. czz
    10.08.2023 06:16

    Пользуясь случаем, спрошу — как нормально объявить пустой асинхронный генератор (который не возвращает ни одного элемента)?


    1. ValeryIvanov
      10.08.2023 06:16
      +1

      1. Зачем?

      2. async def empty_agenerator(): return; yield


      1. czz
        10.08.2023 06:16

        1. Реализуем интерфейс, в котором есть этот генератор. Наша реализация не должна ничего генерировать.

        2. Не пройдет статический анализ:

          1. unreachable code

          2. yield требует указать значение, если генерируемый тип не включает None


        1. ValeryIvanov
          10.08.2023 06:16
          +1

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

          Но как правило, асинхронного итератора вполне достаточно. Встроенного способа создания пустого асинхронного класса-итератора, я не нашёл, но его с лёгкостью можно написать самому:

          from typing import AsyncIterator
          
          class EmptyGenerator(AsyncIterator):
              __slots__ = ()
          
              def __aiter__(self):
                  return self
          
              async def __anext__(self):
                  raise StopAsyncIteration
          


          1. czz
            10.08.2023 06:16

            Спасибо, выглядит отличным решением. В интерфейсах использовать более общие AsyncIterator[...], а для реализаций написать фабрику типизированных пустых асинхронных итераторов.


  1. DimaFromMai
    10.08.2023 06:16

    Если вдруг кто не смог прочитать очень длинную cut строчку (на старой версии сайта её прочитать нормально нельзя):


    Это что же получается, kesn опять открыл postman и сломал вёрстку на сайте? Поразительно, никогда такого не было, и вот опять! В принципе, тут можно писать текст любой длины (похоже, у них на бэкенде не Char(255), а Text). Они проверяют длину только на фронтенде, а бэкенд принимает строку любой длины. И это, блин, забавно) Вообще мой девиз — 'кто ищет, тот всегда найдёт', поэтому я ищу постоянно. Кстати, на Хабре скоро выйдет статья про программирование глазами Погромиста, там в том числе про уязвимости на сайтах будет — поэтому если не хотите пропустить, то подписывайтесь на меня в телеге: @blog_pogromista


    1. 9982th
      10.08.2023 06:16

      Вы заставили меня перепроверить, я все еще на старой версии и прочитать это целиком можно, уж не знаю, что именно вы понимаете под "нормально".


      1. DimaFromMai
        10.08.2023 06:16

        Скриншот того, как это выглядит у меня:



        1. kesn Автор
          10.08.2023 06:16
          +1

          Хм, я планировал вот так:

          Ох уж этот Хабр, даже верстка ломается не консистентно


  1. Andrey_Solomatin
    10.08.2023 06:16
    +1

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

    Как педант, педанту, в Питоне всё рантайм.


  1. Andrey_Solomatin
    10.08.2023 06:16

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

    На этой магии всё остальное работает. Иногда смотришь внутрь и удивляешься.

    Как вы думает, что такое NamedTuple в этом примере из документации? https://docs.python.org/3/library/typing.html?highlight=namedtuple#typing.NamedTuple

    class Employee(NamedTuple):
        name: str
        id: int

    https://github.com/python/cpython/blob/main/Lib/typing.py#L2770


  1. Andrey_Solomatin
    10.08.2023 06:16

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

    Не совсем. Можно поменять аннотацию list на typing.Sequence. Мы по прежнему можем передать list в качестве аргумента, на как только начнем его мутировать внтури, статический анализатор запоёт.

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


    1. kesn Автор
      10.08.2023 06:16
      +2

      Ну тогда уж можно передавать пустой tuple - и аннотация Sequence будет верная, и мутировать не получится


    1. trankov
      10.08.2023 06:16

      Я мог неверно понять автора, но разве речь не про:

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


  1. Andrey_Solomatin
    10.08.2023 06:16

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


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

    def foo(*, bucket, key):
        ...
    
    foo(bucket="key", key="bucket")
    foo(key="bucket", bucket="key")