Обновить

Комментарии 26

На мой взгляд, самый вопиющий речекряк питона - это существование StopIteration

А по-моему это гениально. Или как в других языках, будем иметь два метода вместо одного, типа Iterator.hasNext() -> boolи Iterator.next().

После того, как я перешёл на Rust... ладно, не перешёл, но плотно изучил, меня каждый раз от этого StopIteration передёргивает.

В Rust (для сравнения) next возвращает Option<Value>, который либо Some(Value), либо None (None - это не питоновский None, это из enum'а None|Some). В рамках синтаксического сахара for вычитывает из итератора пока не получит None. Но любой желающий может взять next() сам сколько нужно раз и посмотреть содержимое.

И правильно обработать None. В питоне "правильно обработать" можно только так:

try:
	foo = next(bar)
except StopIteration:
	foo = "something else"

Сравните это с rust'овой версией:

let foo = bar.next().unwrap_or("something else")

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

UDP задумался, есть ли дефолт у next. Есть, спасибо. Можно foo=next(bar, "something else"), хотя моя претензия по поводу эксепшена в неисключительной ситуации всё равно остаётся.

Какая претензия может еще оставаться? Какое должно быть поведение по умолчанию если нет следующего элемента, а ты вызываешь next()? Почему именно ваше поведение по умолчанию лучше возбуждения StopIteration? В крайнем случае, реализуйте __iter__ и __next__ у класса объекта bar задав свое поведение при случае когда последовательность закончилась.
Что будет делать Rust если убрать .unwrap_or("something else")? Чем подход bar.next().unwrap_or("something else") лучше подхода next(bar, "something else")?

Тем, что вместо unwrap я могу написать так:

if let Some(x) = foo.next(){
    ...
}

Как вы такое на Python напишете?

Это преимущества не Rust, а функциональной парадигмы.

Pattern Matching до сих пор нет даже в Kotlin: ждут появления его в Java (оно задерживается). Бреслав где-то говорил, что "патмат" требует примерно столько же строк в реализации с нуля, сколько весь остальной язык.

Да пожалуйста, если не хотите StopIteration:

class Sentinel: ...

if x := next(foo, Sentinel):
     ...

Только это не нормальный Питоний код же. Вы часто видели, чтобы next() вызывался явным образом, а не в контексте for? Или вам часто приходится кидать StopIteration вручную?

Мне кажется, что вы не до конца рассмотрели идею со StopIteration .
Это исключение появилось как часть протокола итераторов и генераторов. А генераторы можно соединять, даже не имея yield from.
А как в цепочке генераторов проще всего сигнализировать на самый верх, что глубинный генератор истощён? Бросаем исключение, которое просто всплывает наверх.
И да, этот механизм - противоречивый и далеко не самый эффективный. Мне лично больше импонирует функциональный подход Раста. А с другой стороны - этот механизм чертовски простой и проверенный временем :)

Рассказываю простой пример, где надо next вызывать вручную.

У нас грок-подобный парсер потока строк. Мы хотим найти match1, после чего мы почти уверены, что будет match2 и match3. "Почти", потому что может оказаться, что нет, и тогда match1 тоже не валидный, и строки под ним тоже, потому что по спецификации, если match1, то match2 и match3 идут строго после.

def parse(iterator):
  data = []
  while True:
    match1 = grok1.match(next(iterator))
    if match1 == EOF:  ## not valid
      break
    if match1:
      match2 = grok2.match(next(iterator))
      match3 = grok3.match(next(iterator))
      if match1 and match2:
         data.append(combine(match1, match2, match2))
   yield from process(data)

А теперь сделайте так, пожалуйcта, чтобы не надо было это обтыкивать try/except и покажите пример красивой реализации break.

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

def parse(iterator):
    data = (
        combine(*matches)
        for matches in read_triplets(iterator)
    )
    yield from process(data)

def read_triplets(iterator):
    while all((        
        (match_1 := grok1.match(next(iterator, EOF))) != EOF,
        (match_2 := grok2.match(next(iterator, EOF))) != EOF,
        (match_3 := grok3.match(next(iterator, EOF))) != EOF,
    )):
        yield (match_1, match_2, match_3)

Пардон, понял.

Но мне всё равно неприятно видеть штатную ситуацию обрабатываемую как исключительную. Исключения - для ошибок или неожиданных ситуаций.

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


А значит, тут можно обойтись ОДНИМ try/except вокруг всего цикла. Что внезапно оказалось даже лучше паттерн-матчинга, которых потребовалось бы аж три.

Данные для обработки есть (предыдущие накоплены). И, главное, я ожидаю окончания, то есть ситуация не исключительная, но почему-то я должен ждать исключения.

Что будет делать Rust если убрать .unwrap_or("something else")?

Код не скомпилируется, ибо в foo будет лежать значение типа Option<SomeType>, а ниже по коду будет ожидаться foo типа SomeType.

Ничего не имею против ни Python, ни PHP, но картинку можно было бы подобрать более подходящую :)

Upd. Картинку изменили :)

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

Классический пример, дефолтные параметры функций:

from time import time

def foo(timestamp=time()):
    print(timestamp)

Все что связано с модулями и импортами это вообще отдельная история.

Классический пример, дефолтные параметры функций:

А можно разъяснить, что не так с дефолтными параметрами? КМК достаточно очевидно, зачем оно.

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

def foo(bar=[]):
  return bar

x = foo()
x.append(4)
y = foo()

В результате x и y ссылаются на один и тот же список.

Такой проблемы нет в "низкоуровневых языках"

Понятно. Ещё одна неочевидная особенность :)

Только не при первом обращении к функции, а при создании интерпретатором объекта функции.

Да, Вы правы

Про дефолтные значения в FAQ еще написано
https://docs.python.org/3/faq/programming.html#id13
Я вообще всем новичкам рекомендую FAQ читать, а потом еще перечитывать. А его к сожалению мало читают, а там как раз много полезного, чтобы поменьше на грабли наступать.

Да, на любую критику языка можно сказать "RTFM". Но это так не работает.

Мне нужен инструмент для быстрого и эффективного решения бизнес-задач, в котором код легко читаем, а результат его выполнения очевиден. А не ночные сессии починки продакшна, потому что 3 сеньора которые аппрувили пулл реквест, не перечитывали в очередной раз FAQ и пропустили вроде как "простую" ошибку.

Как Вы хотите, к сожалению, тоже не работает.

Я тоже так хочу, как вы говорите: чтобы было было все просто и очевидно.

Но любая сложная система (к которой относится любой развитый язык программирования) полна различных "особенностей". Вопрос только в том, насколько их много и насколько они хитрые.

Надо быть реалистами и ожидать подвоха даже от самых "простых", "удобных" и "дружелюбных" инструментов. Поэтому, чем лучше R этот самый TFM, тем меньше сюрпризов будет.

Статистические языки программирования
языки со статической типизацией точно называются статистическими?
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Информация

Сайт
skillbox.ru
Дата регистрации
Дата основания
Численность
501–1 000 человек
Местоположение
Россия