Kiedy po raz pierwszy zetknęłam się z wyrażeniem lambda i przeczytałam definicję, że jest to jednolinijkowa anonimowa funkcja, to nie bardzo rozumiałam po co mi to i czy nie lepiej zrobić normalną funkcję, którą można reużywać? Z drugiej strony znalazłam też informację, że lambda zostało wprowadzone na prośbę programistów. Funkcję lambda można znaleźć również w innych językach jak Java, C# czy C++. Pomyślałam wtedy, że to musi jednak być przydatne. 

Według mnie lambda najlepiej zrozumieć na konkretnych przykładach. Bardzo często korzysta się z niej w kombinacji z wbudowanymi funkcjami takimi jak filter(), map(), reduce() czy sorted(). Mam nadzieję, że po przeczytaniu tego artykułu zobaczysz, że lambda nie jest wcale taka straszna i sam zaczniesz jej używać w swoim kodzie.

Lambda w Pythonie

Dla tych, którzy jeszcze nie znają jeszcze wyrażenia lambda, krótkie wyjaśnienie, co to jest.

Koncepcyjnie, wyrażenie lambda robi to samo co funkcja zadeklarowana przy użyciu def.

def funkcja(a,b):
    return a+b

#Za pomocą lambda można zapisać to w takiej postaci:

lambda a,b: a+b

Jak już pewnie zauważyłeś składnia wygląda tak, że po lambda przypisujemy zmienne dla argumentów, a po dwukropku znajduje się ciało funkcji. Na razie nic skomplikowanego, prawda?

Różnica między lambda a normalną funkcją jest taka, że w normalnej funkcji możesz tworzyć rozbudowany wielolinijkowy kod z zagnieżdżonymi funkcjami wewnętrznymi, a lambda służy do tworzenia krótkich jednolinijkowych funkcji. Druga różnicą jest to, że do zwykłej funkcji możesz się odwołać w dowolnym miejscu w kodzie (i w zasadzie taki jest cel jej istnienia, aby wielokrotnie reużywać dany fragment), lambda jest funkcją anonimową, czyli nie posiada nazwy. Jeśli chcesz się do niej odwołać, to musisz ją przypisać do jakiejś zmiennej.

zmienna=lambda a,b: a+b.

#Wartości argumentów podaje się w następujący sposób
zmienna(3,5)

>>> zmienna
10

Zgodnie z rekomendacją zawartą w PEP-8 powinno się używać instrukcji def zamiast przypisywać wyrażenie lambda do zmiennej.

Po co nam lambda?

Lambda można użyć wszędzie tam, gdzie jako argumentu, czy parametru potrzebujemy funkcji. Za przykład podam tutaj wbudowaną funkcję map(), która przyjmuje dwa argumenty. Pierwszym jest nazwa funkcji. Pamiętaj, że przy podawaniu funkcji jako argumentu, podajesz tylko samą nazwę bez nawiasów (). Drugim argumentem jest obiekt iterowalny. Tak najprościej mówiąc map() iteruje po obiekcie podanym jako argument i na każdym  elemencie wykonuje funkcję podaną jako pierwszy argument.

Przykład:

def my_func(n):
    return n*(n-1)

numbers = [1,5,12,30]
with_func = list(map(my_func, numbers))
with_lambda = list(map(lambda a: a*(a-1), numbers))

print(with_func, with_lambda)


>>>
[0, 20, 132, 870] [0, 20, 132, 870]

Samo map() zwraca map object, który można przekonwertować na inny obiekt, np. na listę, krotkę itd. Generalnie map() zostało wyparte przez list comprehension, które jest szybsze, bo nie wymaga dodatkowej konwersji.

Sorted()

Funkcja ta, jak sama nazwa wskazuje, służy do sortowania. Warto zauważyć, że sorted() zwraca listę, chociaż jako argument przyjmuje obiekt iterowalny.

print(sorted("AnalityK"))
>>>
['A', 'K','a', 'i', 'l', 'n', 't', 'y']

Opcjonalnie sorted() przyjmuje parametry: key i reverse.

Parametr reverse przyjmuje wartość logiczną, domyślnie jest to False. sorted() sortuje liczby od najmniejszej do największej, a stringi wg kolejności analfabetycznej, najpierw wielkie litery, a potem małe. Jeśli ustawi się reverse=True, to posortuje wszystko w odwrotnej kolejności.

print(sorted("Analityk", reverse=True))
>>>
['y', 't', 'n', 'l', 'i', 'a', 'K','A']

Key przyjmuje funkcję, według której ma posortować sekwencję. Na przykład chcielibyśmy posortować wyrazy w zdaniu alfabetycznie ignorując to, czy wyraz zaczyna się od dużej czy małej litery:

print(sorted("Alicja Kot ma psa ale nie ma kota".split(), key=str.lower))
>>>
['ale', 'Alicja', 'Kot', 'kota', 'ma', 'ma', 'nie', 'psa']

Jak się dobrze domyślasz, to właśnie w parametrze key będziemy używać lambda.

Sortowanie słowników

things = {'a': 11, 'b': 2, 'c': 0, 'd': 33}

print(sorted(things))
print(sorted(things.values())) 
print(sorted(things.items()))

>>>
['a', 'b', 'c', 'd']
[0, 2, 11, 33]
[('a', 11), ('b', 2), ('c', 0), ('d', 33)]

Jak widać na powyższym przykładzie, domyślnie sorted() ze słownikiem posortuje tylko klucze. Jeśli użyjemy dict.items(), to również posortuje wg kluczy. A co jeśli chcemy posortować elementy według wartości słownika? W takim przypadku użyjemy również dict.items() tylko w kluczu musimy określić według, którego elementu będziemy posortować. Mam nadzieję, że zauważyłeś, że dict.items() zwraca listę z krotkami (klucz, wartość).

print(sorted(things.items(), key=lambda x: x[1]))
>>>
[('c', 0), ('b', 2), ('a', 11), ('d', 33)]

Funkcja sorted() iteruje po itemach z listy(tak samo było w map()). W kluczu lambda x: x[1] argumentem x jest tupla złożona z pary klucz-wartość, x[0] jest kluczem, a x[1] wartością.

Zadanie

Do zadania wybrałam jeden z zestawów danych Eurostatu o tytule „Share of government budget appropriations or outlays on research and development”. Jak się domyślacie, Polska nie stoi zbyt wysoko w tym zestawieniu 🙂

Zadanie polega na tym, żeby posortować wartości, żeby ustalić,w których krajach wydatki na badania i rozwój (R&D) mają największy udział w budżecie.

Ja ściągnęłam zestawienie w formacie JSON. Tutaj generalnie polecam lekturę https://ec.europa.eu/eurostat/web/json-and-unicode-web-services/getting-started/rest-request .

Wzięłam tylko dane z ostatniego podanego roku, czyli z 2018: http://ec.europa.eu/eurostat/wdds/rest/data/v2.1/json/en/tsc00007?time=2018 . Nasz JSON wygląda następująco:

{"version":"2.0","label":"Share of government budget appropriations or outlays on research and development","href":"http://ec.europa.eu/eurostat/wdds/rest/data/v2.1/json/en/tsc00007?time=2018","source":"Eurostat","updated":"2020-01-31","status":{"0":"d","3":":","7":"p","21":":","22":":","32":":","36":":","38":":"},"extension":{"datasetId":"tsc00007","lang":"EN","description":"Data on Government Budget Appropriations or Outlays on Research and Development (GBAORD) refer to budget provisions, not to actual expenditure, i.e. GBAORD measures government support for R&D using data collected from budgets. The GBAORD indicator should be seen as a complement to indicators based on surveys of R&D performers, which are considered to be a more accurate but less timely way of measuring R&D activities. In this table, total GBAORD is expressed as a percentage of total general government expenditure.","subTitle":"% of total general government expenditure","status":{"label":{"d":"definition differs (see metadata)",":":"not available","p":"provisional"}}},"class":"dataset","value":{"0":1.55,"1":1.21,"2":0.56,"4":0.73,"5":1.54,"6":2.11,"7":1.75,"8":1.44,"9":1.79,"10":1.29,"11":1.25,"12":1.41,"13":1.4,"14":1.57,"15":1.06,"16":1.66,"17":0.65,"18":0.93,"19":2.21,"20":1.05,"23":0.87,"24":1.49,"25":0.57,"26":0.57,"27":1.7,"28":2.02,"29":0.69,"30":0.8,"31":0.49,"33":1.56,"34":0.96,"35":0.87,"37":1.37},"dimension":{"unit":{"label":"unit","category":{"index":{"PC_GEXP":0},"label":{"PC_GEXP":"Percentage of government expenditure"}}},"geo":{"label":"geo","category":{"index":{"AT":0,"BE":1,"BG":2,"CH":3,"CY":4,"CZ":5,"DE":6,"DK":7,"EA19":8,"EE":9,"EL":10,"ES":11,"EU27_2020":12,"EU28":13,"FI":14,"FR":15,"HR":16,"HU":17,"IE":18,"IS":19,"IT":20,"JP":21,"KR":22,"LT":23,"LU":24,"LV":25,"MT":26,"NL":27,"NO":28,"PL":29,"PT":30,"RO":31,"RU":32,"SE":33,"SI":34,"SK":35,"TR":36,"UK":37,"US":38},"label":{"AT":"Austria","BE":"Belgium","BG":"Bulgaria","CH":"Switzerland","CY":"Cyprus","CZ":"Czechia","DE":"Germany (until 1990 former territory of the FRG)","DK":"Denmark","EA19":"Euro area - 19 countries  (from 2015)","EE":"Estonia","EL":"Greece","ES":"Spain","EU27_2020":"European Union - 27 countries (from 2020)","EU28":"European Union - 28 countries (2013-2020)","FI":"Finland","FR":"France","HR":"Croatia","HU":"Hungary","IE":"Ireland","IS":"Iceland","IT":"Italy","JP":"Japan","KR":"South Korea","LT":"Lithuania","LU":"Luxembourg","LV":"Latvia","MT":"Malta","NL":"Netherlands","NO":"Norway","PL":"Poland","PT":"Portugal","RO":"Romania","RU":"Russia","SE":"Sweden","SI":"Slovenia","SK":"Slovakia","TR":"Turkey","UK":"United Kingdom","US":"United States"}}},"time":{"label":"time","category":{"index":{"2018":0},"label":{"2018":"2018"}}}},"id":["unit","geo","time"],"size":[1,39,1]}

Wygląda średnio zachęcająco, czyż nie? 🙂 Do ściągnięcia pliku w Pythonie użyjemy modułu urllib.request, a do parsowania oczywiście json.load(). Trzeba pamiętać o tym, że json.load() przyjmuje string.

import json,urllib.request

data = urllib.request.urlopen("http://ec.europa.eu/eurostat/wdds/rest/data/v2.1/json/en/tsc00007?time=2018").read()
output = json.loads(data)

Nasze dane po parsowaniu mają postać słownika. To teraz możemy je trochę uporządkować. Utworzymy drugi słownik, w którym kluczem będzie nazwa kraju, a wartością udział wydatków na R&D w budżecie państwa.

Po analizie pliku wyszło mi, że nazwy kraju znajdują się w output['dimension']['geo']['category']['label'] .

{'AT': 'Austria', 'BE': 'Belgium', 'BG': 'Bulgaria', 'CH': 'Switzerland', 'CY': 'Cyprus', 'CZ': 'Czechia', 'DE': 'Germany (until 1990 former territory of the FRG)', 'DK': 'Denmark', 'EA19': 'Euro area - 19 countries  (from 2015)', 'EE': 'Estonia', 'EL': 'Greece', 'ES': 'Spain', 'EU27_2020': 'European Union - 27 countries (from 2020)', 'EU28': 'European Union - 28 countries (2013-2020)', 'FI': 'Finland', 'FR': 'France', 'HR': 'Croatia', 'HU': 'Hungary', 'IE': 'Ireland', 'IS': 'Iceland', 'IT': 'Italy', 'JP': 'Japan', 'KR': 'South Korea', 'LT': 'Lithuania', 'LU': 'Luxembourg', 'LV': 'Latvia', 'MT': 'Malta', 'NL': 'Netherlands', 'NO': 'Norway', 'PL': 'Poland', 'PT': 'Portugal', 'RO': 'Romania', 'RU': 'Russia', 'SE': 'Sweden', 'SI': 'Slovenia', 'SK': 'Slovakia', 'TR': 'Turkey', 'UK': 'United Kingdom', 'US': 'United States'}

Nasze wartości, które będziemy sortować znajdują się w output['value'] .

{'0': 1.55, '1': 1.21, '2': 0.56, '4': 0.73, '5': 1.54, '6': 2.11, '7': 1.75, '8': 1.44, '9': 1.79, '10': 1.29, '11': 1.25, '12': 1.41, '13': 1.4, '14': 1.57, '15': 1.06, '16': 1.66, '17': 0.65, '18': 0.93, '19': 2.21, '20': 1.05, '23': 0.87, '24': 1.49, '25': 0.57, '26': 0.57, '27': 1.7, '28': 2.02, '29': 0.69, '30': 0.8, '31': 0.49, '33': 1.56, '34': 0.96, '35': 0.87, '37': 1.37}

Jeśli się przyjrzysz to brakuje w kluczach, niektórych numerów, np. ‚3’, ’21’,’22’. Oznacza to kraje, dla których brak danych.

r_and_d = {}

country_names=output['dimension']['geo']['category']['label']
values=output['value']

Ponieważ wartości są ułożone w tej samej kolejności jak kraje, które są ułożone alfabetycznie, więc pozwoliłam sobie użyć enumerate().

for index, country in enumerate(country_names.items()):
    r_and_d[country[1]]=values.get(f'{index}', 'brak danych')

Nasz słownik r_and_d wygląda teraz tak:

{'Austria': 1.55, 'Belgium': 1.21, 'Bulgaria': 0.56, 'Switzerland': 'brak danych', 'Cyprus': 0.73, 'Czechia': 1.54, 'Germany (until 1990 former territory of the FRG)': 2.11, 'Denmark': 1.75, 'Euro area - 19 countries  (from 2015)': 1.44, 'Estonia': 1.79, 'Greece': 1.29, 'Spain': 1.25, 'European Union - 27 countries (from 2020)': 1.41, 'European Union - 28 countries (2013-2020)': 1.4, 'Finland': 1.57, 'France': 1.06, 'Croatia': 1.66, 'Hungary': 0.65, 'Ireland': 0.93, 'Iceland': 2.21, 'Italy': 1.05, 'Japan': 'brak danych', 'South Korea': 'brak danych', 'Lithuania': 0.87, 'Luxembourg': 1.49, 'Latvia': 0.57, 'Malta': 0.57, 'Netherlands': 1.7, 'Norway': 2.02, 'Poland': 0.69, 'Portugal': 0.8, 'Romania': 0.49, 'Russia': 'brak danych', 'Sweden': 1.56, 'Slovenia': 0.96, 'Slovakia': 0.87, 'Turkey': 'brak danych', 'United Kingdom': 1.37, 'United States': 'brak danych'}

Nie możemy takiego słownika posortować, ponieważ niektóre wartości są typu float, a inne stringami („brak danych”), więc żeby uczynić nasze dane sortable, stworzymy kolejny słownik, w którym wyeliminujemy, kraje o których nie mam danych. W następnym kroku możemy już posortować wg wartości malejąco.

r_and_d_sortable = {k:v for k,v in r_and_d.items() if type(v)!=str}
[print(item) for item in sorted(r_and_d_sortable.items(), key=lambda x: x[1], reverse=True)]

>>>
('Iceland', 2.21)
('Germany (until 1990 former territory of the FRG)', 2.11)
('Norway', 2.02)
('Estonia', 1.79)
('Denmark', 1.75)
('Netherlands', 1.7)
('Croatia', 1.66)
('Finland', 1.57)
('Sweden', 1.56)
('Austria', 1.55)
('Czechia', 1.54)
('Luxembourg', 1.49)
('Euro area - 19 countries  (from 2015)', 1.44)
('European Union - 27 countries (from 2020)', 1.41)
('European Union - 28 countries (2013-2020)', 1.4)
('United Kingdom', 1.37)
('Greece', 1.29)
('Spain', 1.25)
('Belgium', 1.21)
('France', 1.06)
('Italy', 1.05)
('Slovenia', 0.96)
('Ireland', 0.93)
('Lithuania', 0.87)
('Slovakia', 0.87)
('Portugal', 0.8)
('Cyprus', 0.73)
('Poland', 0.69)
('Hungary', 0.65)
('Latvia', 0.57)
('Malta', 0.57)
('Bulgaria', 0.56)
('Romania', 0.49)

Możemy teraz sobie porównać z tabelką na stronie https://ec.europa.eu/eurostat/databrowser/view/tsc00007/default/table?lang=en i zobaczyć, czy wszystko się zgadza.

Niestety nasuwa się taka smutna konkluzja, że Polska znajduje się znacznie poniżej średniej europejskiej… Dotyczy to także innych obszarów z kategorii ‚Science, Technology and Innovation’. Na stronie Eurostatu są zrobione ładne wizualizacje danych . Mam nadzieję, że Polska z każdym rokiem będzie rosła w siłę razem z wami, przyszłymi Data Scientist’ami, Python Developer’ami itd.

Uwaga:

W naszym zadaniu nie zmienialiśmy żadnych danych, tylko je sortowaliśmy. Generalnie warto pamiętać o tym, co napisałam w artykule o Kopiowaniu list i słowników , że jeśli w jakichś zmiennych „zaciągamy” dane z innych list/słowników, to jak będziemy zmieniać te dane, to zmienią się one także w pozostałych zmiennych, ponieważ obiekt jest wspólny. Dlatego dobrym pomysłem jest pracować na kopii oryginalnego słownika, najlepiej wykonanej poprzez copy.deepcopy(), jeśli mamy zagnieżdżone struktury.