Pull to refresh

Comments 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, тем меньше сюрпризов будет.

Статистические языки программирования
языки со статической типизацией точно называются статистическими?
Sign up to leave a comment.