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
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.
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).
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.
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.
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()
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With