Обновить

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

Стало выглядеть как f строки

А если токен будет из двух букв - это нарушает какое-то соглашение в таких случаях или так исторически так сложилось? fo от frozen object тоже нормально и нет путаницы с f-строками.

Если не зацикливаться именно на буквах, то токен уже состоит из двух конкретных символов:

  1. f" или f' — токен начала f-строки

  2. f{ — токен начала frozedict или frozenset

Для LLM и машинного поиска (e.g. grep) такой токен отлично различим, для человека — ну, думаю, привыкнуть можно быстро.

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

Я не знаю, почему люди так хотят :)

Как я писал выше, я был за $. Но демократия решила иначе.

Вот тут было первое голосование https://discuss.python.org/t/frozenset-and-frozendict-comprehensions/101584/48 
Люди хотят, чтобы было похоже на f строку :) Я сам был за $

прочел все 75 комментариев. Люди хотят frozenset(...) and frozendict(...) offer much clearer mnemonic value. Зачем вводить f{}, когда возможно очевидное frozenset(iterable) непонятно. кстати предложенный {}.frosen(), кмк, идеальный вариант.

Но, собственно, где я и где python...

Но ведь frozenset(iterable) не позволяет нормально оптимизировать код. В обсуждении по ссылке как раз есть примеры, почему так сделать нормально не получится. Сравните опкоды `frozenset({1, 2})` и `frozenset((1, 2))`. В том же обсуждении есть примеры, почему .take_frozenset и .take_frozendict не могут быть единственными дешевыми конструкторами. Например: при многострочных выражениях - непонятно, какой тип будет у объекта до самого конца. И другие важные штуки.

It is interesting

дичь конечно, но как есть:
почему не упростить синтаксис до mydict.take_frosen() myset.take_frosen(). тогда я могу нормально работать с getattr(obj, "take_frosen")() или по какой то причине необходимо использовать разные названия для одного и того же действия?
почему не стали использовать что нибудь типа myset as frosen?
Как будет проверяться что frosen? будет ли if myset is frosen:

Форматтер строки к сетам/диктам.... о да, это прекрасно нет. Это же очередной выстрел в ногу. Потому что у строки есть/был u'', f'', r'', t'' и теперь f'' будет у dict но он делает не то что вы полумали. а с автодополнением f{1,2,3} в vscode превратится в строку... "1,2,3" и никак не в фрозенсет.

Я почти ничего не понял :)

set.take_frozenset и dict.take_frozendict - разные методы для разных типов. их названия точно отражают то, что происходит внутри. Зачем сокращать названия builtin классов - не очень понятно.

почему не стали использовать что нибудь типа myset as frosen?

Потому что в питоне нет концепции кастов. as используется для создания имен, а не типов. Добавлять такое - точно не надо :)

Как будет проверяться что frosen?

Не очень понял вопрос. Как и сейчас? isinstance(obj, frozenset)

Форматтер строки к сетам/диктам

Нет, форматтер строки тут не при чем. Мы добавляем новый токен f{, который называется FLBRACE, по аналогии с { (LBRACE).

а с автодополнением f{1,2,3} в vscode превратится в строку

Автодопонение будет работать корректно. Сейчас же `{1, 2, 3}` не превращается в строку. И `f{1, 2, 3}` не будет. Не думаю, что тут могут возникнуть проблемы.

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

я не думаю, что тут есть реальная проблема. вы же просто помните, что {} - словарь; вот так же и f{} - иммутабельный словарь.

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

Как знание английского языка помогает вам понять `{1: 2}`? В чем принципиальное отличие от `f{1: 2}`?

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

Спасибо за вашу обратную связь! Вашу позицию не разделяю, но уважаю :)

да, но компилироваться это будет примерно в следующее:

  1. найти имя fdict в локальных/нелокальных/глобальных переменных

  2. проверить, что его вообще можно вызвать

  3. закинуть аргументы на стек

  4. вызвать функцию

  5. функция на C создаст нужный объект дергая внутренности интерпретатора

А литерал:

  1. закинуть аргументы на стек

  2. создать объект сразу нужным опкодом

Вы совершенно правы :) Сейчас есть похожая штука: сравните

» ./python.exe -m dis <<< 'dict(a=1, b=2)'      
  0           RESUME                   0

  1           LOAD_NAME                0 (dict)
              PUSH_NULL
              LOAD_SMALL_INT           1
              LOAD_SMALL_INT           2
              LOAD_CONST               1 (('a', 'b'))
              CALL_KW                  2
              POP_TOP
              LOAD_COMMON_CONSTANT     7 (None)
              RETURN_VALUE

И:

» ./python.exe -m dis <<< '{"a":1, "b": 2}'  
  0           RESUME                   0

  1           LOAD_CONST               0 ('a')
              LOAD_SMALL_INT           1
              LOAD_CONST               1 ('b')
              LOAD_SMALL_INT           2
              BUILD_MAP                2
              POP_TOP
              LOAD_COMMON_CONSTANT     7 (None)
              RETURN_VALUE

Вроде разница не очень большая, но: BUILD_MAP работает так:

inst(BUILD_MAP, (values[oparg*2] -- map)) {
    PyObject *map_o = _Py_BuildMap_StackRefSteal(values, oparg);
    DEAD(values);
    ERROR_IF(map_o == NULL);
    map = PyStackRef_FromPyObjectStealMortal(map_o);
}

Где _Py_BuildMap_StackRefSteal делает тоже самое, что и простой вызов _PyDict_FromItems. Ну а CALL_KW - один из самых медленных опкодов вообще.

Всё хорошо. Только метод назвал бы не take_frosendict, а as_frosendict. То же самое для frozenset. Так как-то понятнее и короче.

Ну, или хотя бы to_frozendict. Просто take - взять, а тут вроде отдавать надо. Поэтому и корёжит.

У take семантика забрать значения, у as оставить на месте и вернуть копию. Можно было бы использовать steal, но от него решили отказаться в пользу существующего прецедента в bytearray.

to_frozen по аналогии с int.to_bytes

to_bytes не уничтожает существующий int, а просто конвертит его. а тут нужен явный глагол, который бы показывал, что само левое значение обнулится:

>>> b = bytearray(b'123')
>>> b
bytearray(b'123')
>>> b.take_bytes()
b'123'
>>> b
bytearray(b'')

В питоне такое назвали take_* :)

надо было назвать yoink_* , с ним сразу понятно что происходит с содержимым :D

dambung_tsss 🌚️️

Вот оно что! Теперь тейк понятен ) Было неочевидно, что это именно взятие с уничтожением взятого. То есть что-то вроде move семантики в плюсах.

Да, что-то в таком духе :)

А в чём смысл вообще неизменяемых словарей? Структура данных точно такая же, производительность не изменилась ни на йоту. Чем оно принципиально отличается от Final[dict]? А так-то эти "оптимальные" take_somthing сильно смахивают на переливание из пустого в порожнее.

А может, наконец, настала пора сделать нормальные константы в Питоне?
А то в крайне требовательных к производительности местах приходится использовать константы 1,2,3 не то что вместо IntEnum, но даже вместо КОНСТАНТ в теле модуля.

мое не экспертное мнение следующее:

как вы будете реализовывать константность? - Ну допустим через const, но в C это модификатор типа, который прописывается при объявлении переменной определенного типа, а Python в этом смысле концептуально отличается + вводить отдельную семантику для обозначения константности - опрометчиво.

+ Тут речь не про новые структуры данных, а про новый синтаксис для их литералов ;)

const нету, но есть
x: Final[int] = 1234
Понятно, что на сегодняшний день это syntax sugar, но никто не мешает сделать, чтобы оно наконец заработало как положено. И при ссылке на x сразу в псевдокод вставляло прям число а не вызов locals()["x"]

Мешает :(

Сейчас, мы не знаем, что такое Final в момент компиляции. Final может быть ваш собственный класс.

  1. Аннотации могут быть ленивыми

  2. Они могут быть строками

  3. Они могут быть вообще не определены в рантайме в форме: if TYPE_CHECKING: from typing import Final

Как такое нормально превратить во что-то адекватное на шаге symtable? Я не знаю. И никто не знает :(

Да и проблему оно описанную в посте не решит; `d: Final = {1: 2}` все еще поддерживает (по текущей typing spec) все действия над dict, в том числе `d[3] = 4`.

Для неизменяемых структур можно делать оптимизации связанные с использованием из нескольких потоков, например, снизить количество refcount операций и/или локов. Подсчет ссылок как в FreeThreaded версии, так и в GIL-enabled JIT версии все еще сильно влияет на производительность.

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

А еще их можно будет дешево шарить в субинтерпретаторах, да :)

И раз уж зашел разговор - исключить из сборщика мусора!

Final не обозначет "иммутабельность", он обозначает "запрет" на re-assignment. Что разные вещи :) Суть frozendict в том, что его нельзя изменить. Такое очень полезно для большого количества задач. Например: данные request, которые пришли к вам от пользователя. Или какой-то контекст, настройки приложения, справочники, тд. Более того, с Free-Threading by default, такое станет еще нужнее. Я предлагаю начать готовиться заранее, а не срочно в 30м году все переделывать.

Вообще, это нигде явно не написано, но простые литералы 1-2-3 это уже и есть константы. У них у всех один и тот же id.

>>> id(1)
4321157912
>>> a = 1
>>> id(a)
4321157912

То есть то, что Вы хотите оптимизировать в данном Вами примере («константы 1,2,3»), создатели интерпретатора уже для Вас оптимизировали. Если я, конечно же, правильно понял Ваш пример.

В псевдокоде мы имеем LOAD_SMALL_INT 1 vs LOAD_GLOBAL 0 (ONE) что таки даёт значительную разницу по времени выполнения (второе - поиск по словарю)

Да, соглашусь — ну, если такие мощные требования к производительности, есть numpy, есть numba, в конце-концов, есть ctypes — для скриптового языка с динамической типизацией это и так довольно неплохо, он не для скоростной математики был придуман.

самое смешное, что типы ctypes работают примерно вдвое медленнее родного int. numpy страшно тяжёлый и тоже медленный на простых операциях, не перекладывающихся на массивное выполнение родного кода или даже на видеопроцессор. array лёгкий но малоразвит (нельзя даже прибавить смещение ко всем числам массива, например) и тоже вдвое медленнее. numba это вообще другой язык программирования встроенный в питон (примерно как webasm относится к javascript).

и?

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

Мне нечего ответить кроме как посоветовать форкнуть Питон и дописать туда константы.

Абсолютно знакомый синтаксис. Ведь мы просто добавили f (как frozen) перед существующими dict и set литералами.

fl = f[1, 2, 3, 4]

После этого в камментах началась резня... :)

К сожалению, f[1] - валидный синтаксис сейчас. Потому я и предлагал $. Но, люди отказались по причине (я не шучу): "мы не хотим $ как в php". Вот так программисты принимают технические решения 🌚️️

Я вообще про кортежи пытался пошутить...

А так да, - & скажут не хотим как в C++. Может предложить #? С идеей, что данные в клетке. Ни войти не выйти.

с # не выйдет :(

Потому что сейчас `#{1, 2}` - валидный комментарий.

может сделать универсальные суффиксы к литералам, как сделали в C++? (1,2,3)f не конфликтует с валидным синтаксисом.

А что же насчёт "_{...}" как в Python? )) или "_{...}_"?

⋯≔≡≣⫷⁅{“one”: 1}⁆⫸≣≡≕⋯

вы мне это зачем отправили?

так кросивее

Особенно доставляет frozen set f(1,2).

“Можно ли мне назвать свою функцию f()? - Можно, а зачем?”

dict.take_frozendict и set.take_frozenset

Ужасные названия. Почему не freeze?

Потому что они не делают freeze :) Они забирают данные из готовых мутабельных структур и создают новые иммутабельные.

Вы имеете в виду, что сам объект не изменяется, а возвращается другой? Я не вижу тут проблемы, таких примеров в языке достаточно, например datetime.replace

Нет, как раз сам объект и изменяется:

>>> b = bytearray(b'123')
>>> b
bytearray(b'123')
>>> b.take_bytes()
b'123'
>>> b
bytearray(b'')

А bytes(b) делает копию или просто увеличивает счётчик ссылок? В смысле, в чём смысл take_bytes ?

Конечно, bytes(b) делает копию:

>>> b = bytearray(b'123')
>>> b2 = bytes(b)
>>> b.append(int.from_bytes(b'4'))
>>> b
bytearray(b'1234')
>>> b2
b'123'

Если данные большие - мы так делать не хотим :)

в Qt, например, все присваивания основных классов делаются через увеличение счётчика ссылок. И лишь при изменении одного из объектов создаётся копия (своеобразный copy-on-write).

QByteArray x = y; // x и y указывают на один блок данных

x.append(4); // вот тут x делает копию и дописывает 4

Переделывать существующие структуры в CoW - не так просто, если мы хотим сохранить стабильный ABI.

Согласен что CoW штука хрупкая, но на C++ основная проблема это указатели (и итераторы), коих в Питоне нет. Навскидку сложно, но на первый взгляд как будто ничего не мешает реализовать такое в CPython, если там такого уже нет, конечно.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации