7  Classes/Packages for Python

7.1 Classes

A class is an abstract structure that can be used to hold both variables and functions. Variables in a class are called attributes, and functions in a class are called methods.

A class is defined in the following way.

class Circle:
    def __init__(self, radius=1):
        self.radius = radius
    
    def area(self):
        return self.radius**2*3.14 

In this example, we define a class Circle, which represents a circle. There is one attribute radius, and one method area. When define a cirlce, we need to specify its radius, and we could use the method area to compute the area of the circle.

cir1 = Circle()
cir2 = Circle(radius=5)

cir1.area()
3.14
cir2.area()
78.5

Here we define two circles. The first circle cir1 is of radius 1. This 1 comes from the default value. Check the definition of Circle.__init__().

The second circle cir2 is of radius 5, and this number is specified when we initialize the Circle instance.

Then we compute the areas of these two circles by calling the area() method. You can also use cir1.radius to get access the radius of the circle. The syntax difference between attributes and methods is the () at the end.

7.1.1 self

You may notice the self variable in the definition of the classes. The self is used to refered to the class its. When you want to get access to the class attributes or the class methods, you may use self.

Take the code as an example.

class Circle:
    def __init__(self, radius=1):
        self.radius = radius

In the __init__ function, there are two radius.

  1. radius is the local variable that is used by the function. It is also the input argument.
  2. self.radius is the class attribute, that is shared by all class methods. For example, we may add another class method to the class Circle.
class Circle:
    def __init__(self, radius=1):
        self.radius = radius
    
    def area(self):
        return self.radius**2*3.14 
    
    def perimeter(self):
        return self.radius*3.14*2

Both area() and perimeter() use the same self.radius.

Note

Class attributes are defined in the __init__() function.

7.1.2 A design example

Assume that we live in a world without Pandas, and we would like to design a table object. Then what do we need?

A table should have multiple rows and multiple columns. We should be able to get access entries by columns and row index. We should also be able to display the table by using the print funciton.

Note

The .__str__() method will be called when you try to print the object. If you don’t explicitly override it, the type of the object will be shown.

Therefore we may write the following class.

class myTableClass():
    def __init__(self, listoflist=None):
        if listoflist is None:
            listoflist = [[]]
        self.nrows = len(listoflist)
        self.ncols = len(listoflist[0])
        self.data = listoflist
        self.shape = (self.nrows, self.ncols)
    
    def get(self, i, j):
        return self.data[i][j]

    def __str__(self):
        tmp = [' '.join([str(x) for x in row]) for row in self.data]
        return '\n'.join(tmp)

This is a very brief table object. We may add more things to it. For example, we could talk about column names.

class myTableClass():
    def __init__(self, listoflist=None, columns=None):
        if listoflist is None:
            listoflist = [[]]
        if columns is None:
            columns = list()
        self.nrows = len(listoflist)
        self.ncols = len(listoflist[0])
        self.data = listoflist
        self.shape = (self.nrows, self.ncols)
        self.columns = columns
    
    def get(self, i, j):
        return self.data[i][j]

    def rename(self, columns=None):
        if columns is not None:
            self.columns = columns

    def __str__(self):
        tmp = [' '.join([str(x) for x in row]) for row in self.data]
        if len(self.columns) != 0:
            tmp.insert(0, self.columns)
        return '\n'.join(tmp)
Note

In Jupyter notebook or similar environment, we might directly call df to show a DataFrame and the shown DataFrame is rendered very pretty. This is due to the IPython.display.display() method, and is part of IPython console components.

7.2 Inheritance

One of the most important feature of classes is inheritance. Attributes and methods can be passed from parents to children, and child classes can override those attributes and methods if needed.

For example, we would like to first write a people class.

class people():
    def __init__(self, name='default', age=20):
        self.name = name
        self.age = age

    def eat(self):
        print('eat something.')

This people class defines a people who can eat. Then using this people class, we could build a children class: student.

class student(people):
    pass
stu1 = student('name1', 10)
stu1.eat()
stu1.name
eat something.
'name1'
type(stu1)
__main__.student

Now you can see that this stu1 is a student, but it has all attributes and methods as a people. However at current stage student and people are exactly the same since we don’t have any new codes for student. Let us improve it a little bit.

class student(people):
    def __init__(self, name='default', age=20, grade=1):
        super().__init__(name, age)
        self.grade = grade

    def eat(self):
        print('eat in the cafe.')

stu1 = student('name1', 10)
stu1.eat()
eat in the cafe.

Now student class override the eat() method from people. If someone is a student, he or she will eat in the cafe instead of just eat something.

In addition, you may also notice that the __init__() constructor function is also overriden. The first part is super().__init__(name, age) which is just call the people’s constructor function. The second part is new in student, that we add a new attribute grade to it. Now stu1 have attributes from people and the new attribute defined in student.

stu1.name, stu1.age
('name1', 10)
stu1.grade
1

7.3 packages / modules

Main reference is RealPython and [1].

7.3.1 import

In most cases we won’t only write one single Python file. If we want to use codes from other files, we need to import.

  • If both files are in the same folder, e.g. file1.py and file2.py, you may just put import file2 in file1.py, and use file2.myfunction() to call functions or variables defined in file2.py.
  • If both files are in the same folder, and you just want to use one function from file1.py in file2.py, you may from file1 import myfunction(), and then directly write myfunction() in file2.py.

Example 7.1 This is from file1.py.

s = "This is from file1.py."
a = [100, 200, 300]
print(s)

def foo(arg):
    print(f'arg = {arg}')

class Foo:
    pass
This is from file1.py.

In file2.py, we could get access to these variables and functions and classes as follows.

import file1
file1.s
'This is from file1.py.'
file1.a
[100, 200, 300]
file1.foo(file1.a)
arg = [100, 200, 300]
file1.Foo()
<assests.codes.file1.Foo at 0x1804b99b100>
Note

An alternative way is to use from <module> import <names> to directly use the names without the file1. prefix.

Please see the following Example to get a feel about how namespace works.

Example 7.2  

s = 'foo'
a = ['foo', 'bar', 'baz']

from file1 import s as string, a as alist
s
'foo'
string
'This is from file1.py.'
a
['foo', 'bar', 'baz']
alist
[100, 200, 300]

We may use dir() to look at all objects in the current namespace.

7.3.2 __name__

__name__ is a variable to tell you want is the current active namespace. See the following example.

Example 7.3  

import file1
file1.__name__
'assests.codes.file1'

The result file1 means that the codes in file1.py are now treated as a package and are imported into other files.

__name__
'__main__'

The result __main__ means that the codes we are writing now are treated as in the “active” enviroment.

You may see the following codes in a lot of places.

if __name__ == '__main__':
    pass

It means that the following codes will only be run in the “active” mode. If you import the codes as a package, these part of codes won’t be run.

7.3.3 Packages

Pacages is a way to group and organize mutliple modules. It allow for a hierachical structuring of the module namespace using dot notation.

Creating a package is straightforward, since it makes use of the operating system’s inherent hierarchical file structure.

Python defines two types of packages, regular packages and namespace packages. The above package is the regular one. Namespace packages allow codes are spread among different folders. We won’t talk about it in this course.

To create a regular package, what you need to do is to organize the files in suitable folders, and then add an __init__.py in each folder. The file can be empty, or you could add any initialization codes for the package which is represented by the folder.

Note

In the past __init__.py is required for a package. After Python 3.3 the namespace package is introduced, the __init__.py is not required (but recommended) for regular packages, and cannot be used for namespace packages.

Let us put the previous file1.py and file2.py into subfolder assests/codes/. To make it into a package assests and a subpackage codes, we need to put __init__.py in each folder.

import assests.codes.file1 as f1
f1.s
'This is from file1.py.'

7.4 Exercieses

Problems are based on [2].

Exercise 7.1 (Heron’s formula) Consider a triangle whose sides are \(a\), \(b\) and \(c\). Heron’s formula states that the area of this triangle is \[\sqrt{s(s−a)(s−b)(s−c)}\quad\text{ where } s=\frac12(a+b+c).\]

Please write a function that given three points computes the area of the triangle with vertices being the given points. The input is required to be a list of three tuples, where each tuple contains two numbers representing the 2d-coordinate of a point.

Exercise 7.2 (array) Write a function to reverse an 1D NumPy array (first element becomes last).

Exercise 7.3 (Compare two numpy arraies) Consider two numpy arraies x and y. Compare them entry by entry. We would like to know how many are the same.

Write a function that the inputs are x and y, and the output is the number of the same numbers.

Exercise 7.4 (Comma Code) Say you have a list value like this: spam = ['apples', 'bananas', 'tofu', 'cats'].

Write a function that takes a list value as an argument and returns a string with all the items separated by a comma and a space, with and inserted before the last item. For example, passing the previous spam list to the function would return ‘apples, bananas, tofu, and cats’. But your function should be able to work with any list value passed to it. Be sure to test the case where an empty list [] is passed to your function.

Exercise 7.5 Create a Car class with two instance attributes:

  1. .color, which stores the name of the car’s color as a string.
  2. .mileage, which stores the number of miles on the car as an integer.

Then instantiate two Car objects — a blue car with 20,000 miles and a red car with 30,000 miles — and print out their colors and mileage. Your expected output are below:

car1 = mycar(color='blue', mileage=20000)
car2 = mycar(color='red', mileage=30000)

print(car1)
print(car2)
A blue car with 20000 mileage.
A red car with 30000 mileage.

Exercise 7.6 Create a GoldenRetriever class that inherits from the Dog class. Give the sound argument of GoldenRetriever.speak() a default value of Bark. Use the following code for your parent Dog class:

class Dog:
    species = "Canis familiaris"

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"{self.name} is {self.age} years old"

    def speak(self, sound):
        return f"{self.name} says {sound}"