6. OOP I: Objects and Methods#

6.1. Overview#

The traditional programming paradigm (think Fortran, C, MATLAB, etc.) is called procedural.

It works as follows

  • The program has a state corresponding to the values of its variables.

  • Functions are called to act on and transform the state.

  • Final outputs are produced via a sequence of function calls.

Two other important paradigms are object-oriented programming (OOP) and functional programming.

In the OOP paradigm, data and functions are bundled together into “objects” — and functions in this context are referred to as methods.

Methods are called on to transform the data contained in the object.

  • Think of a Python list that contains data and has methods such as append() and pop() that transform the data.

Functional programming languages are built on the idea of composing functions.

So which of these categories does Python fit into?. Actually Python is a pragmatic language that blends object-oriented, functional and procedural styles, rather than taking a purist approach.

On one hand, this allows Python and its users to cherry pick nice aspects of different paradigms.

On the other hand, the lack of purity might at times lead to some confusion.

Fortunately this confusion is minimized if you understand that, at a foundational level, Python is object-oriented.

By this we mean that, in Python, everything is an object.

In this lecture, we explain what that statement means and why it matters.

6.2. Objects#

In Python, an object is a collection of data and instructions held in computer memory that consists of

  1. a type

  2. a unique identity

  3. data (i.e., content)

  4. methods

These concepts are defined and discussed sequentially below.

6.2.1. Type#

Python provides for different types of objects, to accommodate different categories of data.

For example

s = 'This is a string'
type(s)
str
x = 42   # Now let's create an integer
type(x)
int

The type of an object matters for many expressions.

For example, the addition operator between two strings means concatenation

'300' + 'cc'
'300cc'

On the other hand, between two numbers it means ordinary addition

300 + 400
700

Consider the following expression

'300' + 400
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[5], line 1
----> 1 '300' + 400

TypeError: can only concatenate str (not "int") to str

Here we are mixing types, and it’s unclear to Python whether the user wants to

  • convert '300' to an integer and then add it to 400, or

  • convert 400 to string and then concatenate it with '300'

Some languages might try to guess but Python is strongly typed

  • Type is important, and implicit type conversion is rare.

  • Python will respond instead by raising a TypeError.

To avoid the error, you need to clarify by changing the relevant type.

For example,

int('300') + 400   # To add as numbers, change the string to an integer
700

6.2.2. Identity#

In Python, each object has a unique identifier, which helps Python (and us) keep track of the object.

The identity of an object can be obtained via the id() function

y = 2.5
z = 2.5
id(y)
2091851504336
id(z)
2091850048784

In this example, y and z happen to have the same value (i.e., 2.5), but they are not the same object.

The identity of an object is in fact just the address of the object in memory.

6.2.3. Object Content: Data and Attributes#

If we set x = 42 then we create an object of type int that contains the data 42.

In fact, it contains more, as the following example shows

x = 42
x
42
x.imag
0
x.__class__
int

When Python creates this integer object, it stores with it various auxiliary information, such as the imaginary part, and the type.

Any name following a dot is called an attribute of the object to the left of the dot.

  • e.g.,imag and __class__ are attributes of x.

We see from this example that objects have attributes that contain auxiliary information.

They also have attributes that act like functions, called methods.

These attributes are important, so let’s discuss them in-depth.

6.2.4. Methods#

Methods are functions that are bundled with objects.

Formally, methods are attributes of objects that are callable – i.e., attributes that can be called as functions

x = ['foo', 'bar']
callable(x.append)
True
callable(x.__doc__)
False

Methods typically act on the data contained in the object they belong to, or combine that data with other data

x = ['a', 'b']
x.append('c')
s = 'This is a string'
s.upper()
'THIS IS A STRING'
s.lower()
'this is a string'
s.replace('This', 'That')
'That is a string'

A great deal of Python functionality is organized around method calls.

For example, consider the following piece of code

x = ['a', 'b']
x[0] = 'aa'  # Item assignment using square bracket notation
x
['aa', 'b']

It doesn’t look like there are any methods used here, but in fact the square bracket assignment notation is just a convenient interface to a method call.

What actually happens is that Python calls the __setitem__ method, as follows

x = ['a', 'b']
x.__setitem__(0, 'aa')  # Equivalent to x[0] = 'aa'
x
['aa', 'b']

(If you wanted to you could modify the __setitem__ method, so that square bracket assignment does something totally different)

6.3. A Little Mystery#

In this lecture we claimed that Python is, at heart, an object oriented language.

But here’s an example that looks more procedural.

x = ['a', 'b']
m = len(x)
m
2

If Python is object oriented, why don’t we use x.len()?

The answer is related to the fact that Python aims for readability and consistent style.

In Python, it is common for users to build custom objects. It’s quite common for users to add methods to their that measure the length of the object, suitably defined.

When naming such a method, natural choices are len() and length().

If some users choose len() and others choose length(), then the style will be inconsistent and harder to remember.

To avoid this, the creator of Python chose to add len() as a built-in function, to help emphasize that len() is the convention.

Now, having said all of this, Python is still object oriented under the hood.

In fact, the list x discussed above has a method called __len__().

All that the function len() does is call this method.

In other words, the following code is equivalent:

x = ['a', 'b']
len(x)
2

and

x = ['a', 'b']
x.__len__()
2

6.4. Summary#

The message in this lecture is clear:

  • In Python, everything in memory is treated as an object.

This includes not just lists, strings, etc., but also less obvious things, such as

  • functions (once they have been read into memory)

  • modules (ditto)

  • files opened for reading or writing

  • integers, etc.

Remember that everything is an object will help you interact with your programs and write clear Pythonic code.

6.5. Example#

We know the boolean data type and we want to print a list of methods of the boolean object True.

To this end, we can use callable() to test whether an attribute of an object can be called as a function

Solution

Firstly, we need to find all attributes of True, which can be done via

print(sorted(True.__dir__()))
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_count', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'is_integer', 'numerator', 'real', 'to_bytes']

or

print(sorted(dir(True)))
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_count', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'is_integer', 'numerator', 'real', 'to_bytes']

Since the boolean data type is a primitive type, you can also find it in the built-in namespace

print(dir(__builtins__.bool))
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_count', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'is_integer', 'numerator', 'real', 'to_bytes']

Here we use a for loop to filter out attributes that are callable

attributes = dir(__builtins__.bool)
callablels = []

for attribute in attributes:
  # Use eval() to evaluate a string as an expression
  if callable(eval(f'True.{attribute}')):
    callablels.append(attribute)
print(callablels)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_count', 'bit_length', 'conjugate', 'from_bytes', 'is_integer', 'to_bytes']