We beginnen met een klein beetje achtergrondinformatie. Volgens Wikipedia is sushi (寿司) een gerecht uit Japan dat in één of andere vorm altijd bestaat uit een hapje rijst van een paar centimeter dik, met daarop of daartussen de overige ingrediënten, waaronder rauwe vis of andere zeevruchten, maar ook gaar gemaakte vis, gebakken ei of groenten. De twee populairste vormen van sushi, samen met een paar onderverdelingen, zijn:

Dit soort van hiërarchische onderverdeling leent zich wonderwel tot een implementatie met gebruik van klassen en overerving.

Opgave

Als je ooit al eens bent gaan eten in een sushi-restaurant, dan heb je waarschijnlijk ook goed gelachen met de grappige zinsneden en vele spelfouten in de Engelse vertaling van de Japanse omschrijvingen van de gerechten. We gaan een eenvoudig programma schrijven dat (in theorie) door de eigenaar van een sushi-restaurant kan gebruikt worden om een menu op te stellen, compleet met Engelse en Japanse (maar wel niet de echte Japanse karakters, sorry daarvoor) omschrijving van de gerechten. We geven hieronder in verschillende stappen aan hoe dit programma tot stand moet komen.

  1. Maak om te beginnen een klasse Ingredient. Aan de initialisatiemethode moeten twee argumenten doorgegeven worden — japans en engels — die corresponderen met de Japanse en Engelse benaming van het ingrediënt. Het argument engels is optioneel, en heeft als standaardwaarde de waarde van het argument japans indien het niet wordt opgegeven (net zoals op menu's de namen van sommige ingrediënten niet vertaald worden, en je het raden hebt waarvoor ze staan). De waarde van beide argumenten moet bijgehouden worden als attribuut van de objecten van de klasse Ingredient.

    (klik hier voor een extra tip)

  2. Voeg twee methoden toe aan de klasse Ingredient: __str__(self) en engels(self). Beide methoden moeten een string teruggeven. De __str__ methode wordt automatisch aangeroepen wanneer een object van de klasse Ingredient wordt afgedrukt of omgezet naar een string. Zorg ervoor dat de methode __str__ de Japanse benaming van het ingrediënt teruggeeft, en de methode engels de Engelse vertaling ervan.

    (klik hier voor een extra tip)

  3. We hebben op basis van de gegevens op de website http://www.bento.com/sushivoc.html1 reeds een bestand aangemaakt dat een aantal veelgebruikte sushi-ingrediënten oplijst. De eerste kolom bevat de originele Japanse benaming van het ingrediënt, en de tweede kolom bevat de Engelse vertaling ervan indien die gekend is. Er zijn een aantal Japanse termen waarvoor (bewust) geen Engelse vertaling werd opgegeven (bijvoorbeeld fugu). Voor deze ingrediënten moet de Japanse benaming ook in de Engelse vertaling gebruikt worden. Je kunt het bestand hier2 downloaden.

    Schrijf een functie indexeer_ingredienten waaraan een geopend bestandsobject als argument moet doorgegeven worden. Deze functie moet alle ingrediënten uit het bestand indexeren onder de vorm van een dictionary, waarbij de Japanse benaming gebruikt wordt als sleutel, en de bijhorende waarde een Ingredient object is dat zowel de Japanse als Engelse benaming van het ingrediënt kan weergeven. Test de correcte werking van deze functie alvorens verder te gaan.

  4. Maak nu een klasse Sushi die gebruikt kan worden om verschillende sushi-gerechten voor te stellen. De klasse Sushi moet een initialisatiemethode hebben waaraan een lijst van Ingredient objecten moet doorgegeven worden.

  5. Voeg vervolgens een methode __str__(self) toe aan de klasse Sushi. Deze methode moet een string teruggeven. Deze string moet de Japanse benaming van alle ingrediënten in het gerecht oplijsten. De inhoud van de string zelf is geformuleerd in het Engels, dus bijvoorbeeld 'buri', 'buri and tsubugai' of 'buri, tsubugai and kanpachi' zijn de correcte manieren om respectievelijk één, twee en drie ingrediënten af te drukken. Het volstaat dus niet om de namen van de ingrediënten louter door komma's van elkaar te scheiden.

    (klik hier voor een extra tip)

    (klik hier voor een extra tip)

  6. Bij het aanmaken van een Sushi object moet een lijst van Ingredient objecten aan de initialisatiemethode doorgeven worden. Om de gebruiker toe te laten op een eenvoudige manier Sushi objecten aan te maken, vragen we je om een functie maak_sushi te schrijven. Aan deze functie moet een string als argument doorgegeven worden, die de Japanse benaming van alle ingrediënten van de sushi oplijst, van elkaar gescheiden door spaties. De functie moet deze string van namen eerst omzetten naar een lijst van de corresponderende Ingredient objecten, en moet daarna een Sushi object teruggeven dat wordt aangemaakt op basis van deze lijst. Uiteraard moet bij het omzetten van de string naar de lijst van Ingredient objecten gebruikgemaakt worden van de dictionary die teruggegeven wordt door de functie indexeer_ingredienten. Op basis van de functie maak_sushi wordt het eenvoudig om een Sushi object aan te maken via een string zoals 'unagi fugu ika sake'. Dit maakt het mogelijk om alle code die je tot nu toe hebt geschreven op de volgende manier te testen:

    >>> sushi = maak_sushi('unagi fugu ika sake')
    >>> print(sushi)
    unagi, fugu, ika and sake

    Let hierbij ook op het feit dat de functie maak_sushi ook sushi's moet kunnen maken op basis van ingrediënten die niet voorkomen in de dictionary. In dit geval moeten Ingredient objecten aangemaakt worden, enkel op basis van de Japanse benaming van het ingrediënt.

  7. Geef de klasse Sushi nu ook een methode engels(self), die een omschrijving van het sushi-gerecht teruggeeft waarbij de benaming van de ingrediënten nu vertaald is naar het Engels. Deze methode moet dus een gelijkaardige string teruggeven als deze die door de methode __str__ wordt teruggegeven, maar waarbij de ingrediënten in het Engels en niet in het Japans worden opgelijst. In dit geval is het echter niet wenselijk om voor de implementatie van de methode engels van een Sushi object die methode __str__ van het object aan te roepen en de ingrediënten in de teruggegeven string één voor één te vertalen. Aangezien bij het aanmaken van een Sushi object een lijst van Ingredient objecten wordt doorgegeven, volstaat het om de methode engels van de individuele Ingredient objecten aan te roepen en deze op een correctie manier op te maken met komma's en het woord and. Hiermee ben je nu ook in staat om de Engelse vertaling van een sushi-gerecht op het menu te zetten.

    >>> sushi = maak_sushi('unagi fugu ika sake')
    >>> print(sushi.engels())
    eel, fugu, squid and salmon

    Aangezien de methoden __str__ en engels van een Sushi object de lijst van ingrediëntnamen op eenzelfde manier weergeven, is het in dit geval ook interessant om een hulpmethode te schrijven die een lijst van ingrediëntnamen kan weergeven, ongeacht de taal waarin deze ingrediënten benoemd worden.

  8. Maak nu een klasse Maki die al zijn attributen overerft van de klasse Sushi. Objecten van de klasse Maki gedragen zich op precies dezelfde manier als objecten van de klasse Sushi, behalve dat in plaats van enkel de namen van de ingrediënten op te lijsten, er nu een meer informatieve omschrijving moet gegeven worden. Hierbij willen we de methoden __str__ en engels telkens een string laten teruggeven van de vorm

    [ingredienten] rolled in [rijst] and [zeewier]

    waarbij [ingredienten] onze grammaticale lijst van ingrediënten voorstelt, en [rijst] en [zeewier] twee andere ingrediënten voorstellen die consistent gebruikt worden over alle types van sushi heen. Voor deze laatste twee ingrediënten moet — net zoals dat trouwens voor alle andere ingrediënten ook het geval is — verzekerd worden dat de correcte taal gebruikt wordt op de correcte plaats. Deze vaste ingrediënten worden echter niet opgesomd in de lijst van ingrediënten die wordt opgegeven bij het aanmaken van sushi. Ze worden daarentegen geïmpliceerd door het type van sushi. Je kunt dus best constanten aanmaken voor deze vaste ingrediënten die je op de juiste plaats opneemt in de klassehiërarchie. Onderstaand codefragment geeft alvast aan hoe (maar niet waar) wij deze vaste ingrediënten gedefinieerd hebben.

    rijst = Ingredient('su-meshi', 'sushi rice')
    zeewier = Ingredient('nori', 'seaweed')
  9. Nu we reeds over twee soorten sushi beschikken, wordt het tijd om onze manier waarop deze sushi-objecten aangemaakt worden te herzien. Als vertrekpunt hebben we reeds een functie maak_sushi geschreven, die een Sushi object aanmaakt op basis van een string van Japanse ingrediëntnamen. We willen dit nu uitbreiden zodat de strings 'unagi fugu' en 'unagi fugu sushi' gezien worden als de omschrijving van een algemene sushi waarvoor er een Sushi object moet aangemaakt worden. Als het laatste woord van de omschrijving echter 'maki' zou zijn, dan moet er een Maki object aangemaakt worden. Aangezien we straks ook nog andere soorten sushi willen definiëren, moet dit onderscheid bovendien zo flexibel mogelijk gebeuren, door gebruik te maken van een implementatie die later makkelijk kan uitgebreid worden. Als algemene regel veronderstellen we dat een sushi wordt omschreven door een reeks van Japanse benamingen van ingrediënten, mogelijks gevolgd door een speciek type van sushi. Indien geen type van sushi wordt opgegeven, dan moet een Sushi object aangemaakt worden, anders moet een sushi-object van het opgegeven type aangemaakt worden.

    (klik hier voor een extra tip)

    We kunnen nu meteen ook de programmacode die zorgt voor het aanmaken van sushi-objecten een beetje herorganiseren. Tot nu toe werden sushi-objecten aangemaakt op basis van de functie maak_sushi, die op zijn beurt enerzijds gebruik maakt van de functie indexeer_ingredienten (om ingrediënten aan te maken) en van een datastructuur die de verschillende types van sushi voorstelt. Als we dan toch objectgericht aan het programmeren zijn, dan is het beter om al deze hulpmiddelen te bundelen in een klasse SushiMaker. We kunnen de initialisatiemethode van deze klasse parameteriseren met een geopend bestandsobject waaruit de vertaling van de sushi-ingrediënten kan gelezen worden. Standaard kan dit bestandsobject geopend worden voor het inlezen van het bestand waarnaar hierboven verwezen wordt. Dan kunnen sushi-objecten op de volgende manier aangemaakt worden

    >>> chef = SushiMaker()
    >>> sushi = chef.maak_sushi('unagi fugu sushi')
    >>> print(sushi)
    unagi and fugu
    >>> print(sushi.engels())
    eel and fugu
    >>> sushi = chef.maak_sushi('unagi fugu maki')
    >>> print(sushi)
    unagi and fugu rolled in su-meshi and nori
    >>> print(sushi.engels())
    eel and fugu rolled in sushi rice and seaweed
  10. Cool! Maar we moeten nu wel nog een aantal soorten sushi toevoegen aan ons arsenaal. Futomaki, temaki en uramaki zijn allemaal ondersoorten van maki, en kunnen dus voorgesteld worden door klassen die erven van de klasse Maki. De manier waarop de omschrijving van elk van deze soorten als string moet gebeuren is de volgende:

    Futomaki: '[ingredienten] rolled in [rijst] and [zeewier], with [zeewier] facing out'
    Temaki: 'cone of [zeewier] filled with [rijst] and [ingredienten]'
    Uramaki: '[ingredienten] rolled in [zeewier] and [rijst], with [rijst] facing out'

    Voor de implementatie van deze verschillende omschrijvingen kan het handig zijn om gebruik te maken van een formaatstring3. Als je bijvoorbeeld de volgende string definieert

    >>> omschrijving = '{ingredienten} rolled in {rijst} and {zeewier}, with {zeewier} facing out'

    dan kan je het volgende doen

    >>>  omschrijving.format(rijst='su-meshi', zeewier='nori', ingredienten='unagi fugu')
    'unagi fugu rolled in su-meshi and nori, with nori facing out'

    Dit is eigenlijk een behoorlijk krachtig instrument, omdat het toelaat om rijst en zeewier in te sluiten in de dictionary, zelfs als ze niet voorkomen in de formaatstring! Met deze informatie in de hand, kan je nu de basisklasse Sushi herschrijven, zodat de omschrijving gebeurt op basis van een eigenschap van het object die als formaatstring gebruikt wordt in combinatie met een dictionary van 'rijst', 'zeewier' en 'ingredienten'. Op die manier volstaat het dat elke kindklasse een eigen formaatstring definieert en alles werkt zoals het moet. Nieuwe kindklassen aanmaken kan dan eenvoudigweg op de volgende manier gebeuren

    class Futomaki(Maki):
        
        omschrijving = '{ingredienten} rolled in {rijst} and {zeewier}, with {zeewier} facing out'
     
    class Temaki(Maki):
        
        omschrijving = 'cone of {zeewier} filled with {rijst} and {ingredienten}'

    Zorg er voor dat dit zowel werkt voor omschrijvingen met Japanse en Engelse benamingen van de ingrediënten, en zorg er ook voor dat de datastructuur met de verschillende soorten sushi in de klasse SushiMaker op een gepaste manier wordt aangepast. Op deze manier moet het volgende nu ook werken:

    >>> chef = SushiMaker()
    >>> sushi = chef.maak_sushi('unagi ohyo uramaki')
    >>> print(sushi)
    unagi and ohyo rolled in nori and su-meshi, with su-meshi facing out
    >>> print(sushi.engels())
    eel and halibut rolled in seaweed and sushi rice, with sushi rice facing out
    >>> sushi = chef.maak_sushi('ikura temaki')
    >>> print(sushi)
    cone of nori filled with su-meshi and ikura
    >>> print(sushi.engels())
    cone of seaweed filled with sushi rice and salmon roe
  11. We zijn er bijna. Er moeten nog een paar klassen aangemaakt worden voor een laatste reeks sushisoorten. Voeg een klasse Nigiri toe die erft van de klasse Sushi, en voeg twee klassen Gunkanmaki en Temarizushi toe die erven van de klasse Nigiri. Omdat nigiri normaal gezien maar één topping heeft, moet je voordeel zien te halen uit de overerving om af te dwingen dat deze voorwaarde geldt voor alle soorten nigiri. Dit doe je door een aangepaste implementatie van de methode __init__ te voorzien binnen de klasse Nigiri. Indien deze voorwaarde geschonden wordt, dan moet een InvalidSushiError opgeworpen worden (deze moet je zelf definiëren, aangezien de Python bibliotheken niet zo volledig zijn dat dit soort fouten reeds bestaat). Vergeet bij het herimplementeren van de methode __init__ ook niet om de methode __init__ van de basisklasse aan te roepen. De omschrijvingen van de soorten sushi die horen bij deze klassen zijn

    Nigiri: 'hand-formed [rijst] topped with [ingredienten]'
    Gunkanmaki: '[ingredienten] on [rijst] wrapped in a strip of [zeewier]'
    Temarizushi: '[ingredienten] pressed into a ball of [rijst]'

    (klik hier voor een extra tip)

    Als laatste test, moet nu ook het volgende werken

    >>> chef = SushiMaker()
    >>> sushi = chef.maak_sushi('fugu ohyo ika unagi')
    >>> print(sushi)
    fugu, ohyo, ika and unagi
    >>> print(sushi.engels())
    fugu, halibut, squid and eel
    >>> sushi = chef.maak_sushi('fugu ohyo ika unagi sushi')
    >>> print(sushi)
    fugu, ohyo, ika and unagi
    >>> print(sushi.engels())
    fugu, halibut, squid and eel
    >>> sushi = chef.maak_sushi('ika sake gunkanmaki')
    Traceback (most recent call last):
    InvalidSushiError: Nigiri heeft slechts één topping

Epiloog

sushi
Deze zeldzame sneeuwrol toont aan dat zelfs de wind soms zin heeft in sushi.