Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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)
like image 349
martineau Avatar asked Oct 19 '12 16:10

martineau


3 Answers

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
like image 134
Blckknght Avatar answered Sep 28 '22 08:09

Blckknght


Overriding the allocator will let you return an object of the appropriate type.

class NamedInt(int):
  def __new__(...):
    if should_be_NamedLong(...):
      return NamedLong(...)
     ...
like image 32
Ignacio Vazquez-Abrams Avatar answered Sep 28 '22 08:09

Ignacio Vazquez-Abrams


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
like image 37
Eryk Sun Avatar answered Sep 28 '22 10:09

Eryk Sun