martineau martineau - 14 days ago 6
Python Question

Avoid having two different numeric subclasses (int and long)?

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)

Answer

Here's how you can solve the DRY issue via multiple inheritance. Unfortunately, it doesn't play well with __slots__ (it causes compile-time TypeErrors) 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