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

(projects:python:vector-multiplication)=
# Vector Multiplication

The aim of this project is to develop a class (object type) for 3-dimensional vectors. The class shall provide typical calculations with vectors as Python operators. Before working through this project you should have read [](python:variables) chapter. For a recap of vector operations see [](math:linalg:vectors).

This project consolidates your knowledge on defining custom object types and demonstrates the possibilities of Python's flexible approach to customization of operator's behavior.

## Class Definition and String Representation

We start with a minimal working example and then add more functionality step by step.

**Task:** Create a class `Vector` representing a vector in 3-dimensional space. The `__init__` method takes the 3 components as arguments.

**Solution:**

```{code-cell} ipython3
# your solution
```

**Task:** Add dunder functions `__str__` and `__repr__` returning human readable representations of a `Vector` object. Test your code by creating and printing a `Vector` object.

**Solution:**

```{code-cell} ipython3
# your test code
```

## Addition

When implementing operations on custom objects we have to decide whether to modify an existing object or to create a new object holding the operation's result. For vectors, `a + b` shouldn't modify `a`, but return a new object.

**Task:** Implement vector addition with `+` operator. The operation should return a new object holding the result. If the second operand is not of type `Vector`, addition should return `NotImplemented`. At this point we do not need `__radd__` because we only accept `Vector` objects for addition. So the left-hand side operand will always be a `Vector` object and Python will call its `__add__` method. Test your code!

**Solution:**

```{code-cell} ipython3
# your test code
```

**Task:** Modify your implementation of addition to accept lists of length 3, too. Don't forget to implement `__radd__` now. Else list plus vector won't work, because the left-hand side `list` object doesn't know how to work with `Vector` objects. Thus, Python tries to call `__radd__` on the right-hand side operand. As always: test your code!

**Solution:**

```{code-cell} ipython3
# your test code
```

## Multiplication by Scalar

**Task:** Implement multiplication of vectors by scalars via `*`. Both variants, scalar times vector and vector times scalar, should be supported.

**Solution:**

```{code-cell} ipython3
# your test code
```

## Inner Products

Now we run out of operators. Of course, we could `*` again for inner products and check types to decide whether we have to do multiplication by scalars or to compute an inner product. The more readable alternative is implementing inner products without an operator.

**Task:** Implement a method `inner` taking a `Vector` or a `list` object as argument and returning the inner product with the vector whose `inner` method has been called. If the argument neither is a vector nor a 3 element list, return `NotImplemented` although the Python intepreter does not care about (because `inner` is a usual method, not an operator). If you know about Python's exception handling mechanism, you should raise `NotImplementedError` here.

**Solution:**

```{code-cell} ipython3
# your test code
```

## Outer Products

**Task:** In analogy to `inner` implement a method `outer` returning the outer product of two vectors.

**Solution:**

```{code-cell} ipython3
# your test code
```

## Equality

At the moment `==` behaves like `is`. But we want to make `==` compare vectors componentwise.

**Task:** Implement equality test via `==` between two `Vector` objects and a vector and a list. If the second operand is of incorrect type both operands are considered unequal.

**Solution:**

```{code-cell} ipython3
# your test code
```

## List-Like Indexing

There's a dunder method `__getitem__` which is called by the Python interpreter whenever indexing syntax `[...]` is applied to an object. The index is passed to the method and the method is expected to return the corresponding item.

**Task:** Implement `__getitem__`. For invalid indices return `None`. If you know about Python's exception handling mechanism, you should raise `IndexError` here.

**Solution:**

```{code-cell} ipython3
# your test code
```

**Task:** Make the `len` function work on `Vector` objects. Return value should be 3.

**Solution:**

```{code-cell} ipython3
# your test code
```
