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.

Note on terminology:

The names type, class are roughly synonymous in Python. Everything in Python is a object. Every object has a type. class statements create new user-defined types which are often called a class.

Howver, the type of an object, sometimes loosely called an object, is not the same as an instance of an object. There is only one type object of a type (e.g. only one int type), but there can be many objects of the same type (many ints), each of which is called an instance of that object (more properly these should be called instances of a type).

This proliferation of names is partly due to different names being used for the same concepts in different object-oriented programming languages (today these are primarily Python, Java and C++).

In [1]:
# create a new type of object with nothing in it:
class mytype():
    pass
    
v=mytype()
type(v)
#dir(v)
Out[1]:
__main__.mytype

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 [2]:
class mytype():
    '''
    This would be the documentation for this type. Hello, world!
    '''
    # constructor:
    def __init__(self,a=0):
        self.x=a
    # "dunder" methods:
    def __eq__(self,a):
        # self.x = a
        return self.x == a
    def __repr__(self):
        return "mytype("+str(self.x)+")"
    # a method (without arguments):
    def str(self):
        return str(self.x)
    def negated(self):
        return -self.x
    
v=mytype(3)

print(v.__eq__(3), v.__eq__(5), v == 5, v, v.negated())
v.str()
True False False mytype(3) -3
Out[2]:
'3'

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

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

# add a method to an existing object:
v=mytype(5)
mytype.doubleit = lambda self: 2*self.x
print(v.doubleit())
mytype(3)
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 [4]:
v = mytype(1)
# 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))
mytype(1) <class '__main__.mytype'>
5 <class 'int'>
mytype(1) <class '__main__.mytype'>

But our type is incomplete, and lacks much functionality:

In [5]:
v=mytype(3)
print(v+1)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [5], in <cell line: 2>()
      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 reverses 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 [6]:
# 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 [7]:
# 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['abc']='abc'
print(v)
{'abc': 'ABC'}