---
marp: true
paginate: true
class: invert
# theme: uncover
footer: Tutorium 09 - 16.12.2023 - Nils Pukropp - https://s.narl.io/s/tutorium-09
header:
---

# Tutorium 09

Korrektur 08 - Vererbung, OOP, Datenkapselung

---

# Korrektur 08

---

# Punkteverteilung

![](img/pointdistribution_ex08.png)

---

# Häufige Fehler

* `@dataclass` nicht verwendet
* `__init__` überschrieben, obwohl `@dataclass` das macht und dann `super().__init__()` vergessen
* Kein Polymorphismus verwendet, also Code Duplikate oder auf `self` gematched/`isinstance()` verwendet
* Code nicht getestet, Datei nicht ausführbar => **0 Punkte**!

---

# Musterlösung - Aufgabe 8 a,b)

```py
import math
from dataclasses import dataclass


@dataclass
class Vec2:
    x: float
    y: float

    def abs(self) -> float:
        return math.sqrt(self.x * self.x + self.y * self.y)
```

---

# Musterlösung - Aufgabe 8 c)

```py
@dataclass
class GameObject:
    position: Vec2
    radius: int
    alive: bool
    color: tuple[int, int, int]

@dataclass
class Projectile(GameObject):
    speed: float

@dataclass
class StaticObject(GameObject):
    rotation: float
```

---

# Musterlösung - Aufgabe 8 c)

```py
class Item(StaticObject):
    amount: int

class Ammunition(Item):
    pass

class Health(Item):
    pass

class Ship(GameObject):
    shots: int
    hp: int

class Asteroid(StaticObject):
    special: bool
```

---

# Musterlösung - Aufgabe 8 d)

```python
class GameObject:

    # ...

    def update(self, width: int, height: int, delta: float):
        if not (0 <= self.position.x < width and 0 <= self.position.y < height):
            self.alive = False

class Projectile(GameObject):
    speed: float

    def update(self, width: int, height: int, delta: float):
        self.position.y -= delta * self.speed
        super().update(width, height, delta)
```

---

# Musterlösung - Aufgabe 8 d)

```python
class StaticObject(GameObject):
    rotation: float

    def update(self, width: int, height: int, delta: float):
        self.position.y += delta
        self.rotation += delta / self.radius
        super().update(width, height, delta)

class Ship(GameObject):
    shots: int
    hp: int

    def update(self, width: int, height: int, delta: float):
        if self.hp <= 0:
            self.hp = 0
            self.alive = False
        super().update(width, height, delta)
```

---

# Musterlösung - Aufgabe 8 e)

```python
@dataclass
class GameObject:

    # ...

    def is_colliding(self, other: "GameObject") -> bool:
        dist = Vec2(self.position.x - other.position.x, self.position.y - other.position.y)
        return dist.abs() <= self.radius + other.radius
```

---

# Musterlösung - Aufgabe 8 f)

```python
class GameObject:

    # ...

    def on_collision(self, other: "GameObject"):
        pass

class Projectile(GameObject):

    # ...

    def on_collision(self, other: 'GameObject'):
        if not isinstance(other, Ship):
            self.alive = False
```

---

# Musterlösung - Aufgabe 8 f)

```python
class StaticObject(GameObject):

    # ...

    def on_collision(self, other: 'GameObject'):
        self.alive = False

class Ship(GameObject):
    # ...
    def on_collision(self, other: 'GameObject'):
        match other:
            case Asteroid():
                self.hp -= other.radius
            case Health():
                self.hp += other.amount
            case Ammunition():
                self.shots += other.amount
```

---

# Musterlösung - Aufgabe 8 f)

```python

@dataclass
class Asteroid(StaticObject):
    special: bool

    def on_collision(self, other: 'GameObject'):
        if not isinstance(other, Asteroid):
            self.alive = False

```

---

# Musterlösung - Aufgabe 8 g)

```python
@dataclass
class Ship(GameObject):

    # ...

    def shoot(self) -> Projectile:
        alive = False
        if self.shots:
            self.shots -= 1
            alive = True
        pos = Vec2(self.position.x, self.position.y)
        return Projectile(pos, 5, alive, (255, 0, 0), 3)
```

---

# Musterlösung - Aufgabe 8 h)

```python
@dataclass
class GameObject:

    # ...

    def draw(self, screen: pygame.Surface):
        pygame.draw.circle(screen, self.color, (self.position.x, self.position.y), self.radius)
```

---

# Override-Dekorator

- ist in `typing`
- Wird über Methoden geschrieben, die überschrieben werden
- Pylance zeigt einen Fehler an, wenn die überschriebene Methode in keiner Oberklasse gefunden wird
- Hilft Fehler vorzubeugen - falsche Signatur, Parameter, ...

---

# Override-Dekorator - Beispiel
```python
from typing import override
from dataclasses import dataclass


@dataclass
class GameObject:

    def on_collision(self, other: 'GameObject'):
        pass

class StaticObject(GameObject):

    @override
    def on_collisoin(self, other: 'GameObject'): # Pylance-Error
        self.alive = False
```
---

# Override-Dekorator - Beispiel
```python
from typing import override
from dataclasses import dataclass


@dataclass
class GameObject:

    def on_collision(self, other: 'GameObject'):
        pass

class StaticObject(GameObject):

    @override
    def on_collision(self): # Pylance-Error
        self.alive = False
```

---

# Datenkapselung

- Man möchte manche Implementierung verstecken
- Wenn andere deinen Code verwenden, dann möchte man eine Schnittstelle anbieten die intuitiv ist.

```python
@dataclass
class MyList[T]:
    internal_list: list[T] = []

    def add(self, item: T) -> None:
        self.internal_list += [other]
```
---
# Datenkapselung - warum ist das schlecht?

```python
from my_collections import MyList

xs = MyList()
xs.internal_list # ????
```

- was sollen wir mit `internal_list`?
- andere sollten nur auf `add()` zugreifen können

---

# Private Attribute

```python
@dataclass
class MyList[T]:
    _internal_list: InitVar[list[T]]
    _length: InitVar[int]

    def __init__(self):
        self.__internal_list = []
        self.__length = 0

    def add(self, item: T):
        self.__internal_list += [item]
        self.__length += 1

    @property
    def length(self) -> int:
        return self.__length
```

---

# Private Attribute

```python
@dataclass
class MyList[T]:
    _internal_list: InitVar[list[T]]
    _length: InitVar[int]

    def __init__(self):
        self.__internal_list = []
        self.__length = 0
```

---

# Private Attribute

```python
@dataclass
class MyList[T]:
    _internal_list: InitVar[list[T]]
    _length: InitVar[int]

    def __init__(self):
        self.__internal_list = []
        self.__length = 0

    def add(self, item: T):
        self.__internal_list += [item]
        self.__length += 1

    @property
    def length(self) -> int:
        return self.__length
```
---

# Private Attribute - Setter

- Manchmal wollen wir trotzdem private Attribute setzen
- Aber vielleicht nur wenn bestimmte Bedingungen erfüllt sind

```python
class GameObject:
    _position: InitVar[tuple[int, int]]

    def __post__init__(self, position: tuple[int, int]):
        assert (0, 0) <= position
        self.__position = position

    @property
    def position(self) -> tuple[int, int]:
        return self.__position
```

---

# Private Attribute - Setter

```python
@dataclass
class GameObject:
    _position: InitVar[tuple[int, int]]

    def __post_init__(self, position: tuple[int, int]):
        assert (0, 0) > position
        self.__position = position

    @property
    def position(self) -> tuple[int, int]:
        return self.__position

    @position.setter
    def position(self, position: tuple[int, int]):
        if (0, 0) > position:
            return
        self.__position = position
```

---

# Comprehensive-Guide to `class`

## `@dataclass`

- Attribute werden im Klassenrumpf definiert
  - können mit einem Standardwert definiert werden
- `__init__`, `__post_init__`, `__repr__`, `__eq__`, `__lt__`, `__le__`, `__gt__`, `__ge__`, ... werden automatisch generiert
- In der Vorlesung benutzen wir nur `dataclass`

---

# Comprehensive-Guide to `class`

## `@dataclass`

- Attribute die im Klassenrumpf definiert werden, werden automatisch in die `__init__` generiert, auch wenn es einen Standardwert gibt!

```python
@dataclass
class A:
    x: int
    y: int = 0

    def __init__(self, x: int, y: int = 0): # das macht @dataclass von selber!
        self.x = x
        self.y = y
```
---

# Comprehensive-Guide to `class`

## `Enum`

- Wenn man eine endliche Aufzählung braucht (endliche Menge)
- macht die Fallunterscheidung einfach weil es endliche Elemente gibt
- Versichert auch dass kein quatsch übergeben wird wie zb bei `str`
- **niemals** mit `@dataclass`, sonst geht alles kaputt

---

# Enum - Beispiel

```python
def eval[T: (int | float)](operator: str, x: T, y: T) -> T:
    match operator:
        case '+':
            return x + y
        case '-':
            return x - y
        case '*':
            return x * y
        case '/' if y != 0:
            return x / y
        case _:
            return 0
```

---

# Enum - Beispiel

```python
from enum import Enum, auto

class Op(Enum):
    ADD = auto()
    SUB = auto()
    DIV = auto()
    MUL = auto()
```
Jetzt passen wir die Methodensignatur an
```python
def eval[T: (int | float)](operator: Op, x: T, y: T) -> T:
```
Jetzt kann nichts beliebiges als `operator` übergeben werden

---

# Blatt 09 - Fragen?

- Abgabe: 18.12. - 09:00
- Testet euren Code!
- Es gibt keine dummen Fragen wenns ums Verständnis geht
