4. Functions and Classes#

Let us learn how to define your own functions, and further organize them into a class for neatness and extensibility.

References
Python Tutorial (https://docs.python.org/3/tutorial/)

  • Section 4.7-4.8: Functions

  • Chapter 6: Modules

  • Chapter 9: Classes

import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

Defining functions#

If you find yourself running the same codes again and again with different inputs, it is time to define them as a function.

Here is a simple example:

def square(x):
    """Compute x*x"""
    # result returned
    return x*x
square(3)
9
a = np.array([1, 2, 3])
# input `x` can be anything for which `x*x` is valid
square(a)
array([1, 4, 9])

The line encosed by “”” “”” is called a Docstring, which is shown by help( ) command.

help(square)
Help on function square in module __main__:

square(x)
    Compute x*x
square?

A function does not need to return anything.

def print_square(x):
    """Print x*x"""
    print(x*x)
# the end of indentation is the end of definition
print_square(a)    
[1 4 9]

A function can return multiple values.

def square_cube(x):
    """Compute x**2 and x**3"""
    # return multiple values separated by comma
    return x**2, x**3
# results can be assigned to variables separated by comma
b, c = square_cube(a)
print(b, c)
[1 4 9] [ 1  8 27]
square_cube(3)
(9, 27)

Arguments and local variables#

A function can take single, multiple, or no arguments (inputs).
An argument can be required, or optional with a default value.
An argument can be specified by the position, or a keyword.

def norm(x, p=2):
    """Give the L^p norm of a vector."""
    y = abs(x) ** p
    return np.sum(y) ** (1/p)
a = np.array([1, 2, -2])
norm(a)  # default p=2
3.0
norm(a, 1)  # specify by position
5.0
norm(p=1, x=a)  # specify by the keywords, in any oder
5.0

Local and global variables#

Arguments and variables assigned in a function are registered in a local namespace.

y = 0  # global variable
norm(a)  # this uses `y` as local variable, y=[1, 4, 9]
print(y)  # the global variable `y` is not affected
0

Any global variables can be referenced within a function.

a = 1  # global variable
def add_a(x):
    """Add x and a."""
    return a + x
print(add_a(1))  # 1 + 1
a = 2
print(add_a(1))  # 1 + 2
2
3

To modify a global variable from inside a function, it have to be declaired as global.

a = 1
def addto_a(x):
    """Add x into a."""
    global a
    a = a + x  # add x to a
addto_a(1)  # a = a + 1
print(a)
addto_a(1)  # a = a + 1
print(a)
2
3

You can modify an argument in a function.

def double(x):
    """Double x"""
    x = 2 * x
    return x
double(1)
2

Script#

Before Jupyter (iPython) notebook was created, to reuse any code, you had to store it in a text file, with .py extension by convention. This is called a script.

This magic command creates a simple script file.

%%file hello.py
print('Hello!')
Overwriting hello.py
%cat hello.py
print('Hello!')

The standard way of running a script is to type in a terminal:

$ python hello.py

In a Jupyter notebook, you can use %run magic command.

%run hello.py
Hello!

You can edit a python script by any text editor.

In Jupyter notebook’s Files window, you can make a new script as a Text file by New menu, or edit an existing script by clicking the file name.

Module#

A script with function definitions is called a module.

%%file lpnorm.py
"""L^p norm module"""

import numpy as np

def norm(x, p=2):
    """The L^p norm of a vector."""
    y = abs(x) ** p
    return np.sum(y) ** (1/p)

def normalize(x, p=2):
    """L^p normalization"""
    return x/norm(x, p)
Overwriting lpnorm.py
%cat lpnorm.py
"""L^p norm module"""

import numpy as np

def norm(x, p=2):
    """The L^p norm of a vector."""
    y = abs(x) ** p
    return np.sum(y) ** (1/p)

def normalize(x, p=2):
    """L^p normalization"""
    return x/norm(x, p)

You can import a module and use its function by module.function().

import lpnorm
help(lpnorm)
Help on module lpnorm:

NAME
    lpnorm - L^p norm module

FUNCTIONS
    norm(x, p=2)
        The L^p norm of a vector.

    normalize(x, p=2)
        L^p normalization

FILE
    /Users/doya/OIST Dropbox/kenji doya/Python/iSciComp/lpnorm.py
a = np.array([-3, 4])
lpnorm.norm(a)
5.0
lpnorm.norm(a,1)
7.0
lpnorm.normalize(a)
array([-0.6,  0.8])

Caution: Python reads in a module only upon the first import, as popular modules like numpy are imported in many modules. If you modify your module, you need to restart your kernel or call importlib.reload().

import importlib
importlib.reload(lpnorm)
<module 'lpnorm' from '/Users/doya/OIST Dropbox/kenji doya/Python/iSciComp/lpnorm.py'>

Package#

A collection of modules are put in a directory as a package.

# see how numpy is organized
%ls $CONDA_PREFIX/lib/python*/site-packages/numpy
/opt/anaconda3/lib/python3.1.c~/site-packages/numpy:
__config__.py           _utils/                 fft/
__init__.cython-30.pxd  array_api/              lib/
__init__.pxd            compat/                 linalg/
__init__.py             conftest.py             ma/
__init__.pyi            core/                   matlib.py
__pycache__/            ctypeslib.py            matrixlib/
_core/                  ctypeslib.pyi           polynomial/
_distributor_init.py    doc/                    py.typed
_globals.py             dtypes.py               random/
_pyinstaller/           dtypes.pyi              testing/
_pytesttester.py        exceptions.py           tests/
_pytesttester.pyi       exceptions.pyi          typing/
_typing/                f2py/                   version.py

/opt/anaconda3/lib/python3.1/site-packages/numpy:
__config__.py           _utils/                 fft/
__init__.cython-30.pxd  array_api/              lib/
__init__.pxd            compat/                 linalg/
__init__.py             conftest.py             ma/
__init__.pyi            core/                   matlib.py
__pycache__/            ctypeslib.py            matrixlib/
_core/                  ctypeslib.pyi           polynomial/
_distributor_init.py    doc/                    py.typed
_globals.py             dtypes.py               random/
_pyinstaller/           dtypes.pyi              testing/
_pytesttester.py        exceptions.py           tests/
_pytesttester.pyi       exceptions.pyi          typing/
_typing/                f2py/                   version.py

/opt/anaconda3/lib/python3.12/site-packages/numpy:
__config__.py           _utils/                 fft/
__init__.cython-30.pxd  array_api/              lib/
__init__.pxd            compat/                 linalg/
__init__.py             conftest.py             ma/
__init__.pyi            core/                   matlib.py
__pycache__/            ctypeslib.py            matrixlib/
_core/                  ctypeslib.pyi           polynomial/
_distributor_init.py    doc/                    py.typed
_globals.py             dtypes.py               random/
_pyinstaller/           dtypes.pyi              testing/
_pytesttester.py        exceptions.py           tests/
_pytesttester.pyi       exceptions.pyi          typing/
_typing/                f2py/                   version.py

Object Oriented Programming#

Object Oriented Programming has been advocated since 1980’s in order to avoid naming conflicts and to allow incremental software development by promoting modularity.

Examples are: SmallTalk, Objective C, C++, Java,… and Python!

Major features of OOP is:

  • define data structure and functions together as a Class

  • an instance of a class is created as an object

  • the data (attributes) and functions (methods) are referenced as instance.attribute and instance.method().

  • a new class can be created as a subclass of existing classes to inherit their attributes and methods.

Defining a basic class#

Definition of a class starts with
class ClassName(BaseClass):
and include

  • any definition of attributes

  • __init__() method called when a new instance is created

  • definition of other methods

The first argument of a method specifies the instance, which is named self by convention.

Here’s a simple class for describing cells in 2D space.

class Cell:
    """Class for a cell"""

    def __init__(self, position = [0,0], radius=0.1, color=[1,0,0,0.5]):
        """Make a new cell"""
        self.position = np.array(position)
        self.radius = radius
        self.color = color
     
    def show(self):
        """Visualize as a circule"""
        c = plt.Circle(self.position,self.radius,color=self.color)
        plt.gca().add_patch(c)
        plt.axis('equal')

Let’s create an instance of a class by calling like a function.

cell = Cell()

Attributes and methods are referenced by .

cell.position
array([0, 0])
cell.show()
_images/6cbc034b54d535d25600d68cb94dc1568584de82d7c44e3316f95ee172843a61.png
cell.color = 'b'
cell.show()
_images/f4667fc965db15704d89b1beb2f8ca8b2a4223079b95eea2b9d9f2323fd64667.png

You can create an array of class instances.

n = 10
cells = [Cell(np.random.rand(2),color=np.random.rand(4)) for i in range(n)]
for i in range(n):
    cells[i].show()
_images/38e930b8ceadbceeefb40d63b1b3d63b832677df9a165b331ea39daec2569329.png

A subclass can inherit attributes and methods of base class.

class gCell(Cell):
    """Class of growing cell based on Cell class"""
    
    def grow(self, scale=2):
        """Grow the area of the cell"""
        self.radius *= np.sqrt(scale)
        
    def duplicate(self):
        """Make a copy with a random shift"""
        c = gCell(self.position+np.random.randn(2)*self.radius, self.radius, self.color)
        return c
c0 = gCell()
c0.show()
c1 = c0.duplicate()
c1.grow()
c1.show()
_images/5355c535716974bfaf22cb197bdc51f1cd7a41451dab2e351a44609ab19cf3dc.png

Let us make a new class using gCell class

class Culture():
    """Class for a cell culture"""

    def __init__(self, n=10, position=None, radius=0.1, color=None):
        """Make a cell culture with n cells"""
        self.number = n  # nuber of cells
        if position == None:  # random position if not specified
            position = np.random.rand(n,2)
        if color == None:  # random colors if not specified
            color = np.random.rand(n,4)
        self.cells = [gCell(position[i],radius=radius,color=color[i]) for i in range(n)]

    def show(self):
        """Visualize as a circules"""
        for i in range(self.number):
            self.cells[i].show()

    def grow(self, scale=2):
        """Grow the area of each cell"""
        for i in range(self.number):
            self.cells[i].grow(scale)
        
    def duplicate(self):
        """Make a copy of each cell"""
        for i in range(self.number):
            c = self.cells[i].duplicate()
            self.cells.append(c)
        self.number *= 2
culture = Culture()
culture.cells[0].position
array([0.95913507, 0.23307791])
culture.show()
_images/970f5907e9aafcbc5556eb70b9c7b116892cec5ee7be90f03e7e606ea93a8069.png
culture.grow()
culture.show()
_images/d84848db117f9b8e295a321441d5f75a11f8549a3b33259597f38c5d12c9a1f3.png
culture.duplicate()
culture.grow(0.5)
culture.show()
_images/0887f0691afa22b2c8e77b5a2ec93bcf69d87d99bb2011f8a0e7b5176d44f1c8.png