---
jupytext:
  formats: content_md///md:myst,content///ipynb
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.13.8
kernelspec:
  display_name: Python 3 (ipykernel)
  language: python
  name: python3
---

(python:crash:everything-object)=
# Everything is an Object

The Python programming language follows some basic design principles, which make the language very clear and in some sense esthetic. One such principle is '**Everything is an object**'.

A Python object is a collection of variables and functions. We may use objects to bring structure into a program's variables and functions. Each object is of a certain type. An object's type specifies a minimum list of variables and functions an object contains or provides. Variables and functions belonging to an object are called *member variables* and *member functions*. A synonym for member function is *method*.

## Object-Oriented Programming

Large programs have lots of variables and functions. By object-oriented programming we denote an approach to structure the morass of variables and functions into objects. Python supports this programming style.

For the moment we look at objects as containers for structuring source code. Later on we will discuss more advanced features of object-oriented programming like defining hierarchies of types (to untangle the morass of types...).

### Example

Think of a Python program which does some geometrical computations and then plots a line and a circle. To specify geometric properties we could use variables
* `line_start_x`,
* `line_start_y`,
* `line_end_x`,
* `line_end_y`,
* `circle_x`,
* `circle_y`,
* `circle_radius`.

In addition, we could have functions
* `draw_line`,
* `draw_circle`,
* `move_line`,
* `move_circle`,
* `rotate_line`,

and so on.

Utilizing the idea of objects we could introduce objects of type `Point` with member variables `x` and `y` as well as an object of type `Line` with member variables `start` and `end` both of type `Point`. Analogously, an object of type `Circle` with member variables `center` (of type `Point`) and `radius` would be nice. Both the `Line` object and the `Circle` object could have member functions `draw` and `move`.

The object hierarchy could look like this:
* `my_line`
  * `start`
    * `x`
    * `y`
  * `end`
    * `x`
    * `y`
  * `draw()`
  * `move(...)`
  * `rotate(...)`
* `my_circle`
  * `center`
    * `x`
    * `y`
  * `radius`
  * `draw()`
  * `move(...)`

### Objects Everywhere
  
Surprisingly, in Python there are no variables which are not an object. Even integers are objects. Most other object-oriented programming languages have some fundamental data types (integers, floats, characters) not represented as objects in the sense of object-oriented programming. In Python there are no such fundamental types!

## Accessing an Object's Members

To get a list of all members (variables and functions) of an object, Python has the built-in function `dir`.

```{code-cell} ipython3
a = 2

dir(a)
```

```{note}
The `dir` function returns a list of strings. Since it's the last line of a cell, this list is printed to screen by JupyterLab automatically.
```

Most of all these members won't be used directly. The `__add__` function, for instance, is called by the Python interpreter whenever we add integers. The following line of code is equivalent to `a + 3`:

```{code-cell} ipython3
a.__add__(3)
```

The notation `object.member` is the syntax to access members of an object.

## Getting an Object's Type

Each object has a type. Objects of identical type provide identical functionality. An object's type is returned by Python's built-in function `type`.

```{code-cell} ipython3
type(a)
```

```{note}
As for `dir` above, the `type` function does not print anything. Screen output is done by JupyterLab automatically here. Note that Python's `print` yields `<class 'int'>` here whereas JupyterLab's `display` produces `int` to visualize `type`'s return value.
```

## Really Everything is an Object

Note that the dot syntax `object.member` already appeared when accessing modules: `module.function`. This is not by chance. Everything is an object in Python!

```{code-cell} ipython3
import numpy as np

type(np)
```

Importing a module leads to a new object of type `module` providing all the functionality of the imported module in form of member functions and variables.

```{code-cell} ipython3
import numpy as np

dir(np)
```

If everything is an object in Python, then functions should be objects, too.

```{code-cell} ipython3
def my_function():
    print('Here we could do something useful.')
    
type(my_function)
```

The value returned by `type` should be an object, too.

```{code-cell} ipython3
type(type(my_function))
```

Built-in functions have their own type.

```{code-cell} ipython3
type(print)
```

(python:crash:everything-object:custom-types)=
## Custom Object Types

We may define new objects with the `class` keyword. Instead of 'object type' one often says 'class'. To create a class for describing geometric points we could write

```{code-cell} ipython3
class Point:
    
    def __init__(self, x, y):
        self.x = x
        self.y = y
```

This defines a new object type or class `Point`. Here `__init__` is the only member function. The function with this special name is implicitly called by Python whenever a new `Point` object has been created. This initialization function expects the coordinates of the new point and stores them internally as member variables. The `self` argument provides access to the newly created object.

A class is like a blueprint. But the methods defined in the blueprint are called for concrete objects. The `self` argument gives access to the object for which a method has been called. Remember: there is only one class (blueprint), but there may be many objects of this type.

```{note}
From a technical point of view instead of `self` we may use any name for the first argument (bad practice!). But the first argument of a method always is the object for which the method has been called.
```

```{note}
Methods with two leading and two trailing underscores are called *magic methods* or *dunder methods* (dunder = double underscore). They are called by the Python interpreter for special purposes. We already met another dunder method: `__add__`, which is called whenever the `+` operator is used on an object. In contrast to most other programming languages virtually every operation in Python can be customized by implementing a suitable dunder method.
```

If we write

```{code-cell} ipython3
center = Point(3, 7)
```

the Python interpreter creates a new object of type `Point`. At this moment the new object has only one method, `__init__` and no member variables. Immediately after creation Python calls the object's `__init__` function. The arguments passed to `__init__` are the newly created object, 3 and 7.

Now we can work with our object as expected.

```{code-cell} ipython3
print(center.x)
center.x = center.x + 2
print(center.x)
```

Object info is as follows:

```{code-cell} ipython3
type(center)
```

```{code-cell} ipython3
dir(center)
```

The reason for `__main__` in the object type will be discussed later on. From the member list we see that there are many automatically created members. Some of them will be discussed later on, too.
