Osa 12

Funktioner som argument

Vi är redan bekanta med metoden sort och funktionen sorted, som används för att sortera listor i deras naturliga ordning. För siffror och strängar fungerar detta vanligtvis bra. För allt som är mer komplicerat än dem så är dock den naturliga ordningen på föremål enligt Python inte alltid det som vi som programmerare avser.

Till exempel sorteras som standard en lista med tupler baserat på det första objektet i varje tupel:

produkter = [("banan", 5.95), ("äppel", 3.95), ("apelsin", 4.50), ("vattenmelon", 4.95)]

produkter.sort()

for produkt in produkter:
    print(produkt)
Exempelutskrift

('apelsin', 4.5) ('banan', 5.95) ('vattenmelon', 4.95) ('äppel', 3.95)

Men vad händer om vi vill sortera listan baserat på priset?

Funktioner som argument

En sorteringsmetod eller -funktion accepterar vanligtvis ett valfritt andra argument som gör att du kan kringgå standardsorteringskriterierna. Detta andra argument är en funktion som definierar hur värdet på varje föremål i listan bestäms. När listan sorteras anropar Python denna funktion när den jämför föremålen med varandra.

Låt oss ta en titt på ett exempel:

def prisordning(foremal: tuple):
    # Returnerar tupelns andra föremål, alltså priset
    return foremal[1]

if __name__ == "__main__":
    produkter = [("banan", 5.95), ("äppel", 3.95), ("apelsin", 4.50), ("vattenmelon", 4.95)]

    # Använd funktionen prisordning för sortering
    produkter.sort(key=prisordning)

    for produkt in produkter:
        print(produkt)
Exempelutskrift

('äppel', 3.95) ('apelsin', 4.5) ('vattenmelon', 4.95) ('banan', 5.95)

Nu är listan sorterad utifrån artiklarnas priser, men vad händer egentligen i programmet?

Funktionen prisordning är faktiskt ganska enkel. Den tar ett objekt som sitt argument och returnerar ett värde för det objektet. Mer specifikt returnerar den det andra objektet i tupeln, som representerar priset. Men sedan har vi den här kodraden, där sort-metoden anropas:

produkter.sort(key=prisordning)

Här anropas sort-metoden med en funktion som argument. Detta är inte en referens till funktionens returvärde, utan en referens till själva funktionen. Sort-metoden anropar denna funktion flera gånger och använder varje objekt i listan som argument i tur och ordning.

Om vi inkluderar en extra print-sats i funktionsdefinitionen för prisordning kan vi verifiera att funktionen verkligen anropas en gång för varje objekt i listan:

def prisordning(foremal: tuple):
    # Skriver ut föremålet
    print(f"Anropade prisordning({foremal})")

    # Returnerar tupelns andra föremål, alltså priset
    return foremal[1]


produkter = [("banan", 5.95), ("äppel", 3.95), ("apelsin", 4.50), ("vattenmelon", 4.95)]

# Använd funktionen prisordning för sortering
produkter.sort(key=prisordning)

for produkt in produkter:
    print(produkt)
Exempelutskrift

Anropade prisordning(('banan', 5.95)) Anropade prisordning(('äppel', 3.95)) Anropade prisordning(('apelsin', 4.5)) Anropade prisordning(('vattenmelon', 4.95)) ('äppel', 3.95) ('apelsin', 4.5) ('vattenmelon', 4.95) ('banan', 5.95)

Ordningen kan vändas med ett annat nyckelordsargument; reverse, som är tillgängligt med både sort-metoden och funktionen sorted:

produkter.sort(key=prisordning, reverse=True)

t2 = sorted(produkter, key=prisordning, reverse=True)

En funktionsdefinition inom en funktionsdefinition

Vi kan också inkludera en namngiven funktion för den nya prisbaserade sorteringsfunktionen som vi har skapat. Låt oss lägga till en funktion med namnet sortera_enligt_pris:

def prisordning(foremal: tuple):
    return foremal[1]

def sortera_enligt_pris(foremalen: list):
    # Använd funktionen prisordning här
    return sorted(foremalen, key=prisordning)

produkter = [("banan", 5.95), ("äppel", 3.95), ("apelsin", 4.50), ("vattenmelon", 4.95)]

for produkt in sortera_enligt_pris(produkter):
    print(produkt)

Om vi vet att hjälpfunktionen prisordning inte används någonstans utanför funktionen sortera_enligt_pris, kan vi placera den första funktionsdefinitionen inom den senare:

def sortera_enligt_pris(foremalen: list):
    # hjälpfunktion definierad inom funktionen
    def prisordning(foremal: tuple):
        return foremal[1]

    return sorted(foremalen, key=prisordning)
Loading
Loading
Loading

Sortering av samlingar av egna objekt

Låt oss med samma princip skriva ett program som sorterar en lista med objekt från vår egen klass Studerande på två olika sätt:

class Studerande:
    """ Klassen modellerar en enkel studerande """
    def __init__(self, namn: str, id: str, poang: int):
        self.namn = namn
        self.id = id
        self.poang = poang

    def __str__(self):
        return f"{self.namn} ({self.id}), {self.poang} sp."


def enligt_id(foremal: Studerande):
    return foremal.id

def enligt_poang(foremal: Studerande):
    return foremal.poang


if __name__ == "__main__":
    s1 = Studerande("Anton", "a123", 220)
    s2 = Studerande("Maja", "m321", 210)
    s3 = Studerande("Anna", "a999", 131)

    studeranden = [s1, s2, s3]

    print("Enligt id:")
    for studerande in sorted(studeranden, key=enligt_id):
        print(studerande)

    print()

    print("Enligt poäng:")
    for studerande in sorted(studeranden, key=enligt_poang):
        print(studerande)
Exempelutskrift

Enligt id: Anton (a123), 220 sp. Anna (a999), 131 sp. Maja (m321), 210 sp.

Enligt poäng: Anna (a999), 131 sp. Maja (m321), 210 sp. Anton (a123), 220 sp.

Som du kan se ovan fungerar sortering efter olika kriterier precis som det är tänkt. Om funktionerna enligt_id och enligt_studiepoang inte behövs någon annanstans finns det sätt att göra implementeringen enklare. Vi återkommer till detta ämne efter dessa övningar.

Loading
Loading

Lambda-uttryck

Vi har mest arbetat med funktioner ur modularitetssynpunkt. Det är sant att funktioner spelar en viktig roll när det gäller att hantera komplexiteten i dina program och undvika upprepning av kod. Funktioner skrivs vanligtvis så att de kan användas många gånger.

Men ibland behöver man något som liknar en funktion som man bara använder en gång. Med lambda-uttryck kan du skapa små, anonyma funktioner som skapas (och kasseras) när de behövs i koden. Den allmänna syntaxen är som följer:

lambda <parametrar> : <uttryck>

Att sortera en lista med tupler efter det andra objektet i varje tupel skulle se ut så här implementerat med ett lambda-uttryck:

produkter = [("banan", 5.95), ("äppel", 3.95), ("apelsin", 4.50), ("vattenmelon", 4.95)]

# Funktionen skapas "i farten" med ett lambda-uttryck:
produkter.sort(key=lambda foremal: foremal[1])

for produkt in produkter:
    print(produkt)
Exempelutskrift

('äppel', 3.95) ('apelsin', 4.5) ('vattenmelon', 4.95) ('banan', 5.95)

Uttrycket

lambda föremål: föremål[1]

Är ekvivalent med funktionsdefinitionen


def pris(foremal):
    return foremal[1]

förutom det faktum att en lambdafunktion inte har något namn. Det är därför lambda-funktioner kallas anonyma funktioner.

I alla andra avseenden skiljer sig inte en lambda-funktion från någon annan funktion, och de kan användas i alla samma sammanhang som en motsvarande namngiven funktion. Följande program sorterar till exempel en lista med strängar i alfabetisk ordning enligt det sista tecknet i varje sträng:

strangar = ["Mikael", "Makke", "Maja", "Markus", "Minna"]

for strang in sorted(strangar, key=lambda strang: strang[-1]):
    print(strang)
Exempelutskrift

Maja Minna Makke Mikael Markus

Vi kan också kombinera list comprehensions, join-metoden och lambda-uttryck. Vi kan till exempel sortera strängar baserat på enbart vokalerna i dem och ignorera alla andra tecken:

strangar = ["Mikael", "Makke", "Maja", "Markus", "Minna"]

for strang in sorted(strangar, key=lambda strang: "".join([m for m in strang if m in "aeiouyäö"])):
    print(strang)
Exempelutskrift

Maja Makke Markus Minna Mikael

Anonyma funktioner kan också användas med andra inbyggda Python-funktioner, inte bara de som används för sortering. Till exempel tar funktionerna min och max också ett nyckelordsargument som heter key. Det används som kriterium för att jämföra objekten när det lägsta eller högsta värdet väljs.

I följande exempel handlar det om ljudinspelningar. Först väljer vi den äldsta inspelningen och sedan den längsta:


class Skiva:
    """ Klassen modellerar en enkel skiva """
    def __init__(self, namn: str, artist: str, ar: int, langd: int):
        self.namn = namn
        self.artist = artist
        self.ar = ar
        self.langd = langd


    def __str__(self):
        return f"{self.namn} ({self.artist}), {self.ar}. {self.langd} min."

if __name__ == "__main__":
    l1 = Skiva("Nevermind", "Nirvana", 1991, 43)
    l2 = Skiva("Let It Be", "Beatles", 1969, 35)
    l3 = Skiva("Joshua Tree", "U2", 1986, 50)

    skivor = [l1, l2, l3]


    print("Äldsta skiva:")
    print(min(skivor, key=lambda skiva: skiva.ar))

    print("Längsta skiva: ")
    print(max(skivor, key=lambda skiva: skiva.langd))
Exempelutskrift

Äldsta skiva: Let It Be (Beatles), 1969. 35 min. Längsta skiva: U2 (Joshua Tree), 1986. 50 min.

Loading

Funktioner som argument inom egna funktioner

Vi konstaterade ovan att det är möjligt att skicka en referens till en funktion som argument till en annan funktion. Som avslutning på detta avsnitt skriver vi en egen funktion som tar en funktion som argument.

# typledtråden callable refererar till en funktion
def utfor_operation(operation: callable):
    # Anropa funktionen som passerades som argument
    return operation(10, 5)

def summa(a: int, b: int):
    return a + b

def produkt(a: int, b: int):
    return a * b


if __name__ == "__main__":
    print(utfor_operation(summa))
    print(utfor_operation(produkt))
    print(utfor_operation(lambda x,y: x - y))
Exempelutskrift

15 50 5

Det värde som returneras av funktionen utfor_operation beror på vilken funktion som skickades som argument. Vilken funktion som helst som tar emot två argument skulle duga, oavsett om den är anonym eller namngiven.

Att skicka referenser till funktioner som argument till andra funktioner är kanske inte något som du kommer att göra dagligen under din programmeringskarriär, men det kan vara en användbar teknik. Följande program väljer ut några rader från en fil och skriver dem till en annan fil. Hur raderna väljs ut bestäms av en funktion som returnerar True endast om raderna ska kopieras:

def kopiera_rader(kalla_namn: str, mal_namn: str, kriterie= lambda x: True):
    with open(kalla_namn) as kalla, open(mal_namn, "w") as mal:
        for rad in kalla:
            # Ta bort all tomrum från början och slutet av raden
            rad = rad.strip()

            if kriterie(rad):
                mal.write(rad + "\n")

# Exempel
if __name__ == "__main__":
    # Ifall tredje parametern inte är angiven, kopiera alla rader
    kopiera_rader("första.txt", "andra.txt")

    # Kopiera alla icke-tomma rader
    kopiera_rader("första.txt", "andra.txt", lambda rad: len(rad) > 0)

    # Kopierar alla rader som innehåller ordet "Python"
    kopiera_rader("första.txt", "andra.txt", lambda rad: "Python" in rad)

    # Kopierar alla rader som inte slutar med en punkt
    kopiera_rader("första.txt", "andra.txt", lambda rad: rad[-1] != ".")

Funktionsdefinitionen innehåller ett standardvärde för nyckelordsparametern kriterie: lambda x: True. Denna anonyma funktion returnerar alltid True oavsett indata. Standardbeteendet är alltså att kopiera alla rader. Som vanligt gäller att om ett värde anges för en parameter med ett standardvärde, ersätter det nya värdet standardvärdet.

Loading
Du har nått slutet av den här delen! Fortsätt till nästa del:

Se dina poäng genom att klicka på cirkeln nere till höger av sidan.