Przypomnienie - jak pobierać dane ze słownika
Proste pobieranie wartości ze słownika
Pobieranie danych przez metodę get w słowniku
Struktura danych
Naiwne wyciągnięcie danych
Get zamiast KeyError
Get z domyślną wartością
Uważaj na domyślną wartość w słowniku
Dogrywka z domyślną wartością
Nie tylko Ja błądziłem
Proste pobranie
Get jest bezpieczniejszy
Get z wartością domyślną jest przydatny
Wartość domyślna, kiedy klucz występuje w słowniku
Ponownie błąd z domyślną wartoścą w słowniku
Czytelność na pierwszym miejscu
Wyjaśnienie
Podsumowanie
Spis treści
W jednym z wpisów pisałem, że pobieranie wartości ze słownika po get jest lepsze niż pobieranie przez nawiasy kwadratowe. Ten sposób pozwala na zwrócenie konkretnej wartości jeśli danego klucza nie ma słowniku. Czy to jest idealne rozwiązanie? Dziś odpowiemy na to pytanie.
Przypomnienie - jak pobierać dane ze słownika
W ramach szybkiego przypomnienia, dane ze słownika pobierać można w zasadize na dwa sposoby.
person = {
"name": "ddeby",
"age": "26"
}
name = person["name"] # Standardowy sposób
name = person.get("name") # Pobieranie przez get
Problem pojawia się jesli chcemy pobrać wartość spod klucza, którego nie ma w słowniku. Pisałem o tym wcześniej, więc tylko krótko możliwości. Niżej 4, które mozna spotkać najczęściej. Nazwałem je roboczo defensywny if , agresywny try , sprytny get oraz dziwny get
Proste pobieranie wartości ze słownika
profession = person["profession"] if "profession" in person else "brak" # defensywny if
try:
profession = person["profession"] # agresywny try
except KeyError:
profession = "brak"
Pobieranie danych przez metodę get w słowniku
profession = person.get("profession", "brak") # sprytny get z domyślną wartością
profession = person.get("profession") or "brak" # dziwny get z operatorem or
Dziś skupimy się na dwóch ostatnich i poznamy jedną aczkolwiek ważną różnicę między nimi.
Od jakiegoś czasu pracuje nad projektem, w którym musiałem parsować ceny. Chciałbym Was teraz przeprowadzić przez podobną ścieżkę myślową, którą Ja miałem. Nie jest to idealnie ten sam przypadek ale jest bardzo zbliżony.
Struktura danych
Dane, który dostałam wyglądały mniej więcej tak jak na przykładzie poniżej
product = {
"name": "Ubongo gra planszowa",
"link": "sample_link",
"prices":[
{
"value": 100,
"currency": "pln"
},
{
"value": 40,
"currency": "usd"
}
]
}
Musiałem wyciągnąć średnią cenę dla projektu. Najczęściej projekt posiadał 1, 2 lub 3 ceny. Jako, że lubię jednolinkowce, a te do takich zastosowań są niemal idealne to od razu napisałem szybkie wyciągnięcie danych do listy.
Naiwne wyciągnięcie danych
prices = [price["value"] for price in product["prices"]]
Osoby, które nie czują sie mocne w jednolinijkowych strukturach odsyłam do wpisu, w którym wyjasniam to kopleksowo. Pobieramy listę cen spod klucza prices , następnie iterujemy po nich i z każdego elementu pobieramy wartość spod klucza value .
Get zamiast KeyError
Oczywiście znałem już problem KeyError i wiedziałem, że może poja dlatego spróbowałem to zrobić nieco lepiej.
prices = [price["value"] for price in product.get("prices", [])]
Get z domyślną wartością
I tu pojawiło się pierwsze zaskoczenie, bo okazało się, że jeśli produkt nie jest jeszcze w sprzedaży to potrafi wysłać cenę bez wartości.
product = {
"name": "Robinson Crusoe: Przygoda Na Przeklętej Wyspie",
"link": "sample_link",
"prices":[
{
"currency": "pln"
},
]
}
Musiałem zatem dodać poprawkę. Niewiele myśląc dodałem najszybszy możliwy fix, czyli get, również w zagnieżdżonym słowniku.
prices = [price.get("value", 0) for price in product.get("prices", [])]
Uważaj na domyślną wartość w słowniku
Skrypt do pobierania cen działał poprawnie aż nagle dostaję informację o błędzie. W teorii wszystko powinno działać, ale to jest praktyka i w praktyce wyszło coś zupełnie innego. Mylnie przyjąłem, że dane zostały wcześniej poprawnie zwalidowane. Może inaczej, dane były zwalidowane ale z błędnymi założeniami. Przyjąłem, że wartość nie jest obowiązkowa i to był błąd. Dlaczego? Zobaczcie jakie dostałem dane
product = {
"name": "Ubongo gra planszowa",
"link": "sample_link",
"prices":[
{
"value": None,
"currency": "pln"
},
]
}
Początkowo ciężko było mi namierzyć błąd, bo pobranie danych działało poprawnie. I skrypt wyłożył się dopiero na etapie liczenia średniej ceny.
avg = sum(prices) / len(prices) if len (prices) else 0 # TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'
Niby wszystko było w porządku. Ten kod przeszedł, ale wyłożył się na późniejszym etapie, bo do listy liczb wpadło coś, co liczbą nie było. I tu właśnie ujawnia się odpowiedź na cały wpis, bo get z domyślną wartością zwróci tę wartośc tylko jeśli danego klucza nie ma słowniku. Przekonałem się o tym boleśnie, dlatego przestrzegam i proszę - uważaj z pobieraniem takich danych. Poprawa wyglądała mniej więcej tak
prices = [price.get("value") or 0 for price in product.get("prices", [])]
Dogrywka z domyślną wartością
Czy to już wszystko? Oczywiście, że nie. Nie minęła godzina i znowu spotkałem ten sam błąd. Tym razem było to jeszcze głupsze. Okazało się, że jeśli dany produkt został wycofany, to zamiast pustej listy pod kluczem prices siedzi None . Przykład takiego produktu możemy zobaczyć niżej
product = {
"name": "Cry Havoc",
"link": "sample_link",
"prices": None,
}
Klucz był, zatem drugi parametr z domyślną wartością nie został zwrócony.
Zdesperowany dograłem ostatnią poprawkę i po wszystkim wyszedł mi taki potwór
prices = [price.get("value") or 0 for price in product.get("prices") or []]
Nie da się tego inaczej określić niż potworem, bo czytelność takiego pobierania w słowniku spadła diametralnie. Gdybym tylko poszedł po rozum do głowy już po pierwszym alarmie i dopisał testy na wszystkie przypadki albo nie szedł na łatwiznę i zrobił to zgodnie z TDD to nie miałbym takich problemów. Dlateg uważaj na metodę get z domyślną wartością w słowniku, bo nigdy nie wiesz czy API nie zwróci Ci czegoś dziwnego.
Nie tylko Ja błądziłem
Początkowo chciałem napisać wpis o ogólnym zastosowaniu słowników, bo moim zdaniem to rewelacyjny sposób przechowywania danych i podczas przeszukiwania zauważyłem, że ktoś miał podobny problem co Ja z cenami. Dlatego właśnie postanowiłem wydzielić to do osobnego wpisu. Prześledźmy zatem podobny tok myślowy ale dla danych, który były w tamtym artykule.
product = {
"name": "Ubongo gra planszowa",
"link": "sample_link",
"offer": {
{
"value": "100",
"currency": "pln"
},
}
}
Proste pobranie
Struktura była o tyle czytelniejsza, że była zwrócona jedna, najniższa cena. Podobnie jak wcześniej możemy to zrobić prosto i naiwnie
price = product["offer"]["value"]
Get jest bezpieczniejszy
Po wcześniejszej historii wiemy, że get jest bezpieczniejszy. Zabezpieczmy się od razu na taki przypadek.
price = product.get("offer", {}).get("value")
Get z wartością domyślną jest przydatny
Podobnie jak wcześniej, wartość ceny może być pusta. Najłatwiej załatać to poprzez wartość domyślną
price = product.get("offer", {}).get("value", 0)
Wartość domyślna, kiedy klucz występuje w słowniku
Zapomnieliśmy o przykładzie, kiedy klucz z ceną występuje ale jest tam wartość None . Obsłużymy zatem i to
price = product.get("offer", {}).get("value") or 0
Ponownie błąd z domyślną wartoścą w słowniku
I na koniec zostaje perełka, czyli ten sam przykład co wcześniej. Oferta jest, ale pusta. Rozwiązanie wydaje się proste, zamiast wartości domyślnej przyjmijmy te None i korzystając z or zwróćmy pusty słownik jako alternatywę. I tu pojawia się problem. Jak zrobić to sensownie, żeby to jakoś wyglądało, bo taki zapis jest bardzo nieczytelny. Jest też trudny do odczytania i debugowania. Zresztą zobaczacie sami
(get("offer") or {}).get("value") or 0
Czytelność na pierwszym miejscu
Idąc w myśl idei, że znacznie więcej kodu się czyta niż pisze poprawiłem to tak, żeby działało tak samo ale było czytelniejsze
offer = product.get("offer") or {}
price = offer.get("value") or 0
Wyjaśnienie
Dla osób, które dalej czują się zagubione postaram się zrobić krótkie wyjasnienie.
Get w słowniku pobiera wartość o podanym kluczu. Jeśli danego klucza nie ma w słowniku, to zwraca None. Metoda ta potrafi przyjąć drugi parametr, który może nadpisać domyślnie zwracaną wartość. Problem pojawia się, jeśli klucz jest w słowniku ale w środku jest własnie None. często nie jest to problem, ale w konkretnych sytuacjach może okazać się problematyczne. I tutaj pojawia się kolejna bardzo ciekawa cecha Python, czyli skorzystanie z operatora OR. Przy takiej konstrukcji jeśli pierwsza wartość będzie boolowskim fałszem, czyli np. 0, None, lub właśnie False, to sięgniemy do drugiej wartości. I to właśnie połączenie tych dwóch zachowań daje takie ciekawe rezultaty.
# Operator or
0 or 1 # 1
0 or "Dawid" # "Dawid"
1 or "Dawid" # 1
None or "Dawid" # "Dawid"
# Pobieranie ze słownika
person = {"name": "Dawid"}
person["name"] # Dawid
person.get("name") # Dawid
person.get("age") # None
person.get("age", "brak") # "brak"
# Połączenie
person.get("age") or "brak"
Podsumowanie
Chciałbym, żeby ten wpis nauczył Was czterech rzeczy. Po pierwsze jeśli ktoś mówi Wam, że coś jest banalne i można zrobić to w godzinę, to zawsze bierzcie na to poprawkę. Po drugie pisanie testów nie jest takie straszne i pisząc zgodnie z TDD możecie uniknąć wielu nieprzewidzianych sytuacji. Po trzecie nigdy nie ufaj w pełni w dane z zewnętrznego źródła. Nigdy nie wiesz, czy na wyjściu danych dostawca przewidział wszystkie przypadki. Wyłap przynajmniej te najbardziej krytyczne. I w końcu najważniejsze, każdą niespodziewaną sytuację i awarię analizujcie. Rozumiem, że czasem każda minuta się liczy i wrzucenie poprawki musi być natychmiastowe, jednak po wrzuceniu takiej poprawki dopiszcie testy na brakujące przypadki, sprawdźcie czy dane zostały na wcześniejszym etapie zwalidowane. Zaoszczędzi Wam to wiele nerwów w przyszłości.
Nie jest tak, że domyślna wartość w słowniku jest zła, powiem nawet że jest bardzo przydatna. Trzeba po prostu korzystać z niej świadomie.