Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Subclassing int in Python

I'm interested in subclassing the built-in int type in Python (I'm using v. 2.5), but having some trouble getting the initialization working.

Here's some example code, which should be fairly obvious.

class TestClass(int):
    def __init__(self):
        int.__init__(self, 5)

However, when I try to use this I get:

>>> a = TestClass()
>>> a
0

where I'd expect the result to be 5.

What am I doing wrong? Google, so far, hasn't been very helpful, but I'm not really sure what I should be searching for

like image 610
me_and Avatar asked Jul 13 '10 14:07

me_and


People also ask

What does subclassing mean in Python?

The process of creating a subclass of a class is called inheritance. All the attributes and methods of superclass are inherited by its subclass also. This means that an object of a subclass can access all the attributes and methods of the superclass.

Can you inherit from int Python?

The bool type inherits from int . Because True and False are (in the sense of inherit from) integers, we can do arithmetic on them. We can even use boolean expressions as numbers (although doing so might result in obscure code).

What is __ new __ in Python?

The __new__() is a static method of the object class. It has the following signature: object.__new__(class, *args, **kwargs) Code language: Python (python) The first argument of the __new__ method is the class of the new object that you want to create.

Is Int a class in Python?

int is a class. The type of a class is usually type . And yes, almost all classes can be called like functions. You create what's called an instance which is an object that behaves as you defined in the class.


2 Answers

int is immutable so you can't modify it after it is created, use __new__ instead

class TestClass(int):     def __new__(cls, *args, **kwargs):         return  super(TestClass, cls).__new__(cls, 5)  print TestClass() 
like image 128
Anurag Uniyal Avatar answered Oct 14 '22 18:10

Anurag Uniyal


Though correct the current answers are potentially not complete.

e.g.

In [1]: a = TestClass()
In [2]: b = a - 5
In [3]: print(type(b))
<class 'int'>

Shows b as an integer, where you might want it to be a TestClass.

Here is an improved answer, where the functions of the base class are overloaded to return the correct type.

    class positive(int):
        def __new__(cls, value, *args, **kwargs):
            if value < 0:
                raise ValueError("positive types must not be less than zero")
            return  super(cls, cls).__new__(cls, value)
    
        def __add__(self, other):
            res = super(positive, self).__add__(other)
            return self.__class__(max(res, 0))
    
        def __sub__(self, other):
            res = super(positive, self).__sub__(other)
            return self.__class__(max(res, 0))
    
        def __mul__(self, other):
            res = super(positive, self).__mul__(other)
            return self.__class__(max(res, 0))
    
        def __div__(self, other):
            res = super(positive, self).__div__(other)
            return self.__class__(max(res, 0))

        def __str__(self):
            return "%d" % int(self)

        def __repr__(self):
            return "positive(%d)" % int(self)

Now the same sort of test


In [1]: a = positive(10)
In [2]: b = a - 9
In [3]: print(type(b))
<class '__main__.positive'>

UPDATE:
Added repr and str examples so that the new class prints itself properly. Also changed to Python 3 syntax, even though OP used Python 2, to maintain relevancy.

UPDATE 04/22:
I found myself wanting to do something similar on two recent projects. One where I wanted an Unsigned() type (i.e. x-y, where x is 0 and y is positive is still zero)
I also wanted a set() like type the was able to be updated and queried in a certain way.
The above method works but it's repetitive and tedious. What if there was a generic solution using metaclasses?

I could not find one so I wrote one. This will only work in recent Python (I would guess 3.8+, tested on 3.10)

First, the MetaClass

class ModifiedType(type):
    """
    ModifedType takes an exising type and wraps all its members
    in a new class, such that methods return objects of that new class.
    The new class can leave or change the behaviour of each
    method and add further customisation as required
    """

    # We don't usually need to wrap these
    _dont_wrap = {
    "__str__", "__repr__", "__hash__", "__getattribute__", "__init_subclass__", "__subclasshook__",
    "__reduce_ex__", "__getnewargs__", "__format__", "__sizeof__", "__doc__", "__class__"}

    @classmethod
    def __prepare__(typ, name, bases, base_type, do_wrap=None, verbose=False):
        return super().__prepare__(name, bases, base_type, do_wrap=do_wrap, verbose=verbose)

    def __new__(typ, name, bases, attrs, base_type, do_wrap=None, verbose=False):
        bases += (base_type,)

        #  Provide a call to the base class __new__
        attrs["__new__"] = typ.__class_new__

        cls = type.__new__(typ, name, bases, attrs)

        if "dont_wrap" not in attrs:
            attrs["dont_wrap"] = {}
        attrs["dont_wrap"].update(typ._dont_wrap)

        if do_wrap is not None:
            attrs["dont_wrap"] -= set(do_wrap)

        base_members = set(dir(base_type))
        typ.wrapped = base_members - set(attrs) - attrs["dont_wrap"]

        for member in typ.wrapped:
            obj = object.__getattribute__(base_type, member)
            if callable(obj):
                if verbose:
                    print(f"Wrapping {obj.__name__} with {cls.wrapper.__name__}")
                wrapped = cls.wrapper(obj)
                setattr(cls, member, wrapped)
        return cls

    def __class_new__(typ, *args, **kw):
        "Save boilerplate in our implementation"
        return typ.base_type.__new__(typ, *args, **kw)

An example usage to create a new Unsigned type

# Create the new Unsigned type and describe its behaviour
class Unsigned(metaclass=ModifiedType, base_type=int):
    """
    The Unsigned type behaves like int, with all it's methods present but updated for unsigned behaviour
    """
    # Here we list base class members that we won't wrap in our derived class as the
    # original implementation is still useful. Other common methods are also excluded in the metaclass
    # Note you can alter the metaclass exclusion list using 'do_wrap' in the metaclass parameters
    dont_wrap = {"bit_length", "to_bytes", "__neg__", "__int__", "__bool__"}
    import functools

    def __init__(self, value=0, *args, **kw):
        """
        Init ensures the supplied initial data is correct and passes the rest of the
        implementation onto the base class
        """
        if value < 0:
            raise ValueError("Unsigned numbers can't be negative")

    @classmethod
    def wrapper(cls, func):
        """
        The wrapper handles the behaviour of the derived type
        This can be generic or specific to a particular method
        Unsigned behavior is:
            If a function or operation would return an int of less than zero it is returned as zero
        """
        @cls.functools.wraps(func)
        def wrapper(*args, **kw):
            ret = func(*args, **kw)
            ret = cls(max(0, ret))
            return ret
        return wrapper

And some tests for the example

In [1]: from unsigned import Unsigned
In [2]: a = Unsigned(10)
   ...: print(f"a={type(a).__name__}({a})")
a=Unsigned(10)

In [3]: try:
   ...:     b = Unsigned(-10)
   ...: except ValueError as er:
   ...:     print(" !! Exception\n", er, "(This is expected)")
   ...:     b = -10  # Ok, let's let that happen but use an int type instead
   ...:     print(f" let b={b} anyway")
   ...:     
 !! Exception
 Unsigned numbers can't be negative (This is expected)
 let b=-10 anyway
In [4]: c = a - b
   ...: print(f"c={type(c).__name__}({c})")
c=Unsigned(20)

In [5]: d = a + 10
   ...: print(f"d={type(d).__name__}({d})")
d=Unsigned(20)

In [6]: e = -Unsigned(10)
   ...: print(f"e={type(e).__name__}({e})")
e=int(-10)

In [7]: f = 10 - a
   ...: print(f"f={type(f).__name__}({f})")
f=Unsigned(0)

UPDATE for @Kazz:
To answer your question. Though it would be simpler to just int(u) * 0.2

Here is a small updated wrapper to handle the exception case e.g. (Unsigned * float) that serves as an example of how to modify behavior to match the desired subclass behaviour without having to individually overload each possible combination of argument types.

    # NOTE: also add '__float__' to the list of non-wrapped methods

    @classmethod
    def wrapper(cls, func):
        fn_name = func.__name__
        @cls.functools.wraps(func)
        def wrapper(*args, **kw):
            compatible_types = [issubclass(type(a), cls.base_type) for a in args]

            if not all(compatible_types):
                # Try converting
                type_list = set(type(a) for a in args) - set((cls.base_type, cls))
                if type_list != set((float,)):
                    raise ValueError(f"I can't handle types {type_list}")
                args = (float(x) for x in args)
                ret = getattr(float, fn_name)(*args, **kw)
            else:
                ret = func(*args, **kw)
                ret = cls(max(0, ret))
            return ret
        return wrapper
like image 38
Jason M Avatar answered Oct 14 '22 17:10

Jason M