Classes

We can create new types of objects with the class statement. Within the class we can define member functions that operate on objects of this class.

In [4]:
# create a new type of object with nothing in it:
class mytype():
    pass
    
v=mytype()
print(dir(v))
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

Objects should define an __init__ function that is called when an object is created and initializes the object (a "constructor").

Within the class we define functions that are the methods for objects of this type.

We can also define other "dunder" (for "double underscore") methods that perform standard operations, including those performed by operators (+, ==, <, etc).

All method definitions take a first argument (called 'self' by convention) that refers to the instance of the object being created or being acted on. Each method can also have other arguments.

In [8]:
class mytype():
    '''
    This would be the documentation for this type.
    '''
    # constructor:
    def __init__(self,a=0):
        self.x=a
    # "dunder" methods:
    def __eq__(self,a):
        return self.x == a
    def __repr__(self):
        return str(self.x)
    # a method (without arguments):
    def negated(self):
        return -self.x
        
v=mytype(3)
print(v == 3, v == 5, v, v.negated())
# help(mytype)
True False 3 -3

Unlike some other OO languages, members are not private. You can modify and inspect objects dynamically.

In [17]:
print(v.x)
v.x=5
print(v.x)

# add a method to an existing object:
mytype.doubleit = lambda self: 2*self.x
print(v.doubleit())
5
5
10

But only objects of built-in types (e.g. ints, lists, dictionaries) can be created using syntax instead of constructor functions. User-defined objects must be created using a constructor and binding it to a variable using the assignment operator.

In [22]:
# and variables are not objects:
print(v,type(v))
# if we assign to v, it now points to an int:
v = 5
print(v,type(v))
v=mytype(1)
print(v,type(v))
1 <class '__main__.mytype'>
5 <class 'int'>
1 <class '__main__.mytype'>

But our type is incomplete, and lacks much functionality:

In [23]:
v=mytype(3)
print(v+1)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-23-4ddcaffe6e30> in <module>()
      1 v=mytype(3)
----> 2 print(v+1)

TypeError: unsupported operand type(s) for +: 'mytype' and 'int'

In many cases it's worth building on the features provided by an existing type ("subclassing"). It "inherits" all the methods of the parent type.

Here's an example that subclasses the int type but reversed the meanings of the + and - operators (not a practical example). To do this we define the __sub__ and __add__ methods using the existing __sub__ and __add__ methods in the int type (to avoid recursion).

In [16]:
# so let's subclass an existing type
# and redefine some operators
class myint(int):
    def __add__(self,x):
        return int.__sub__(self,x)
    def __sub__(self,x):
        return int.__add__(self,x)

# new type is just like int but +/- are reversed:
x=myint(1)
print(x, x+1, x-1, x+x)
1 0 2 0

We can also define the function call and indexing (__setitem__) operators. In this example we define a new type of dictionary that converts stored values to upper-case. As before, we re-use dict's __setitem__ method:

In [40]:
# another example of subclassing an existing type
# changing the behaviour of the [] operator for a dict:
class updict(dict):
    def __setitem__(self,k,v):
        dict.__setitem__(self,k,str.upper(v))
        
v=updict()
v[0]='x'
print(v)
{0: 'X'}