Python: functions, instances and classes

inbuilt objects

Python has a number of inbuilt objects, many of which you have likely encountered before.

In [1]:
import builtins

If we type in builtins followed by a dot and then tab we will see a list of available inbuilt objects. We see each object is color-coded and marked as a category:

  • orange (i for instance)
  • blue (f for function)
  • green (c for class)
In [ ]:
builtins.
i f c
copyright abs bool
credits all bytearray
Ellipsis any bytes
exit ascii classmethod
False bin complex
help breakpoint enumerate
license callable filter
None chr float
NotImplemented compile frozenset
quit delattr GeneratorExit
True dir int
display list
divmod map
eval memoryview
exec object
format range
getattr reversed
globals set
hasattr slice
hash staticmethod
hex str
id super
input tuple
isinstance type
issubclass zip
iter
len
locals
max
min
next
oct
open
ord
pow
print
repr
round
setattr
sorted
sum
vars

using inbuilt instances

Let's first examine instances. These are objects, under the hood instances of classes have a data model blueprint defined by their class. The instances True and False are for example instances of the bool class.

In [2]:
True
Out[2]:
True
In [3]:
False
Out[3]:
False

The bool class will contain a method which instructs what the + operator does for these instances.

In [4]:
True + False
Out[4]:
1

0,1,2,3,4,5,6,7,8,9,... are instances of the int class and the int class also instructs what the + operator does for these instances.

In [5]:
1 + 2
Out[5]:
3

"a", "b", "c", ... are instances of the str class and the str class instructs what the + operator does for these instances.

In [6]:
"a" + "b"
Out[6]:
'ab'

Note the different behaviour between these three classes with the + operator. In the bool and int classes, numeric addition is performed, while in the case of the str class, str concatenation is instead performed.

using inbuilt functions

Before discussing classes in any more detail, we need to understand what functions are. functions are also objects. When we refer to a function like an instance, we will only be informed that the function being used is a function.

In [7]:
chr
Out[7]:
<function chr(i, /)>

We need to call a function with parenthesis. The parenthesis are used to enclose input arguments. To find out more details about the function and the functions input arguments we can either type in the function name followed by a tab and shift which will open up part of the docstring as a tool tip, or alternatively we can type ? before the function name to view the full docstring output within a cell.

In [8]:
? chr
Signature:  chr(i, /)
Docstring: Return a Unicode string of one character with ordinal i; 0 <= i <= 0x10ffff.
Type:      builtin_function_or_method

Unicode maps each possible character to a numeric integer. In the case of the function chr we see a single positional input argument i which must be of the type int. This function will return the character corresponding to the numeric int in the form of a str. int 37 refers to the single character str "%" for example.

In [9]:
chr(37)
Out[9]:
'%'

Note if we type in the incorrect number of positional input arguments, or use the wrong datatype for each position input argument in a function we will get a TypeError.

In [10]:
chr()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-10-f8697962e2bf> in <module>
----> 1 chr()

TypeError: chr() takes exactly one argument (0 given)
In [11]:
chr("%")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-11-a5e86de87f13> in <module>
----> 1 chr("%")

TypeError: an integer is required (got type str)

The ord function does the reverse conversion of the "chr" function.

In [12]:
? ord
Signature:  ord(c, /)
Docstring: Return the Unicode code point for a one-character string.
Type:      builtin_function_or_method

The function ord input argument expects a single positional input argument the str c which it expects to be a single character str; we can set it to "%" which returns the int 37 as expected.

In [13]:
ord("%")
Out[13]:
37
In [14]:
ord()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-14-321977ffe3d0> in <module>
----> 1 ord()

TypeError: ord() takes exactly one argument (0 given)
In [15]:
ord("%%")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-15-fe91e93fab17> in <module>
----> 1 ord("%%")

TypeError: ord() expected a character, but string of length 2 found
In [16]:
ord(37)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-16-d8d036eaaaa9> in <module>
----> 1 ord(37)

TypeError: ord() expected string of length 1, but int found

The function round can be used to round a float to a specified number of decimal places. Examining the docstring we see one positional input argument number which is designed to be of the data type float and one keyword input argument ndigits=None. These input arguments are seperated by a comma , delimiter.

In [17]:
? round
Signature:  round(number, ndigits=None)
Docstring:
Round a number to a given precision in decimal digits.

The return value is an integer if ndigits is omitted or None.  Otherwise
the return value has the same type as the number.  ndigits may be negative.
Type:      builtin_function_or_method

We can call the function without the keyword input argument specified. When we do so it will take on its default value which is "None" i.e. no decimal places and return the int 3.

In [18]:
round(3.14)
Out[18]:
3

Alternatively to round to 1 decimal place, we can explicitly provide the keyword input argument ndigits and assign it to 1 which will return the float 3.1 as expected.

In [19]:
round(3.14, ndigits=1)
Out[19]:
3.1

Note that we have to always specify the positional input arguments in order. If we are going to specify all the input arguments (positional and keyword), we can treat the keyword arguments as positional arguments. When we only want to specify a subset of keyword input arguments, we must explictly use the name of the keyword input argument and the assignment operator to the value to prevent confusion.

In [20]:
round(3.14, 1)
Out[20]:
3.1

The pow function is used to exponentiate a base using an exponent. We see that it has 2 positional input arguments, the base and exp and one optional keyword input argument mod which has a default value of None.

In [21]:
? pow
Signature:  pow(base, exp, mod=None)
Docstring:
Equivalent to base**exp with 2 arguments or base**exp % mod with 3 arguments

Some types, such as ints, are able to use a more efficient algorithm when
invoked using the three argument form.
Type:      builtin_function_or_method

Supposing we want 2 to the power of 3, we can use:

In [22]:
pow(2, 3)
Out[22]:
8

This gives the same result as:

In [23]:
2 ** 3
Out[23]:
8

Now supposing we want to calculate the modulus of this number and 3. The modulus is the number of times a number fully divides into a value returning an int.

In [24]:
pow(2, 3, mod=3)
Out[24]:
2

This is equivalent to using the exponentiation operator * and then modulus operator % but the pow function has a more efficient algorithm for calculating this value in a single step.

In [25]:
2 ** 3 % 3
Out[25]:
2

creating a custom function

To create a custom function we must use the format:

In [ ]:
def function_name(arg0, arg1, kwarg2=default2, kwarg3=default3, ...):
    """
    Docstring for the function.
    """
    code belonging to function ...
    return output


code not belonging to the function ...

The function name, function_name in this case is typically snake_case i.e. lower case and each space in the name is replaced by an underscore. Numbers can be added to the function name but the function_name cannot begin with a number.

Parenthesis ( ) are used to enclose the input arguments, with a comma , used as a delimiter between the input arguments. Keyword input arguments are listed after positional input arguments and assigned a default value. Note it is possible to create a function without any input arguments.

After the parenthesis a colon : is used which denotes the beginning of a Python code block. Anything belonging to the code block is indented by four spaces.

Above the code the """ to """ allows a multiple line comment which can be used to make a docstring. Note the docstring is indented in line with the code.

The code in the code block ends when the indentation ends. It is good practice to end the function with 2 blank lines so it is obvious that this is the end of the definition of the function.

Functions can optionally have a return statement which can be used to return an output when the function is called and assigned to a variable.

Let's define a function greeting, this function will have no input arguments and no output value to return. All it will do is print the str "hello" to the output of a cell.

In [26]:
def greeting():
    """
    This function will print the str hello.
    """
    print("hello")
    
    

When this function is called without parenthesis, we will just be informed that it is a function.

In [27]:
greeting
Out[27]:
<function __main__.greeting()>

To view the functions document string as a tooltip we can type in the functions name followed by a shift and tab. Alternatively we can output the docstring to a cell by use of the ?.

In [28]:
? greeting
Signature:  greeting()
Docstring: This function will print the str hello.
File:      c:\users\phili\documents\<ipython-input-26-6fe8c53cb452>
Type:      function

We can call this function using parenthesis which will print the greeting "hello".

In [29]:
greeting()
hello

Note that using a print statement in a function is not the same as having a function with a return output. We can see this by calling the function and assigning it to a variable x.

In [30]:
x = greeting()
hello

If we attempt to look at the output of x, we see nothing returned.

In [31]:
x

If we look at the type of x we see that it is an instance of the class NoneType.

In [32]:
type(x)
Out[32]:
NoneType

Let's define a similar function that returns the str hello instead of printing it.

In [33]:
def greeting_2():
    """
    This function will return the str hello.
    """
    return "hello"
    
    

When we call the function in a cell, without assigning it to a value, the return value gets printed and the function appears to behave in the same manner.

In [34]:
greeting_2()
Out[34]:
'hello'

Notice the difference when we assign the output to a variable of object name y. The value is stored with the variable y and not printed.

In [35]:
y = greeting_2()

Now the variable y can be output to a cell and displays the value it was assigned, the str``hello```.

In [36]:
y
Out[36]:
'hello'
In [37]:
type(y)
Out[37]:
str

Let's create a function to calculate a persons body mass index, abbreviated bmi. This function will have two positional input arguments, the weight in kg and the height in cm. It will return one value, the persons bmi.

In [38]:
def bmi_calc(weight_kg, height_cm):
    """
    Calculates the bmi of a person rounded to 1 decimal place.
    height_cm is the persons height in cm
    weight_kg is the persons weight in kg
    """
    bmi = weight_kg / ( (height_cm / 100) ** 2)
    bmi = round(bmi, ndigits=1)
    return bmi

Now let's go to use this function. To view the functions document string as a tooltip we can type in the functions name followed by a shift and tab. Alternatively we can output the docstring to a cell by use of the ?.

In [39]:
? bmi_calc
Signature:  bmi_calc(weight_kg, height_cm)
Docstring:
Calculates the bmi of a person rounded to 1 decimal place.
height_cm is the persons height in cm
weight_kg is the persons weight in kg
File:      c:\users\phili\documents\<ipython-input-38-49e66cb0286d>
Type:      function
In [40]:
bmi_calc(72, 167)
Out[40]:
25.8

Now let's create a unit conversion function, which converts the length in inches to the length in cm. This function will have one keyword input argument which has a default value of 1 and a single return value which is the length in cm.

In [41]:
def inch2cm(inches=1):
    """
    Unit conversion from inches to cm.
    By default 1 inch will be converted into cm.
    """
    cms = inches * 2.54
    return cms

Let's now go to use this function. To view the functions document string as a tooltip we can type in the functions name followed by a shift and tab. Alternatively we can output the docstring to a cell by use of the ?.

In [42]:
? inch2cm
Signature:  inch2cm(inches=1)
Docstring:
Unit conversion from inches to cm.
By default 1 inch will be converted into cm.
File:      c:\users\phili\documents\<ipython-input-41-4329fd457ddf>
Type:      function

Let's convert 1 inch to cm.

In [43]:
inch2cm()
Out[43]:
2.54
In [44]:
inch2cm(1)
Out[44]:
2.54
In [45]:
inch2cm(inches=1)
Out[45]:
2.54

Now let's convert 10 inches to cm.

In [46]:
inch2cm(10)
Out[46]:
25.4
In [47]:
inch2cm(inches=10)
Out[47]:
25.4

*args and **kwargs

So far we have looked at a fixed number of positional input arguments and a fixed number of keyword input arguments. Sometimes we want more flexibility in our functions. For example if we look at the print function docstring we see that the positional input arguments are value, ... which indicates the fact that we can provide one or multiple values for the positional input arguments.

In [48]:
? print
Docstring:
print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)

Prints the values to a stream, or to sys.stdout by default.
Optional keyword arguments:
file:  a file-like object (stream); defaults to the current sys.stdout.
sep:   string inserted between values, default a space.
end:   string appended after the last value, default a newline.
flush: whether to forcibly flush the stream.
Type:      builtin_function_or_method
In [49]:
print("apple")
apple
In [50]:
print("apple", "banana")
apple banana
In [51]:
print("apple", "banana", "melon")
apple banana melon

The keyword input argument sep can be used to control what is printed as a seperator between each value addded and has a default value of a space " " and end can be used to control what is pritned at the end of the print statement and has a default value of "\n" which is an escape character meaning new line.

Let's have a look at creating our own function that prints out each value input on a new line. To do this we will use a for loop.

The for loop is indented by four spaces as it belongs to the code block of the function.

Recall that the top line def varying_args(*args): ends in a colon : designating the beginning of a code block.

The for loop also ends in a colon : designating the beginning of a for loop code block. Code belonging to this is indented by 8 spaces (4 indicating that the code in the for loop belongs to the function and a further 4 indicating that the code belongs to the for loop code block).

In [52]:
def varying_args(*args):
    """
    This will print each positional input argument on a new line.
    """
    for value in args:
        print(value)
        
        
In [53]:
varying_args(1)
1
In [54]:
varying_args(1, 2)
1
2
In [55]:
varying_args(1, 2, 3)
1
2
3

Alternatively to allow for multiple keyword input arguments we can use **kwargs.

Iterating through this is the same as iterating through a dict. Recall that a dict uses { } to enclose its number of key:value pairs which are seperated by use of a comma , delimiter.

In [56]:
d1 = {"key0":0,
     "key1":1,
     "key2":2,}
In [57]:
d1 = {"key0":0,"key1":1,"key2":2}

To iterate through a dict we need to use the dictionary method items. We will explorer the use of methods in more detail shortly but for now consider it as a function called from an object. This particular function items() has no input arguments. The output of this is essentially a list of tuples, where each tuple consists of the key and value.

In [58]:
d1.items()
Out[58]:
dict_items([('key0', 0), ('key1', 1), ('key2', 2)])

We can loop through each tuple usign a for loop much in the same manner as before. Let's create a function that prints each key and value pair on a new line for a variable number of keyword arguments.

In [59]:
def varying_kwargs(**kwargs):
    """
    This will print each keyword input argument pair on a new line.
    """
    for (key,value) in kwargs.items():
        print(key,value)
        
        
In [60]:
varying_kwargs(a=1)
a 1
In [61]:
varying_kwargs(a=1, b=2)
a 1
b 2
In [62]:
varying_kwargs(a=1, b=2, c=3)
a 1
b 2
c 3

This kind of functionality is useful if we want to create a summation function that returns the sum of all the input arguments.

In [63]:
def summation(*args):
    """
    Returns the sum of all positional input arguments.
    """
    input_sum = 0
    for arg in args:
        input_sum += arg
    return input_sum

        
In [64]:
summation(1, 2, 3)
Out[64]:
6

In some cases we can set up our function to behave different depending on the number of input arguments provided by the user. The code can be directed by using the inbuilt len function to determine the number of input arguments alongside if, elif and else blocks. We can update the function varying_args to inform the user they have provided no positional input arguments.

Once again pay attention to the colons : which designate the beginning of a new code block and indentations by multiples of 4 spaces.

In [65]:
def varying_args(*args):
    """
    This will print each positional input argument on a new line or if no positional input arguments are provided.
    Inform the user that no positional input arguments were provided.
    """
    if len(args) == 0:
        print("You have provided no positional input arguments")
    else:
        for arg in args:
            print(arg)
            
            
In [66]:
varying_args()
You have provided no positional input arguments
In [67]:
varying_args(1)
1
In [68]:
varying_args(1, 2)
1
2
In [69]:
varying_args(1, 2, 3)
1
2
3

*args and **kwargs can also be used within a function to provide all positional input arguments in the form of a tuple and all keyword arguments in the form of a dictionary. Recall the function bmi_calc that we previously defined. We can see that it requires 2 positional input arguments.

In [70]:
? bmi_calc
Signature:  bmi_calc(weight_kg, height_cm)
Docstring:
Calculates the bmi of a person rounded to 1 decimal place.
height_cm is the persons height in cm
weight_kg is the persons weight in kg
File:      c:\users\phili\documents\<ipython-input-38-49e66cb0286d>
Type:      function

We can provide them in the form of a tuple.

In [71]:
metrics = (72, 167)
In [72]:
bmi_calc(*metrics)
Out[72]:
25.8

Recall the function inch2cm that we previously defined. We can see that it requires 1 keyword input argument inches.

In [73]:
? inch2cm
Signature:  inch2cm(inches=1)
Docstring:
Unit conversion from inches to cm.
By default 1 inch will be converted into cm.
File:      c:\users\phili\documents\<ipython-input-41-4329fd457ddf>
Type:      function

We can create a dictionary with this keyword input argument in the form of a str for the key and our assigned value.

In [74]:
length = {"inches":2}
In [75]:
inch2cm(**length)
Out[75]:
5.08

using inbuilt classes

We can create an instance of the int class num_1 by inputting:

In [76]:
num_1 = 1

More explicitly we can use the int class. Let's have a look at the docstring for the int class. To view the doctring as a tooltip we can type in the class name followed by a shift and tab. Alternatively we can output the docstring to a cell by use of the ?. This is identical to the way we view a docstring of a function.

In [77]:
? int
Init signature:  int(self, /, *args, **kwargs)
Docstring:     
int([x]) -> integer
int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments
are given.  If x is a number, return x.__int__().  For floating point
numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string,
bytes, or bytearray instance representing an integer literal in the
given base.  The literal can be preceded by '+' or '-' and be surrounded
by whitespace.  The base defaults to 10.  Valid bases are 0 and 2-36.
Base 0 means to interpret the base from the string as an integer literal.
>>> int('0b100', base=0)
4
Type:           type
Subclasses:     bool, IntEnum, IntFlag, _NamedIntConstant, Handle

In the docstring we see an init signature (an abbreviation for initialization). The init signature can be thought of as a function that is ran when the class is created.

The init signatures input arguments are self which is a reference to the instance of the class being created. self is not provided by the user as an input argument but automatically assigned from the object name. This will be discussed in more details later.

The init signature also has *args and **kwargs which essentially means it is setup to take in a varying number of positional and input arguments and under the hood we can think of the code beng setup to measure the len of the input arguments and accomodate allowed lengths through an if, elif and else code block.

In [78]:
num_2 = int(1)
In [79]:
num_2
Out[79]:
1
In [80]:
num_3 = int("1", base=10)
In [81]:
num_3
Out[81]:
1
In [82]:
num_4 = int("1", 10)
In [83]:
num_4
Out[83]:
1

Any not allowed combination gives a type error.

In [84]:
num_5 = int("1", 1, 1)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-84-412e4f160310> in <module>
----> 1 num_5 = int("1", 1, 1)

TypeError: int() takes at most 2 arguments (3 given)

Now that an instance is created we can type in the instance name num_1 followed by a dot . and tab to view all the instances, functions and classes which can be called from the instance of the int class.

In [ ]:
num_1.
instances functions
denominator as_integer_ratio
imag bit_length
numerator conjugate
real from_bytes

All the instances and functions available are defined in the int class.

The instances listed are instances of classes (they can be instances of the same class as num_1 i.e. int or of different classes) that can be referenced with respect to the int instance num_1. They are an attribute of this number (which can be thought of as a property of this number). The word attribute is defined as an object which is called with reference to another object. We can think of the dot indexing as object.attribute.

In the case of a int the attribute numerator will correspond to the value of num_1 and therefore return an int with the same value and the attribute denominator will always return an int of the value 1 because an int does not have a fractional component.

In [85]:
num_1.numerator
Out[85]:
1
In [86]:
num_1.denominator
Out[86]:
1

The attributes real will also correspond to the value of num_1 and therefore return an int with the same value. The attribute imag will return an int of the value 0 as an int is not complex.

In [87]:
num_1.real
Out[87]:
1
In [88]:
num_1.imag
Out[88]:
0

Recall that the imaginary component of a number originates from the square root of a negative value.

In [89]:
(-1) ** 0.5
Out[89]:
(6.123233995736766e-17+1j)

The above calculation returns a real component that is a float to the negative 17 decimal place (effectively 0 but non-zero doue to float rounding errors used in the calculation) and an imaginary component of 1j where j denotes an imaginary number.

In the case above all the attributes are also instances of the class int. These in turn can be used to call other attributes and methods.

In [90]:
type(num_1)
Out[90]:
int
In [91]:
type(num_1.real)
Out[91]:
int

we can type in the instance name num_1.real followed by a dot . and tab to view all the instances, functions and classes which can be called from the int instance.

In [92]:
num_1.real.real
Out[92]:
1

We can also have a look at the function conjugate. Attributes generally just read off a value or property, in this case just informing us that conjugate is a function.

In [93]:
num_1.conjugate
Out[93]:
<function int.conjugate>

To use the function we need to provide parenthesis enclosing any input arguments. A function that is called from another object is known as a method.

To view the doctring as a tooltip we can type in the function name (referenced from the object using dot notation) followed by a shift and tab. Alternatively we can output the docstring to a cell by use of the ?.

In [94]:
? num_1.conjugate
Docstring: Returns self, the complex conjugate of any int.
Type:      builtin_function_or_method

In this case we see that there are no input arguments to provide.

In [95]:
num_1.conjugate()
Out[95]:
1
In [96]:
type(num_1.conjugate())
Out[96]:
int

The complex conjugate reverses the sign of the imaginary component. In this case the complex conjugate also returns the int 1 as the imaginary component of an int is zero.

Let's create an instance of the complex class. Here we see we have two keyword input arguments, the real and the imaginary part. Recall that the imaginary part originates the square root of a negative number.

In [97]:
? complex
Init signature:  complex(real=0, imag=0)
Docstring:     
Create a complex number from a real part and an optional imaginary part.

This is equivalent to (real + imag*1j) where imag defaults to 0.
Type:           type
Subclasses:     
In [98]:
num_6 = complex(real=1, imag=2)
In [99]:
num_6
Out[99]:
(1+2j)

We see that num_6 is the type complex as expected.

In [100]:
type(num_6)
Out[100]:
complex

We can also create a complex number directly by using j.

In [101]:
num_7 = 1 + 2j
In [102]:
num_7
Out[102]:
(1+2j)

We can access its list of attributes by typing in the object name followed by a dot and tab.

In [ ]:
num_7.

We see that the complex class has a subset of the attributes defined in the int class.

instances functions
imag conjugate
real
In [103]:
num_7.real
Out[103]:
1.0
In [104]:
num_7.imag
Out[104]:
2.0

We can use the method conjugate to calculate the complex conjugate of the complex instance num_7.

In [105]:
num_7.conjugate()
Out[105]:
(1-2j)

Let's return to using instances of the int class.

In [106]:
num_1 = 1
In [107]:
num_2 = 2

The int class is setup to perform numeric operations with the following operators, +, -, , **, /, //, %, ==, !=, <, <=, >, >=, +=, -=, =, **=, /=, //=, %=.

Let's have a look at the + operator in more detail.

In [108]:
num_1 + num_2
Out[108]:
3

Under the hood the operator actually uses a method, the method is known as a datamodel method or a double underscore (dunder) method as it begins and ends with a double underscore.

In [109]:
num_1.__add__(num_2)
Out[109]:
3

Methods can also be called directly from a class and applied to an instance:

In [110]:
int.__add__(num_1, num_2)
Out[110]:
3

Let's conceptualize the syntax in the above three lines as:

In [ ]:
self + other
In [ ]:
self.__add__(other)
In [ ]:
class.__add__(self, other)

Now let's look at the conjugate method. This can be called from an instance or from the class with an instance provided in parenthesis.

In [111]:
num_1.conjugate()
Out[111]:
1
In [112]:
int.conjugate(num_1)
Out[112]:
1

The two following lines are equivalent. In the top case, the method is called from the class and the instance is supplied in parenthesis.

In [ ]:
class.method(self)

In the bottom case, the method is called from the instance. Therefore the instance is implied.

In [ ]:
self.method()

When creating a class, the word self is used as a placeholder to referring to an arbitrary instance of the class yet to be created. When we initialize the class.

In [113]:
? int
Init signature:  int(self, /, *args, **kwargs)
Docstring:     
int([x]) -> integer
int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments
are given.  If x is a number, return x.__int__().  For floating point
numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string,
bytes, or bytearray instance representing an integer literal in the
given base.  The literal can be preceded by '+' or '-' and be surrounded
by whitespace.  The base defaults to 10.  Valid bases are 0 and 2-36.
Base 0 means to interpret the base from the string as an integer literal.
>>> int('0b100', base=0)
4
Type:           type
Subclasses:     bool, IntEnum, IntFlag, _NamedIntConstant, Handle
In [114]:
num_1 = int(1)

As part of the initialization the object name in this case num_1 replaces the placeholder self.

Behind the scenes during initialization the __init__ datamodel (or dunder) method is called directly from the class. We do not call this method and instead instantiate the class using the procedure above however it becomes important when creating custom classes.

In [115]:
? int.__init__
Signature:       int.__init__(self, /, *args, **kwargs)
Call signature:  int.__init__(*args, **kwargs)
Type:           wrapper_descriptor
String form:    <slot wrapper '__init__' of 'object' objects>
Namespace:      Python builtin
Docstring:      Initialize self.  See help(type(self)) for accurate signature.

the object class

Everything in object orientated programming is an object. In other words the object class is the basic blueprint of all other classes.

In [116]:
? object
Init signature:  object()
Docstring:     
The base class of the class hierarchy.

When called, it accepts no arguments and returns a new featureless
instance that has no instance attributes and cannot be given any.
Type:           type
Subclasses:     type, weakref, weakcallableproxy, weakproxy, int, bytearray, bytes, list, NoneType, NotImplementedType, ...

Let's create two instances of the object class called obj_1 and obj_2 respectively.

In [117]:
obj_1 = object()
In [118]:
obj_2 = object()

Notice that we can use the function dir an abbreviation for directory listings to view all the attributes of an instance of the object class.

In [119]:
dir(obj_1)
Out[119]:
['__class__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__']

The reason we can use this is because the datamodel method __dir__ has been defined. We can also use the functions repr and print because the datamodel methods __repr__ and __str__ have been defined.

In [120]:
repr(obj_1)
Out[120]:
'<object object at 0x0000019D3F4D43F0>'
In [121]:
print(obj_2)
<object object at 0x0000019D3F4D4540>

We can also check whether obj_1 and obj_2 are equal as the datamodel method __eq__ is defined.

In [122]:
obj_1 == obj_2
Out[122]:
False
In [123]:
obj_1 == obj_1
Out[123]:
True

Note however that we cannot perform addition because there has been no definition for the datamodel method __add__.

In [124]:
obj_1 + obj_2
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-124-e72bbf020931> in <module>
----> 1 obj_1 + obj_2

TypeError: unsupported operand type(s) for +: 'object' and 'object'
In [125]:
obj_1.__add__(obj_2)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-125-852fde4d200e> in <module>
----> 1 obj_1.__add__(obj_2)

AttributeError: 'object' object has no attribute '__add__'

creating a custom class

We can now look at defining our own class.

To do so we use the keyword class.

Then we provide a class name. The convention is to use CamelCase capitalization for user created classes with lower case class names being reserved for the inbuilt classes such as int and str. This makes it easier to distinguish the former from the latter.

Parenthesis is used to enclose the parent classes. In Python every class is a modified object class and if no parent class is defined, the parent class is assumed to be object. The colon is used to begin a code block.

Each class will contain some functions which are defined in an identical manner as before. However as these functions belong to the class, they are indented by 1 level (4 spaces) and code belonging to the function is indented by 8 spaces (2 levels).

In [126]:
class ClassName(object):
    """Test ClassName"""
    
    
    def greeting():
        """Greeting 1"""
        print("Hello")
        
        
    def greeting_2():
        """Greeting 2"""
        print("Have a nice day")

Notice the CustomClass has no init signature.

In [127]:
? ClassName
Init signature:  ClassName()
Docstring:      Test ClassName
Type:           type
Subclasses:     

The __init__ datamodel method can be defined. This will be carried out when we instantiate an instance of the class. Let's just copy the class and rename the function greeting to __init__.

In [128]:
class ClassName2(object):
    """Test ClassName"""
    
    
    def __init__():
        """Greeting 1"""
        print("Hello")
        
        
    def greeting_2():
        """Greeting 2"""
        print("Have a nice day")
In [129]:
? ClassName2
Init signature:  ClassName2()
Docstring:      Test ClassName
Init docstring: Greeting 1
Type:           type
Subclasses:     

Now, let's instantiate an instance of each class ClassName and ClassName2. The instance ins_1 of ClassName initates and nothing is shown in the cell output but the instance ins_2 fails to instantiate and instead returns a TypeError. We are informed 1 positional input argument was provided but the __init__ signature defiined takes 0 input arguments.

In [130]:
ins_1 = ClassName()
In [131]:
ins_2 = ClassName2()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-131-04d6ec52b3ff> in <module>
----> 1 ins_2 = ClassName2()

TypeError: __init__() takes 0 positional arguments but 1 was given

This is because we have provided an instance name ins_2 but failed to account for it in the definition of the __init__ method. We can create a new version of this class with the 0th positional nput argument in each of the methods being self.

In [132]:
class ClassName3(object):
    """Test ClassName"""
    
    
    def __init__(self):
        """Greeting 1"""
        print("Hello")
        
        
    def greeting_2(self):
        """Greeting 2"""
        print("Have a nice day")


        

Now we can instantiate an instance from this new class and the code in the __init__ datamodel method is carried out.

In [133]:
ins_3 = ClassName3()
Hello

We can also call our method greeting_2 from the instance inst_3 using the dot notation.

In [134]:
ins_3.greeting_2()
Have a nice day

Attributes can be created by indexing into the instance name with a new object name and assigning it to a new value.

In [135]:
ins_3.attribute = 1
In [136]:
ins_3.attribute
Out[136]:
1

In this case the attribute is unique to the instance and other instances of the same class will not have this attribute unless it is also manually assigned.

In [137]:
ins_3_b = ClassName3()
Hello
In [138]:
ins_3_b.attribute
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-138-f69f54665805> in <module>
----> 1 ins_3_b.attribute

AttributeError: 'ClassName3' object has no attribute 'attribute'

We can also create an attribute within a method by indexing into self when defining the class. We need the 0th positional input argument in the attribute to be self, so we can select the instance self.

In [139]:
class ClassName4(object):
    """Test ClassName"""
    
    
    def __init__(self):
        """Greeting 1"""
        print("Hello")
        
    
    def create_attribute(self):
        """Create an Attribute"""
        self.attribute = 1    
        
        
    def greeting_2(self):
        """Greeting 2"""
        print("Have a nice day")


        

We can instantiate the class.

In [140]:
ins_4 = ClassName4()
Hello

If we try and access the attribute, we get an AttributeError informing us that the attribute doesn't exist.

In [141]:
ins_4.attribute
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-141-68a483dffa26> in <module>
----> 1 ins_4.attribute

AttributeError: 'ClassName4' object has no attribute 'attribute'

If we call our method create_attribute from the instance, this will create the attribute.

In [142]:
ins_4.create_attribute()

Now we can access it without issue.

In [143]:
ins_4.attribute
Out[143]:
1

The __init__ method is called during initialization, it can be used to create attributes directly during initialization and to call other methods.

In [144]:
class ClassName5(object):
    """Test ClassName"""
    
    
    def __init__(self):
        """Greeting 1 and attribute creation"""
        print("Hello")
        self.attribute_1 = 1
        self.create_attribute()
        
    
    def create_attribute(self):
        """Create an Attribute"""
        self.attribute_2 = 2    
        
        
    def greeting_2(self):
        """Greeting 2"""
        print("Have a nice day")


        
In [145]:
ins_5 = ClassName5()
Hello

These attribute created in the __init__ can be accessed directly and the attribute created in the method create_attribute called during initialization can also be accessed directly.

In [146]:
ins_5.attribute_1
Out[146]:
1
In [147]:
ins_5.attribute_2
Out[147]:
2

These attributes will be the same for any instance of the class.

In [148]:
ins_5_b = ClassName5()
Hello
In [149]:
ins_5_b.attribute_1
Out[149]:
1
In [150]:
ins_5_b.attribute_2
Out[150]:
2

We can create instance specific attributes during initialization by requesting information from the user when they use the __init__ datamodel method.

Let's assume we are wanting to create a class of students, we can request the students name and students number using the student_name and student_phone_number positional input arguments. The docstring provides the format both of these pieces of information should be laid out when instantiating the class.

We can then assign this information as attributes. The attributes can have any object name but it is common to use the same name as the positional input argument so the code is easier to follow.

In [151]:
class Student(object):
    """A class of students"""
    
    
    def __init__(self, student_name, student_phone_number):
        """
        student_first_name should be a str in the format: "First Last"
        student_number should be a str in the format: "+ww xxx yyy zzzz"
        """
        self.student_name = student_name
        self.student_phone_number = student_phone_number
       
    
    def get_student_name(self):
        """Returns the student name"""
        return self.student_name
        

    def get_student_phone_number(self):
        """Returns the student phone number"""
        return self.student_phone_number    
    
    

Let's now look at our docstring.

In [152]:
? Student
Init signature:  Student(student_name, student_phone_number)
Docstring:      A class of students
Init docstring:
student_first_name should be a str in the format: "First Last"
student_number should be a str in the format: "+ww xxx yyy zzzz"
Type:           type
Subclasses:     

And initiate our student lucie.

In [153]:
lucie = Student("Lucie McDonald", "+44 123 123 1234")

We can use the methods to get Lucies name and number.

In [154]:
lucie.get_student_name()
Out[154]:
'Lucie McDonald'
In [155]:
lucie.get_student_phone_number()
Out[155]:
'+44 123 123 1234'

In the above code we have not provided any restrictions on the data that can be input for each positional input argument when using the __init__ method and the attributes can be accessed directly.

In [156]:
lucie.student_name
Out[156]:
'Lucie McDonald'
In [157]:
lucie.student_phone_number
Out[157]:
'+44 123 123 1234'

It is common to assert data to be within a specific format when using the __init__ signature and create hidden attributes. attributes beginning with an underscore are hidden.

In [158]:
class Student(object):
    """A class of students"""
    
    
    def __init__(self, student_name, student_phone_number):
        """
        student_first_name should be a str in the format: "First Last"
        student_number should be a str in the format: "+ww xxx yyy zzzz"
        """
        assert type(student_name)==str
        assert type(student_phone_number)==str
        self._student_name = student_name
        self._student_phone_number = student_phone_number
       
    
    def get_student_name(self):
        """Returns the student name"""
        return self._student_name
        

    def get_student_phone_number(self):
        """Returns the student phone number"""
        return self._student_phone_number    
    

    def set_student_phone_number(self , student_phone_number):
        """
        student_number should be a str in the format: "+ww xxx yyy zzzz"
        """
        assert type(student_phone_number)==str
        self._student_phone_number = student_phone_number
        
        

Now when the number is given in the wrong format, e.g. int instead of str, an AssertionError will be flagged up.

In [159]:
lucie = Student("Lucie McDonald", 121231234)
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-159-ca7db4d8d9d3> in <module>
----> 1 lucie = Student("Lucie McDonald", 121231234)

<ipython-input-158-ed10c2367667> in __init__(self, student_name, student_phone_number)
      9         """
     10         assert type(student_name)==str
---> 11         assert type(student_phone_number)==str
     12         self._student_name = student_name
     13         self._student_phone_number = student_phone_number

AssertionError: 
In [160]:
lucie = Student("Lucie McDonald", "+44 123 123 1234")

The function repr displays a formal representation of the object in a str format and print displays informat representation of the object in str format. Often both are set to be the same but repr is sometimes used to give additional information.

Notice that when we attempt to print lucie we get the following memory reference. We can change how this displays by defining out own __str__ datamodel method.

In [161]:
print(lucie)
<__main__.Student object at 0x0000019D3F754CA0>
In [162]:
repr(lucie)
Out[162]:
'<__main__.Student object at 0x0000019D3F754CA0>'
In [163]:
class Student(object):
    """A class of students"""
    
    
    def __init__(self, student_name, student_phone_number):
        """
        student_first_name should be a str in the format: "First Last"
        student_number should be a str in the format: "+ww xxx yyy zzzz"
        """
        assert type(student_name)==str
        assert type(student_phone_number)==str
        self._student_name = student_name
        self._student_phone_number = student_phone_number
       
    
    def get_student_name(self):
        """Returns the student name"""
        return self._student_name
        

    def get_student_phone_number(self):
        """Returns the student phone number"""
        return self._student_phone_number    
    

    def set_student_phone_number(self , student_phone_number):
        """
        student_number should be a str in the format: "+ww xxx yyy zzzz"
        """
        assert type(student_phone_number)==str
        self._student_phone_number = student_phone_number
    
    
    def __str__(self):
        string = f"Student Name: {self._student_name} Student Phone Number: {self._student_phone_number}"
        return string
   

    def __repr__(self):
        string = f"Instance of Student class. Student Name: {self._student_name} Student Phone Number: {self._student_phone_number}"
        return string
     
In [164]:
lucie = Student("Lucie McDonald", "+44 123 123 1234")
In [165]:
print(lucie)
Student Name: Lucie McDonald Student Phone Number: +44 123 123 1234
In [166]:
repr(lucie)
Out[166]:
'Instance of Student class. Student Name: Lucie McDonald Student Phone Number: +44 123 123 1234'