4

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)
martineau
  • 119,623
  • 25
  • 170
  • 301

3 Answers3

2

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

class NamedInt(int):
  def __new__(...):
    if should_be_NamedLong(...):
      return NamedLong(...)
     ...
Community
  • 1
  • 1
Ignacio Vazquez-Abrams
  • 776,304
  • 153
  • 1,341
  • 1,358
  • Granted this would eliminate the need for the `NamedWholeNumber` class (and probably be worth doing), but it seems like most of the code that had been in it would just end up getting moved into the `NamedInt.__new__()` method and I'll still need to have a separate `NamedLong`subclass -- or am I missing something? – martineau Oct 19 '12 at 16:55
  • That sounds about right, but remember that Python 2.x has separate `int` and `long` classes regardless of the value passed to `int()`. – Ignacio Vazquez-Abrams Oct 19 '12 at 16:58
2

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
Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • Good answer, but doesn't handle `NamedInt('HexBased', 'deadbeef', 16)`. – martineau Oct 19 '12 at 22:14
  • Hmm, good point. I think it can be fixed with varargs. I'll edit to do that. – Blckknght Oct 20 '12 at 08:24
  • Thanks for the fix. Was difficult to decide between this and @eryksun's answer, as both address the DRY issue extremely well -- but ultimately chose this one because it's the most straightforward and understandable IMHO. BTW, it's possible to add a `__slots__` attribute to just the `NamedInt` subclass (as eryksun did) which seems to address that need for the likely more common `int` case (and _is_ an important characteristic for the usage intended). – martineau Oct 20 '12 at 15:31
  • @martineau: subclass instances have a dict if superclass instances have one. You can add new slots, but you can't subtract them. – Eryk Sun Oct 20 '12 at 17:14
  • 2
    It will work if `Named` has `__slots__ = ()`, and `NamedInt` has `__slots__ = '_name'`. – Eryk Sun Oct 20 '12 at 17:38
  • I suspected that just adding a `__slots__` to `NamedInt` didn't really accomplishing anything due to inheritance and was meaning to look into it further to try to come up with a proper fix -- which you've apparently done, so thanks again, your effort(s) are much appreciated. – martineau Oct 20 '12 at 18:50
2

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
Eryk Sun
  • 33,190
  • 5
  • 92
  • 111
  • Definitely leading the pack IMHO. I'm trying to decide between it and @Blckknght's. I like that fact that yours supports the `__slots__` optimization, non-decimal based string values, and only requires single inheritance. – martineau Oct 19 '12 at 22:12
  • Unfortunately I've decided to go with @Blckknght's answer for the reasons stated in a comment I added to it. It was an arduous choice to make, and nonetheless, your approach seems completely viable and just a good by most measures. I also found your approach to implementing class decorators very clever -- and learned some new techniques from it. Thanks! – martineau Oct 20 '12 at 15:41