Вас приветствует ваш зануда!
Если вы следите за моей ленивой активностью, то заметили бы, что у меня много от чего пригорает. Вот, например:
У меня пригорает от низкосортных статей на потоке: Питон против Безумного Макса, или как я посты на Хабре замораживал
У меня пригорает от Django: Окей, Джанго, у меня к тебе несколько вопросов
И от Яндекса тоже: Собеседование в Яндекс: театр абсурда :/
И от рекрутеров: Я единственный из 1400, или самый крутой рекрутинг, что я проходил
Посмотришь так - я уже давно должен был сгореть. Но я, аки феникс, возрождаюсь, и сегодня у меня горит от, внезапно, Питона, на котором я пишу больше десяти лет. Если вам интересно, что же, по моему мнению, с ним не так - то прошу под кат.
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)
Но есть несколько "но":
Линтеру это может не понравиться
Кто-нибудь решит, что ему хочется изменять список в момент выполнения функции, и всё сломается
Другой разраб может это увидеть и не понять, прям как в меме:
Нет const
Проблемы выше не было бы, если бы был const
, который бы говорил: вот эту штуку изменять нельзя. Но в питоне так не принято. В питоне кто угодно может изменять что угодно когда угодно.
Аргументы
Я люблю, когда аргументы задают по имени:
call(me='maybe')
Сразу понятно, что, кого и как.
Я также не против позиционных аргументов, когда это просто, ну например:
max([1, 2, 3, 4, 5])
Но в целом именные аргументы (kwargs
) всегда лучше позиционных (args
):
При рефакторинге ничего не поломается: я могу менять местами и добавлять аргументы, и всё будет работать
Лучше читается, даже если вы называете ваши переменные как гоблин: сравните
display(hehe, trololo)
vsdisplay(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
Как запустить что-то в потоке и вывести результат?
Ну, есть
ThreadPoolExecutor
- красиво:
from concurrent.futures import ThreadPoolExecutor
fn = lambda: 5
with ThreadPoolExecutor() as pool:
future = pool.submit(fn)
print(future.result())
Но тут вы можете сказать: это слишком просто! Давай, напиши что-нибудь по-джуновски! Вот, получайте - куча бойлерплата, чтобы окостылить класс
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)
Казалось бы, вот вам low-level
Thread
, вот вам high-levelThreadPoolExecutor
- юзай что нравится. Но вдруг вы фанат процессов и жить без них не можете? Ха, питон позаботился о вас, ведь вы можете работать с потоками при помощи своего любимого модуля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 to1
* Using the Python Development Mode
* Passingdebug=True
toasyncio.run()
* Callingloop.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)
ivankudryavtsev
10.08.2023 06:16+4А меня бесит, что нельзя понять стандартным образом сколько я прождал GIL. Да и вообще, профайлинг GIL отстойный. Да, ничего не делающие аннотации типов - тоже такое себе.
Прям интересно услышать мнения минусующих данный опус. От чего у вас подгорает?
Abobcum
10.08.2023 06:16+4Аннотации типов позволяют использовать статический анализатор. А вот включить его или нет остаётся за пользователем, как и все в питоне - полная свобода.
Kanut
10.08.2023 06:16+14В общем, питон — динамический. Очень динамический. Даже слишком. По моему опыту, в 99% случаев мне эта динамика вообще не впёрлась
Так может вы просто выбрали для своих задач не тот язык программирования? :)
syrslava
10.08.2023 06:16+16Тогда мне искренне интересно, для каких задач нужна такая динамика, и почему Python не остаётся узкоспециализированным языком?
Kanut
10.08.2023 06:16+7Ну если вам нужно писать много и быстро разных "прототипов" чего-то, то динамические языки вполне себе удобны. Поэтому питон так любят научные работники или там тестеры железа в индустрии.
Я на нём пишу всякие скрипты для генерации всего подряд: wrapper'ов, конфигураций, упаковки пакетов и т.д. и т.п. Тоже заметно быстрее и удобнее чем использовать для этого те же C#/Java на которых у нас собственно пишется весь "продуктивный" код.
sshikov
10.08.2023 06:16А нахрена вы "прототипы" пишете на Java, когда много-много лет есть отличные котлин и груви? Причем берете и ставите типы — и получаете меньше динамики. Что-то мешает? Я понимаю когда мешает отсутсвие под рукой JVM, но у вас же есть?
Kanut
10.08.2023 06:16+5Вы не правильно поняли. На Java я их как раз таки не пишу. Их я пишу на Питоне.
А на Java у нас всё ещё есть ряд легаси-проектов которые не особо имеет смысл целиком переписывать заново на данный момент.
sshikov
10.08.2023 06:16Ну хорошо, на Java не пишете, я неправильно сформулировал вопрос. Я тоже не предлагаю вам легаси приложение на груви или котлин переписывать.
Тоже заметно быстрее и удобнее чем использовать для этого те же C#/Java
Речь вот об этом. Вам питон удобнее Java. Для скриптов — верю. Так вот груви или котлин — удобнее питона. Их недостаток — что нужно JVM. Это не всем нравится. Но у вас же это есть.
Почему вы прототипы не пишете на условном груви, при наличии под рукой JVM? Вот просто пример — я пытался скажем на питоне писать некий скрипт для мониторинга приложения. Если приложение на Java — то его метрики зачастую уже лежат в JMX. А добраться до JMX из скрипта на груви в сто раз проще, чем из скрипта на питоне. И таких примеров я мог бы вспомнить десятки. Это не значит, что груви всегда выигрывает у питона для таких проектов — но очень часто таки да (при том что синтаксис у груви и котлина, как по мне, местами сильно приятнее питоновского).
Kanut
10.08.2023 06:16+4Потому что мне так проще. Вот как-то так получается :)
Плюс ещё пожалуй потому что наши инженеры или эмбеддеры в питон тоже умеют, а в джаву/котлин/груви нет.
sshikov
10.08.2023 06:16Ну "я так не умею" — это тема, несомненно. Мы учим :)
У нас девопс весь на дженкинсе, там так или иначе либо "а ля груви", либо прямо чистый груви. Так что многие умеют.
Kanut
10.08.2023 06:16+1Вот только вопрос зачем их именно этому учить? Чтобы лично мне было удобнее? Так себе аргумент. Особенно учитывая что у них то JVM не стоит.
И наши инженеры это не девопс. Это именно что инженеры: электротехника, мехатроника, машиностроение. Хорошо что хоть на чём-то умеют :)
sshikov
10.08.2023 06:16Чтобы лично мне было удобнее?
Ну как вы догадываетесь, я вряд ли смогу объективно измерить, что мне дает груви вместо питона. Субъективно — как правило объем кода сокращается (не на порядок, а скажем в пару раз), а сопровождение упрощается. Функциональность при этом либо не страдает, либо больше. Качество и функциональность доступных библиотек — как правило в моем случае сильно выше. Скажем, не так просто будет найти аналог условного Apache POI, если вдруг придется поработать с файлами Office. Ну это тема неисчерпаемая, не буду углубляться.
что у них то JVM не стоит.
Ну, у нас вот Java применяется, и я с трудом могу представить себе аргумент такого вида "у нас тут на хосте оно не стоит".
Kanut
10.08.2023 06:16Ну, у нас вот Java применяется, и я с трудом могу представить себе аргумент такого вида "у нас тут на хосте оно не стоит".
Ну так Java она у нас применяется. Не у них :)
0xd34df00d
10.08.2023 06:16+5Ну если вам нужно писать много и быстро разных "прототипов" чего-то, то динамические языки вполне себе удобны.
Смотря с чем сравнивать. Если с
те же C#/Java
где языки тяжеловесные по синтаксису/необходимости аннотаций типов/етц, то да, конечно. Но динамика просто скрывает некоторые из этих вопросов.
Я пишу прототипы на том же х-ле, и вполне отлично и быстро получается, и не представляю, как бы их писать на питоне.
Поэтому питон так любят научные работники
Исторические и культурные причины.
Олсо, другие научные работники любят матлаб, третьи — идрис. Зависит от того, в какой ветви науки эти работники работают.
Zara6502
10.08.2023 06:16+2как ни странно, но мне проще на C# написать в первую очередь из-за удобства отладки кода, а с питоном как-то странно - кидаешь в черный ящик и непонятно почему оно не работает.
Zara6502
10.08.2023 06:16из плюсов только не нужна IDE и создание проекта (но в C# внезапно тоже есть консольный компилятор и писать можно в блокноте, но по понятным причинам я так никогда не делаю.
Kanut
10.08.2023 06:16C# требует слишком много телодвижений. Питон можно просто в консоли быстро набросать и запустить.
Zara6502
10.08.2023 06:16+2каких телодвижений? если вы о процессе компиляции и запуска - ну да, две команды в консоли, но неудобства от такого перевешивают мифическую скорость.
а работа в самой консоли для меня совершенно неудобна, мне проще запустить VS и там же набрать код для питона и запустить - и синтаксис подсвечивается и дебажить проще. а сразу с нуля что-то без ошибок написать это уровня Hello World? Тем более в структуре кода постоянно то туда то сюда бегаешь пока пишешь, как это вменяемо делать в консоли?
economist75
10.08.2023 06:16+4Все верно написано, но доставляют трудности ~1% из этого. Ясность синтаксиса позволяет прощать Питону любые несуразности. И потом, они чем-то даже приятны, дают бронзовый налет эрудированности и поводы для душевных разговоров.
Нет ЯП без недостатков, питонисты их не скрывают вообще. Надо отдать должное - все статьи подобного рода (даже иностранные) - пропитаны здоровой самоиронией, читать приятно. Стоит ли исправлять - да. Но именно неспешно, как было сделано с unicode в Python 2->3. Тогда было жестко, но язык никто не бросил.
HemulGM
10.08.2023 06:16+21Ясность синтаксиса позволяет прощать Питону любые несуразности
По-моему в статье как раз идет речь о том, что синтаксис и приемы не совсем ясные
economist75
10.08.2023 06:16Статья про несуразности в синтаксисе. Их в нем немного, иначе бы таких статей была на одна в год, а больше, а язык ни за что не стал бы Top-3 на десятилетие.
Ясность синтаксиса - это про среднюю длину ключевого слова, объем кода в символах и строках, число скобок, спецсимволов итд. Код на Python лаконичен и читается с двух метров. Да, это заслуга значащих отступов и удобного Py-синтаксиса без ";" и т.н. закрывающих (циклы, условия, функции, классы итд) команд.
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;
economist75
10.08.2023 06:16Паскаль - тоже хороший и довольно ясный язык. Но убедить в одинаковой читаемости разных ЯП еще никогда никому не удавалось, это ведь вкусовщина. Хотя...
Без пробелов и комментов код на Питон в ~2 раза меньше занимает в байтах (из ваших же примеров). Значит он вдвое быстрее набирается и исправляется, содержит вдвое меньше опечаток.
Есть несколько общепризнанных критериев существенности отличий, p=0.05, 10%, 20%. Различия на +100% - точно существенны. Думаю что многие пользователи видят эти различия и выбирают то, что им важнее. Метрики популярности тому свидетели.
Такие архитектурные изыски сабжа как кавычки 3-х разных типов, отступы, срезы, списковые включения, итераторы, генераторы - оказались очень удачными, кмк. Я почти уверен что все это мелькало в других ЯП раньше, но собрать все подобные трюки в одном языке - это было смелое решение, и "ставка сыграла".
HemulGM
10.08.2023 06:16С длинной вы не совсем правы.
1. Чем длиннее ключевое слово, тем сложнее сделать опечатку. Либо эта опечатка приведет лишь к синтаксической и самой простой ошибке во время компиляции или анализа кода, а не исполнения.
2. Редакторы кода сами помогают написать ключевое слово. В том числе и операторные скобки.
3. Время, которое тратится на написание кода увеличивается минимально из-за длинных ключевых слов.
4. Возьмём, например, ключевое слово var, которое позволяет объявить переменную. Это ключевое слово оберегает ещё и от того, что переменная может быть использована из другой области видимости (про глобальные переменные в Питоне я знаю, и это только подтверждает мои слова, наличием слова global)
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)
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);
ValeryIvanov
10.08.2023 06:16Вы же сами приводите доказательства того, что питон выразительнее и читабельнее паскаля.
Просто мимо. На каждую манипуляцию с файлом нужно искать или писать утилитарный метод? Неужели, в паскале нет менеджера контекста? Или какого-нибудь оператора, который закрывал ресурс при выходе из блока. Видел недавно язык(zig, кажется), где такой оператор назывался defer.
Создание списка(хотя у вас почему-то числа просто выводятся на экран) занимает на порядок больше строк чем в питоне, что уже является большим минусом, так как ухудшает читаемость кода. Анонимная функция, кстати, тоже читабельности не способствует. Даже в жабе, которая славится своей многословностью, завезли Stream API с лямбдами и если чего-то подобного нет в паскале, то это явно минус.
Опять же, распаковка выглядит удобнее нежели постоянное обращение к Item. Такая же болячка есть у жабы, а вот C# удалось её побороть, добавив возможность деконструкции у KeyValuePair.
HemulGM
10.08.2023 06:16+1Все, о чем вы говорите - лишь синтаксический сахар.
Можно десятки способов реализовать для чтения файла. Вплоть до освобождения при выходе из блока. Это не проблема и не сложность.
Range - всего лишь функция, которая создаёт генератор. Добавить в него функцию ToArray дело 20 секунд.
Опять же, это просто сахар.
И нет, у Питона нет локаничности. Есть переизбыток механик, которые позволяют сильно кратко что-то описать, ухудшив понимание кода. Одна механика в коде - нормально, множество друг в друге - органы проблема. Просто мешанина из операторов, в которой нельзя просто разобраться, не отделив одно от другого.
Не надо забывать, что Паскаль - язык с ручным управлением памяти. И в нем реализован механизм подсчёта ссылок, которого достаточно.
ValeryIvanov
10.08.2023 06:16Все, о чем вы говорите - лишь синтаксический сахар.
И этот синтаксический сахар кратно уменьшает сложность кода.
Можно десятки способов реализовать для чтения файла. Вплоть до освобождения при выходе из блока. Это не проблема и не сложность.
Было бы славно, если бы в ответ на примеры кода @Andrey_Solomatin вы бы привели идентичные примеры на паскале. Тогда бы уже и можно было бы рассуждать о том, избыточен ли синтаксис питона или нет.
И нет, у Питона нет локаничности. Есть переизбыток механик, которые позволяют сильно кратко что-то описать, ухудшив понимание кода. Одна механика в коде - нормально, множество друг в друге - органы проблема. Просто мешанина из операторов, в которой нельзя просто разобраться, не отделив одно от другого.
В любом языке можно намешать всё в кучу и получить нечитаемое месиво. Это не проблема языка. Хороший программист не должен преследовать цели "сильно кратно что-то описать", но должен стремиться сохранить баланс между длиной кода и его читаемостью. Питон позволяет писать и подробно, и кратко, что не может быть минусом.
Не надо забывать, что Паскаль - язык с ручным управлением памяти. И в нем реализован механизм подсчёта ссылок, которого достаточно.
Ручное управление памятью, как правило не способствует читаемости кода. Наверное, только Rust сумел выделиться на этом поприще.
Andrey_Solomatin
10.08.2023 06:16-1Речь не о возможностях языка, а о читаемости.
Говорить о читаемости не используя всех возможностей языка смысла нет.
Явное открытие и автоматическое закрытие с контекстным менеджером это как раз о читаемости. Прочитав первую строчку сразу понятно, что не надо дочитав секцию до конца проверять, закрыли ли объект.
Списковые выражения это инструмент улучшения читаемости. Если их развернуть в циклы, я пол дня проведу листая простыни кода туда сюда.
С темы литералов для коллекций в плавно съехали. Вот например в джаве это целый цирк с конями. https://stackoverflow.com/a/6802502/1310066
vba
10.08.2023 06:16+1Автору спасибо,
А что за линтер такой, что проверяет на неизменяемость(immutability)?
Мне тоже не нравится система типов, типа :D :class Class1: @staticmethod def build() -> "Class1": # Господи, за что ? pass
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.
vba
10.08.2023 06:16Так это, Self вроде уже завезли
Так то в 3.11, А у нас часть продакшена на 3.7 ешшо ;( . Ну и тоже, почему не сделать просто имя класса без кавычек, зачем вводить Self?
whoisking
10.08.2023 06:16+5Self как минимум лучше тем, что уменьшает вероятность багов при изменении названия класса. Как бы вы его не переименовали в будущем, аннотация останется всегда верной.
Andy_U
10.08.2023 06:16+2Ну и тоже, почему не сделать просто имя класса без кавычек, зачем вводить Self?
Так добавьте первой строчкой
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()
mini_nightingale
10.08.2023 06:16+14TL;DR: "Мой простой Питон оказался нифига не таким простым, да как так то, а?"
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
nivorbud
10.08.2023 06:16+7Многие подобные вещи имхо надо рассматривать в историческом контексте. И питон и, например, джанго развивались параллельно и быстрыми темпами.
Мой первый проект на джанго датируется 2007-м годом. Тогда джанго была еще в альфа версии, и там еще не было 90% современных возможностей и батареек.
И если кто-то из 2023-го года взглянет на тот мой проект (а он работает до сих пор, облепленный костылями), то придет в ужас, задавая вопросы типа: а почему здесь не использовано наследование моделей, а почему здесь не использованы методы классов, почему для работы с деревьями используется кривой самопис, а не взят готовый джанговский модуль...??? А ответ прост - не было этого ничего в 2007 году: пайтон был еще в первой и второй версиях, джанго - в альфа версии и т.д. Ну а дальше - груз совместимости.
Fox_exe
10.08.2023 06:16+1О, да, пробовал я както в Django добавить Websockets и Async, соответственно... Это был ад танцы с бубном, т.к. часть кода - синхронна, а часть - асинхронна. А для кеша вообще пришлось писать свой модуль, склеенный из двух других (синхронного и асинхронного), т.к. ни тот ни другой не могли нормально работать с sync_to_async / async_to_sync...
Gadd
10.08.2023 06:16+11А ещё, забытая запятая в конце строки с присваиванием (упомянутое в статье) может подарить тонны WTF??? при попытках разобраться, откуда тут кортеж? И только опыт лишает или сокращает это удовольствие.
a = 42,
Опциональные скобки - это зло. Хотя часто это удобно.
Andrey_Solomatin
10.08.2023 06:16У меня достаточно часто такие вещи ловятся статическим анализатором. Но для этого надо расставлять аннотации по проекту.
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]Aquahawk
10.08.2023 06:16+8Вот объясните мне, почему п. 3 это уродливо? Я слышу это уже лет 10 в контексте разных языков программирования. Но этот код же элементарно читается, понятно что он делает, в большинстве ситуаций он будет самым эффективным. Что в нём такого плохого?
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))
farm
10.08.2023 06:16+7занимает 3 строчки вместо 1
тогда почему бы не записать в одну?
sum(my_list[i] for i in range(10, 21))
Aquahawk
10.08.2023 06:16занимает 3 строчки вместо 1
читается дольше чем варианты 1 и 2
Т.е. стоит играть в code golf с с каждой функцией? Может и классы не писать, не закладываться под расширение. Большая часть паттернов приводит к увеличению количества кода. Ну и покажите мне того человека которому сложно прочитать 3 строчки кода? Я никогда этого не понимал и продолжаю не понимать.
ammo
10.08.2023 06:16-2Если б вы пытались понять так же усердно, как передергиваете мои аргументы, то может и получилось бы
Sulerad
10.08.2023 06:16+9А как же следующее?
sum(my_list[i] for i in range(10, 21))
itertools работает с итераторами, у которых далеко не всегда есть произвольный доступ. Если вдруг у вас он есть, то лучше использовать его явно (ну или что-то кроме itertools)
Andrey_Solomatin
10.08.2023 06:16Первый вариант может быть за гранью возможности измерений, разница будет слишком маленькая.
Третий вариант норм, нет в нём ничего ужасного. Если нужно часто, напишите свою функцию.
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']
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
Еще есть
**
.
san-smith
10.08.2023 06:16+4Есть ощущение, что примерно половина названных проблем характерна и для других языков программирования (в том числе — статически типизированных) или не является проблемой на самом деле (если человек уже знаком с этими концепциями). А вторая половина — наследие языка с долгой историей.
Статья забавная, тут не поспоришь, но порой удивление автора вызывает не меньшее удивление.
danilovmy
10.08.2023 06:16это у Алекса стилистика такая. Тут не надо удивляться, просто наслаждайтесь.
@kesn - спасибо в очередной раз, услада для мозга. Хотя ты знаешь, про генераторы я с тобой не согласен, тот же пропуск элемента или как рапараллелить асинк генератор. А про вложенный break - как останавливать внешние циклы, если они организованы генераторами тоже уже есть на habr.
egaoharu_kensei
10.08.2023 06:16Интересно было почитать, но добавлю пару слов в пользу питона.
Питон - очень офигенский язык: у него простой синтаксис, по нему много материалов, он много где применяется, есть много библиотек и он очень хорошо сочетается с другими языками, что позволяет нивелировать его минусы, т.е. я могу спокойно писать расширения на с/с++ и юзать их из питона с высокой скоростью и этого, в свою очередь, хватит для большинства задач, а если нет, то могу тестировать прототипы на питоне, а потом всё выводить в прод на тех же самых плюсах.
Я это к тому, что не смотря на то, что GIL, питонячий синтаксис, скорость и т.д. местами пугают разработчиков на других языках, его знание позволяет сильно повысить скорость разработки продукта и иметь такой козырь в рукаве точно не будет лишним.
slonopotamus
10.08.2023 06:16+3у него простой синтаксис
По сравнению с чем?
trinxery
10.08.2023 06:16-3Он простой скорее в значении "ненагруженный"; обычно есть единственно правильный способ что-то записать, когда в других языках есть и несколько способов объявить функцию, ставить/не ставить фигурные скобки (и сами скобки нагружают), разные отступы между всем чем можно и прочее.
egaoharu_kensei
10.08.2023 06:16+1По сравнению с низкоуровневыми языками. Например, если сравнивать с С++, то не нужно контролировать утечки памяти, объявлять тип данных (хотя такое есть в питоне), писать кучу фигурных скобок и многое другое. В сумме такие вещи делают код проще для восприятия, а это многого стоит.
Efrit
10.08.2023 06:16+2Интересно, для меня одного
код проще для восприятия
в случае, если в нём явно объявлены типы данных?)
egaoharu_kensei
10.08.2023 06:16-1Хорошо подмечено). Дело в том, что в питоне также есть аннотация типов, но она в отличие от низкоуровневых ЯП на производительность не влияет и как раз нужна для лучшего восприятия.
Тогда может показаться, что в питоне код будет сложнее для восприятия, но суть в том, что если давать нормальные названия переменным и функциям, то аннотация типов нужна далеко не в каждом случае, в то время как в низкоуровневых ЯП это необходимо делать всегда.
Для разрабов на других ЯП это может показаться дичью, но со временем привыкаешь. Вот в JS действительно беда, а здесь ещё норм)
ris58h
10.08.2023 06:16Как аннотации типов влияют на производительность в низкоуровневых ЯП? Раскройте мысль, пожалуйста.
slonopotamus
10.08.2023 06:16+3Положительно влияют. Сложить int + int процессор может одной ассемблерной инструкцией. Но для этого надо заранее знать что там именно int.
egaoharu_kensei
10.08.2023 06:16Указывая тип переменных, заранее известно сколько ресурсов требуется выделить оперативной памяти и в момент компиляции система подготовит необходимое кол-во памяти под наши переменные, а в том же самом питоне распределение памяти идет на момент выполнения каждой строки кода (грубо говоря).
Например, эта разница хорошо видна при использовании разных интерпретаторов: если использовать Cython вместо Cpython, то код в основном будет отличаться только указанием типов переменных, а скорость увеличится в разы.
Pastoral
10.08.2023 06:16-6Если вы следите за моей ленивой активностью, то заметили бы, что у меня много от чего пригорает.
А то. Ведь у Вас замкнуло - Вы пытаетесь минимизировать энергетические затраты мозга на понимание вызывающими гормональное поощрение методами.
Захотелось убрать под спойлер
Классика: если я не понимаю, значит я глупый -> если я не понимаю, значит это неубедительно -> если я не понимаю, значит это неверно. Достигнуто: если я не понимаю, значит я самый умный.
Это я не сам догадался, это профессор Савельев на YouTube объяснил, он знатный популяризатор, даже в книжки евойные можно не глядеть.
Всё едино - Эппл так же хейтят. Но преимущество Эппл в умении не обращать внимание на собаку будучи караваном. А несчастные типа Python вполне могут понаделать глупостей, и действительно глупости делают.
Как я такое замечаю? Да по замене "я хочу достичь этого" на "я хочу сделать так".
Кому не нравится пост - обломайтесь. Моя карма, как хочу так и трачу (за Интернет уплачено, развлекаюсь как моей душеньке угодно).
sci_nov
10.08.2023 06:16+4Мне кажется, что Python - для быстрого прототипирования, для создания эталонных алгоритмов, функций, тестов. Использовать его в продукте - дело такое, надо несколько раз подумать.
max851
10.08.2023 06:16+2Скажите это тоннам легаси аля 2.2-2.7, которые работают уже лет пятнадцать (сам не верю этой цифре... как время то пролетело)
PS пример с прода, чтобы не быть голословным https://github.com/SpriteLink/NIPAP
egaoharu_kensei
10.08.2023 06:16+1Согласен с вашими словами кроме продукта: Cython, Numba, библиотеки типа itertools, numpy, pandas, joblib, asyncio и расширения на других ЯП в совокупности позволяют добиться хорошей производительности, а этого хватит чтобы написать средних объемов сайт или неплохую рекомендательную систему.
Да, для беспилотников и прочих задач, где нужна максимальная скорость, питона не хватит, но для задач попроще - с головой, например, большая часть кода ml-библиотеки sklearn написана на Cython и этого вполне хватает чтобы учить модели на данных конских размеров, учить нейросетки на питоне - одно удовольствие, тестеры и девопсеры тоже пишут на нём скрипты. Так что всё не так плохо)
sci_nov
10.08.2023 06:16Так да, тестеры и девопсы пишут вспомогательные скрипты. Учить нейросети - тоже вспомогательная задача (research). В продукте всё равно всё будет статическое и написано как правило на С. Продукт - я имею ввиду продаваемое устройство.
egaoharu_kensei
10.08.2023 06:16Учить нейросети - это не ресёрч, ресёрч - это когда их создают). Рекомендательные системы, модели кредитного скоринга и т.д. почти всегда пишутся и тащатся в прод на питоне.
Я могу на плюсах написать любое нужное расширение и импортировать его из питона, получив такую же скорость. Можно с помощью Cython или numba прямо на питоне тащить high-load прод, единственное - это computer vision или очень большие сервисы: в этих задачах очень часто питон используется для прототипирования, а в прод всё или почти всё тащится на низкоуровневых языках, но это действительно интересные и сложные задачи, но таких от общего количества не очень много.
Я это к тому, что на сегодняшний день питон можно использовать в качестве основного языка в не во всех, но во многих задачах, особенно с учётом большого количества фреймворков, написанных на более быстрых языках.
bazilevichla
10.08.2023 06:16Если честно, мне до опытного программиста далеко, так что какие-то вещи я пока не понял, НО
Вот там, где я понял, не могу не согласиться. А примеры с Calendar и символьным адом, так вообще непроизвольно вызвали у меня смех.
В общем, статья длинная, новичкам не вполне понятная, но определённо познавательная, спасибо!)
event1
10.08.2023 06:16+1Тут всё понятно, если обратится к истории. Питон создавался чтобы быстро и интересно слеплять крестовые библиотеки. Штуки, которые слишком сложны для баш-скрипта, но до полноценного проекта не дотягивают. А потом язык и Гвидо были взяты на вооружение гуглом и там стали делать из питона джаву. Добавили ABC, исключения стали исключениями (раньше можно было кидать что угодно), аннотации типов. Всё что нужно для "настоящего" проекта ПО. Результат — текущее состояние, где соседствуют старый и новый подходы.
funca
10.08.2023 06:16Гугл ставили опыты над питоном буквально несколько лет. Пока не поняли, что сделать из уже ежа не выйдет. Наработки были выброшены на всеобщее обозрение в виде патчей, из которых были приняты лишь несколько штук. А Гугл потом сделали по мотивам Go и взялись за JavaScript, получив V8.
funca
10.08.2023 06:16+2Хорошая работа. Из свежего можно было добавить pattern matching, но здесь поводов для глумления хватит на отдельный пост.
Основная проблема питона в том, что они тащут в язык и стандартную библиотеку абсолютно сырые вещи. Вероятно чтобы потом, с помощью сообщества, доделать как надо. Но это 'как надо' никогда не наступает потому, что сообществу в первую очередь нужны не эти ваши эксперименты, а чтобы при обновлении ни чего не ломалось. Иными словами - обратная совместимость. Поэтому все эти уродцы, единожды попав в питон, остаются в практически неизменном виде там на многие годы.
Alpensin
10.08.2023 06:16А было бы интересно про pattern matching. По-моему крутая вещь. Отлично что ее взяли из раста. И там она очень даже к месту.
Lamaster
10.08.2023 06:16Язык старше Java, что вы от него хотите. И даже при всей своей многолетней истории даже не пытается избавиться от родовых болячек.
На питоне не пишу, но, лично для меня джависта, было удивлением, что переменная родительского класса и наследника - это не одна и та же область памяти.
Andy_U
10.08.2023 06:16было удивлением, что переменная родительского класса и наследника - это не одна и та же область памяти.
Вы могли бы пример кода привести?
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: Сейчас прочитал, что это переменная класса, а не экземпляра. Ладно, я не знаю питон)
Andy_U
10.08.2023 06:16В вашем коде изначально и в конце - она. А после присваивания двойки - таки экземпляра. Поменяйте print() на
print(a.field, id(a.field), What.field, id(What.field))
и все увидите.
slonopotamus
10.08.2023 06:16Язык старше Java, что вы от него хотите.
В отличие от Java, у питона случился переход 2->3 с довольно значительными поломами обратной совместимости, тут нет непрерывности.
khajiit
10.08.2023 06:16Зато сейчас требуется устанавливать аж 3 jvm, если вы используете какое-нибудь копроративное поделие вроде апплета для BMC: актуальную, с поддержкой современного софта, устаревшую, в которой работает несовременный, и еще одну для старых извращенцев, не осиливших за охреневшие бабки и 17 лет переехать на что-нибудь посвежее..
czz
10.08.2023 06:16Пользуясь случаем, спрошу — как нормально объявить пустой асинхронный генератор (который не возвращает ни одного элемента)?
ValeryIvanov
10.08.2023 06:16+1Зачем?
async def empty_agenerator(): return; yield
czz
10.08.2023 06:16Реализуем интерфейс, в котором есть этот генератор. Наша реализация не должна ничего генерировать.
-
Не пройдет статический анализ:
unreachable code
yield требует указать значение, если генерируемый тип не включает None
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
czz
10.08.2023 06:16Спасибо, выглядит отличным решением. В интерфейсах использовать более общие
AsyncIterator[...]
, а для реализаций написать фабрику типизированных пустых асинхронных итераторов.
DimaFromMai
10.08.2023 06:16Если вдруг кто не смог прочитать очень длинную cut строчку (на старой версии сайта её прочитать нормально нельзя):
Это что же получается, kesn опять открыл postman и сломал вёрстку на сайте? Поразительно, никогда такого не было, и вот опять! В принципе, тут можно писать текст любой длины (похоже, у них на бэкенде не Char(255), а Text). Они проверяют длину только на фронтенде, а бэкенд принимает строку любой длины. И это, блин, забавно) Вообще мой девиз — 'кто ищет, тот всегда найдёт', поэтому я ищу постоянно. Кстати, на Хабре скоро выйдет статья про программирование глазами Погромиста, там в том числе про уязвимости на сайтах будет — поэтому если не хотите пропустить, то подписывайтесь на меня в телеге: @blog_pogromista
9982th
10.08.2023 06:16Вы заставили меня перепроверить, я все еще на старой версии и прочитать это целиком можно, уж не знаю, что именно вы понимаете под "нормально".
DimaFromMai
10.08.2023 06:16Скриншот того, как это выглядит у меня:
kesn Автор
10.08.2023 06:16+1Хм, я планировал вот так:
Ох уж этот Хабр, даже верстка ломается не консистентно
Andrey_Solomatin
10.08.2023 06:16+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
Andrey_Solomatin
10.08.2023 06:16Недавно я осознал, что если функция "чистая", то есть не модифицирует входные аргументы, то такой код абсолютно нормальный:
Не совсем. Можно поменять аннотацию list на typing.Sequence. Мы по прежнему можем передать list в качестве аргумента, на как только начнем его мутировать внтури, статический анализатор запоёт.
def foo(var: int, checks: Sequence[Callable] = []): for check in checks: check(var)
kesn Автор
10.08.2023 06:16+2Ну тогда уж можно передавать пустой tuple - и аннотация Sequence будет верная, и мутировать не получится
trankov
10.08.2023 06:16Я мог неверно понять автора, но разве речь не про:
def foo(var: int, checks: list[Callable] = list()): for check in checks: check(var)
Andrey_Solomatin
10.08.2023 06:16Я не понимаю, почему где-то мне можно использовать имена аргументов, а
где-то нельзя, и почему в разных случаях по-разному. Можно, пожалуйста, я
буду писать так, как считаю нужным?
Я пробовал читать документацию про это, там всё очень сложно запутанно. Для себя использую только звёздочку. Аругменты в функции позиционные, но вызывать нужно как именные, а то будет ошибка в рантайме.def foo(*, bucket, key): ... foo(bucket="key", key="bucket") foo(key="bucket", bucket="key")
iamkisly
Это был интересный, хорошо оформленный пост! Держи плюсик в карму)
А я не люблю питон, но пишу на нем потому что "нам еще нужно на что-то кушац".. шутки шутками, но мне правда интересно было бы почитать исследование, а как много разработчиков занимаются тем, что искренне не любят и местами ненавидят.