Comments 23
Вы пытались использовать typing.Final? Какая у него производительность? Это стандартная защита от переназначения, которая срабатывает ещё до запуска кода.
Почему вы решили расширить библиотеку, а не препроцессор (аля свой typing.Final), если вам требуется скорость? Согласитесь, что проверить переназначение полей класса перед деплоем или сборкой намного быстрее и эффективнее.
Тесты производительности мы описали прямо в README.md в репозитории (в самом конце). Окружение, в котором делались тесты — ipython.
typing.Final, во-первых, нововведение версии 3.8, а, с учетом гарантии (на текущий момент) совместимости с python 3.6, использовать его нет смысла, а во-вторых, даже описание говорит о том, что это все еще не более, чем аннотация типа. В документации по модулю typing так и говорится, что во время выполнения не производится никаких проверок.
Препроцессоры (содержимое модуля typing, и основанные на нем, к примеру, mypy, если я правильно понял) в основной своей массе не являются чем-либо пригодным для работы с ними в runtime. За исключением NamedTuple и TypedDict там, в общем-то, не с чем работать в каком-либо виде, кроме как с аннотациями. А аннотации сами по себе — это те же словари (type.__annotations__
), что небыстро и костыльно. Вдобавок, даже mypy, по слухам, весьма своеобразно поддерживает модуль typing. Ну и, к тому же, за отсутствием typing.Final как такового, мы вряд ли сможем поймать присвоение атрибуту класса (и, тем более, константе модуля) какого-либо значения, которое в остальных аспектах полностью удовлетворяет требованиям аннотации типа. Например:
MyStates.INITIAL = MyStates.PROCESSED
За исключением "финальности", остальные проверки типов ничего плохого в этом не увидят (это как раз беда "мутабельности" в рантайме).
https://github.com/python/cpython/blob/3.8/Lib/typing.py#L415 вот "реализация" Final в cpython 3.8. Патчить тайпчекер любыми способами было бы, на мой взгляд, сильно сложнее, нежели написать то, что мы создали. При этом мы обеспечили действительную иммутабельность в рантайме без необходимости пользователям нашей библиотеки каким-либо образом видоизменять свой деплой.
Можно случайно присвоить значение своей константе во время выполнения, а отладка и откат сломанных объектов — отдельное приключение.
Я всё понимаю, но это уж из разряда злобных буратино. В Питоне много таких мест, где можно «нечаянно» отпилить себе что-нибудь расчёской, не самая большая это, ИМХО, проблема.
Эдак во втором питоне тоже «нечаянно» можно было присвоить:
True = False
Хоть на уровне модуля максимум, но тоже «страашененько».
В третьем «слава богу» запретили, вздохнул спокойно=).
Но в целом `Enum` быстрый — это хорошо. Не понятно почему его в штатную библиотеку таким эффективным не включили. С другими типами не выпендривались же.
Дополнительно, в отличие от модуля с константами или класса со статическими атрибутами, Enum (даже штатный) решает очень важную задачу, которую иными средствами решить будет затруднительно: через typing и соответствующие синтаксические конструкции языка можно дать разработчику возможность очень строго контролировать что и где меняется. По той простой причине, что никакую другую аннотацию кроме : int
не получится навесить на констату модуля или статический классовый атрибут, если его значение и правда целое число. Как тогда на уровне TYPE_CHECKING хотя бы выводить предупреждения? Enum же можно присваивать "как есть" — то есть, my_obj.obj_attr: StdEnum = StdEnum.ENUM_MEMBER
. Поскольку гарантируется, что isinstance(StdEnum.ENUM_MEMBER, StdEnum)
, тайпчекер поймает любые другие значения, кроме членов нашего Enum. А уж доставать значения из члена уже можно "потом", когда объект планируется передать куда-нибудь наружу (то есть, сериализовать). Причем, в нашей реализации pickle уже поддерживается, равно, как и в штатном Enum.
Ну как-то не знаю. Не думаю, что это будет уж сильно полезно в реальных проектах/библиотеках.
class MyEnum(metaclass=FastEnum):
ONE: 'MyEnum'
TWO: 'MyEnum'
По-моему, вот эти аннотации для элементов перечисления выглядят неестественно и некрасиво. Понятно, что таким образом вы заменили auto()
, но на самом деле нет. Ваше решение имеет серьёзные недостатки: оно ограниченно и несовместимо с Enum из stdlib.
Что если я хочу в value хранить объекты произвольного типа? Ваше решение умеет хранить только целые числа, более того, вы неявно обрабатываете кортежи в значениях, что выглядит довольно странно. Что если я хочу хранить в значении NamedTuple
? Enum из stdlib может являться субклассом str или int, он может использоваться как комбинация флагов (Flag
, начиная с 3.7) и т. д. Вы выбросили всю эту функциональность.
Мне кажется, что более разумно было бы не создавать свой велосипед, которым никто не будет пользоваться кроме вас, а попытаться улучшить и ускорить реализацию Enum в stdlib.
Ваше решение имеет серьёзные недостатки: оно ограниченно и несовместимо с Enum из stdlib.
Полную совместимость с stdlib Enum никто и не обещал. Более того, из синтаксиса применения вполне очевидно, что они несовместимы (у нас реализация предоставляет только метакласс, а в стандартной библиотеке — пачка базовых классов)
Что если я хочу хранить в значении NamedTuple?
In [30]: class C(NamedTuple):
...: name: str
...: age: int
...:
In [31]: class B(metaclass=FastEnum):
...: VASYA = C('Vasya', 42),
...: PETYA = C('Petya', 13),
...:
In [32]: B.VASYA
Out[32]: <B.VASYA: C(name='Vasya', age=42)>
In [33]: B.VASYA.value
Out[33]: C(name='Vasya', age=42)
In [34]: B.VASYA.value.name
Out[34]: 'Vasya'
Вы выбросили всю эту функциональность.
Нет, мы ее просто не реализовывали. Список реализованных функций, идентичных тому, что предоставляет стандартная библиотека опубликован. При желании Вы можете через формальное описание логики в init hook написать и свою реализацию mutually exclusive flag-like.
Мне кажется, что более разумно было бы не создавать свой велосипед, которым никто не будет пользоваться кроме вас
Давайте время покажет, будут пользоваться, или нет. Заявлять "никто" только лишь потому, что не реализованы IntEnum и FlagEnum — это равнозначно заявлению, что только и исключительно ими, собственно, и пользуются. А, с учетом того, что все операции так или иначе в них будут проходить через .value
, скорости будут настолько впечатляющими, что лучше уж магическими константами, чем с ними. Enum, который используется сам по себе и то лучше выглядит (в случае, если его .name
и .value
нужны крайне редко). Да я даже тест провел:
In [43]: class F(IntFlag):
...: A = 1
...: B = 2
...: C = 3
...:
In [62]: btime = %timeit -o B.PETYA.value.age | B.VASYA.value.age
140 ns ± 0.451 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
In [63]: intflag = %timeit -o F.A | F.B
1.15 µs ± 7.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [68]: intflag.average / btime.average
Out[68]: 8.205303629774782
Как видите, те же 8 раз (это при том, что в моем случае с NamedTuple я еще дополнительно поле кортежа доставал)
Да, каждому пользователю библиотеки некоторую автоматизацию того, что уже предоставляется стандартной библиотекой, придется делать самостоятельно. Вполне вероятно, в нас полетят пулл-реквесты, и, может быть, библиотека пополнится наиболее востребованными дополнениями. Но свою главную задачу наша реализация решает: она быстрая, она позволяет исключить иммутабельность в рантайме, она реализует концепт Enum и, в конце концов, она расширяемая.
Хорошо, в вашем Enum можно хранить произвольные типы в value, но я всё равно не понимаю, зачем сделано вот так:
В отличие от стандартной библиотеки, мы обрабатываем только первое значение после знака = в объявлении класса в качестве значения члена:
A = 1, 'One' в стандартной библиотеке весь кортеж 1, "One" рассматривается как значение value;
A: 'MyEnum' = 1, 'One' в нашей реализации только 1 рассматривается как значение value.
Какую проблему это решает? Это очень неявно и противоречит принципам pythonic. Насколько я понимаю, 'One' должно записаться в какое-то другое поле, которое надо явно определять через конструктор?
Заявлять "никто" только лишь потому, что не реализованы IntEnum и FlagEnum — это равнозначно заявлению, что только и исключительно ими, собственно, и пользуются.
Я не говорил "только лишь потому", я говорил, что вы позиционируете ваше решение как замену Enum из stdlib, которое работает значительно быстрее, но при этом не покрывает всю функциональность стандартной библиотеки, а также отличается в API. Лично мне, например, нужен IntEnum, чтобы в cli-приложениях использовать returncode, который не нужно явно приводить к int. Мелочь, но приятно. Также удобно использовать флаги с перегруженным оператором in
.
Вы привели пример с медленными флагами, но у вас вообще нет флагов. Поэтому я и написал, что было бы здорово ускорить Enum в stdlib, к тому же, я знаю, что это возможно, потому что такие попытки уже были и даже есть экспериментальный код, который проходит большинство тестов. То есть люди уже задумывались над производительностью Enum, не вы первые обратили на это внимание. Может быть нужно было хотя бы создать issue в багтрекере Python, чтобы привлечь внимание к проблеме?
Это очень неявно и противоречит принципам pythonic. Насколько я понимаю, 'One' должно записаться в какое-то другое поле, которое надо явно определять через конструктор?
Так наоборот же. Explicit > implicit. Если требуется в value хранить кортеж — пишем этот кортеж как единственное значение явно. Если требуется разбить его на поля в членах Enum — описываем это явно. Какой принцип нарушен? Тем более, в документации это описано.
вы позиционируете ваше решение как замену Enum из stdlib
Мы предлагаем более быструю реализацию перечислений, но не утверждали, что предлагаем эквивалентную реализацию, и уж тем более, как совместимую замену штатному.
а также отличается в API
Одно только отсутствие auto()
в таком случае будет приводить к API incompatibility. Но это заявленная несовместимость, не могу понять, что не так?
Может быть нужно было хотя бы создать issue в багтрекере Python, чтобы привлечь внимание к проблеме?
Они в курсе. Только с учетом того, что мы в 3.6 и 3.7 продолжаем испытывать проблемы с производительностью перечислений, они не справились. Их аргумент был — "может поменять public API" (и то, только для предложенного и в итоге принятого патча, что так и не исправило проблему доступа к name
и value
). Что ж, нам, в свою очередь, ехать, а не шашечки. Как только тот экспериментальный код будет принят в стандартную библиотеку и окажется либо сопоставимым по скорости с нашим, либо еще быстрее, мы с радостью переползем на него.
Лично мне, например, нужен IntEnum, чтобы в cli-приложениях использовать returncode, который не нужно явно приводить к int.
И, что замечательно, Вы, даже если решите воспользоваться FastEnum, продолжите иметь возможность объявить для int-like штатный Enum, в этом конкретном случае Вас вообще никак не затрагивает его скорость работы (лишняя даже секунда на завершение работы приложения — это пустяки). Тем более Вас вряд ли в этом конкретном Enum будет интересовать какая-либо совместимость с FastEnum на уровне сериализации.
Впрочем, это был интересный опыт.
Представляю версию 1.3.0, в которой можно вот так:
class IntEnum(int, metaclass=FastEnum):
SYNTAX_ERROR = 1
CONFIG_FORMAT_MISFITS = 2
POLICY_VIOLATION = 3
IntEnum.POLICY_VIOLATION == 3 # True
import sys
sys.exit(IntEnum.CONFIG_FORMAT_MISFITS) # $? == 2 in bash
Только интами не ограничивается, как минимум, тестировалось на str
, float
(в тесткейс внесено)
Посмотрел код, посравнивал со встроенным енумом — имхо, очень сырая имплементация получилась.
Колоссальные отличия в апи от встроенного енума и полное отсутсвие обратной совместимости, ничем немотивированная необходимость использования заглавных имён, отсутствие поддержки наследование от других типов, например, IntEnum.
Пропатчив стандартный Enum можно получить сопоставимые результаты по скорости, сохранив при этом полную совместимость с существующим кодом (выложу как-нибудь такой патч).
Исходя из вышесказанного, я совершенно не понимаю зачем нужна штука, описанная в посте.
Upd. коммент написал до обсуждения с iroln, только сейчас прошел модерацию. Вижу что вроде как добавилась поддержка миксинов, но остальные вопросы остались.
ничем немотивированная необходимость использования заглавных имён
Посмотрите enum.EnumMeta.__getattr__
. Это то самое место, избежав использования которого мы ускорили доступ к члену перечисления в три раза. Плата совсем небольшая: пусть и не обязательное с точки зрения PEP8, но много где применяемое правило писать имена константных атрибутов классов заглавными буквами.
Пропатчив стандартный Enum можно получить сопоставимые результаты по скорости, сохранив при этом полную совместимость с существующим кодом (выложу как-нибудь такой патч).
Только, если можно, сразу в виде PR сюда и ссылкой в это обсуждение. Спасибо.
Посмотрите enum.EnumMeta.__getattr__
. Это то самое место, избежав использования которого мы ускорили доступ к члену перечисления в три раза.
Я знаю, за счёт чего вы достигли такую скорость. Только вопрос был про заглавные атрибуты, и я не вижу связи между удалением __getattr__
и необходимостью писать всё с большой буквы.
Только, если можно, сразу в виде PR сюда и ссылкой в это обсуждение. Спасибо.
Хотите PR? Он есть у меня :)
bpo-39102: Increase Enum performance up to 10x times (3x average) #17669 (https://github.com/python/cpython/pull/17669)
Попробовать патч на python можно установив этот пакет: https://github.com/MrMrRobat/fastenum
(код отличается от PR, т. к. решил выкинуть вещи, связанные с поддержкой Python <3.6 и DynamicClassAttribute. Наверное всё же приведу к одному виду с PR, как будет время)
Вот результаты бенчмарка моего патча: (https://github.com/MrMrRobat/fastenum/tree/master/benchmark):
TOTAL TIME:
PatchedEnum : 6.602916 seconds in total, 0.274735 average (Fastest)
BuiltinEnum : 13.805300 seconds in total, 0.548496 average, slower than PatchedEnum by x 1.996453 (in average)
QratorEnum : 29.979191 seconds in total, 0.336723 average, slower than PatchedEnum by x 1.225629 (in average)
...
Общее время в целых 29.979191 секунд получилось из-за очень медленного dir(QratorEnum), так что сморите лучше на средние показатели скорости.
При этом, что поразительно: что штатная версия, что ad-hoc патч, что заплатка в PR защищают только и исключительно .name
и .value
. Установить в рантайме произвольное свойство члена перечисления (даже такое, которое заявлено как часть типа этого перечисления, то есть, устанавливается при инициализации членов перечисления) — не проблема. Это нарушает концепт неизменяемости перечислений. Оставлю это в качестве вопроса личной совести — поднимать ли эту проблему в рамках BPO или нет. Я здесь отдельно оговорю, что защититься в рантайме от object.__setattr__(member, prop_name, prop_val)
практически невозможно. Есть лишь возможность защититься от непредполагаемых свойств с помощью __slots__
, но это невозможно, если миксин-класс имеет ненулевой __itemsize__
(как, например, int).
Еще хотел бы прояснить отдельно момент: зачем в тестах производительности оценивается dir()
? Я не нашел информации о том, что это каким-либо образом является основополагающим вызовом при работе любого рантайма, более того, в Data Model, наоборот, особо подчеркивается, что его основное назначение — отчет о составе объекта при работе в интерактивном режиме интерпретатора.
В любом случае, спасибо за проделанную работу. Как только примут в апстрим (и, надеюсь, разошлют бекпорт-мерджи в 3.6 и 3.7) проанализирую, насколько оно способно полностью заменить нашу реализацию.
При этом, что поразительно: что штатная версия, что ad-hoc патч, что заплатка в PR защищают только и исключительно .name и .value. Установить в рантайме произвольное свойство члена перечисления (даже такое, которое заявлено как часть типа этого перечисления, то есть, устанавливается при инициализации членов перечисления) — не проблема.
То, о чём вы говорите применимо абсолютно к любому объекту, объявленному в питоне, в том числе и к енумам в вашей реализации. Это просто факт и неотъемлемая часть языка, и я не вижу в этом особой проблемы. У нас есть средства, которые позволяют ограничить непреднамеренное изменение свойств, и как мне кажется этого достаточно.
Есть лишь возможность защититься от непредполагаемых свойств с помощью__slots__
, но это невозможно, если миксин-класс имеет ненулевой__itemsize__
(как, например,int
).
Это как раз не решает проблему переопределения существующих атрибутов. Да и добавление новых тоже, кстати. Как я сказал выше — всё можно «пропатчить и заапдейтить» :)
Еще хотел бы прояснить отдельно момент: зачем в тестах производительности оценивается dir()?
Я начал заниматься этим патчем задолго до того, как увидел Вашу реализацию и тогда тестировал разницу с реализацией discordpy.enum
, которая обещала иметь такое же api, как встроенный enum. Так что изначально оно там больше для демонстрации разного набора атрибутов.
В любом случае, спасибо за проделанную работу.
Приятно слышать, спасибо и вам что смотивировали её закончить :)
(и, надеюсь, разошлют бекпорт-мерджи в 3.6 и 3.7)
Было бы здорово. Можете написать об этом в issue или PR.
У нас есть средства, которые позволяют ограничить непреднамеренное изменение свойств, и как мне кажется этого достаточно.
Так я и говорю: даже эти средства не применяются в данном случае
from enum import Enum
class A(Enum):
pass
class AImpl1(A):
bar = 1
foo = 2
AImpl1.bar.voodoo = 3
from fastenum import fastenum
assert fastenum.enabled
class PatchedEnum(Enum):
foo: str
def __init__(self, value, foo):
self.foo = foo
bar = 1, 'bar'
baz = 2, 'baz'
PatchedEnum.bar.foo = 'foo'
PS. По поводу issue — пока рано. Будем смотреть, на что будут опираться мейнтейнеры при реализации финального патча, основанного на Ваших предложениях. Обычно они самостоятельно бэкпортируют очень годные изменения (а многократное увеличение скорости работы в стандартной библиотеке — именно такое изменение).
Кстати, Арсений, было бы неплохо в рамках этого же BPO решить сразу и вопрос с алиасами (класс-наследник финального перечисления, не объявляющий своих членов перечисления). Схема, описанная нами с "бесшовным" рефакторингом, на мой взгляд, вполне имеет право на жизнь.
Быстрый ENUM