67

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

asked Jul 13, 2010 at 14:49
2

2 Answers 2

86

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()
answered Jul 13, 2010 at 14:57
Sign up to request clarification or add additional context in comments.

Comments

31

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
answered Sep 13, 2017 at 11:22

4 Comments

what do I need to do to make positive(10)*0.2 work ?
The whole point of this example is that operations on instances of this class return objects of the same class. Results of adding 0.2 (a float) to a positive type integer can't be of type positive, it makes no sense. I would suggest using the example given by @Anurag Uniyal, above where the resulting object can be of a different type.
The new generic version of my ansser handles your case (though I can't see a reason to use it)
Testing your code answer, I don't be able to subclass from Unsigned... How to do ?

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.