729G46 Informationsteknologi och programmering¶

Tema 6, Föreläsning 6.1-6.2¶

Johan Falkenjack, johan.falkenjack@liu.se¶

Föreläsningsöversikt Fö 6.1 & 6.2¶

  • Tema 6: Temauppgift, seminarie, rapport
  • Repetition av termer för objektorienterad programmering
  • Klass-diagram (UML)
  • OOP: Arv i Python
  • Undantag (Exceptions) som exempel på OOP
  • Objektorienterad design, olika mönster
  • Interaktivt text-baserat program

Tema 6¶

  • Tema 6: strukturera kod med hjälp av Pythons stöd för OOP.
  • Temauppgift:
    • Del 1: Husdjur med leksaker + Person med dataserie(r)
    • Del 2: Program för att-göra-listor

Seminarie 6 / Rapport Tema 6¶

  • Uppgift: ta fram UML-diagram för ett bok-katalogsystem som ska kunna göra följande
    • lagra information om de böcker som är i systemet
    • låna ut böcker
    • hålla reda på de personer som varje utlånad bok är utlånad till
  • Seminarie: Varje pargrupp tar med sig ett förslag som används som diskussionsunderlag på seminariet.
  • Rapport: Varje pargrupp beskriver ett förslag med UML-diagram, beskrivning av klasser, samt diskussion

Repetition: klasser, instanser

Objektorienterad terminologi

Meddelande från framtiden:

Ni behöver ha koll på instanser och klasser i AI-kursen nästa höst.

Från Laboration 1, 729G78 Artificiell intelligens

class ReflexAgentWithState(BaseAgent):
    
    class State:
        
        def __init__(self):
            self.bump = False
            self.previous_action = ""
            self.actions = ["GoRight", "GoLeft", "GoForward", "GoBack"]
        
        def __repr__(self):
            if self.bump:
                return self.previous_action + " resulted in a bump"
            else:
                return self.previous_action
    
    def update_state_with_percept(self, percept, state):
        if percept[1] == "bump":
            state.bump = True
        else:
            state.bump = False
        return state
    def choose_action(self, state):
        actions = state.actions
        if state.bump:
            actions.remove(state.previous_action)
        return random.choice(actions)
    
    def update_state_with_action(self, action, state):
        state.previous_action = action
        # Print the representation (i.e. __repr__) of the state
        print(state)
        return state

Klasser och objekt¶

  • Klass: mallkod som används när en instans av en klass skapas
    • Vi kan skapa nya datatyper med hjälp av klasser (de inbyggda datatyperna i Python är klasser)
    • Exempel: "3953 bananer!" är ett värde av datatypen str ("string"/sträng).
  • Instans och objekt är synonymer.
    • Objekt är faktiska värden som skapas genom att använda definitionen av en klass.
    • Exempel: listan ["hej", 36, True] är en instans av klassen list.

Instansvariabler och metoder

  • En grundläggande anledning som bidragit till utveckling av objektorienterade programmeringsspråk är behov av att kunna gruppera ihop en specifik datastruktur och funktionalitet som jobbar med just den datastrukturen: inkapsling.
  • Klassen är mallen för datastrukturen som består av instansvariabler (variabler som är kopplade till en specifik instans). I Python använder man self för att referera till den den egna instansen: self.instansvariabel
  • En funktion som är definierad i en klass är bunden till den klassens instanser och kallas för metod.

Repetition: klasser, instanser


No description has been provided for this image

Vanliga mönster när man designar en klass¶

  • Namngivning av klasser: substantiv i singular
  • Namngivning av metoder: verb
  • Namngivning av instansvariabler: substantiv

Exempel på klassnamn¶

  • DataPoint
  • DataPointCollection
  • Word
  • Sentence
  • Paragraph
  • Document
  • Person
  • ContactList
  • Book
  • Library
  • BookCollection

Vanliga prefix till metodnamn¶

  • get_: hämta ett värde från ett objekt (antingen beräknat eller direkt från ett attribut)
  • set_: sätt att värde (bör egentligen undvikas)
  • add_: lägg till ett värde
  • remove_: ta bort ett värde
  • load_: ladda data från fil
  • save_: spara data till fil
  • is_X: returnera True om X stämmer, annars False
  • has_X: returnera True om objektet har X, annars False

Klassdiagram i UML

Objektorientering utan kod

Klassdiagram i UML


No description has been provided for this image

Exempel: Klassdiagram


No description has been provided for this image

Klassdiagram: Relationer mellan objekt¶

  • Objekt av en klass kan ha olika typer av relationer till objekt av andra klasser.
  • Association: allmän relation
    • roll (i praktiken variabelnamn), t.ex. "lästa_böcker" och
    • multiplicitet (<lägst antal>..<högst antal>, * betyder godtyckligt många), exempel: 0..1, 1..1, 0..*
UML_Association_Arrow.svg

Relationen association¶

No description has been provided for this image
  • ClassA → ClassB
  • Objekt av ClassA känner till <multiplicitet> st instanser av ClassB.
  • Instans/en/erna av ClassB har rollen <roll>.
  • En dubbelriktad association kan ritas utan pilar.

Exempel¶

No description has been provided for this image

UML, klassdiagram: Association¶

  • Exempel på roller:
    • lästa_böcker
    • squares
    • contacts
  • Exempel på multiplicitet
    • 0..1: noll eller en
    • 0..*: noll eller flera
    • 1..*: en eller fler
    • 3: exakt tre
    • 0..3: noll till tre
    • 5..9: fem till nio

UML: Relationer¶

No description has been provided for this image

UML: Relationer (överkurs)¶

No description has been provided for this image

Relationer i klassdiagram¶

  • Relationer informerar oss om
    • Vilka klasser känner till vilka andra klasser?
    • Hur många instanser deltar i relationen? (Kardinalitet)
    • Vilken roll har en klass i relation till en annan?
  • Innebär att en instans av en klass har en referens till en instans av en annan klass (eller ytterligare en instans av den egna klassen)
  • Exempel på referenser
    • en instansvariabel låter oss referera till en enskild instans
    • en lista/ett dictionary låter oss referera till flera instanser

Lite kort om arv¶

Hierarkiska relationer¶

No description has been provided for this image
http://www.biologycorner.com/worksheets/taxonomy_interpret.html>

Arv / härleda klasser¶

  • När en klass ärver från en annan klass följer alla attribut, dvs instansvariabler och metoder, med.
  • Syfte: Abstraktion och kodåtervinning.

Arv: Exempel

UML_DataFile_inheritance.svg


class DataFile(object):

    def __init__(self, filepath=None):
        self.filepath = filepath

data = DataFile("data.txt")
print(data)

Basklass, superklass, föräldraklass¶

  • Alla klasser i Python har en basklass (eng. base class) - en klass som de bygger vidare på.
  • Kallas också superklass* (superclass) eller föräldraklass (parent class). (*super från latinets "över", inte super som i "extra mycket")
  • Om klassen DataFile bygger vidare på object, kallar man object för basklassen för klassen DataFile.

  • En superklass till klassen MyClass kan i sin tur ha en superklass, och så vidare, hela vägen tills vi når object. Alla dessa klasser kallar vi för superklasser till MyClass.
    • Ibland pratar vi om direkt superklass kontra ancestors för att hålla isär den närmaste superklassen och dess superklasser.

Härledd klass, subklass, barnklass¶

  • Varje klass i Python, utom object, är en härledd klass (eng. derived class), dvs. en klass som har en basklass.
  • Kallas också subklass (subclass) eller barnklass (child class).
  • DataFile är en klass härledd från klassen object.

  • En subklass till MyClass kan i sin tur ha en subklass, och så vidare. Precis som med superklasser kallar vi alla dessa klasser för subklasser till MyClass.
    • Ibland pratar vi om direkt subklass kontra descendants.

Syntax klass med arv¶

class Base(object):
    pass

class Derived(Base):
    pass

Från Laboration 1 i AI-kursen igen

class ReflexAgentWithState(BaseAgent):
    
    class State:
        
        def __init__(self):
            self.bump = False
            self.previous_action = ""
            self.actions = ["GoRight", "GoLeft", "GoForward", "GoBack"]
        
        def __repr__(self):
            if self.bump:
                return self.previous_action + " resulted in a bump"
            else:
                return self.previous_action
    
    def update_state_with_percept(self, percept, state):
        if percept[1] == "bump":
            state.bump = True
        else:
            state.bump = False
        return state
    def choose_action(self, state):
        actions = state.actions
        if state.bump:
            actions.remove(state.previous_action)
        return random.choice(actions)
    
    def update_state_with_action(self, action, state):
        state.previous_action = action
        # Print the representation (i.e. __repr__) of the state
        print(state)
        return state

Arv i Python forts.¶

  • En härledd klass har alla attribut (instansvariabler och metoder) som dess basklass har.
  • En härledd klass kan ges ytterligare attribut.
  • En härledd klass kan överskugga (override) attribut från dess basklass.
    • T.ex. är det vanligt att klasser överskuggar __str__ eftersom den __str__ de ärvt från object bara skriver ut vilken klass ett objekt tillhör och på vilken minnesadress objektet är lagrat.

Överskuggning, exempel¶

In [3]:
class Bag(object):
    
    def __init__(self):
        self.contents = list()
    
    def put(self, obj):
        self.contents.append(obj)
        
    def __str__(self):
        res ="A bag containing:\n"
        for item in list(self.contents):
            res += "- " + item + "\n"
        return res
        
my_bag = Bag()
my_bag.put("phone")
my_bag.put("keys")
my_bag.put("banana")

print(my_bag)
A bag containing:
- phone
- keys
- banana

In [5]:
class Bag(object):
    
    def __init__(self):
        self.contents = list()
    
    def put(self, obj):
        self.contents.append(obj)

    def __str__(self):
        return "Bag contents: " + str(self.contents)
        
my_bag = Bag()
my_bag.put("phone")
my_bag.put("keys")
my_bag.put("banana")

print(my_bag)
Bag contents: ['phone', 'keys', 'banana']

Undantag (eng. Exceptions)


No description has been provided for this image

Exempel på hur arv och OOP används i Python

Exempel på kod som utlöser undantag¶

In [71]:
# Exempel 1
number = 5
name = "Ada"
print(number + name)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[71], line 4
      2 number = 5
      3 name = "Ada"
----> 4 print(number + name)

TypeError: unsupported operand type(s) for +: 'int' and 'str'
In [72]:
# Exempel 2
ans = 5 / (42 * 0)
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[72], line 2
      1 # Exempel 2
----> 2 ans = 5 / (42 * 0)

ZeroDivisionError: division by zero
In [73]:
# Exempel 3
printt("Hello world")
    
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[73], line 2
      1 # Exempel 3
----> 2 printt("Hello world")

NameError: name 'printt' is not defined

Exception - exempel på OOP¶

  • Varje typ av undantag är definerad som en klass i Python
  • När ett undantag inträffar skapas en instans av den undantagstypen
  • Undantaget innehåller information om vad som hänt (data)

Undantag - Exceptions¶

  • Undantag (eng. Exception) är ett sätt att hantera fel i kod som kan utlösas (eng. throw/raise) när ett program kör (körfel).
  • Vid fel kan ett undantag utlösas. Programmet avbryter sitt normala flöde för att gå in i "undantagsläge".
  • Om kod för att hantera ett undantag saknas, kraschar programmet.
  • Skillnad mot if/else:
    • Villkor: if/else - kontrollera först, utför sen
    • Undantagshantering: try/except - utför först, hantera ev. fel
  • Undantag ska inte ses som ett alternativ till villkorssatser utan just som ett sätt att hantera undantag.

Exempel på olika undantag i Python¶

  • ZeroDivisionError: inträffar när man försökt dividera ett tal med 0
  • IOError: inträffar t.ex. om man försöker öppna en fil som inte finns
  • IndexError: inträffar när man använder ett ogiltigt index för en specifik lista
  • KeyError: inträffar när man försöker komma åt värdet för en nyckel som inte finns i ett dictionary
  • TypeError: inträffar t.ex. när man försöker använda operatorn + på en sträng och ett heltal

Hantera undantag i Python¶

  • Om man vill hantera undantag i Python lägger man koden som kan utlösa undantag i ett try-block.
  • Undantag hanteras sedan i ett except-block.
  • Vid behov kan ett finally-block kan läggas till sist. Koden i detta block körs sist och alltid; dvs efter felfritt utförande av try-blocket samt efter att något except-block uförts.
  • Minst ett except-block, eller ett finally-block måste defineras för varje try-block.

Mönster för undandagshantering i Python

# nyckelordet try används för att påbörja ett try-block
try:
    # här lägger man koden som kan utlösa ett undantag
    # "försök utföra dessa satser"

# nyckelordet except används för att påbörja ett except-block
except [typ av undantag] [as <namn på undantagsinstans>]:
    # här lägger man koden för vad som ska hända vid undantag

except [typ av undantag] [as <namn på undantagsinstans>]:
    # flera olika typer av undandtag kan hanteras
    # genom att lägga till flera except-block

# koden i finally-blocket utförs både efter felfritt try-block
# och efter except-block körts vid fel i try-block
finally:
    # kod för att städa upp, t.ex. stänga fil eller nätverks-
    # anslutning

(Hakparenteserna ska inte skrivas ut, de visar att det som står innanför dem är valfritt att skriva. Det som står inom mindre än/större än-tecken måste vara med. Mindre än/större än-tecknen ska inte heller skrivas i din kod.)

Exempel: Fånga undantag¶

In [74]:
# krasch.py
# Exempel på kod som kraschar
letters = ["a", "b", "c", "d", "e", "f"]

index = 0
while True:
    print(f"letters[index]: {letters[index]}")
    index += 1
print("Program done.")
letters[index]: a
letters[index]: b
letters[index]: c
letters[index]: d
letters[index]: e
letters[index]: f
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[74], line 7
      5 index = 0
      6 while True:
----> 7     print(f"letters[index]: {letters[index]}")
      8     index += 1
      9 print("Program done.")

IndexError: list index out of range

Exempel: Fånga undantag¶

In [75]:
# undantag1.py
# Undantag hanterat, kraschar inte
letters = ["a", "b", "c", "d", "e", "f"]

try:
    index = 0
    while True:
        print("letters[index]: {}".format(letters[index]))
        index += 1
    print("Loop done.")
except:
    print("An exception was raised.")
print("Program done.")
letters[index]: a
letters[index]: b
letters[index]: c
letters[index]: d
letters[index]: e
letters[index]: f
An exception was raised.
Program done.
  • Notera att detta inte är något man bör göra i verkligheten.
  • Per definition svårt att göra riktiga exempel, då förväntade undantag inte är några verkliga undantag.

Exempel: Fånga och namnge undantag¶

In [76]:
# undantag2.py
letters = ["a", "b", "c", "d", "e", "f"]
try:
    index = 0
    while True:
        print(letters[index])
        index += 1
# Om undantag inträffar kommer variabeln error referera till en
# instans av klassen Exception
except Exception as error:
    print("An exception was raised.")
    print(error)
print("Program done.")
a
b
c
d
e
f
An exception was raised.
list index out of range
Program done.

Exempel: Fånga och hantera specifika undantag på olika sätt¶

In [83]:
# undantag3.py
import random

def run_until_exception():
    values = ["a", 2, "c", 4, "e", "f"]
    try:
        while True:
            # slumpa fram två index som ev. kan vara ogiltiga
            index1 = random.randint(0, len(values))
            index2 = random.randint(0, len(values))
            # använd operatorn +. Fel om operanderna inte är samma datatyp.
            print(f"{values[index1]} + {values[index2]} -> {values[index1] + values[index2]}")
    except TypeError as exception:
        print(f"TypeError: {exception}. Tried {values[index1]} + {values[index2]}")
    except IndexError as exception:
        print(f"IndexError: {exception}, {len(values)=}, {index1=}, {index2=}")

run_until_exception()
print("Program done.")
c + f -> cf
4 + 2 -> 6
2 + 4 -> 6
TypeError: can only concatenate str (not "int") to str. Tried c + 4
Program done.

Klasshierarki över alla inbyggda undantag i Python 3.10¶

BaseException
 ├── SystemExit
 ├── KeyboardInterrupt
 ├── GeneratorExit
 ├── Exception
      ├── StopIteration
      ├── StopAsyncIteration
      ├── ArithmeticError
      |    ├── FloatingPointError
      |    ├── OverflowError
      |    ├── ZeroDivisionError
      ├── AssertionError
      ├── AttributeError
      ├── BufferError
      ├── EOFError
      ├── ImportError
      |    ├── ModuleNotFoundError
      ├── LookupError
      |    ├── IndexError
      |    ├── KeyError
      ├── MemoryError
      ├── NameError
      |    ├── UnboundLocalError
      ├── OSError
      |    ├── BlockingIOError
      |    ├── ChildProcessError
      |    ├── ConnectionError
      |    |    ├── BrokenPipeError
      |    |    ├── ConnectionAbortedError
      |    |    ├── ConnectionRefusedError
      |    |    ├── ConnectionResetError
      |    ├── FileExistsError
      |    ├── FileNotFoundError
      |    ├── InterruptedError
      |    ├── IsADirectoryError
      |    ├── NotADirectoryError
      |    ├── PermissionError
      |    ├── ProcessLookupError
      |    ├── TimeoutError
      ├── ReferenceError
      ├── RuntimeError
      |    ├── NotImplementedError
      |    ├── RecursionError
      ├── SyntaxError
      |    ├── IndentationError
      |         ├── TabError
      ├── SystemError
      ├── TypeError
      ├── ValueError
      |    ├── UnicodeError
      |         ├── UnicodeDecodeError
      |         ├── UnicodeEncodeError
      |         ├── UnicodeTranslateError
      ├── Warning
           ├── DeprecationWarning
           ├── PendingDeprecationWarning
           ├── RuntimeWarning
           ├── SyntaxWarning
           ├── UserWarning
           ├── FutureWarning
           ├── ImportWarning
           ├── UnicodeWarning
           ├── BytesWarning
           ├── EncodingWarning
           ├── ResourceWarning

Objektorienterad design

Introduktion

Vad vill vi göra? / Hur ska vi göra det? (Mönster)¶

  • Exempel på "Vad vill vi göra?":
    1. Dela upp ett problem
    2. Delegera / skicka vidare uppgiften
    3. Återanvända kod
    4. Förenkla användning av kod

Vad vill vi göra? / Hur ska vi göra det? (Mönster)

  • Exempel på "Vad vill vi göra?":
    1. Dela upp ett problem
    2. Delegera / skicka vidare uppgiften
    3. Återanvända kod
    4. Förenkla användning av kod
  • Exempel på "Hur ska vi göra det?"
    • 1a) Dela upp en funktion i flera funktioner/metoder
    • 1b) Skapa olika klasser med olika ansvarsområden

Vad vill vi göra? / Hur ska vi göra det? (Mönster)¶

  • Exempel på "Vad vill vi göra?":
    1. Dela upp ett problem
    2. Delegera / skicka vidare uppgiften
    3. Återanvända kod
    4. Förenkla användning av kod
  • Exempel på "Hur ska vi göra det?"
    • 2a) Anropa en annan funktion/metod
    • 2b) Anropa en metod i ett annat objekt

Vad vill vi göra? / Hur ska vi göra det? (Mönster)¶

  • Exempel på "Vad vill vi göra?":
    1. Dela upp ett problem
    2. Delegera / skicka vidare uppgiften
    3. Återanvända kod
    4. Förenkla användning av kod
  • Exempel på "Hur ska vi göra det?"
    • 3a) återanvänd klasser som redan definierats
    • 3b) skapa nya klasser genom att kombinera redan definierade klasser
    • 3c) skapa nya klasser som ärver av redan definierade klasser

Vad vill vi göra? / Hur ska vi göra det? (Mönster)¶

  • Exempel på "Vad vill vi göra?":
    1. Dela upp ett problem
    2. Delegera / skicka vidare uppgiften
    3. Återanvända kod
    4. Förenkla användning av kod
  • Exempel på "Hur ska vi göra det?"
    • 4a) Dela upp problemet i olika abstraktionsnivåer. Funktioner som anropar andra funktioner.
    • 4b) Dela upp problemet i olika abstraktionsnivåer. Objekt som använder andra objekt

Exempel på problem¶

  • Hur många filer finns under katalogen "Mina dokument" på min dator?
  • Exempel på funktionell lösning:
    • Funktion som räknar antalet filer i en given katalog och anropar sig själv med varje underkatalog som argument och summerar det hela.
  • Exempel på objektorienterad lösning:
    • Representera katalogen som ett objekt som vet hur många filer och underkataloger den innehåller. Katalogen frågar varje underkatalog om hur många filer den har och summerar det hela.
  • OOP: Vilka objekt har vi? Vilka frågor/uppgifter kan en instans som agerar som behållare delegera vidare till de instanser som den innehåller?

Delegering av uppgifter i verkliga världen¶

Jämförelse med verkligheten: Beslut från "ledningsgruppen"¶

  • Beslut fattas i "ledningsgruppen" om att alla avdelningar ska inventera sina böcker.
  • Varje avdelningschef ber varje gruppchef om en inventering av böckerna i deras grupper
  • Varje gruppchef ber medlemmarna i gruppen om en inventering av böckerna på deras kontor.

Delegering i OOP

Definiera klasser

Repetition

Metoden __str__¶

Metoden __str__¶

  • Det finns en uppsättning metoder som vi kan definiera i en klass som Python vet om och använder i specifika situationer.
  • Exempel
    • Metoden __str__ används när en instans av en klass (ett objekt) ska visas som en sträng
    • __str__ tar inte emot några argument förrutom self
    • __str__ ska returnera en sträng (representationen av instansen)
  • __str__ används av
    • funktionen str för att konvertera ett objekt till en sträng
    • funktionen print när ett objekt ska skrivas ut
  • Anropa inte metoden __str__ direkt, utan använd funktionen str om om du vill ha strängen, eller låt den anropas implicit, t.ex. när du vill skriva ut ett objekt med print

En klass för spelaren i ett spel

UML_Player.svg

class Player(object):
    
    def __init__(self, name):
        self.name = name
        self.inventory = []
        self.hp = 100
    
    def add_item(self, item_name):
        self.inventory.append(item_name)
    
    def __str__(self):
        return f"Player name: {self.name}. HP: {self.hp}. Items: {','.join(self.inventory)}"

if __name__ == "__main__":
    p1 = Player("Nario")
    p1.add_item("Mushroom")
    p1.add_item("Star")
    
    print(p1)

En person i ett banksystem

UML_Person.svg

class Person(object):
    
    def __init__(self, name):
        self.name = name
        self.accounts = []
    
    def add_account(self, account_id):
        self.accounts.append(account_id)
    
    def __str__(self):
        return f"Name: {self.name}. Accounts: {', '.join(self.accounts)}"

if __name__ == "__main__":
    p1 = Person("Nario")
    p1.add_account("123456")
    p1.add_account("123457")
    
    print(p1)

En egen klass som innehåller instanser av en annan egen klass

Befolkning i stad → en stad innehåller många hushåll

Aggregering av värden genom att skicka vidare uppgiften neråt, "delegera vidare"

Exempel på aggregering av data¶

  • City kan tillhandahålla befolkningssiffra genom att fråga alla Household-objekt efter hur många som ingår i hushållet.
  • Vi kan rita detta som en association från City till Household, men lämpligare är att använda en aggregation

UML_City_Household.svg

Exempel på aggregering av data¶

  • City kan tillhandahålla befolkningssiffra genom att fråga alla Household-objekt efter hur många som ingår i hushållet.
  • Vi säger att City aggregerar Household och illustrerar detta med UMLs Aggregations-relation.
    • En aggregation innebär att det aggregerande objektet samlar ett flertal objekt som existerar oberoende av det aggregerande objektet.

UML_City_Household_Aggregates.svg

Uppdelning av problem. Exempel på aggregering av data + implicit användning av __str__

class City(object):
    
    def __init__(self, name):
        self.households = []
        self.name = name
    
    def add_household(self, household):
        self.households.append(household)
    
    def get_population(self):
        total_population = 0
        for household in self.households:
            total_population += household.get_num_members()
        return total_population
    
    def __str__(self):
        output = f"City: {self.name}\nPopulation: {self.get_population()}\n"
        for household in self.households:
            output += f" * {household}\n"
        return output
class Household(object):
    
    def __init__(self, name):
        self.name = name
        self.members = []
    
    def add_member(self, name):
        self.members.append(name)
    
    def get_num_members(self):
        return len(self.members)
    
    def __str__(self):
        return f"Household '{self.name}', ({self.get_num_members()} ppl)"

flaxtown = City("Linköping")

household1 = Household("Rydsvägen 1")
household1.add_member("A")
household1.add_member("B")

household2 = Household("Rydsvägen 2")
household2.add_member("C")

flaxtown.add_household(household1)
flaxtown.add_household(household2)

print(flaxtown)
In [13]:
class City(object):
    
    def __init__(self, name):
        self.households = []
        self.name = name
        
    def add_household(self, household):
        self.households.append(household)
        
    def get_population(self):
        total_population = 0
        for household in self.households:
            total_population += household.get_num_members()
        return total_population
    
    def __str__(self):
        output = f"City: {self.name}\nPopulation: {self.get_population()}\n"
        for household in self.households:
            output += f" * {household}\n"
        return output

class Household(object):
    
    def __init__(self, name):
        self.name = name
        self.members = []
        
    def add_member(self, name):
        self.members.append(name)
        
    def get_num_members(self):
        return len(self.members)    
    def __str__(self):
        return f"Household '{self.name}', ({self.get_num_members()} ppl)"
In [14]:
flaxtown = City("Linköping")

household1 = Household("Rydsvägen 1")
household1.add_member("A")
household1.add_member("B")

household2 = Household("Rydsvägen 2")
household2.add_member("C")

flaxtown.add_household(household1)
flaxtown.add_household(household2)

print(flaxtown)
City: Linköping
Population: 3
 * Household 'Rydsvägen 1', (2 ppl)
 * Household 'Rydsvägen 2', (1 ppl)

Exempel: OOP-väska

Användardefinierad klass som innehåller instanser av andra användardefinierade klasser

Uppdelning av uppgifter genom att dela upp en modell (klass) i flera klasser med olika ansvarsområden

Modell för en väska¶

  • En väska kan öppnas och stängas.
  • Vi kan stoppa in saker i en väska, men inte hur många saker som helst.
  • En väska är ett förvaringsutrymme som har ett blixtlås.
  • Om blixtlåset är öppet kan vi stoppa in saker.
  • Om blixtlåset är trasigt kan vi inte stoppa in saker.
  • Varje gång vi öppnar eller stänger väskan finns det en viss sannolikhet att blixtlåset går sönder.

bagscript.py¶

  • Vi har "abstraherat bort" all logistik (alla detaljer kring vad som faktiskt händer) till implementationen av klassen Bag.
  • Vår kod interagerar med instansen av Bag på dess modellerade abstraktionsnivå.
from zcb import *

# Bag är en klass definierad i filen zcb.py
# Vad ser koden ut att göra?

bag = Bag()
bag.open()
bag.add("candy")
bag.close()

bag.open()
bag.add("egg")
bag.close()

bag.add("flower")
print(bag)

Klassen Bag


UML_Bag.svg
  • Klassen Bag i sin tur använder sig av andra abstraktioner. Vi har klasserna Zipper och Container som är abstraktioner av (modellerar) blixtlås och förmåga att vara behållare.

zcb.py - klassen Bag¶

UML_Bag.svg
class Bag(object):
    
    def __init__(self, color="green"):
        self.color = color
        self.zipper = Zipper()
        self.container = Container()
    
    def open(self):
        self.zipper.open()
    
    def close(self):
        self.zipper.close()
    
    def add(self, item):
        item = item.upper()
        print(f"Trying to add '{item}' to bag..")
        if self.zipper.is_open():
            self.container.add(item)
        else:
            print("Open zipper first.")
    
    def __str__(self):
        return f"A {self.color} bag. {self.zipper} {self.container}"

zcb.py - Klassen Zipper¶

UML_Zipper.svg
class Zipper(object):
    
    def __init__(self):
        self.state = "open"
        self.chance_to_break = 0.2
    
    def is_open(self):
        return self.state == "open"
    
    def is_broken(self):
        return self.state == "broken"
    
    def open(self):
        print("Trying to open zipper...")
        self.try_to_break()
        
        if not self.is_broken():
            self.state = "open"
            print(self)
        else:
            print(f"Cannot open zipper. {self}")
    
    def close(self):
        print("Trying to close zipper...")
        self.try_to_break()
        if not self.is_broken():
            self.state = "closed"
            print(self)
        else:
            print(f"Cannot close zipper. {self}")
    
    def try_to_break(self):
        if random.random() <= self.chance_to_break:
            self.state = "broken"
            print("The zipper broke!")
    
    def __str__(self):
        return f"The zipper is {self.state}."

zcb.py - klassen Container¶

UML_Container.svg
class Container(object):
    
    def __init__(self, capacity=5):
        self.items = []
        self.capacity = capacity
    
    def add(self, item):
        if len(self.items) < self.capacity:
            self.items.append(item)
            print(f"{item} added.")
        else:
            print("f{item} not added. Container full.")
    
    def __str__(self):
        if not self.items:
            items_str = "None"
        else:
            items_str = ", ".join(self.items)
        
        fill_level = len(self.items)/self.capacity * 100
        return f"The container is {fill_level:.2f}% full. Items: {items_str}."

Klassdiagram i UML med alla klasser¶

  • Bag känner till en instans av klassen Zipper. Zipper har rollen zipper i Bag.
  • Bag känner till en instans av klassen Container. Container har rollen container i Bag.
UML_Zipper_Container_Bag.svg

Klassdiagram i UML med alla klasser¶

  • Bag känner till en instans av klassen Zipper. Zipper har rollen zipper i Bag.
  • Bag känner till en instans av klassen Container. Container har rollen container i Bag.
  • Vi säger att Bag är en komposition av Zipper och Container och visar detta med UMLs Composition-relation.
    • En komposition används när delarna inte kan existera oberoende av helheten.
UML_Zipper_Container_Bag_Composition.svg

Relationer i klassdiagram¶

  • Informerar oss om:
    • Vilka klasser känner till vilka andra klasser?
    • Vilken typ av relation råder mellan klasserna?
    • Hur många instanser deltar i relationen? (multiplicitet)
    • Vilken roll har en klass i relation till en annan?
  • Varje relation innebär att åtminstone objekt av den ena klassen kan innehålla en eller flera referenser till objekt av den andra klassen.
    • (Vid arv är referensen till basklassen implicit och vi kommer åt den genom att anropa funktionen super.)

  • Exempel på referenser
    • en instansvariabel låter oss referera till en enskild instans
    • en lista/ett dictionary låter oss referera till flera instanser

Klasser och objekt¶

  • Om klassen Citron har en metod mumsa brukar man i text (t.ex. dokumentation) hänvisa till metoden mumsa som Citron.mumsa.
  • Om Book och Page är klasser så ska "Book innehåller Page-objekt" tolkas som
    • "Book-objekt innehåller Page-objekt", dvs
    • "Instanser av klassen Book innehåller instanser av klassen Page", dvs
    • "Instanser av klassen Book har en instansvariabel som refererar till instanser av klassen Page"

Bank-exempel¶

En första version¶

  • En bank har 0 eller fler personer som kunder.
  • Varje person har 0 eller fler konton.
  • Varje konto har ett kontonummer, en lista över transaktioner och ett saldo.
UML_Bank_Client_Account.svg

bank1.py¶

class Bank(object):
    
    def __init__(self):
        self.clients = []
    
    def __str__(self):
        output = "THE BANK\n"
        for client in self.clients:
            output += f"{client}\n"
        return output    

class Client(object):
    
    def __init__(self, name):
        self.name = name
        self.accounts = []
    
    def __str__(self):
        output = f"Client name: {self.name}\n  Accounts:\n"
        if len(self.accounts) > 0:
            for account in self.accounts:
                output += f"    {account}"
        else:
            output += "    No accounts."
        return output
class Account(object):
    
    def __init__(self, account_id):
        self.account_id = account_id
        self.transactions = []
        self.balance = 0.0
    
    def __str__(self):
        return f"#{self.account_id}: {self.balance}"

if __name__ == "__main__":
    bank = Bank()
    c1 = Client("Nario")
    bank.clients.append(c1)
    
    bank.clients.append(Client("Duigi"))
    a1 = Account(1)
    bank.clients[0].accounts.append(a1)
    
    print(bank)

bank1.py¶


  • Ser ni "buggen"?

  • Som koden är organiserad ser det ut som att kontot a1 borde tillhöra Duigi, men det kommer tillhöra Nario, då Nario är den första klienten i listan clients
if __name__ == "__main__":
    bank = Bank()
    c1 = Client("Nario")
    bank.clients.append(c1)
    
    bank.clients.append(Client("Duigi"))
    a1 = Account(1)
    bank.clients[0].accounts.append(a1)
    
    print(bank)

Metoder istället för att jobba direkt med instansvariablerna¶

  • Bank-klassen
    • new_client: skapa kund, se till att varje kund har ett konto
    • new_account: skapa nytt konto, måste ha en kund att koppla det till instansvariaben num_accounts för antal konton som skapats
  • Client-klassen
    • add_account: lägg till konto till kund
    • En kund (Client-instans) har minst ett konto (en Account-instans)
  • Relationer
    • Bank-instansen känner till alla Client-instanser och alla Account-instanser
    • Client-instanser har referenser till alla dess Account-instanser

Metoder istället för att jobba direkt med instansvariablerna¶


UML_Bank_Client_Account_v2.svg

bank2.py¶

class Bank(object):
    
    def __init__(self):
        self.num_accounts = 0
        self.clients = []
        self.accounts = []
    
    def new_client(self, client_name):
        self.clients.append(Client(client_name))
        self.new_account(client_name)
    
    def new_account(self, client_name):
        client_found = False
        # leta efter kund
        for client in self.clients:
            if client.name == client_name:
                client_found = True
                break
        # avbryt om inte kunden hittas
        if not client_found:
            print(f"ERROR: No such client, '{client_name}'")
            return
        # skapa konto, lägg till banken och kunden
        else:
            self.num_accounts += 1
            new_account = Account(self.num_accounts)
            client.add_account(new_account)
            self.accounts.append(new_account)
    
    def __str__(self):
        output = "THE BANK\n"
        for client in self.clients:
            output += f"{client}\n"
        return output
class Client(object):
    
    def __init__(self, name):
        self.name = name
        self.accounts = []
    
    def add_account(self, account):
        self.accounts.append(account)
    
    def __str__(self):
        output = f"Client name: {self.name}\n  Accounts:\n"
        for account in self.accounts:
            output += f"    {account}"
        return output

class Account(object):
    
    def __init__(self, account_id):
        self.account_id = account_id
        self.transactions = []
        self.balance = 0.0
    
    def __str__(self):
        return f"#{self.account_id}: {self.balance}"

if __name__ == "__main__":
    bank = Bank()
    bank.new_client("Nario")
    bank.new_client("Duigi")
    print(bank)
  • Logistik för Client- och Accountinstanser i Bank-klassen

  • Osynlig logistik vid användning - delegerats till Bank-klassen

Exempel på förändring av implementation av modell/abstaktion: dictionarys istället för listor¶

  • Bank-klassen
    • ingen förändring av klassdiagrammet för att använda dictionaries istället för listor
    • instansvariabel för att ha koll på antal kunder, num_clients (används som nyckel)
    • client_id som argument till new_account
  • Client-klassen
    • instansvariaben client_id

Exempel på förändring av implementation av modell/abstaktion: dictionarys istället för listor¶


UML_Bank_Client_Account_v3.svg

bank3.py¶

class Bank(object):
    
    def __init__(self):
        self.num_accounts = 0
        self.num_clients = 0
        self.clients = {}
        self.accounts = {}
    
    def new_client(self, client_name):
        self.num_clients += 1
        # endast för att göra efterföljande kod lättare att läsa
        client_id = self.num_clients
        self.clients[self.num_clients] = Client(client_name, client_id)
        self.new_account(client_id)
    
    def new_account(self, client_id):
        # avbryt om inte kunden hittas
        if client_id not in self.clients:
            print(f"ERROR: Client id '{client_id}' not found.")
        else:
            self.num_accounts += 1
            new_account = Account(self.num_accounts)
            # samma Account-instans läggs till både banken och kunden
            self.clients[client_id].add_account(new_account)
            self.accounts[self.num_accounts] = new_account
    
    def __str__(self):
        output = "THE BANK\n\n"
        for client in self.clients.values():
            output += f"{client}\n"
        return output
class Client(object):
    
    def __init__(self, name, client_id):
        self.name = name
        self.client_id = client_id
        self.accounts = []
    
    def add_account(self, account):
        self.accounts.append(account)
    
    def __str__(self):
        output = f"Client name: {self.name} (id: {self.client_id})\nAccounts:\n"
        for account in self.accounts:
            output += f"  {account}"
        return output

class Account(object):
    
    def __init__(self, account_id):
        self.account_id = account_id
        self.transactions = []
        self.balance = 0.0
    
    def __str__(self):
        return f"#{self.account_id}: {self.balance}"

if __name__ == "__main__":
    bank = Bank()
    bank.new_client("Nario")
    bank.new_client("Duigi")
    print(bank)

Interaktiva text-gränssnitt¶

interaktionsloop1.py med interaktionsloop¶

In [ ]:
from bank3 import *


def new_client(bank):
    print("new_client() körs")


def show_clients(bank):
    print("show_clients() körs")


def main():
    bank = Bank()
    while True:
        user_input = input("Vad vill du göra? ").lower()
        if user_input == 'q':
            break
        elif user_input == "ny kund":
            new_client(bank)
        elif user_input == "visa kunder":
            show_clients(bank)
        else:
            print("Okänt kommando.")


if __name__ == '__main__':
    main()
    

interaktionsloop2a.py med dictionary och funktionsobjekt¶

In [ ]:
from bank3 import *


def new_client(bank):
    print("new_client() körs")


def show_clients(bank):
    print("show_clients() körs")


def main():
    commands = {'ny kund': new_client,
                'visa kunder': show_clients}
    bank = Bank()

    while True:
        user_input = input("Vad vill du göra? ").lower()
        if user_input == 'q':
            break
        elif user_input in commands:
            commands[user_input](bank)
        else:
            print("Okänt kommando.")


if __name__ == '__main__':
    main()

interaktionsloop2b.py med dictionary och funktionsobjekt¶

In [ ]:
from bank3 import *


def new_client(bank):
    print("new_client() körs")


def show_clients(bank):
    print("show_clients() körs")


def main():
    commands = {'ny kund': new_client,
                'visa kunder': show_clients}
    bank = Bank()

    while True:
        user_input = input("Vad vill du göra? ").lower()
        try:
            if user_input == 'q':
                break
            commands[user_input](bank)
        except KeyError:
            print("Okänt kommando.")


if __name__ == '__main__':
    main()

interaktionsloop3.py med interaktiv funktion som skapar ny kund¶

from bank3 import *

def new_client(bank):
    while True:
        confirmed = ""
        client_name = input("Ange kundens namn: ")
        while confirmed not in ["j", "n"]:
            confirmed = input(f"Du skrev '{client_name}', är det OK? [j/n]: ")
        if confirmed == "j":
            bank.new_client(client_name)
            break
        else:
            print("Ok, skriv in igen!")
def show_clients(bank):
    print(bank)

def main():
    commands = {'ny kund': new_client,
                'visa kunder': show_clients}
    bank = Bank()
    
    while True:
        user_input = input("Vad vill du göra? ").lower()
        try:
            if user_input == 'q':
                break
            commands[user_input](bank)
        except KeyError:
            print("Okänt kommando.")
In [ ]:
from bank3 import *


def new_client(bank):
    while True:
        confirmed = ""
        client_name = input("Ange kundens namn: ")
        while confirmed not in ["j", "n"]:
            confirmed = input(f"Du skrev '{client_name}', är det OK? [j/n]: ")
        if confirmed == "j":
            bank.new_client(client_name)
            break
        else:
            print("Ok, skriv in igen!")


def show_clients(bank):
    print(bank)


def main():
    commands = {'ny kund': new_client,
                'visa kunder': show_clients}
    bank = Bank()

    while True:
        user_input = input("Vad vill du göra? ").lower()
        try:
            if user_input == 'q':
                break
            commands[user_input](bank)
        except KeyError:
            print("Okänt kommando.")

main()

interaktionsloop4.py med utbruten bekräftelsefunktion¶

from bank3 import *

def get_user_confirmation(message, yes, no):
    user_input = ""
    while user_input not in [yes, no]:
        user_input = input(f"{message} [{yes}/{no}]: ")
    return user_input == yes

def new_client(bank):
    while True:
        client_name = input("Ange kundens namn: ")
        message = f"Du skrev '{client_name}', är det OK?"
        if get_user_confirmation(message, "j", "n"):
            bank.new_client(client_name)
            break
        else:
            print("Ok, skriv in igen!")

def show_clients(bank):
    print(bank)
def main():
    commands = {'ny kund': new_client,
                'visa kunder': show_clients}
    bank = Bank()
    
    while True:
        user_input = input("Vad vill du göra? ").lower()
        try:
            if user_input == 'q':
                break
            commands[user_input](bank)
        except KeyError:
            print("Okänt kommando.")

if __name__ == '__main__':
    main()
In [ ]:
from bank3 import *

def get_user_confirmation(message, yes, no):
    user_input = ""
    while user_input not in [yes, no]:
        user_input = input(f"{message} [{yes}/{no}]: ")
    return user_input == yes


def new_client(bank):
    while True:
        client_name = input("Ange kundens namn: ")
        message = f"Du skrev '{client_name}', är det OK?"
        if get_user_confirmation(message, "j", "n"):
            bank.new_client(client_name)
            break
        else:
            print("Ok, skriv in igen!")


def show_clients(bank):
    print(bank)


def main():
    commands = {'ny kund': new_client,
                'visa kunder': show_clients}
    bank = Bank()

    while True:
        user_input = input("Vad vill du göra? ").lower()
        try:
            if user_input == 'q':
                break
            commands[user_input](bank)
        except KeyError:
            print("Okänt kommando.")


if __name__ == '__main__':
    main()

Instans som huvudprogram¶

interaktionsloop4.py¶

from bank3 import *

def get_user_confirmation(message, yes, no):
    user_input = ""
    while user_input not in [yes, no]:
        user_input = input(f"{message} [{yes}/{no}]: ")
    return user_input == yes

def new_client(bank):
    while True:
        client_name = input("Ange kundens namn: ")
        message = f"Du skrev '{client_name}', är det OK?"
        if get_user_confirmation(message, "j", "n"):
            bank.new_client(client_name)
            break
        else:
            print("Ok, skriv in igen!")

def show_clients(bank):
    print(bank)
def main():
    commands = {'ny kund': new_client,
                'visa kunder': show_clients}
    bank = Bank()
    
    while True:
        user_input = input("Vad vill du göra? ").lower()
        try:
            if user_input == 'q':
                break
            commands[user_input](bank)
        except KeyError:
            print("Okänt kommando.")

if __name__ == '__main__':
    main()

interaktionsloop5.py med instans som huvudprogram¶

from bank3 import *

class BankApp(object):
    
    def __init__(self):
        self.commands = {'ny kund': self.new_client,
                         'visa kunder': self.show_clients}
        self.bank = Bank()
    
    def run(self):
        while True:
            user_input = input("Vad vill du göra? ").lower()
            try:
                if user_input == 'q':
                    break
                self.commands[user_input]()
            except KeyError:
                print("Okänt kommando.")
    
    def get_user_confirmation(self, message, yes, no):
        user_input = ""
        while user_input not in [yes, no]:
            user_input = input(f"{message} [{yes}/{no}]: ")
        return user_input == yes
    def new_client(self):
        while True:
            client_name = input("Ange kundens namn: ")
            message = f"Du skrev '{client_name}', är det OK?"
            if self.get_user_confirmation(message, "j", "n"):
                self.bank.new_client(client_name)
                break
            else:
                print("Ok, skriv in igen!")
    
    def show_clients(self):
        print(self.bank)

if __name__ == '__main__':
    app = BankApp()
    app.run()
In [ ]:
from bank3 import *

class BankApp(object):

    def __init__(self):
        self.commands = {'ny kund': self.new_client,
                         'visa kunder': self.show_clients}
        self.bank = Bank()

    def run(self):
        while True:
            user_input = input("Vad vill du göra? ").lower()
            try:
                if user_input == 'q':
                    break
                self.commands[user_input]()
            except KeyError:
                print("Okänt kommando.")

    def get_user_confirmation(self, message, yes, no):
        user_input = ""
        while user_input not in [yes, no]:
            user_input = input(f"{message} [{yes}/{no}]: ")
        return user_input == yes

    def new_client(self):
        while True:
            client_name = input("Ange kundens namn: ")
            message = f"Du skrev '{client_name}', är det OK?"
            if self.get_user_confirmation(message, "j", "n"):
                self.bank.new_client(client_name)
                break
            else:
                print("Ok, skriv in igen!")

    def show_clients(self):
        print(self.bank)


if __name__ == '__main__':
    app = BankApp()
    app.run()