Immutable instances, constant values

B

Ben Finney

Howdy all,

I've recently packaged 'enum' in PyPI. In its description, I make the
claim that it creates "immutable" enumeration objects, and that the
enumeration values are "constant" values.

This raises questions.

Is there any difference between a Python immutable value, and a
constant? I suppose "constant" also implies that the *name* binds
unchangeably to a particular value. Is that meaningful?

How does one actually ensure an object is immutable? Is it a matter of
overriding a bunch of methods, or is ther a neater way?

Is it bad style to make a user-defined class that creates immutable
objects? Why?

In the case of 'enum', can anyone argue for or against an enumerated
type being immutable? Its values being constant?
 
B

Bengt Richter

Howdy all,

I've recently packaged 'enum' in PyPI. In its description, I make the
claim that it creates "immutable" enumeration objects, and that the
enumeration values are "constant" values.

This raises questions.

Is there any difference between a Python immutable value, and a
constant? I suppose "constant" also implies that the *name* binds
unchangeably to a particular value. Is that meaningful?

How does one actually ensure an object is immutable? Is it a matter of
overriding a bunch of methods, or is ther a neater way?

Is it bad style to make a user-defined class that creates immutable
objects? Why?

In the case of 'enum', can anyone argue for or against an enumerated
type being immutable? Its values being constant?
My notion of enum comes from (what I remember of) Pascal, which is
basically an ordered set of names of integers forming a type, and
ord(one_of_the_names) gets you the index value. Python's ord seems
to demand a string of length one, and doesn't seem to attempt coercion,
so that might not fly without a mod.

But what we have is named integers, much as True and False are built in
names for integer subtypes with value 1 and 0. So I'd say enums should
also be int subtypes...

Anyway, I would hope that the name->ord(name) mapping would be immutable
once defined (though not necessarily obsessively preventing the ususal end runs).
Hm, might as well be more concrete ...
... names = names.split()
... top = len(names)
... # define method functions outside class so they'll be closures accessing nested names
... def __new__(cls, name=names[0]):
... try:
... i = names.index(name)
... return int.__new__(cls, i)
... except ValueError:
... if isinstance(name, int) and 0< name < top: return int.__new__(cls, name)
... raise ValueError, 'illegal %s enum value %r'%(cls.__name__, name)
... def __repr__(self): return '%s(%s)' %(self.__class__.__name__, names[self])
... # return names with names attribute of class or instance
... class getnames(object):
... def __set__(*ignore): raise AttributeError, 'names protected'
... getnames.__get__ = lambda *ignore: names[:]
... return type(ename, (int,),{'__new__':__new__, '__repr__':__repr__, '__str__':__repr__,
... 'names':getnames()})
...
...
>>> Colors = makeEnum('Color', 'red green blue')
>>> Colors
>>> Colors() Color(red)
>>> Colors(1) Color(green)
>>> r,g,b = (Colors(name) for name in Colors.names)
>>> r Color(red)
>>> g Color(green)
>>> b Color(blue)
>>> 'ABC'[g] 'B'
>>> int(g) 1
>>> Colors(5)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "<stdin>", line 11, in __new__
ValueError: illegal Color enum value 'orange'

Just some thoughts.
Oh, names are kind of protected by [:], but really should throw
an exception if you try to mutate.
>>> Colors.names ['red', 'green', 'blue']
>>> Colors.names[2] 'blue'
>>> Colors.names[2] = 'indigo'
>>> Colors.names[2]
'blue'

It would be easy to return a tuple.
It's not that easy to protect against Colors.names = something
since __metaclass__ skips factory local to global if not in class scope,
and passing a '__metaclass__': mcdefinition in the dict arg to type does
not result in a call, it just becomes another passive class variable.
Must be a way though. Too tired for now...

Regards,
Bengt Richter
 
B

Ben Finney

Bengt Richter said:
Ben Finney said:
I've recently packaged 'enum' in PyPI.
[...]
My notion of enum comes from (what I remember of) Pascal

You might want to investigate the 'enum' package for my idea of how an
enumerated type can work.
which is basically an ordered set of names of integers forming a
type, and ord(one_of_the_names) gets you the index value.

Getting a numeric index might be useful in a language such as Pascal,
with no built-in dict or sequence types. In Python, where any
immutable object can be a dict key, and any sequence can be iterated,
it seems of no use.
But what we have is named integers, much as True and False are built
in names for integer subtypes with value 1 and 0.

That's an implementation detail; surely code shouldn't be expecting
any particular relationship between integers and boolean values?
So I'd say enums should also be int subtypes...

Likewise, that seems best left as an implementation detail. Why expect
any particular mapping to int values? Doing arithmetic or boolean
logic on enumerated values seems against their purpose.
 
B

Bengt Richter

Bengt Richter said:
Ben Finney said:
I've recently packaged 'enum' in PyPI.
[...]
My notion of enum comes from (what I remember of) Pascal

You might want to investigate the 'enum' package for my idea of how an
enumerated type can work.
I guess I saw an earlier version, and got confused as to the goal, sorry.
I will look in PyPI.
Getting a numeric index might be useful in a language such as Pascal,
with no built-in dict or sequence types. In Python, where any
immutable object can be a dict key, and any sequence can be iterated,
it seems of no use.
Does your concept of enumeration not have a fixed order of a set of names?
If it does, what is more natural than using their index values as keys
to other ordered info? OTOH, the index values (and hence my enums) are[1] not
very good as unique dict keys, since they compare[2] promiscuously with each other
and other number types. Hm, maybe if hash were defined class-unique, e.g.,
def __hash__(self): return hash((int(self), type(self).__name__)
Ok, did that, _seems_ to work (fixed __repr__ too):
[1] were & [2] compared ;-)
>>> from makeenum import makeEnum
>>> Few = makeEnum('Few','zero one two three')
>>> Few() Few('zero')
>>> d = dict((Few(n), Few.names.index(n)) for n in Few.names)
>>> d {Few('zero'): 0, Few('three'): 3, Few('one'): 1, Few('two'): 2}
>>> d[1]
Traceback (most recent call last):
File said:
1

But still can work as integer:
>>> 'ABC'[Few(1)] 'B'
>>> 'ABC'[Few('one')] 'B'
>>> 'ABC'[Few('two')]
'C'
That's an implementation detail; surely code shouldn't be expecting
any particular relationship between integers and boolean values?
Au contraire, much code depends on it, e.g.,
>>> def verboselen(s): return '%r has %d element%s'%(s, len(s), ('s','')[len(s)==1]) ...
>>> verboselen(range(3)) '[0, 1, 2] has 3 elements'
>>> verboselen(range(0)) '[] has 0 elements'
>>> verboselen(range(1))
'[0] has 1 element'
1
Likewise, that seems best left as an implementation detail. Why expect
any particular mapping to int values? Doing arithmetic or boolean
logic on enumerated values seems against their purpose.
I guess I will have to look at your enum in PyPI to understand what
you mean by "their purpose" ;-)

To me the int correspondence is as expectable and natural as a,b,c=range(3)
(at least as a default) though I think different enumerations should be
different types. Note that the ordering of int values makes the instances
nicely sortable too, e.g.,
>>> d.items() [(Few('zero'), 0), (Few('three'), 3), (Few('one'), 1), (Few('two'), 2)]
>>> sorted(d.items())
[(Few('zero'), 0), (Few('one'), 1), (Few('two'), 2), (Few('three'), 3)]

or more directly
>>> d.keys() [Few('zero'), Few('three'), Few('one'), Few('two')]
>>> sorted(d.keys())
[Few('zero'), Few('one'), Few('two'), Few('three')]

Enumerations defined as monotonic but non-contiguous sequences of named int
values are conceivable too. They can be useful in defining bit masks with
distinguishable types, but that act like ints. Kind of a sparse enumeration.
Maybe I'll add that in.

But bottom line, I really thing the int base type is more than an implementation
detail. I think it's natural for an _ordered_ set of names ;-)

I'll go look at PyPI now ;-)

Regards,
Bengt Richter
 
B

Ben Finney

Bengt Richter said:
Does your concept of enumeration not have a fixed order of a set of
names?

It does. The values are iterable in the same order they were specified
when creating the Enum.
If it does, what is more natural than using their index values as
keys to other ordered info?

I don't see why. If you want sequence numbers, use enumerate(). If
not, the enum object itself can be used directly as an iterable.
OTOH, the index values (and hence my enums) are[1] not very good as
unique dict keys, since they compare[2] promiscuously with each
other and other number types.

Indeed, that's why (in my design) the values from the enum are only
useful for comparing with each other. This, to me, seems to be the
purpose of an enumerated type.
To me the int correspondence is as expectable and natural as
a,b,c=range(3) (at least as a default) though I think different
enumerations should be different types.

That's a complete contradiction, then. If you want them to be
different types, you don't want them to be integers.
Note that the ordering of int values makes the instances nicely
sortable too, e.g.,

That only means the enum values need to compare in the same sequence;
it doesn't mean they need to correspond to integer values.
But bottom line, I really thing the int base type is more than an
implementation detail. I think it's natural for an _ordered_ set of
names ;-)

I think I've addressed all your current concerns; I don't believe an
inherent correlation to integers is necessary at all.

It's no more necessary than saying that ["a", "b", "c"] requires that
there be some specific correlation between the values of that list and
the integers 0, 1, 2. If you *want* such a correlation, in some
particular case, use enumerate() to get it; but there's nothing about
the values themselves that requires that correspondence.
I'll go look at PyPI now ;-)

Feedback appreciated :)
 
B

Bengt Richter

It does. The values are iterable in the same order they were specified
when creating the Enum.


I don't see why. If you want sequence numbers, use enumerate(). If
not, the enum object itself can be used directly as an iterable.
I changed mine so the enum _class_ is iterable, but enum instances are not.
OTOH, the index values (and hence my enums) are[1] not very good as
unique dict keys, since they compare[2] promiscuously with each
other and other number types.

Indeed, that's why (in my design) the values from the enum are only
useful for comparing with each other. This, to me, seems to be the
purpose of an enumerated type.
Have you tried yet to use two different enum instances as keys in
the same dict? Then try to sort the keys(or items is the values are
misc different enums). I hit that, and changed __cmp__ to compare
(typename, <intvalue or other if not int subtype>) tuples. That sorts
items grouped by enum type if they're keys. I think you can always
pass a stricter cmp to sorted if you want to assert type equality.
That's a complete contradiction, then. If you want them to be
different types, you don't want them to be integers.
No, it's not a contradiction. Different int _sub_types are different
types ;-)
That only means the enum values need to compare in the same sequence;
it doesn't mean they need to correspond to integer values. True.


I think I've addressed all your current concerns; I don't believe an
inherent correlation to integers is necessary at all.
Necessary wasn't the question for me. It's whether it's desirable. YMMV ;-)
It's no more necessary than saying that ["a", "b", "c"] requires that
there be some specific correlation between the values of that list and
the integers 0, 1, 2. If you *want* such a correlation, in some
particular case, use enumerate() to get it; but there's nothing about
the values themselves that requires that correspondence.
Yet it is comforting to know that ['a', 'b', 'c'][0] will interpret
the [0] to mean the first in the sequence (unless someone is doing
a list-like repr trick based on some other type ;-).

I haven't yet converted my generated enum classes to singletons,
but I did make it so you can define a named enumeration class and
iterate the class itself (its metaclass defines __getitem__). What
you get is the particular enum class instance, e.g. (... time passes,
never mind, I cached instanced for an effective singleton set of named numbers.

The class methods are introduced via metaclass in the makeEnum factory
and it's a very hacky workaround for the fact that execution of a
class definition body only has local and module global namespace, so
you can't directly reference anything local to the factory function.
This of course goes for the methods of the main class being constructed too,
so they are sneaked in via the metaclass before instantiating the class ;-/
(Anyone have an idea how to do this cleanly and have the same functionality
--being able to iterate the _class_ object etc.?

Anyway, this is my "feedback" ;-) What's missing in this?

----< makeenum.py >----------------------------------------------------------
def makeEnum(ename, names):
"""
Factory function to returns enumeration class with name ename,
and enumerating space-delimited names in names string.
The class is an int subtype, and instances have int values as
well as corresponding names. Different enum instances with the
same int value are distinct as dict keys, but equal used as ints,
though they are not directly comparable unless the same type.

The return class itself defines an iterator that will return
all possible instances in order. The class names property returns
a tuple of the names, and the len(TheClass) returns the number of
names or of the iteration sequence.
"""
global __global_for_mc_hack__
class __global_for_mc_hack__(type):
"""
metaclass to define methods on the enum class per se, and to pass
through closure-dependent methods (which see names & top) to the
returned class, as well as being target itself for closure-dependent
methods of the class (which can't be defined in the class body
since names in a class body are either local or module global).
"""
def __new__(mcls, cname, cbases, cdict):
cdict.update(mcls.edict)
return type.__new__(mcls, cname, cbases, cdict)
def __getattr__(cls, name):
"""make instances accessible by attribute name"""
if isinstance(name, basestring):
return cls(name) # make/retrieve-from-cache an instance
raise IndexError, '%r not a name in "%s"'%(i, cls.__name__)

# the closure cell variables
names = names.split()
top = len(names)
cache = {}

# define method functions outside class so they'll be closures accessing nested names
def __contains__(cls, other): return type(other)==cls and 0<=int(other)<top
def __getitem__(cls, i):
"""make class iterable and indexable, returning fresh instances with given values"""
if isinstance(i, basestring) and i in names or isinstance(i, (int, long)) and (0<=i<top):
return cls(i) # make an instance
raise IndexError, '%r out of range for "%s"'%(i, cls.__name__)
# stuff closure-type method functions into global metaclass to define methods
# of the enum class per se
__global_for_mc_hack__.__contains__ = __contains__
__global_for_mc_hack__.__len__ = lambda cls: len(names)
__global_for_mc_hack__.names = property(lambda cls: tuple(names))
__global_for_mc_hack__.__getitem__ = __getitem__

def __new__(cls, name=names[0]):
try: return cache[name]
except KeyError:
try:
i = names.index(name)
e = int.__new__(cls, i)
cache[name] = cache = e
return e
except ValueError:
if isinstance(name, int) and 0<= name < top:
e = int.__new__(cls, name)
cache[name] = cache[names[name]] = e
return e
raise ValueError, 'illegal %s enum value %r'%(cls.__name__, name)
def __repr__(self): return '%s(%r)' %(self.__class__.__name__, names[self])

# pass closure-type method functions to global metaclass. Ick ;-/
__global_for_mc_hack__.edict = dict(
__new__ = __new__, __repr__=__repr__, __str__=__repr__)

class enum(int):
__metaclass__ = __global_for_mc_hack__
def __cmp__(self, other):
if isinstance(other, int): oval = int(other)
else: oval = other
# allow sort by type names on incompatible types XXX make optional??
return cmp( (type(self).__name__, int(self)),
(type(other).__name__, oval))

#assert type(self)==type(other), (
# 'incompatible cmp types: %s vs %s'%(type(self).__name__, type(other).__name__))
#return cmp(int(self), int(other))
def __hash__(self): return hash((int(self), type(self).__name__))
enum.__name__ = ename
del __global_for_mc_hack__
return enum
-----------------------------------------------------------------------------
Feedback appreciated :)
The above in use looks like:
>>> from makeenum import makeEnum
>>> Count = makeEnum('Count', 'eeny meeny miny moe')
>>> Count.names ('eeny', 'meeny', 'miny', 'moe')
>>> Count[1] Count('meeny')
>>> d = dict((c, int(c)) for c in Count)
>>> d {Count('moe'): 3, Count('miny'): 2, Count('meeny'): 1, Count('eeny'): 0}
>>> Fruit = makeEnum('Fruit', 'apple banana peach pear')
>>> d.update((c, int(c)) for c in Fruit)
>>> d
{Count('eeny'): 0, Fruit('peach'): 2, Count('moe'): 3, Fruit('apple'): 0, Count('miny'): 2, Fru
t('banana'): 1, Fruit('pear'): 3, Count('meeny'): 1} ...
Count('eeny'): 0
Count('meeny'): 1
Count('miny'): 2
Count('moe'): 3
Fruit('apple'): 0
Fruit('banana'): 1
Fruit('peach'): 2
Fruit('pear'): 3
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "makeenum.py", line 65, in __new__
raise ValueError, 'illegal %s enum value %r'%(cls.__name__, name)
ValueError: illegal Fruit enum value 'plum'
>>> d[Fruit('pear')] 3
>>> d[3]
Traceback (most recent call last):
File said:
>>> d[Fruit(3)] 3
>>> d[Fruit('pear')] = 'juicy'
>>> d[Fruit['pear']] 'juicy'
>>> lf = list(Fruit)
>>> lf [Fruit('apple'), Fruit('banana'), Fruit('peach'), Fruit('pear')]
>>> lf2 = list(Fruit)
>>> map(id, lf) [49320972, 49321068, 49321132, 49321164]
>>> map(id, lf2) [49320972, 49321068, 49321132, 49321164]
>>> len(Fruit) 4
>>> Fruit
>>> Fruit() Fruit('apple')
>>> Fruit(0) Fruit('apple')
>>> Fruit('apple') Fruit('apple')
>>> type(Fruit('apple'))
>>> id(Fruit('apple')), id(lf[0])
(49320972, 49320972)

Almost forgot, I added attribute style access:
>>> Fruit.pear Fruit('pear')
>>> d[Fruit.pear] 'juicy'
>>> d[Count(3)] 3
>>> Count[3] Count('moe')
>>> d[Count.moe] 3
>>> d[Fruit.pear] += ', very'
>>> d[Fruit.pear]
'juicy, very'

Note,
Traceback (most recent call last):
File "<stdin>", line 1, in ?
KeyError: 3

[QUOTE= said:
>>> type(Fruit)
>>> type(Fruit).mro(type(Fruit))
[/QUOTE]
[<class 'makeenum.__global_for_mc_hack__'>, <type 'type'>, <type 'object'>]

I guess an option could be passed to makeEnum to disallow inter-type comparisons.
Wouldn't be that hard. I guess I'll do it, but I don't want to re-do this post ;-)

Regards,
Bengt Richter
 
S

Steven D'Aprano

Is there any difference between a Python immutable value, and a
constant? I suppose "constant" also implies that the *name* binds
unchangeably to a particular value. Is that meaningful?

That's precisely how I understand "constant" to be useful. Consider:

c = 2.9979246e+08 # speed of light in metres per second

def energy(mass):
"""Converts mass in kilograms to energy in joules"""
return mass*c**2

That works fine until the name c is rebound to some other value. The fact
that the float object 2.9979246e+08 is immutable is necessary but not
sufficient.

How does one actually ensure an object is immutable? Is it a matter of
overriding a bunch of methods, or is ther a neater way?

Is it bad style to make a user-defined class that creates immutable
objects? Why?

I can't see why it would be bad style. That's what FrozenSet does.
In the case of 'enum', can anyone argue for or against an enumerated
type being immutable? Its values being constant?

According to Gary Larson, there are four fundamental types of people. You
can tell them apart from their reaction to being given a half-filled glass:

"The glass is half full."

"The glass is half empty."

"Half full. No, half empty! No, half full. Wait... what was the question?"

"Hey! I ordered a cheeseburger!"

We might handle this with:

states = Enum("full", "empty", "indeterminate", "cheeseburger")

which then get referred to with:

states.full
states.empty
states.indeterminate
states.cheeseburger

I might decide that "argumentative" is a better label than "cheeseburger",
and mutate the appropriate enum value. What should happen? I see three
possibilities:

(1) states.cheeseburger still hangs around, but is not equivalent to
states.argumentative, in much the same way that reloading a module can
leave obsolete objects in your namespace (reloading doesn't automatically
cause names that point to objects from a module to rebind to new objects).

(2) references to states.cheeseburger raise an exception

(3) references to states.cheeseburger are equivalent to
states.argumentative. A rose by any other name. It shouldn't matter what
label we put on the enums, in principle.

Possibility (1) is obviously Bad with a Capital B, and should be avoided.

Possibility (2) has the saving grace that it is no different to what
happens if you rebind class attributes. There is an argument that if you
can rebind class attributes, you should be able to rebind enums and face
the consequences (good bad or indifferent) as best you can.

Possibility (3) is the logical solution. It shouldn't matter what labels
we put on the enums. But I fear not practical unless the enums are
implemented as Singletons (multitons?), and perhaps not even then.

Alternatively, you could bypass the whole problem by making enums
immutable.
 
B

Bengt Richter

That's precisely how I understand "constant" to be useful. Consider:

c = 2.9979246e+08 # speed of light in metres per second

def energy(mass):
"""Converts mass in kilograms to energy in joules"""
return mass*c**2

That works fine until the name c is rebound to some other value. The fact
that the float object 2.9979246e+08 is immutable is necessary but not
sufficient.



I can't see why it would be bad style. That's what FrozenSet does.


According to Gary Larson, there are four fundamental types of people. You
can tell them apart from their reaction to being given a half-filled glass:

"The glass is half full."

"The glass is half empty."

"Half full. No, half empty! No, half full. Wait... what was the question?"

"Hey! I ordered a cheeseburger!"

We might handle this with:

states = Enum("full", "empty", "indeterminate", "cheeseburger")

which then get referred to with:

states.full
states.empty
states.indeterminate
states.cheeseburger

I might decide that "argumentative" is a better label than "cheeseburger",
and mutate the appropriate enum value. What should happen? I see three
possibilities:

(1) states.cheeseburger still hangs around, but is not equivalent to
states.argumentative, in much the same way that reloading a module can
leave obsolete objects in your namespace (reloading doesn't automatically
cause names that point to objects from a module to rebind to new objects).

(2) references to states.cheeseburger raise an exception

(3) references to states.cheeseburger are equivalent to
states.argumentative. A rose by any other name. It shouldn't matter what
label we put on the enums, in principle.

Possibility (1) is obviously Bad with a Capital B, and should be avoided.

Possibility (2) has the saving grace that it is no different to what
happens if you rebind class attributes. There is an argument that if you
can rebind class attributes, you should be able to rebind enums and face
the consequences (good bad or indifferent) as best you can.

Possibility (3) is the logical solution. It shouldn't matter what labels
we put on the enums. But I fear not practical unless the enums are
implemented as Singletons (multitons?), and perhaps not even then.

Alternatively, you could bypass the whole problem by making enums
immutable.
as in
...
states.full
states.empty
states.indeterminate
states.cheeseburger ...
full
empty
indeterminate
cheeseburger
>>> states
>>> states[1] states.empty
>>> states['empty'] states.empty
>>> states[states.empty] states.empty
>>> list(states) [states.full, states.empty, states.indeterminate, states.cheeseburger]
>>> map(str, states) ['full', 'empty', 'indeterminate', 'cheeseburger']
>>> len(states) 4
>>> states.full in states True
>>> int(states.full) 0
>>> map(int, states) [0, 1, 2, 3]
>>> 'ABC'[states.empty] 'B'
>>> states.empty == 1
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "makeenum.py", line 80, in __cmp__
assert type(self)==type(other), (
AssertionError: incompatible cmp types: states vs int

We can optionally make cmp work on int(self), int(other) for the enum...
>>> states = makeEnum('states', 'full empty indeterminate cheeseburger', strict=int)
>>> 'ABC'[states.empty] 'B'
>>> states.empty == 1 True
>>> type(states.empty)
>>> a = list(states)
>>> b = list(states)
>>> [x is y for x,y in zip(a,b)] [True, True, True, True]
>>> [x is y for x,y in zip(a,states)]
[True, True, True, True]

(all the states are effectively singleton instances of the states class)
and immutable:
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "makeenum.py", line 34, in __setattr__
raise AttributeError, '%s attributes may not be modified'%cls.__name__
AttributeError: states attributes may not be modified Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "makeenum.py", line 103, in __setattr__
raise AttributeError, '%s instance attributes may not be set'% type(self).__name__
AttributeError: states instance attributes may not be set
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: object does not support item assignment

But a slice returns a list, so the transient list absorbs the assignment and disappears
>>> states[:][0] = 'something'
>>> states[:] [states.full, states.empty, states.indeterminate, states.cheeseburger]
>>> states[:]+['something'] [states.full, states.empty, states.indeterminate, states.cheeseburger, 'something']
>>> Fruit = makeEnum('Fruit', 'apple banana')
>>> Fruit[0]
Fruit.apple
contains demands type equality
>>> Fruit[0] in states False
>>> states[0] in Fruit False
>>> int(Fruit[0]) 0
>>> int(states[0])
0

Fruit was created strict, so
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "makeenum.py", line 80, in __cmp__
assert type(self)==type(other), (
AssertionError: incompatible cmp types: Fruit vs states
i.e., Fruit was created with default strict cmp requiring equal types, but
the last states was done with int casting for comparison, so
True

This thing is evolving, adding some features from Ben Finney's PyPI enum ;-)

Regards,
Bengt Richter
 
B

Ben Finney

Bengt Richter said:
Bengt Richter said:
If [an enumeration has a fixed sequence], what is more natural
than using their index values as keys to other ordered info?
I don't see why. If you want sequence numbers, use enumerate(). If
not, the enum object itself can be used directly as an iterable.

I changed mine so the enum _class_ is iterable, but enum instances
are not.

I'm not really understanding your design.

In my enum package, an enumeration is an instance of Enum. Those
instances are iterable, producing EnumValue instances; the EnumValue
instances are also available as named attributes of the Enum instance.
... print str(val)
...
red
blue
green

Why would the Enum *class* be iterable?
OTOH, the index values (and hence my enums) are[1] not very good
as unique dict keys, since they compare[2] promiscuously with
each other and other number types.
Indeed, that's why (in my design) the values from the enum are only
useful for comparing with each other. This, to me, seems to be the
purpose of an enumerated type.

Have you tried yet to use two different enum instances as keys in
the same dict?

What do you mean by "enum instances"? I presume you mean "values from
a single enum".
... print colour_german[val]
...
rot
blau
grün
Then try to sort the keys(or items is the values are misc different
enums).

Oh, perhaps you mean "enum values from different enum instances". No,
those won't compare against each other; there's no meaningful
relationship, since different enum instances are conceptually
different types.
I hit that, and changed __cmp__ to compare (typename, <intvalue or
other if not int subtype>) tuples.

I think this is a flaw, based on expecting too strong a relationship
between the enum value instance, and an integer value.
That sorts items grouped by enum type if they're keys.

Why should colours.blue compare before fruits.orange? How is that
meaningful?
Necessary wasn't the question for me. It's whether it's desirable.
YMMV ;-)

I'm still trying to understand what is served by having some exposed
relationship between an enum value instance and an integer value.
It's no more necessary than saying that ["a", "b", "c"] requires
that there be some specific correlation between the values of that
list and the integers 0, 1, 2. If you *want* such a correlation, in
some particular case, use enumerate() to get it; but there's
nothing about the values themselves that requires that
correspondence.

Yet it is comforting to know that ['a', 'b', 'c'][0] will interpret
the [0] to mean the first in the sequence (unless someone is doing a
list-like repr trick based on some other type ;-).

Again, you're only talking about *sequence*, not correspondence to
integers. Your case above isn't an argument in favour of "the 'a'
value should coerce to the 0 value". Why then should an enum value
instance coerce to any particular integer value?
The class methods are introduced via metaclass in the makeEnum factory
and it's a very hacky workaround for the fact that execution of a
class definition body only has local and module global namespace, so
you can't directly reference anything local to the factory function.

Here you've lost me, since I don't understand why you want
enumerations to be classes at all. Are you going to be making
instances of an entire enumeration? If not, why make classes instead
of objects?
 
B

Ben Finney

Steven D'Aprano said:
That's precisely how I understand "constant" to be useful.

The Python docs don't really talk about such a thing. However, the
None name cannot be re-bound. Is that all that's required to have a
"constant" in Python?

If so, it's part of the assignment statement, and not something the
object itself can decide. True?

Really hoping someone can come up with an answer for this.
I can't see why it would be bad style. That's what FrozenSet does.

"from foo import *" is supported in the language, but is still bad
style. FrozenSet was *added* to the language (2.2), so that's evidence
that Guido thinks it's a good idea. But I'd still like direct
discussion: is making one's class instances immutable bad style?
 
S

Steven D'Aprano

The Python docs don't really talk about such a thing. However, the
None name cannot be re-bound. Is that all that's required to have a
"constant" in Python?

I think None is a special case. The docs have said that None will become
a keyword in the future.
If so, it's part of the assignment statement, and not something the
object itself can decide. True?

Yes, that would be how I interpret constants: You want a name which can't
be re-bound to something else.

One work-around is to use the convention of writing the name in all caps:

CONSTANT = some_value

and then trust that your module user doesn't rebind CONSTANT. I'd like to
see support for something a little stronger than just "cross your fingers
and hope", without necessarily going all the way to Pascal's full
enforcement of constants. "We're all adults here" -- if somebody *really*
wants to rebind CONSTANT, I'm not going to stop them, but I want them to
jump through a hoop to do it, as a reminder that the module creator thinks
they shouldn't be doing it.

The obvious analogy is with name mangling of __private attributes in
classes.

Really hoping someone can come up with an answer for this.

So am I :)

Have you looked at the source for Frozenset? It was a pure Python module
in Python 2.3, before becoming a built-in in 2.4.
 
B

Bengt Richter

Bengt Richter said:
If [an enumeration has a fixed sequence], what is more natural
than using their index values as keys to other ordered info?
I don't see why. If you want sequence numbers, use enumerate(). If
not, the enum object itself can be used directly as an iterable.

I changed mine so the enum _class_ is iterable, but enum instances
are not.

I'm not really understanding your design.

In my enum package, an enumeration is an instance of Enum. Those
instances are iterable, producing EnumValue instances; the EnumValue
instances are also available as named attributes of the Enum instance.
I have a similar situation, except I have an enum factory function makeEnum
instead of a class Enum, and the factory returns not an instance of Enum,
but an enum as a unique type, i.e., a class. But it is functionally analogous,
except I differentiate by
... print str(val)
...
red
blue
green
That corresponds to
... print str(val),'-(repr)->', repr(val)
...
red -(repr)-> Colours.red
blue -(repr)-> Colours.blue
green -(repr)-> Colours.green

I added the repr to show that the vals were instances of something,
and they are actuall instances of the Colours type, which was uniquely
created to represent the enumeration, along with its fixed set of internally
cached immutable instances, references to which are returned by the Colours
constructor (__new__) instead of creating potentially duplicate instances.
So these instances can serve as sentinels too. The id's don't change, no
matter how many you construct (of a given member of the instance set). Of
course there are len(Colours) fixed (once created and cached) instances
of Colours in all.
>>> list(Colours) [Colours.red, Colours.blue, Colours.green]
>>> map(str, Colours) ['red', 'blue', 'green']
>>> map(int, Colours) [0, 1, 2]
>>> int(Colours.blue) 1
>>> Colours[1]
Colours.blue

Why would the Enum *class* be iterable?
That corresponds to asking why my makeEnum should be iterable,
and the it is not. What makeEnum makes functionally corresponds
to your Enum instance, except my "instance" is a unique manufactured class.
OTOH, the index values (and hence my enums) are[1] not very good
as unique dict keys, since they compare[2] promiscuously with
each other and other number types.
Indeed, that's why (in my design) the values from the enum are only
useful for comparing with each other. This, to me, seems to be the
purpose of an enumerated type.

Have you tried yet to use two different enum instances as keys in
the same dict?

What do you mean by "enum instances"? I presume you mean "values from
a single enum".
Yes, by analogy to your functionality, but since my "single enum" is
actually a class returned by makeEnum, not an instance of Enum, i.e.,
<class 'makeenum.Colours'>

and the "values" are immutable singleton instances that you can access via
the class (which I gave some unusual capabilities -- i.e., some methods
that apply to it as an ordered collection of immutable instances, and
which aren't accessible as methods of the instances (though the instances
have special methods of their own, and inherit methods from int).
... print colour_german[val]
...
rot
blau
grün
That's a plain dict with colour "values" as keys. Same as (checking the order first
to get the zip correspondence right ;-)
[Colours.red, Colours.blue, Colours.green]

... print 'colour_german[%r] => %s' %(val, colour_german[val])
...
colour_german[Colours.red] => rot
colour_german[Colours.blue] => blau
colour_german[Colours.green] => grün

Oh, perhaps you mean "enum values from different enum instances". No, Yes, again by analogy.
those won't compare against each other; there's no meaningful
relationship, since different enum instances are conceptually
different types.
Well, yes, I made that the strict default cmp, but I can optionally make
"Enum instances" (makeEnum returned classes) whose values cmp differently.
But now that I think of it, sorting is pretty controllable anyway, e.g,
suppose we had a Coins enum:
>>> Coins = makeEnum('Coins', 'penny nickel dime quarter')
>>> Colours[:] + Coins[:]
[Colours.red, Colours.blue, Colours.green, Coins.penny, Coins.nickel, Coins.dime, Coins.quarter]
>>> sorted(Colours[:] + Coins[:], key=repr)
[Coins.dime, Coins.nickel, Coins.penny, Coins.quarter, Colours.blue, Colours.green, Colours.red]
>>> sorted(Colours[:] + Coins[:], key=str)
[Colours.blue, Coins.dime, Colours.green, Coins.nickel, Coins.penny, Coins.quarter, Colours.red]
>>> map(str, sorted(Colours[:] + Coins[:], key=str)) ['blue', 'dime', 'green', 'nickel', 'penny', 'quarter', 'red']
>>> map(str, sorted(Colours[:] + Coins[:], key=int)) ['red', 'penny', 'blue', 'nickel', 'green', 'dime', 'quarter']
>>> map(str, sorted(Colours[:] + Coins[:], key=type)) ['red', 'blue', 'green', 'penny', 'nickel', 'dime', 'quarter']
>>> map(str, sorted(Colours[:] + Coins[:], key=lambda x:(type(x).__name__, int(x)))) ['penny', 'nickel', 'dime', 'quarter', 'red', 'blue', 'green']
>>> sorted(Colours[:] + Coins[:], key=lambda x:(type(x).__name__, int(x)))
[Coins.penny, Coins.nickel, Coins.dime, Coins.quarter, Colours.red, Colours.blue, Colours.green]
>>> sorted(Colours[:] + Coins[:], key=repr)
[Coins.dime, Coins.nickel, Coins.penny, Coins.quarter, Colours.blue, Colours.green, Colours.red]

I think this is a flaw, based on expecting too strong a relationship
between the enum value instance, and an integer value.
Actually, I'm not "expecting" it, I am defining it that way ;-)
Note the parallels between
>>> Bool = makeEnum('Bool', 'False True')
>>> issubclass(Bool, int) True
>>> issubclass(bool, int) True
>>> isinstance(Bool.False, int) True
>>> isinstance( False, int) True
>>> isinstance(Bool.True, int) True
>>> isinstance( True, int) True
>>> bf = bool(0)
>>> bf2 = bool(0)
>>> bf is bf2 True
>>> Bool
>>> Bf = Bool(0)
>>> Bf2 = Bool(0)
>>> Bf is Bf2 True
>>> Bf Bool.False
>>> Bf2 Bool.False
>>> bf False
>>> bf2 False
>>> map(int, [True, False]) [1, 0]
>>> map(int, [Bool.True, Bool.False])
[1, 0]
Why should colours.blue compare before fruits.orange? How is that
meaningful?
Like Finneys come before Richters in the telephone book ;-)
I'm still trying to understand what is served by having some exposed
relationship between an enum value instance and an integer value.
The relationship is effectively __int__ giving the index position in
the originally specified sequence of names, without having to do an index
operation, and without having to do anything but use a reference to
the value in a context where the integer value is useful.
It's no more necessary than saying that ["a", "b", "c"] requires
that there be some specific correlation between the values of that
list and the integers 0, 1, 2. If you *want* such a correlation, in
some particular case, use enumerate() to get it; but there's
nothing about the values themselves that requires that
correspondence.
That's true for arbitrary values in a list, but IMO it's not true for
values whose names were originally specified in a particular order for
(presumably -- or doesn't your enum care about the order??) a reason.
Enums in other languages have that default correspondence typically,
from what I can recall, and promotion to integer in arithmetic contexts
is also common. Having different enumerations be distinct _types_ is
exactly what C++ does. And Pascal.
Yet it is comforting to know that ['a', 'b', 'c'][0] will interpret
the [0] to mean the first in the sequence (unless someone is doing a
list-like repr trick based on some other type ;-).

Again, you're only talking about *sequence*, not correspondence to
integers. Your case above isn't an argument in favour of "the 'a'
value should coerce to the 0 value". Why then should an enum value
instance coerce to any particular integer value?
The particular integer values have no relation to their position in
an _arbitrary_ list, but they do have that relation to their position
in the originally specified sequence of their names.
Here you've lost me, since I don't understand why you want
enumerations to be classes at all. Are you going to be making
instances of an entire enumeration? If not, why make classes instead
of objects?
For the same reason C++ does. Different enumerations are logically
different types, not just different objects.

Somehow ISTM maybe you are using the enum name in an extended way that
really maybe calls for another name. Some kind of homogeneous frozen list
with names for the elements, which e.g. is more like struct than enum in C++.

What are the real use cases for your enums? And how are the same problems
usually solved? I guess I see the default case for C++ as a model, and
might extend that to a sparse sequence of integer values with names, but
without allowing instances of intervening values like C++. I don't know,
I have a feeling some experiments are just featuritis. Since sorts can
easily be controlled with a key function, maybe configuring with different
__cmp__ and __eq__ etc. is overkill. What's needed is some real use cases.
Maybe there is a place for both kinds of enums, if we give them different names ;-)

Regards,
Bengt Richter
 
G

George Sakkis

Steven said:
Yes, that would be how I interpret constants: You want a name which can't
be re-bound to something else.

One work-around is to use the convention of writing the name in all caps:

CONSTANT = some_value

and then trust that your module user doesn't rebind CONSTANT. I'd like to
see support for something a little stronger than just "cross your fingers
and hope", without necessarily going all the way to Pascal's full
enforcement of constants. "We're all adults here" -- if somebody *really*
wants to rebind CONSTANT, I'm not going to stop them, but I want them to
jump through a hoop to do it, as a reminder that the module creator thinks
they shouldn't be doing it.

Another workaround if you tradeoff strictness with convenience is
define a CONST metaclass in which contants are accessed as attributes
and which raises an Exception in __setattr__:

class CONST(type):
def __new__(cls, name, bases, dict):
def __setattr__(self,attr,val):
raise AttributeError('Cannot reassign constant %s.%s'
% (name, attr))
cls.__setattr__ = __setattr__
return type.__new__(cls, name, bases, dict)


class PhysicsConstants(object):
__metaclass__ = CONST
c = 2.9979246e+08

AttributeError: Cannot reassign constant PhysicsConstants.c


George
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,994
Messages
2,570,222
Members
46,809
Latest member
moe77

Latest Threads

Top