Methods and functions

In the introduction notebook we have learned about the concept of function which is similar to a mathematical function such as \(y = f(x, a)\) with \(f(x, a) = a * x^2\). Here we have inputs \(x, a\), an output \(y\) and a function definition \(f(x, a) = a * x^2\). To recap such as function is written in the following way in Python:

def f(x, a):
    y = a * x**2
    return y

And used naturally as:

out = f(3, 4)
out
36

There are different types of functions and inputs in Python and now we do a quick tour of them.

Built-in functions

We have already seen that if we define a string such as:

mystring = 'this is my string'

we can get the number of characters using a function called len():

len(mystring)
17

len() is a a built-in function of Python. It is accessible without any import statement and can be used in different contexts. For example if we have a list, we can also ask its length:

mylist = [1,6,3,9]
len(mylist)
4

There is a series of such functions in Python. In this course, we will mostly be using len, as well as type() and print().

The type() function tells us what kind of variable we are dealing with. For example we can ask what’s the type of mystring:

type(mystring)
str

We see that we are dealing here with a string i.e. a text variable. If we define a simple number:

b = 3
type(b)
int

we see that we are dealing with an integer. Conversely if we define a number with a comma:

c = 3.4
type(c)
float

Python tells us that we are dealing with a floating point number. As you can see, contrary to other languages, in Python we don’t explicitly define the type of variables, it’s rather Python that infers the type from our input.

The print() function allows us to explicitly display a variable and is mostly used when dealing with longer chunks of code. For example:

print(mystring)
this is my string

Methods

In addition to functions, all variables in Python have functions that are specifically attached to them, in which case they are called methods. The functioning of these method-functions is the same as that of regular functions except for the way we call them. As they are attached to a variable, we can call them by using a dot notation.

For example any string can be capitalized using the capitalize() method which is used like this:

mystring.capitalize()
'This is my string'

So the only difference is that instead of writing f(mystring) we write mystring.f().

In our capitalize example, the function takes no input, which is why we only have an empty parenthesis. However sometimes there is an additional input x in which case we have to write mystring.f(x). For example the find() method, which indicates where in the string one finds a specific substring:

mystring
'this is my string'
mystring.find('my')
8

For the moment you can consider functions and methods in the same way. The concept of method comes from the object oriented programming that we will not explicitly cover during this course.

Getting infos

If you want to know all the functions and methods associated with a variable, you can use the function dir(). For example for our string this gives:

dir(mystring)
['__add__',
 '__class__',
 '__contains__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getitem__',
 '__getnewargs__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__iter__',
 '__le__',
 '__len__',
 '__lt__',
 '__mod__',
 '__mul__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__rmod__',
 '__rmul__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 'capitalize',
 'casefold',
 'center',
 'count',
 'encode',
 'endswith',
 'expandtabs',
 'find',
 'format',
 'format_map',
 'index',
 'isalnum',
 'isalpha',
 'isascii',
 'isdecimal',
 'isdigit',
 'isidentifier',
 'islower',
 'isnumeric',
 'isprintable',
 'isspace',
 'istitle',
 'isupper',
 'join',
 'ljust',
 'lower',
 'lstrip',
 'maketrans',
 'partition',
 'removeprefix',
 'removesuffix',
 'replace',
 'rfind',
 'rindex',
 'rjust',
 'rpartition',
 'rsplit',
 'rstrip',
 'split',
 'splitlines',
 'startswith',
 'strip',
 'swapcase',
 'title',
 'translate',
 'upper',
 'zfill']

All the functions are placed between two __ characters, the other are methods. We see that in that list we find again the len() function as well as the capitalize() method. Often one is unsure what a function exactly does and what input it needs. In that case one can either Google the answer, or use the built-in help.

To access the built-in help, one can just use the help() function or the ?mark. For example if we wanted to know what the capitalize() functions was doing we would type:

help(mystring.capitalize)
Help on built-in function capitalize:

capitalize() method of builtins.str instance
    Return a capitalized version of the string.
    
    More specifically, make the first character have upper case and the rest lower
    case.
mystring.capitalize?
Signature: mystring.capitalize()
Docstring:
Return a capitalized version of the string.

More specifically, make the first character have upper case and the rest lower
case.
Type:      builtin_function_or_method

In Jupyter, you also get infos on a function by placing the cursor within the function parenthesis and typing Shift+Tab.

Compounding functions

When we apply a series of functions we can of course separate all steps:

myint = -23
abs_int = abs(myint)
float_abs_int = float(abs_int)
print(float_abs_int)
23.0

But we can also just nest them into each other:

float_abs_int = float(abs(myint))
print(float_abs_int)
23.0

The same is true for methods which are then chained together:

mystring = 'this is my string'
mystring2 = mystring.upper()
print(mystring2)
THIS IS MY STRING
mystring2.find('MY')
8

is equivalent to:

mystring.upper().find('MY')
8

How much you separate different steps should be a compromise between compactness of code and readability.

Custom functions

We have already seen how to create our own functions. We here give some more details on additional options. Above we defined:

def f(x, a):
    y = a * x**2
    return y

Let’s recapitulate the important parts of this piece of code:

  • def says we are defining a function

  • my_function is the name of the function, i.e. like \(f\) in our mathematical example

  • x and a are our inputs like in \(f(x, a)\)

  • returnspecifies what the output of the function is

  • this output here is y, the equivalent of \(y\) in our mathematical function \(y = f(x, a)\)

You can then use it as any function:

f(3, 3)
27

When we call the function, we can either pass just the inputs and make sure that we have the rigth number of them or we can explicitly write the name of the input variables. In the first case, we need to pass the inputs in the correct order, as Python just assumes that we use the same order as defined in the function. In the second case, we don’t have to follow the order:

f(a=4, x=6) 
144

Often we write functions that can take multiple inputs but some of those inputs usually take a given value. To spare us having to repeatedly state this value that rarely changes, we can define default values for our function:

def f(x, a=3):
    y = a * x**2
    return y

Now when we call the function, if we don’t specify an input for a, Python will just assume that its value is 3:

f(x=2)
12

or

f(2)
12

It’s generally a good policy to explicitly state the input variables when calling a function, as it avoid confusion. Let’s imagine that we have the following new function:

def f2(x, a=3, b=5):
    y = a * x**2 + b
    return y

If we call f2 like this:

f2(3,4)
41

It means that in the function we used x=3, a=4 and b=5. It would be clearer in that case to use:

f2(x=3, a=4)
41

or even

External packages

We have learned in the introduction that sets of specialized functions are imported from external packages. We have seen that we need to first install such packages (just the first time we use them) and then import them in the notebooks. For example the library that implements the computationally efficient lists called arrays is Numpy. If we want to import it we write:

import numpy

Many of the most common libraries have abbreviations to keep the code shorter. This is the case for Numpy which almost always gets imported as:

import numpy as np

Numpy has an extensive set of functions that we can use by calling them using the dot notation. For example there is a function that computes the natural log of a number:

np.log(4)
1.3862943611198906

As you can see, we use these functions in the same way as the built-in functions or our custom functions: a function name followed by a parenthesis and the necessary inputs. Here again you can get information about a function using the help:

np.log?
Call signature:  np.log(*args, **kwargs)
Type:            ufunc
String form:     <ufunc 'log'>
File:            ~/mambaforge/envs/improc_beginner/lib/python3.9/site-packages/numpy/__init__.py
Docstring:      
log(x, /, out=None, *, where=True, casting='same_kind', order='K', dtype=None, subok=True[, signature, extobj])

Natural logarithm, element-wise.

The natural logarithm `log` is the inverse of the exponential function,
so that `log(exp(x)) = x`. The natural logarithm is logarithm in base
`e`.

Parameters
----------
x : array_like
    Input value.
out : ndarray, None, or tuple of ndarray and None, optional
    A location into which the result is stored. If provided, it must have
    a shape that the inputs broadcast to. If not provided or None,
    a freshly-allocated array is returned. A tuple (possible only as a
    keyword argument) must have length equal to the number of outputs.
where : array_like, optional
    This condition is broadcast over the input. At locations where the
    condition is True, the `out` array will be set to the ufunc result.
    Elsewhere, the `out` array will retain its original value.
    Note that if an uninitialized `out` array is created via the default
    ``out=None``, locations within it where the condition is False will
    remain uninitialized.
**kwargs
    For other keyword-only arguments, see the
    :ref:`ufunc docs <ufuncs.kwargs>`.

Returns
-------
y : ndarray
    The natural logarithm of `x`, element-wise.
    This is a scalar if `x` is a scalar.

See Also
--------
log10, log2, log1p, emath.log

Notes
-----
Logarithm is a multivalued function: for each `x` there is an infinite
number of `z` such that `exp(z) = x`. The convention is to return the
`z` whose imaginary part lies in `[-pi, pi]`.

For real-valued input data types, `log` always returns real output. For
each value that cannot be expressed as a real number or infinity, it
yields ``nan`` and sets the `invalid` floating point error flag.

For complex-valued input, `log` is a complex analytical function that
has a branch cut `[-inf, 0]` and is continuous from above on it. `log`
handles the floating-point negative zero as an infinitesimal negative
number, conforming to the C99 standard.

References
----------
.. [1] M. Abramowitz and I.A. Stegun, "Handbook of Mathematical Functions",
       10th printing, 1964, pp. 67.
       https://personal.math.ubc.ca/~cbm/aands/page_67.htm
.. [2] Wikipedia, "Logarithm". https://en.wikipedia.org/wiki/Logarithm

Examples
--------
>>> np.log([1, np.e, np.e**2, 0])
array([  0.,   1.,   2., -Inf])
Class docstring:
Functions that operate element by element on whole arrays.

To see the documentation for a specific ufunc, use `info`.  For
example, ``np.info(np.sin)``.  Because ufuncs are written in C
(for speed) and linked into Python with NumPy's ufunc facility,
Python's help() function finds this page whenever help() is called
on a ufunc.

A detailed explanation of ufuncs can be found in the docs for :ref:`ufuncs`.

**Calling ufuncs:** ``op(*x[, out], where=True, **kwargs)``

Apply `op` to the arguments `*x` elementwise, broadcasting the arguments.

The broadcasting rules are:

* Dimensions of length 1 may be prepended to either array.
* Arrays may be repeated along dimensions of length 1.

Parameters
----------
*x : array_like
    Input arrays.
out : ndarray, None, or tuple of ndarray and None, optional
    Alternate array object(s) in which to put the result; if provided, it
    must have a shape that the inputs broadcast to. A tuple of arrays
    (possible only as a keyword argument) must have length equal to the
    number of outputs; use None for uninitialized outputs to be
    allocated by the ufunc.
where : array_like, optional
    This condition is broadcast over the input. At locations where the
    condition is True, the `out` array will be set to the ufunc result.
    Elsewhere, the `out` array will retain its original value.
    Note that if an uninitialized `out` array is created via the default
    ``out=None``, locations within it where the condition is False will
    remain uninitialized.
**kwargs
    For other keyword-only arguments, see the :ref:`ufunc docs <ufuncs.kwargs>`.

Returns
-------
r : ndarray or tuple of ndarray
    `r` will have the shape that the arrays in `x` broadcast to; if `out` is
    provided, it will be returned. If not, `r` will be allocated and
    may contain uninitialized values. If the function has more than one
    output, then the result will be a tuple of arrays.