Mer om funktioner, namespaces och scope
Skriv lösningarna till uppgifterna i en och samma fil och testa koden själv innan du använder rättningsskriptet. Att kunna testa sin kod är en viktig del av att programmera!
Att lära dig från uppgifterna
- Känna till pythons olika namnrymder och hur de förhåller sig till varandra.
- Förstå hur importer fungerar i förhållande till namnrymder.
- Kunna skapa enklare closures.
Man kan få max 60 poäng och för att få godkänt krävs 30 poäng (45 för väl godkänt). Försök dock att lösa alla uppgifter då inte alla fel upptäcks av rättningsskriptet. Om ni har lite marginal kan ni kanske bli godkända även om assistenten som rättar hittar något sådant fel.
Det som täcks i det här kapitlet är inte nödvändigt för att kunna lösa de flesta uppgifter i kursen men hjälper till att förstå hur Python fungerar under ytan och kan vara användbart i mer avancerade sammanhang. Vi kommer också att spendera mycket av andra halvan av kursen med objektorienterad programmering, som kan ses som ett syntaktiskt bekvämare sätt att arbeta med closures, som vi introducerar nedan.
Vi har tidigare sett att variabler som skapas inuti en funktion inte är synliga utanför funktionen och att vi kan ha två variabler med samma namn på olika platser i koden utan att de krockar med varandra. Vi kallar detta för scoping och det är en viktig del av hur Python (och många andra programmeringsspråk) fungerar. En variabels scope är det område i koden där variabeln är synlig och kan användas.
Vi kan också prata om det omvända från scope, nämligen namnrymd, eller namespace. En namnrymd är en samling av namn (variabler, funktioner, klasser, etc.) som delar samma scope.
Namnrymder
När vi skapar en variabel i Python genom att tilldela den för första gången så skapas den i den aktuella namnrymden. Är det inne i en funktion så skapas variabeln i den lokala namnrymden för den funktionen. När vi skapar en variabel utanför alla funktioner så skapas den i den globala namnrymden. I Python finns det också en inbyggd namnrymd som innehåller alla inbyggda funktioner och variabler, t.ex. print
, len
, int
, etc.
När vi försöker använda ett namn, t.ex. på en variabel eller en funktion, så letar Pythontolken efter namnet i den aktuella namnrymden. Om namnet inte finns där så letar den i den omgivande namnrymden, och så vidare. I bilden till höger kan vi tänka oss att vi använder print
inne i en funktion. Först letar Pythontolken efter en lokal variabel, sedan en global variabel, och slutligen en inbyggd variabel, där den kommer hitta funktionen print
. Om namnet inte finns i någon av dessa namnrymder så hade vi fått ett NameError
.
Vi kan explicit få en referens till den globala namnrymden med hjälp av funktionen globals
. Denna funktion returnerar ett dictionary med alla namn i den globala namnrymden som nycklar och deras värden som värden. Vi kan t.ex. skriva globals()['x']
för att komma åt den globala variabeln x
även om vi är inne i en funktion där det finns en lokal variabel med samma namn.
På samma sätt kan vi explicit få en referens till den lokala namnrymden med hjälp av funktionen locals
. Denna funktion returnerar ett dictionary med alla namn i den lokala namnrymden som nycklar och deras värden som värden. Vi kan t.ex. skriva locals()['y']
för att komma åt den lokala variabeln y
inne i en funktion. Anropar vi locals
utanför en funktion så får vi den globala namnrymden, eftersom vi inte är inne i någon funktion med en lokal namnrymd.
Moduler och namnrymder
Prefix
Vi har vid några tillfällen importerat moduler i våra skript, t.ex. math
eller random
. När vi gör en import så skapas en ny namnrymd och en referens till den namnrymden. Alla namn (funktioner, variabler, klasser, etc.) som definieras i modulen hamnar i den nya namnrymden och kan nås genom att använda modulens namn som prefix.
Beroende på var vi gör importen så kan variabeln med referensen till den nya namnrymden skapas i olika namnrymder. Om vi gör importen i den globala namnrymden så skapas variabeln i den globala namnrymden. Om vi gör importen inuti en funktion så skapas variabeln i den lokala namnrymden för den funktionen (se t.ex. bilden till höger). Normalt sett gör vi importer längst upp i ett skript och därför i den globala namnrymden, men det är fullt möjligt att göra importen inuti en funktion om vi vill begränsa var vi ska ha tillgång till modulen.
Det är lätt att bli förvirrad över att vi i en namnrymd kan ha en variabel som i sin tur kan användas för att komma åt en helt annan namnrymd. Vi kan dock tänka på det som mängder. Om den globala namnrymden är en mängd så innehåller den mängden i sin tur mängder som representerar de importerade modulerna.
T.ex. om vi importerar math
i den globala namnrymden kan vi se den globala namnrymden som en mängd där ett av elementen är math
. Elementet math
är i sin tur en referens till en annan mängd med namn, nämligen alla namn som definieras i math
-modulen. När vi skriver math.sqrt
så letar Pythontolken först upp math
i den globala namnrymden, och sedan letar den upp sqrt
i den namnrymden som math
refererar till.
Precis som mängder kan innehålla andra mängder så kan namnrymder innehålla referenser till andra namnrymder. Detta är en grundläggande del av hur moduler och paket fungerar i Python.
Vi kan ta reda på vilka namn som finns i en viss namnrymd med hjälp av funktionen dir
. Om vi t.ex. skriver dir(math)
så får vi en lista med alla namn som finns i math
-modulen. Detta kan vara användbart för att utforska en modul och se vilka funktioner och variabler som finns tillgängliga.
>>> import math
['__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'comb', 'copysign', 'cos', 'cosh', 'degrees', 'dist', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'isqrt', 'lcm', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'nextafter', 'perm', 'pi', 'pow', 'prod', 'radians', 'remainder', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'tau', 'trunc', 'ulp']
Vi kan få ytterligare lite information genom att inspektera en importerad moduls __dict__
-attribut, som är ett dictionary med alla namn i modulen som nycklar och vad namnen referererar till som värden. Detta kan vara användbart för att se vilka typer av objekt som finns i modulen, t.ex. om de är funktioner, klasser, variabler, etc. men det kan vara svårt att tolka vad allt betyder:
>>> math.__dict__
{'__name__': 'math', '__doc__': 'This module provides access to the mathematical functions\ndefined by the C standard.', '__package__': '', '__loader__': <_frozen_importlib_external.ExtensionFileLoader object at 0x7f5626c489a0>, '__spec__': ModuleSpec(name='math', loader=<_frozen_importlib_external.ExtensionFileLoader object at 0x7f5626c489a0>, origin='/home/johsj47/miniconda3/envs/py3_10/lib/python3.10/lib-dynload/math.cpython-310-x86_64-linux-gnu.so'), 'acos': <built-in function acos>, 'acosh': <built-in function acosh>, 'asin': <built-in function asin>, ...
...
Alias
Vi kan dock också importera moduler och ge dem specifika namn i den aktuella namnrymden. Detta görs med import ... as ...
syntaxen. T.ex. kan vi skriva import random as rnd
för att importera random
-modulen och ge den namnet rnd
i den aktuella namnrymden. Detta kan vara användbart om vi vill undvika namnkonflikter eller om vi vill ha ett kortare namn att använda. Vi ska dock undvika att använda onödiga alias då det lätt kan göra koden svårare att läsa.
Vissa välkända moduler har konventionellt kortare namn som används ofta, t.ex. import numpy as np
eller import pandas as pd
. Man ska därför generellt sett undvika att använda andra alias för dessa moduler än de konventionella, och undvika att använda dessa konventionella förkortningar som alias för andra moduler. Om möjligt bör man generellt undvika att använda alias alls för mindre kända moduler.
Vi kan också importera en modul baserat på en sträng som innehåller modulens namn med hjälp av funktionen __import__
. Detta kan vara användbart om vi vill importera moduler dynamiskt baserat på användarinput eller konfigurationsfiler. Detta bör undvikas i vanliga fall eftersom det kan göra koden svårare att läsa och förstå men vi kommer använda det just för att fördjupa vår förståelse.
>>> math = __import__('math')
>>> math.sqrt(16)
4.0
Enstaka namn
Slutligen kan vi importera enstaka namn från en modul med from ... import ...
syntaxen. T.ex. kan vi skriva from math import sqrt
för att importera bara sqrt
-funktionen från math
-modulen och göra den tillgänglig i den aktuella namnrymden utan att behöva använda math
-prefixet. Detta kan vara användbart om vi bara behöver en eller några få funktioner från en modul och vill undvika att importera hela modulen.
Även här ska man dock vara försiktig med att undvika namnkonflikter och göra koden svårare att läsa. Om vi t.ex. importerar sqrt
från math
och sedan definierar en egen funktion med samma namn i den aktuella namnrymden så kommer den egna funktionen att skugga den importerade funktionen, vilket kan leda till förvirring.
Det finns också en syntax för att importera alla namn från en modul med from ... import *
, men detta bör undvikas eftersom det kan leda till namnkonflikter och göra koden svårare att förstå. Att vi ens tar upp det här är för att det tyvärr är så vanligt förekommande att man kommer stöta på det i kod som skrivits av andra, förr eller senare.
Uppgift 13.1 (10p)
Skapa en funktion get_value_from_module(module_name, name)
som tar in namnet på en modul och ett namn som existerar i den modulen som strängar. Funktionen ska returnera det objekt som namnet refererar till. Vi antar att modulen finns men om inget objekt med det angivna namnet finns i modulen ska None
returneras.
Anropet funcname = get_value_from_module('random', 'randint')
ska alltså göra samma sak som from random import randint as funcname
. Skillnaden är att module_name
och name
är strängar som skickas in som argument till funktionen.
Tips: Funktionerna __import__
och dir
är lämpliga att använda, samt module.__dict__
.
Exempel
>>> random_integer_between = get_value_from_module('random', 'randint')
>>> print(random_integer_between)
<bound method Random.randint of <random.Random object at 0x26c4bc30>>
>>> random_integer_between(1, 10)
4
>>> random_float_between = get_value_from_module('random', 'randfloat')
>>> print(random_float_between)
None
Uppgift 13.2 (10p)
Skriv en funktion list_functions(module_name)
som tar in namnet på en modul som en sträng. Funktionen ska returnera en lista med namnen på alla funktioner i modulen (som strängar). Antag att modulen finns.
Kom ihåg att inte allt i ett namespace, t.ex. i en modul, nödvändigtvis är en funktion (math.pi
t.ex.), så ni måste kontrollera att varje namn ni returnerar faktiskt refererar till en funktion. Ni kan använda funktionen callable()
för att kontrollera detta.
Exempel
>>> list_functions('collections')
['_chain', '_repeat', '_starmap', '_iskeyword', '_eq', '_itemgetter', '_recursive_repr', '_proxy', 'deque', 'defaultdict', '_OrderedDictKeysView', '_OrderedDictItemsView', '_OrderedDictValuesView', '_Link', 'OrderedDict', '_tuplegetter', 'namedtuple', '_count_elements', 'Counter', 'ChainMap', 'UserDict', 'UserList', 'UserString']
>>> list_functions('itertools')
['__loader__', 'tee', 'accumulate', 'combinations', 'combinations_with_replacement', 'cycle', 'dropwhile', 'takewhile', 'islice', 'starmap', 'chain', 'compress', 'filterfalse', 'count', 'zip_longest', 'pairwise', 'permutations', 'product', 'repeat', 'groupby', '_grouper', '_tee', '_tee_dataobject']
Closures
Den omgivande namnrymden och LEGB-reglerna
Ovan har vi pratat om lokala, globala och inbyggda namnrymderna. Det finns dock också en fjärde namnrymd som kallas den omgivande namnrymden, som är den namnrymd som omger den aktuella funktionen. Denna namnrymd blir relevant först när vi har nästlade funktioner, dvs. funktioner definierade inuti andra funktioner. Den omgivande namnrymden är då den lokala namnrymden för den yttre funktionen.
Pythons scoping-regler brukar förkortas LEGB som är en förkortning för Local, Enclosing, Global, Built-in. Det är den ordning som Python använder för att leta efter namn när de används i en funktion. Först letar Pythontolken i den lokala namnrymden (Local), sedan i den omgivande namnrymden (Enclosing), sedan i den globala namnrymden (Global). Slutligen letar Pythontolken i namnrymnden med inbyggda funktioner och variabler (Built-in). Precis som vi beskrev ovan, fast med den extra omgivande namnrymden inlagd mellan lokal och global.
OBS! LEGB-reglerna beskriver hur Pythontolken letar efter existerande namn. När vi däremot tilldelar en variabel så skapas den alltid i den aktuella namnrymden, dvs. den lokala inne i en funktion eller den globala utanför en funktion, om inget annat anges.
Closures och högre ordningens funktioner
Vi har tidigare stött på begreppet högre ordningens funktioner, dvs. funktioner som tar andra funktioner som argument eller returnerar funktioner som resultat. Vi använde t.ex. en högre ordningens funktion i förberedelsematerialet för seminariet om godtyckligt nästlade strukturer. I det fallet skrev vi en funktion get_value
som tog en funktion som användes för att hantera eventuella nästlade värden.
Idag ska vi ta konceptet med högre ordningens funktioner ett steg längre och titta på funktioner som returnerar funktioner. Vi ska använda detta för att skapa så kallade closures. En closure är en funktion som “kommer ihåg” variabler från den omgivande namnrymden där den skapades, även efter att den omgivande funktionen har avslutats.
Tilldela variabler i den omgivande namnrymden
Det klassiska exemplet på en closure är en räknare som räknas upp varje gång den anropas:
|
|
Notera att vi på sista raden i make_counter
returnerar den nästlade funktionen counter
, inte ett anrop till counter
. Detta innebär att när vi anropar make_counter
så får vi tillbaka själva funktionen counter
, som vi sedan kan anropa flera gånger. Eftersom counter
är definierad inuti make_counter
så har den tillgång till variabeln count
, som är definierad i den omgivande namnrymden. Detta innebär att varje gång vi anropar counter
så ökar vi värdet på count
och returnerar det nya värdet.
Vi kan använda vår räknare så här:
|
|
Här använde vi nyckelordet nonlocal
för att tala om för Python att vi vill använda variabeln count
från den omgivande namnrymden, dvs. från funktionen make_counter
. Utan nonlocal
skulle Pythontolken anta att vi försöker skapa en ny, lokal, variabel count
när den tilldelas i funktionen counter
. Eftersom vi använder den utökade tilldelningen +=
, som antar att variabeln vi adderar till redan finns, så skulle vi därför fått ett fel här. Det här är en klassisk fälla när man arbetar med closures i Python.
I praktiken vill vi helst undvika att skapa closures med nonlocal
utan istället använda muterbara objekt som listor eller dictionaries för att lagra tillståndet i den omgivande namnrymden. Detta är oftast enklare och mindre felbenäget än att använda nonlocal
.
Använda referenser i den omgivande namnrymden
Vi kan använda samma generella koncept för att skapa abstrakta datatyper, t.ex. en kö, som den vi tittade på i föreläsningen för Tema 3:
|
|
Vi kan använda vår kö på följade sätt:
>>> enqueue, dequeue = make_queue()
>>> enqueue(1)
>>> enqueue(2)
>>> enqueue(3)
>>> dequeue()
1
>>> dequeue()
2
>>> enqueue(4)
>>> dequeue()
3
>>> dequeue()
4
>>> dequeue()
...
IndexError: dequeue from empty queue
Notera att vi inte behövde använda nonlocal
här eftersom vi inte försöker ändra referensen till listan items
. Vi ändrar bara innehållet i listan och vi behöver alltså bara slå upp items
, inte tilldela den. Listan är ett muterbart objekt, så när vi anropar append
eller pop
så ändrar vi innehållet i den lista som items
refererar till, utan att ändra själva referensen.
Här får vi en påminnelse om hur Pythons LEGB-regler fungerar. Vi kan slå upp variabler i den omgivande namnrymden (Enclosing) utan att behöva använda nonlocal
, men vill vi tilldela dem så måste vi använda nonlocal
.
Notera också att vi här returnerar två funktioner, enqueue
och dequeue
, som båda har tillgång till den gemensamma variabeln items
i den omgivande namnrymden, dvs. den lokala namnrymden för funktionen make_queue
. Detta gör att vi kan implementera kö-funktionaliteten utan att exponera den interna representationen av kön (dvs. listan items
) för användaren av kön.
Closures med initiala värden
Vi kan skapa closures där vissa variabler sätts när make_
-funktionen anropas. Ett exempel på detta är en funktion som skapar en adderingsfunktion med ett givet värde som ska adderas vid varje anrop:
|
|
Vi kan använda vår adderingsfunktion så här:
>>> add5 = make_adder(5)
>>> add10 = make_adder(10)
>>> add5(3)
8
>>> add10(3)
13
>>> add5(10)
15
>>> add10(10)
20
>>> add5(-2)
3
>>> add5(add10(0))
15
Uppgift 13.3 (10p)
Skriv en funktion make_countdown(n)
som tar in ett heltal n
och returnerar en funktion som varje gång den anropas returnerar nästa tal i nedräkningen från n
till 0
. Om nedräkningen redan nått 0
ska den fortsätta returnera 0
.
Tips: Titta på make_counter
ovan.
Tips: Notera att det första värdet som ska returneras är n
, inte n-1
, så vad måste det initiala lagrade värdet vara?.
Exempel
>>> countdown_from_3 = make_countdown(3)
>>> print(countdown_from_3())
3
>>> print(countdown_from_3())
2
>>> print(countdown_from_3())
1
>>> print(countdown_from_3())
0
>>> print(countdown_from_3())
0
Uppgift 13.4 (10p)
Skriv en funktion make_decrementable_counter()
som skapar en lokal variabel count
och returnerar två funktioner. En funktion som räknar upp count
varje gång den anropas och en funktion som räknar ner count
varje gång den anropas. Båda funktionerna ska returnera det nya värdet på count
efter att de räknat upp eller ner. Variabeln count
ska börja på 0 och vara dold, så att den inte är åtkomlig direkt utanför de två funktionerna.
Tips: Titta på make_counter
och make_queue
ovan.
Exempel
>>> inc, dec = make_decrementable_counter()
>>> print(inc())
1
>>> print(inc())
2
>>> print(dec())
1
>>> print(dec())
0
>>> print(inc())
1
Uppgift 13.5 (10p)
Skriv en funktion secure(string, password)
som tar in en sträng och ett lösenord. Funktionen ska returnera en ny funktion som tar in ett lösenord som argument. Om lösenordet är korrekt ska den returnerade funktionen returnera den ursprungliga strängen, annars ska den returnerade funktionen returnera "Access denied!"
.
Exempel
>>> cjohnson = secure('The cake is a lie.', 'tier3')
>>> cjohnson('swordfish')
'Access denied!'
>>> cjohnson('tier3')
'The cake is a lie.'
Uppgift 13.6 (10p)
Skriv en funktion make_hidden_dict
som skapar en lokal variabel hidden_dict
, som ska innehålla ett dictionary, och returnerar två funktioner, set_value(key, value)
och get_value(key)
. Dessa funktioner ska användas för interagera med hidden dict
. Funktionen set_value
lägger till eller uppdaterar ett nyckel-värde-par och get_value
hämtar värdet för en given nyckel. Om get_value
anropas med en nyckel som inte finns ska None
returneras. Datan ska vara dold och hidden_dict
ska inte vara åtkomlig direkt utanför de två funktionerna.
Exempel
>>> set_hidden, get_hidden = make_hidden_dict()
>>> set_hidden('a', 1)
>>> set_hidden('b', 2)
>>> print(get_hidden('a'))
1
>>> print(get_hidden('b'))
2
>>> print(get_hidden('c'))
None
Fördjupning: Meddelandehantering och inkapsling
En förlängning av att låta funktioner returnera flera olika funktioner som har tillgång till samma lokala variabler är att bara returnera en funktion. Denna funktion ger i sin tur tillgång till andra funktioner som kan manipulera de lokala variablerna. Detta är ett sätt att skapa inkapslade datastrukturer som inte kan manipuleras direkt utifrån utan bara genom de funktioner som tillhandahålls.
Vi kan definiera om vår make_queue
-funktion så att den bara returnerar en funktion som i sin tur kan användas för att lägga till och ta bort element från kön. Vi gör det genom att skapa en funktion som kan ta emot meddelanden göra viss kontroll av meddelanden som kommer in, och anropa lämpliga interna funktioner, eller “metoder”. Det här sättet att kapsla in data i ett objekt som kan ta emot meddelanden ligger till grund för vad som brukar kallas för objektorienterad programmering. Vi kommer att återkomma till detta längre fram i kursen.
Här har vi också lagt till viss felhantering (om än inte “riktig” felhantering) och en metod för att lista de tillgängliga metoderna. Det finns många sätt att ytterligare snygga till och förbättra den här koden men det är inte syftet här.
|
|
>>> q = make_queue()
>>> print(q("enqueue", (1,)))
None
>>> print(q("enqueue", (2,)))
None
>>> print(q("dequeue"))
1
>>> print(q("dequeue"))
2
>>> print(q("dequeue"))
QueueError: empty queue
>>> print(q("methods"))
['enqueue', 'dequeue', 'methods']
>>> print(q("enqueue", (1, 2)))
ArgumentError: method 'enqueue' takes exactly 1 arguments (2 given)
>>> print(q("remove_last"))
MethodError: 'queue' object has no method 'remove_last'
Uppgift 13.7 (0p)
BONUSUPPGIFT
Skriv en funktion make_stack
som ska returnera en funktion som kan ta emot meddelanden med olika kommandon för att lagra data på en stack. Ett meddelande består av en sträng som anger vilket kommando som skall utföras och en tupel med eventuella argument för kommandot. Tupeln med argument ska kunna utelämnas om kommandot inte kräver några argument. Själva innehållet i stacken ska bara vara åtkomligt med hjälp av kommandona nedan.
Kom ihåg att en stack är en abstrakt datatyp som följer principen “Last In, First Out” (LIFO), vilket betyder att det senaste elementet som lades till är det första som tas bort. Vi kan lagra den interna representationen av stacken i en lista och använda list.append
för att lägga till element sist i listan och list.pop
för att ta bort och returnera det sista elementet.
Följande kommandon ska stödjas:
"push"
: Lägg till ett element överst på stacken. Kräver ett argument,value
, som är värdet som ska läggas till."pop"
: Ta bort och returnera det översta elementet från stacken. Kräver inga argument. Om stacken är tom ska en sträng med ett felmeddelande returneras:"IndexError: pop from empty stack"
."peek"
: Returnera det översta elementet på stacken utan att ta bort det. Kräver inga argument. Om stacken är tom ska en sträng med ett felmeddelande returneras:"IndexError: peek from empty stack"
."is_empty"
: ReturneraTrue
om stacken är tom, annarsFalse
. Kräver inga argument."size"
: Returnera antalet element i stacken. Kräver inga argument."clear"
: Ta bort alla element från stacken. Kräver inga argument."methods"
: Returnera en lista med alla tillgängliga kommandon som en lista av strängar.
Om ett okänt kommando ges ska en sträng med ett felmeddelande returneras: "MethodError: 'stack' object has no method 'unknown_command'"
där unknown_command
är det kommando som gavs. Om fel antal argument skickas med till ett kommando ska en sträng med ett felmeddelande returneras: "ArgumentError: method 'command' takes exactly n arguments (m given)"
där command
är kommandot, n
är det förväntade antalet argument och m
är det faktiska antalet argument som gavs.
Tips: Definiera en funktion accept_message
som är den som returneras från make_stack
. Den här funktionen kontrollerar att kommandot finns, har rätt antal argument, och anropar, om allt stämmer, sedan en lämplig intern funktion för att utföra respektive kommando.
Tips: Det kan vara lämpligt att ha ett dictionary som kopplar ihop kommandon och de funktioner som utför motsvarande operationer. Ett motsvarande dictionary kan hålla koll på hur många argument varje kommando tar.
>>> my_stack = make_stack()
>>> my_stack('is_empty')
True
>>> my_stack('push', (10,))
>>> my_stack('push', (20,))
>>> my_stack('push')
"ArgumentError: method 'push' takes exactly 1 arguments (0 given)"
>>> my_stack('size')
2
>>> my_stack('peek')
20
>>> my_stack('pop')
20
>>> my_stack('clear')
>>> my_stack('pop')
'IndexError: pop from empty stack'
>>> my_stack('is_empty')
True
>>> my_stack('methods')
['push', 'pop', 'peek', 'is_empty', 'size', 'clear', 'methods']
>>> my_stack('size')
0
>>> my_stack('add_first')
"MethodError: 'stack' object has no method 'add_first'"
Uppgift 13.8 (0p)
BONUSUPPGIFT
Skriv en funktion make_gamepiece
som tar in två argument, x
och y
, som anger startpositionen för en spelpjäs på ett spelbräde. Funktionen ska returnera en funktion som kan hantera olika kommandon för spelpjäsen. Kommandon består av ett strängargument som anger kommandot och en tupel med argument för kommandot. Tupeln med argument ska kunna utelämnas om kommandot inte kräver några argument.
Följande kommandon ska stödjas:
"move_to"
: Flytta spelpjäsen till en ny position. Kräver två argument,new_x
ochnew_y
, som anger den nya positionen. Om fel antal argument ges ska en sträng med ett felmeddelande returneras:"ArgumentError: method 'move_to' takes exactly 2 arguments (0 given)"
där0
är det antal argument som faktiskt gavs."location"
: Returnera den nuvarande positionen för spelpjäsen som en tupel(x, y)
. Kräver inga argument. Om några argument ges ska en sträng med ett felmeddelande returneras:"ArgumentError: method 'location' takes exactly 0 arguments (1 given)"
där1
är det antal argument som faktiskt gavs."name"
: Returnera namnet på spelpjäsen som en sträng."methods"
: Returnera en lista med alla tillgängliga kommandon som en lista av strängar.
Om ett okänt kommando ges ska en sträng med ett felmeddelande returneras: "MethodError: 'gamepiece' object has no method 'unknown_command'"
där unknown_command
är det kommando som gavs.
>>> player = make_gamepiece(0, 0, 'Pawnny')
>>> player('location')
(0, 0)
>>> player('move_to')
"ArgumentError: method 'move_to' takes exactly 2 arguments (0 given)"
>>> player('move_to', (10,))
"ArgumentError: method 'move_to' takes exactly 2 arguments (1 given)"
>>> player('move_to', (5, 10))
>>> player('location', ())
(5, 10)
>>> player('name')
'Pawnny'
>>> player('methods')
['move_to', 'location', 'name', 'methods']
>>> player('sit_down')
"MethodError: 'gamepiece' object has no method 'sit_down'"
Sidansvarig: Johan Falkenjack
Senast uppdaterad: 2025-08-05