Bygg en Python Multiplication Table App med OOP

By rik

I denna artikel kommer vi att konstruera en applikation för multiplikationstabeller genom att utnyttja objektorienterad programmering (OOP) i Python.

Du kommer att öva på de centrala principerna inom OOP och hur de kan tillämpas i en fungerande app.

Python är ett mångfacetterat programmeringsspråk som ger oss utvecklare möjlighet att välja den mest lämpliga metoden för varje situation. Inom objektorienterad programmering finner vi ett av de mest använda sätten för att skapa skalbara applikationer under de senaste decennierna.

Grunder i OOP

Vi ska snabbt gå igenom det mest grundläggande konceptet i OOP med Python, nämligen klasser.

En klass fungerar som en mall där vi fastställer strukturen och beteendet för objekt. Denna mall ger oss möjligheten att skapa instanser, som är individuella objekt skapade i enlighet med klassens struktur.

En enkel bokklass, med attribut för titel och färg, skulle definieras som nedan:

class Book:
    def __init__(self, title, color):
        self.title = title
        self.color = color

För att skapa instanser av klassen Book måste vi anropa klassen och tillhandahålla argument.

# Instansiera objekt av Book-klassen
blue_book = Book("Den blå pojken", "Blå")
green_book = Book("Sagan om grodan", "Grön")

En grafisk framställning av vårt program kan se ut så här:

Det som är intressant är att när vi kontrollerar typen av `blue_book` och `green_book` instanserna, får vi ”Book”.

# Skriv ut boktyper

print(type(blue_book))
# <class '__main__.Book'>
print(type(green_book))
# <class '__main__.Book'>

Med dessa koncept tydliga, kan vi påbörja projektet 😃.

Projektbeskrivning

Som utvecklare/programmerare spenderas inte merparten av tiden på att skriva kod. Enligt thenewstack ägnas endast en tredjedel av tiden åt att skriva eller refaktorera kod.

De resterande två tredjedelarna spenderas med att läsa andras kod och analysera problemet som vi arbetar med.

För detta projekt kommer jag att formulera ett problem och vi kommer att analysera hur vi utvecklar vår applikation från det. Genom detta genomgår vi hela processen, från att tänka ut lösningen till att implementera den med kod.

En lärare i grundskolan önskar ett spel för att pröva multiplikationsförmågan hos elever i åldrarna 8 till 10 år.

Spelet måste ha ett system för liv och poäng, där eleven börjar med 3 liv och måste uppnå ett visst antal poäng för att vinna. Programmet ska visa ett meddelande om att eleven förlorat om alla liv har förbrukats.

Spelet ska ha två lägen: slumpmässiga multiplikationer och multiplikationstabeller.

Det första läget ska ge eleven en slumpmässig multiplikation från 1 till 10, och eleven måste svara rätt för att få en poäng. Om det inte sker, förlorar eleven ett liv och spelet fortsätter. Eleven vinner när hen når 5 poäng.

Det andra läget måste visa en multiplikationstabell från 1 till 10, där eleven ska mata in resultatet av varje multiplikation. Om eleven misslyckas 3 gånger, förlorar hen, men om hen slutför två tabeller avslutas spelet.

Jag är medveten om att kraven kan verka lite omfattande, men jag lovar att vi kommer att lösa dem i den här artikeln 😁.

Dela och härska

Den viktigaste förmågan inom programmering är problemlösning. Det innebär att du måste ha en plan innan du börjar skriva kod.

Jag rekommenderar alltid att ta ett stort problem och dela upp det i mindre delar som kan lösas enkelt och effektivt.

Så om du ska skapa ett spel, börja med att dela upp det i de viktigaste komponenterna. Dessa delproblem blir mycket lättare att hantera.

Sedan kan du tydligt se hur du ska köra och integrera allt med hjälp av kod.

Låt oss göra en graf över hur spelet kan se ut.

Grafen klargör relationerna mellan objekten i applikationen. Som du ser är de två huvudobjekten slumpmässig multiplikation och tabellmultiplikation. Det enda de har gemensamt är attributen Poäng och Liv.

Med all denna information i åtanke, låt oss övergå till koden.

Skapa spel-föräldraklassen

När vi arbetar med objektorienterad programmering strävar vi efter det renaste sättet att undvika upprepad kod. Det kallas DRY (Don’t Repeat Yourself).

Obs: Det här syftet handlar inte om att skriva färre rader kod (kodkvalitet ska inte bedömas utifrån det), utan snarare om att abstrahera den mest frekvent använda logiken.

Enligt den tidigare tanken måste moderklassen för vår app fastställa strukturen och önskat beteende för de andra två klasserna.

Låt oss undersöka hur det kan göras.

class BaseGame:

    # Längd som meddelandet centreras på
    message_lenght = 60
    
    description = ""    
        
    def __init__(self, points_to_win, n_lives=3):
        """Bas-spelklass

        Args:
            points_to_win (int): De poäng som krävs för att slutföra spelet
            n_lives (int): Antalet liv eleven har. Standard är 3.
        """
        self.points_to_win = points_to_win

        self.points = 0
        
        self.lives = n_lives

    def get_numeric_input(self, message=""):

        while True:
            # Hämta användarens input
            user_input = input(message) 
            
            # Om input är numerisk, returnera den
            # Om den inte är det, skriv ut ett meddelande och upprepa
            if user_input.isnumeric():
                return int(user_input)
            else:
                print("Input måste vara ett nummer")
                continue     
             
    def print_welcome_message(self):
        print("PYTHON MULTIPLIKATIONSSPEL".center(self.message_lenght))

    def print_lose_message(self):
        print("TYVÄRR FÖRLORADE DU ALLA DINA LIV".center(self.message_lenght))

    def print_win_message(self):
        print(f"GRATTIS DU UPPNÅDDE {self.points}".center(self.message_lenght))
        
    def print_current_lives(self):
        print(f"Du har för närvarande {self.lives} liv\n")

    def print_current_score(self):
        print(f"\nDitt poäng är {self.points}")

    def print_description(self):
        print("\n\n" + self.description.center(self.message_lenght) + "\n")

    # Grundläggande körningsmetod
    def run(self):
        self.print_welcome_message()
        
        self.print_description()

Oj, det verkar vara en ganska omfattande klass. Låt mig förklara den mer ingående.

Låt oss först förstå klassattributen och konstruktorn.

Klassattribut är i grunden variabler som skapas inuti klassen, men utanför konstruktorn eller någon annan metod.

Instansattribut är variabler som endast skapas inuti konstruktorn.

Den största skillnaden mellan dessa två är deras omfattning. Klassattribut är tillgängliga både från ett instansobjekt och klassen. Instansattribut är å andra sidan endast tillgängliga från ett instansobjekt.

game = BaseGame(5)

# Åtkomst till spelets meddelandelängds klassattribut från klassen
print(game.message_lenght) # 60

# Åtkomst till klassattributet message_lenght från klassen
print(BaseGame.message_lenght)  # 60

# Åtkomst till instansattributet points från instansen
print(game.points) # 0

# Åtkomst till instansattributet points från klassen
print(BaseGame.points) # Attributfel

En annan artikel kan gå mer detaljerat in på detta ämne. Håll utkik för att läsa den.

Funktionen get_numeric_input används för att förhindra att användaren anger annat än numerisk data. Som du kanske märker är metoden utformad för att fråga användaren tills den får ett numeriskt värde. Vi kommer att använda den senare i barnklasserna.

Utskriftsmetoderna ger oss möjlighet att undvika att skriva ut samma information varje gång en händelse inträffar i spelet.

Slutligen är körningsmetoden bara en samlingsmetod som klasserna RandomMultiplication och TableMultiplication använder för att interagera med användaren och göra allt funktionellt.

Skapa barnklasserna

När vi har skapat den överordnade klassen som bestämmer strukturen och delar av funktionaliteten i vår applikation, är det dags att konstruera de faktiska spellägesklasserna genom att använda oss av arv.

Slumpmässig multiplikationsklass

Den här klassen kommer att köra det ”första läget” i vårt spel. Den använder modulen random, som ger oss möjligheten att fråga användaren om slumpmässiga multiplikationer från 1 till 10. Här är en utmärkt artikel om random (och andra viktiga moduler) 😉.

import random # Modul för slumpmässiga multiplikationer
class RandomMultiplication(BaseGame):

    description = "I det här spelet måste du svara rätt på slumpmässiga multiplikationer.\nDu vinner om du når 5 poäng, eller förlorar om du förlorar alla dina liv."

    def __init__(self):
        # Antal poäng som behövs för att vinna är 5
        # Skicka argumentet "points_to_win" med värdet 5
        super().__init__(5)

    def get_random_numbers(self):

        first_number = random.randint(1, 10)
        second_number = random.randint(1, 10)

        return first_number, second_number
        
    def run(self):
        
        # Anropa föräldraklassen för att skriva ut välkomstmeddelandena
        super().run()
        

        while self.lives > 0 and self.points_to_win > self.points:
            # Får två slumpmässiga siffror
            number1, number2 = self.get_random_numbers()

            operation = f"{number1} x {number2}: "

            # Frågar användaren att svara på multiplikationen
            # Förhindrar fel
            user_answer = self.get_numeric_input(message=operation)

            if user_answer == number1 * number2:
                print("\nDitt svar är rätt\n")
                
                # Lägger till en poäng
                self.points += 1
            else:
                print("\nTyvärr, ditt svar är fel\n")

                # Minskar ett liv
                self.lives -= 1
            
            self.print_current_score()
            self.print_current_lives()
            
        # Exekveras endast när spelet är avslutat
        # Och inget av villkoren är sanna
        else:
            # Skriver ut det slutliga meddelandet
            
            if self.points >= self.points_to_win:
                self.print_win_message()
            else:
                self.print_lose_message()

Det här är också en omfattande klass 😅. Men som jag sa tidigare är det inte antalet rader som räknas, utan snarare hur läsbar och effektiv den är. Och det som är bra med Python är att det gör det möjligt för utvecklare att skapa ren och läsbar kod som om de pratade normal engelska.

Den här klassen innehåller en sak som kan verka förvirrande, men jag ska förklara det så enkelt som möjligt.

    # Föräldraklass
    def __init__(self, points_to_win, n_lives=3):
        "...
    # Barnklass
    def __init__(self):
        # Antalet poäng som krävs för att vinna är 5
        # Skicka argumentet "points_to_win" med värdet 5
        super().__init__(5)

Konstruktorn i barnklassen anropar superfunktionen, som refererar till föräldraklassen (BaseGame). Det berättar i princip för Python:

Fyll i attributet ”points_to_win” i föräldraklassen med värdet 5!

Det är inte nödvändigt att lägga in self inuti super().__init__()-delen enbart för att vi anropar super inuti konstruktorn, det skulle leda till redundans.

Vi använder även superfunktionen i körningsmetoden, och vi kommer att se vad som händer i den kodbiten.

    # Grundläggande körningsmetod
    # Föräldrametod
    def run(self):
        self.print_welcome_message()
        
        self.print_description()
    def run(self):
        
        # Anropa föräldraklassen för att skriva ut välkomstmeddelandena
        super().run()
        
        .....

Som du kanske märker skriver körningsmetoden i föräldraklassen ut välkomst- och beskrivningsmeddelandet. Men det är bra att behålla den funktionen och även lägga till extra i barnklasserna. Därför använder vi super för att köra all kod från föräldrametoden innan vi kör nästa del.

Den andra delen av körningsfunktionen är ganska enkel. Den frågar användaren efter ett nummer med meddelandet om multiplikationen hen ska svara på. Sedan jämförs resultatet med den faktiska multiplikationen, och om de är lika läggs en poäng till, om de inte är lika förloras ett liv.

Det är värt att nämna att vi använder while-else-loopar. Det här överstiger omfattningen för den här artikeln, men jag kommer att publicera en artikel om det om några dagar.

Slutligen använder get_random_numbers-funktionen random.randint, som returnerar ett slumpmässigt heltal inom ett angivet intervall. Sedan returneras en tupel med två slumpmässiga heltal.

Multiplikationstabellsklass

Det ”andra läget” ska visa spelet i ett multiplikationstabellformat och säkerställa att användaren svarar rätt på minst 2 tabeller.

För det syftet använder vi superfunktionen igen och ändrar attributet för den överordnade klassen points_to_win till 2.

class TableMultiplication(BaseGame):

    description = "I det här spelet ska du lösa hela multiplikationstabellen korrekt.\nDu vinner om du löser 2 tabeller."
    
    def __init__(self):
        # Behöver slutföra 2 tabeller för att vinna
        super().__init__(2)

    def run(self):

        # Skriv ut välkomstmeddelandena
        super().run()

        while self.lives > 0 and self.points_to_win > self.points:
            # Hämtar ett slumpmässigt nummer
            number = random.randint(1, 10)            

            for i in range(1, 11):
                
                if self.lives <= 0:
                    # Ser till att spelet inte kan fortsätta
                    # om användaren förlorar alla sina liv

                    self.points = 0
                    break 
                
                operation = f"{number} x {i}: "

                user_answer = self.get_numeric_input(message=operation)

                if user_answer == number * i:
                    print("Bra! Ditt svar är rätt")
                else:
                    print("Tyvärr, ditt svar är fel") 

                    self.lives -= 1

            self.points += 1
            
        # Körs endast när spelet är avslutat
        # Och inget av villkoren är sanna
        else:
            # Skriver ut det slutliga meddelandet
            
            if self.points >= self.points_to_win:
                self.print_win_message()
            else:
                self.print_lose_message()

Som du förstår ändrar vi bara körningsmetoden för den här klassen. Det är magin med arv, vi skriver logiken en gång och använder den på flera ställen, och glömmer bort det 😅.

I körningsmetoden använder vi en for-loop för att hämta siffrorna från 1 till 10 och bygga multiplikationen som visas för användaren.

Återigen, om livet är förbrukat eller poängen som krävs för att vinna har nåtts, kommer while-loopen att avbrytas och meddelandet om vinst eller förlust visas.

JA, vi har konstruerat spelets två lägen, men hittills kommer ingenting att hända om vi kör programmet.

Låt oss avsluta programmet genom att implementera lägesvalet och instansiera klasserna beroende på valet.

Implementering av lägesval

Användaren ska kunna välja vilket läge hen vill spela. Så låt oss se hur man implementerar det.

if __name__ == "__main__":

    print("Välj spelläge")

    choice = input("[1],[2]: ")

    if choice == "1":
        game = RandomMultiplication()
    elif choice == "2":
        game = TableMultiplication()
    else:
        print("Välj ett giltigt spelläge")
        exit()

    game.run()

Först ber vi användaren att välja mellan 1 eller 2 lägen. Om input är ogiltig slutar skriptet att köras. Om användaren väljer det första läget, kommer programmet att köra slumpmässigt multiplikationsläge, och om hen väljer det andra, körs tabellmultiplikationsläget.

Så här kan det se ut.

Sammanfattning

Grattis, du har precis byggt en Python-app med objektorienterad programmering.

All kod finns i Github-förvaret.

I denna artikel har du lärt dig att:

  • Använda konstruktorer i Python-klasser
  • Skapa en funktionell applikation med OOP
  • Använda superfunktionen i Python-klasser
  • Tillämpa de grundläggande principerna för arv
  • Implementera klass- och instansattribut

Lycka till med kodningen 👨‍💻

Utforska sedan några av de bästa Python IDE för bättre produktivitet.