a new data type: NamedValue -- similar to Enum
Enum is great! Okay, okay, my opinion might be biased. ;) There is one area where Enum is not great -- for a bunch of unrelated values. What would be nice is if we had something similar to Enum for the - repr() - constantness of the name/value relationship While I was looking in the stdlib for an example, I came across sre_constants, which was chock full of constants -- and it even has a minimal version of what I'm proposing: class _NamedIntConstant(int): def __new__(cls, value, name): self = super(_NamedIntConstant, cls).__new__(cls, value) self.name = name return self def __repr__(self): return self.name My proposal, compared/contrasted with Enum: like Enum, NamedValues will be in collections (a class) that will keep those names from being deleted or rebound to other values like Enum, NamedValues will have `value` and `name` attributes, and also a `type` attribute like Enum, NamedValues are attributes of their containing class like Enum, iterating over the containing class will yield the names/values defined like Enum, a class of NamedValues do not have to all be the same data type unless one is mixed in like Enum, a particular name/value combination is a singleton unlike Enum, duplicates are allowed unlike Enum, new values can be added after class definition unlike Enum, a NamedValue can always be used as-is, even if no data type has been mixed in -- in other words, there is no functional difference between MyIntConstants(int, NamedValue) and MyConstants(NamedValue). If sre_constants was using a new data type, it should probably be IntEnum instead. But sre_parse is a good candidate for NamedValues: class K(NamedValues): DIGITS = frozenset("0123456789") OCTDIGITS = frozenset("01234567") HEXDIGITS = frozenset("0123456789abcdefABCDEF") ASCIILETTERS = frozenset("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") WHITESPACE = frozenset(" \t\n\r\v\f") _REPEATCODES = frozenset({MIN_REPEAT, MAX_REPEAT}) _UNITCODES = frozenset({ANY, RANGE, IN, LITERAL, NOT_LITERAL, CATEGORY}) and in use: >>> K.DIGITS K.DIGITS >>> K.DIGITS.name 'DIGITS' >>> K.DIGITS.value frozenset("0123456789") Thoughts? -- ~Ethan~
Okay, I'll bite. On Sat, Dec 12, 2020 at 6:00 PM Ethan Furman <ethan@stoneleaf.us> wrote:
unlike Enum, duplicates are allowed
What does this even mean? ``` class K( NamedValues): A = 1 A = 2 ``` Why would I want that?
unlike Enum, new values can be added after class definition
Sure, that points to maybe a subclass of Enum?
unlike Enum, a NamedValue can always be used as-is, even if no data type has been mixed in -- in other words, there is no functional difference between MyIntConstants(int, NamedValue) and MyConstants(NamedValue).
I'm not following this either. Can you give an example of something that doesn't work with Enum (and shouldn't work) but should work with NamedValues? Finally, why the plural in the name, where Enum and friends all have singular names? -- --Guido van Rossum (python.org/~guido) *Pronouns: he/him **(why is my pronoun here?)* <http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-c...>
On 12/12/20 6:40 PM, Guido van Rossum wrote:
unlike Enum, duplicates are allowed
What does this even mean? ``` class K( NamedValue): A = 1 A = 2 ```
That's invalid. Duplicates allowed means:
``` class K( NamedValue): A = 1 B = 1 ```
B is not an alias for A. Presumably one has the same number with different meaning. If that were an Enum: ```
K.B K.A
unlike Enum, new values can be added after class definition
Sure, that points to maybe a subclass of Enum?
Not really -- adding a new member to an Enum is a royal pain.
unlike Enum, a NamedValue can always be used as-is, even if no data type has been mixed in -- in other words, there is no functional difference between MyIntConstants(int, NamedValue) and MyConstants(NamedValue).
I'm not following this either. Can you give an example of something that doesn't work with Enum (and shouldn't work) but should work with NamedValues?
Class MyEnum(Enum): ONE = 1 TWO = 2 MyEnum.ONE + 3 # TypeError Class MyValue(NamedValue): ONE = 1 TWO = 2 MyValue.TWO + 3 5
Finally, why the plural in the name, where Enum and friends all have singular names?
Because I constantly forget, and have to go back to remove the plural 's'. By the way, the use of classes is not strictly required -- we could get everything except for immutable class access by using mostly the same method as sre_parse._NamedIntConstant if we want a /slightly/ lighter weight structure -- which is to say the NamedValueMeta is no where near as complicated as EnumMeta. -- ~Ethan~
On Sat, Dec 12, 2020 at 07:01:55PM -0800, Ethan Furman wrote:
That's invalid. Duplicates allowed means:
``` class K( NamedValue): A = 1 B = 1 ```
B is not an alias for A. Presumably one has the same number with different meaning. If that were an Enum:
```
K.B K.A
Ah! Talking about Boy Looks, I had never noticed that behaviour before. (But then I don't regularly use duplicate Enum values.) What is the reason for that behaviour in Enums?
I'm not following this either. Can you give an example of something that doesn't work with Enum (and shouldn't work) but should work with NamedValues?
Class MyEnum(Enum): ONE = 1 TWO = 2
MyEnum.ONE + 3 # TypeError
Class MyValue(NamedValue): ONE = 1 TWO = 2
MyValue.TWO + 3 5
Isn't this the solution to that?
class MyValue(int, Enum): ... ONE = 1 ... TWO = 2 ... MyValue.TWO + 3 5
-- Steve
On 12/12/20 7:52 PM, Steven D'Aprano wrote:
On Sat, Dec 12, 2020 at 07:01:55PM -0800, Ethan Furman wrote:
That's invalid. Duplicates allowed means:
``` class K( NamedValue): A = 1 B = 1 ```
B is not an alias for A. Presumably one has the same number with different meaning. If that were an Enum:
```
K.B K.A
Ah! Talking about Boy Looks, I had never noticed that behaviour before. (But then I don't regularly use duplicate Enum values.)
What is the reason for that behaviour in Enums?
Different names for the same thing, the second names (and third, and ...) are aliases for the first. For example, if you're me and constantly forget to drop the 's', you might have class Border(Enum): LINE = 'line' LINES = 'line' and then, no matter which I type, I get the right answer. Silly example, but the point is that Enum members are singletons, and there should only be one canonical member to represent a single semantic value -- hence the recommendation to compare Enums using `is`. NamedValues, on the other hand, don't have the concept of set membership and one canonical name for a single value.
[...]
Class MyEnum(Enum): ONE = 1 TWO = 2
MyEnum.ONE + 3 # TypeError
Class MyValue(NamedValue): ONE = 1 TWO = 2
MyValue.TWO + 3 5
Isn't this the solution to that?
class MyValue(int, Enum): ... ONE = 1 ... TWO = 2 ... MyValue.TWO + 3 5
It certainly can be abused for that, but the intended purpose of IntEnum is not to support math operations, but rather to interoperate with existing APIs that are using ints as magic numbers. -- ~Ethan~
On Sun, Dec 13, 2020 at 12:34:27AM -0800, Ethan Furman wrote: [me]
class MyValue(int, Enum): ... ONE = 1 ... TWO = 2 ... MyValue.TWO + 3 5
It certainly can be abused for that, but the intended purpose of IntEnum is not to support math operations, but rather to interoperate with existing APIs that are using ints as magic numbers.
Sure, but "ints as magic numbers" are a bit of a special case. If TOPLEFT and BOTTOMRIGHT are magic numbers (TOPLEFT + BOTTOMRIGHT)//2 is unlikely to make any semantic sense. But I only used int because you did :-) A better example, stolen from your earlier one: class K(frozenset, Enum): DIGITS = frozenset("0123456789") LETTERS = frozenset(string.ascii_letters) DIGITS | LETTERS makes perfect semantic sense. Tell me that's abuse, I double-dare you :-) The bottom line here is that all of the functionality you suggest makes sense, but I'm not convinced that the right API is a new and independent data type unrelated to Enum. Everything you suggest sounds to me like a variation on Enum: - a way to add new members to an Enum after creation - a way to create duplicate Enum values that aren't aliases - a way for enums to automatically inherit behaviour from their values, without explicitly mixing another subclass. These feel like add-ons to the basic Enum data type, not a full-blown independent data type. Or maybe it's just that I don't like the name NamedValue. (Too vague -- `x = 1` is a named value. Maybe if you called it FancyEnum or something I'd love it :-) -- Steve
On Sat, Dec 12, 2020 at 06:00:17PM -0800, Ethan Furman wrote:
Enum is great! Okay, okay, my opinion might be biased. ;)
There is one area where Enum is not great -- for a bunch of unrelated values.
I don't know how to interpret that. Surely *in practice* enums are always going to be related in some sense? I don't expect to create an enum class like this: class BunchOfRandomStuff(Enum): ANIMAL = 'Mustela nivalis (Least Weasel)' PRIME = 503 EULER_MASCHERONI_CONSTANT = 0.5772156649015329 BEST_PICTURE_1984 = 'Amadeus' DISTANCE_MELBOURNE_SYDNEY = (16040497, "rack mount units") in any meaningful piece of code, although of course such an eclectic collection is syntactically legal, even if semantically meaningless. If I did create such an unusual collection of enums, what is the standard Enum lacking that makes it "not great"? It seems to work fine to me. [...]
My proposal, compared/contrasted with Enum:
like Enum, NamedValues will be in collections (a class) that will keep those names from being deleted or rebound to other values
like Enum, NamedValues will have `value` and `name` attributes, and also a `type` attribute
I don't understand this. Are you suggesting that NamedValues will have a `type` attribute **like Enum**, or **in addition** to what Enum provides (value and name)? I cannot see any sign of a `type` attribute on Enums, but maybe I'm having a Boy Look. What is the `type` attribute of a NamedValue instance? [snip a bunch of ways that NamedValues are exactly like Enums]
unlike Enum, duplicates are allowed
Um, do you mean duplicate names? How will that work? class Stuff(NamedValues): SPAM = 1 SPAM = 2 Stuff.SPAM # what is this??? Enums support duplicate values, and I can't see how duplicate names could work, so I don't get this.
unlike Enum, new values can be added after class definition
Is there a use-case for this? If there is such a use-case, could we not just given Enums an API for adding new values, rather than invent a whole new Enum-by-another-name?
unlike Enum, a NamedValue can always be used as-is, even if no data type has been mixed in -- in other words, there is no functional difference between MyIntConstants(int, NamedValue) and MyConstants(NamedValue).
Sorry, I don't get that either. How can Enums not be used "as-is"? What does that mean? Are you suggesting that NamedValue subclasses will automatically insert `int` into their MRO? Otherwise, I don't get how there can be no functional difference between a NamedValue subclass that inherits from int and one that doesn't. Surely you aren't suggesting that this must be meaningful? class Spam(NamedValue): EGGS = "My hovercraft is full of eels." Spam.EGGS + 1 # returns what? Of course I don't think you mean that should actually work, but I cannot think what other interpretation to give your description.
If sre_constants was using a new data type, it should probably be IntEnum instead. But sre_parse is a good candidate for NamedValues:
class K(NamedValues): DIGITS = frozenset("0123456789") [...snip additional name/value pairs...]
and in use:
>>> K.DIGITS K.DIGITS >>> K.DIGITS.name 'DIGITS' >>> K.DIGITS.value frozenset("0123456789")
Why not just use an Enum? Sorry Ethan, perhaps I am being slow today, but apart from that "add new members after creation" I fail to see how this is anything but just an Enum. What am I missing? -- Steve
On 12/12/20 7:25 PM, Steven D'Aprano wrote:
On Sat, Dec 12, 2020 at 06:00:17PM -0800, Ethan Furman wrote:
Enum is great! Okay, okay, my opinion might be biased. ;)
There is one area where Enum is not great -- for a bunch of unrelated values.
I don't know how to interpret that. Surely *in practice* enums are always going to be related in some sense?
I certainly hope so -- that was one of the points in creating Enum.
I don't expect to create an enum class like this:
class BunchOfRandomStuff(Enum): ANIMAL = 'Mustela nivalis (Least Weasel)' PRIME = 503 EULER_MASCHERONI_CONSTANT = 0.5772156649015329 BEST_PICTURE_1984 = 'Amadeus' DISTANCE_MELBOURNE_SYDNEY = (16040497, "rack mount units")
Which is why I said, "Enum is not great for a bunch of unrelated values".
If I did create such an unusual collection of enums, what is the standard Enum lacking that makes it "not great"? It seems to work fine to me.
Lots of things work -- calling `__len__` instead of `len()` works, but `__len__` is not the best way to get the length of an object. Enums are not great for a bunch of unrelated values because: - duplicate values would all get aliased to one name - ordinary values should not be compared using `is` - standard Enums cannot be seamlessly used as their actual value (example in other email) [...]
I don't understand this. Are you suggesting that NamedValues will have a `type` attribute **like Enum**, or **in addition** to what Enum provides (value and name)?
To be honest, Enum may have a "type" attribute at this point, I don't remember. NamedValues would definitely have a "type" attribute whose primary purpose is to make the value attribute work. As an example, consider sre_constant.MAXREPEAT vs sre_constant.MAX_REPEAT (the only difference is the underscore -- took me a few moments to figure that out). The sre_constant._NamedIntConstant class adds a name attribute, and returns that as the repr(). ```
sre_constants.MAXREPEAT MAXREPEAT sre_constants.MAX_REPEAT MAX_REPEAT
Not very illuminating. I ended up getting the actual value by calling `int()` on them.
int(sre_constants.MAXREPEAT) 4294967295 int(sre_constants.MAX_REPEAT) 42
By adding a "type" attribute, getting something useful becomes a little easier:
@property def value(self): return self._type_(self) ``` or maybe ``` @property def value(self): return self._type_.__repr__(self) ```
unlike Enum, duplicates are allowed
Um, do you mean duplicate names? How will that work?
No, duplicate values -- but in an Enum the names given to the duplicate value become aliases to the original name/value, while duplicates in NamedValue would remain different objects.
unlike Enum, new values can be added after class definition
Is there a use-case for this?
Yes.
If there is such a use-case, could we not just given Enums an API for adding new values, rather than invent a whole new Enum-by-another-name?
While NamedValues have a similarities to Enum (.name, .value, human readable repr()), they are not Enums.
unlike Enum, a NamedValue can always be used as-is, even if no data type has been mixed in -- in other words, there is no functional difference between MyIntConstants(int, NamedValue) and MyConstants(NamedValue).
Sorry, I don't get that either. How can Enums not be used "as-is"? What does that mean?
It means that you can't do things with the actual value of Color.RED, whether that value is an int, a string, or a whatever, without going through the value attribute.
Are you suggesting that NamedValue subclasses will automatically insert `int` into their MRO?
No, I'm saying that a NamedValue subclass will have int, or string, or frozenset, or whatever the actual values' types are, in their mro: ``` def __new__(cls, value, name): actual_type = type(value) new_value_type = type(cls.__name__, (cls, type(value)), {}) obj = actual_type.__new__(new_value_type, value) obj._name_ = name obj._type_ = actual_type return obj ``` The subclasses are created on the fly. The production code will cache the new subclasses so they're only created once.
If sre_constants was using a new data type, it should probably be IntEnum instead. But sre_parse is a good candidate for NamedValues:
class K(NamedValues): DIGITS = frozenset("0123456789") [...snip additional name/value pairs...]
and in use:
>>> K.DIGITS K.DIGITS >>> K.DIGITS.name 'DIGITS' >>> K.DIGITS.value frozenset("0123456789")
Why not just use an Enum?
Why use a Counter instead of defaultdict instead of dict? Because, depending on the task, one is more appropriate than the others. -- ~Ethan~
participants (3)
-
Ethan Furman
-
Guido van Rossum
-
Steven D'Aprano