When working on essentially a custom enumerated type implementation, I ran into a situation where it appears I had to derive separate yet almost identical subclasses from both int
and long
since they're distinct classes in Python. This seems kind of ironic since instances of the two can usually be used interchangeably because for the most part they're just created automatically whenever required.
What I have works fine, but in the spirit of DRY (Don't Repeat Yourself), I can't help but wonder if there isn't any better, or at least a more succinct, way to accomplish this. The goal is to have subclass instances that can be used everywhere -- or as close to that as possible -- that instances of their base classes could have been. Ideally this should happen automatically similar to the way the built-in int()
actually returns a long
whenever it detects one is required.
Here's my current implementation:
class NamedInt(int):
"""Subclass of type int with a name attribute"""
__slots__ = "_name" # also prevents additional attributes from being added
def __setattr__(self, name, value):
if hasattr(self, name):
raise AttributeError(
"'NamedInt' object attribute %r is read-only" % name)
else:
raise AttributeError(
"Cannot add attribute %r to 'NamedInt' object" % name)
def __new__(cls, name, value):
self = super(NamedInt, NamedInt).__new__(cls, value)
# avoid call to this subclass's __setattr__
super(NamedInt, self).__setattr__('_name', name)
return self
def __str__(self): # override string conversion to be name
return self._name
__repr__ = __str__
class NamedLong(long):
"""Subclass of type long with a name attribute"""
# note: subtypes of variable length 'long' type can't have __slots__
def __setattr__(self, name, value):
if hasattr(self, name):
raise AttributeError(
"NamedLong object attribute %r is read-only" % name)
else:
raise AttributeError(
"Cannot add attribute %r to 'NamedLong' object" % name)
def __new__(cls, name, value):
self = super(NamedLong, NamedLong).__new__(cls, value)
# avoid call to subclass's __setattr__
super(NamedLong, self).__setattr__('_name', name)
return self
def __str__(self):
return self._name # override string conversion to be name
__repr__ = __str__
class NamedWholeNumber(object):
"""Factory class which creates either a NamedInt or NamedLong
instance depending on magnitude of its numeric value.
Basically does the same thing as the built-in int() function
does but also assigns a '_name' attribute to the numeric value"""
class __metaclass__(type):
"""NamedWholeNumber metaclass to allocate and initialize the
appropriate immutable numeric type."""
def __call__(cls, name, value, base=None):
"""Construct appropriate Named* subclass."""
# note the int() call may return a long (it will also convert
# values given in a string along with optional base argument)
number = int(value) if base is None else int(value, base)
# determine the type of named numeric subclass to use
if -sys.maxint-1 <= number <= sys.maxint:
named_number_class = NamedInt
else:
named_number_class = NamedLong
# return instance of proper named number class
return named_number_class(name, number)
Here's how you can solve the DRY issue via multiple inheritance. Unfortunately, it doesn't play well with __slots__
(it causes compile-time TypeError
s) so I've had to leave that out. Hopefully the __dict__
values won't waste too much memory for your use case.
class Named(object):
"""Named object mix-in. Not useable directly."""
def __setattr__(self, name, value):
if hasattr(self, name):
raise AttributeError(
"%r object attribute %r is read-only" %
(self.__class__.__name__, name))
else:
raise AttributeError(
"Cannot add attribute %r to %r object" %
(name, self.__class__.__name__))
def __new__(cls, name, *args):
self = super(Named, cls).__new__(cls, *args)
super(Named, self).__setattr__('_name', name)
return self
def __str__(self): # override string conversion to be name
return self._name
__repr__ = __str__
class NamedInt(Named, int):
"""NamedInt class. Constructor will return a NamedLong if value is big."""
def __new__(cls, name, *args):
value = int(*args) # will raise an exception on invalid arguments
if isinstance(value, int):
return super(NamedInt, cls).__new__(cls, name, value)
elif isinstance(value, long):
return NamedLong(name, value)
class NamedLong(Named, long):
"""Nothing to see here."""
pass
Overriding the allocator will let you return an object of the appropriate type.
class NamedInt(int):
def __new__(...):
if should_be_NamedLong(...):
return NamedLong(...)
...
Here's a class decorator version:
def named_number(Named):
@staticmethod
def __new__(cls, name, value, base=None):
value = int(value) if base is None else int(value, base)
if isinstance(value, int):
NamedNumber = Named # NamedInt / NamedLong
else:
NamedNumber = cls = NamedLong
self = super(NamedNumber, cls).__new__(cls, value)
super(NamedNumber, self).__setattr__('_name', name)
return self
def __setattr__(self, name, value):
if hasattr(self, name):
raise AttributeError(
"'%r' object attribute %r is read-only" % (Named, name))
else:
raise AttributeError(
"Cannot add attribute %r to '%r' object" % (name, Named))
def __repr__(self):
return self._name
__str__ = __repr__
for k, v in locals().items():
if k != 'Named':
setattr(Named, k, v)
return Named
@named_number
class NamedInt(int):
__slots__ = '_name'
@named_number
class NamedLong(long): pass
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With