Списъчни изрази и генератори
Проблем
Отчет в цикъл
Поръчките вече имат и град:
class Order: def __init__(self, customer, city, total): self.customer = customer self.city = city self.total = totalorders = [ Order("Мария", "София", 75.50), Order("Георги", "Варна", 12.90), Order("Елена", "София", 40.00), Order("Димитър", "Пловдив", 120.00), Order("Ана", "Варна", 64.00),]Искаш клиентите с поръчки поне 50 €, подредени по азбучен ред:
result = []for o in orders: if o.total >= 50: result.append(o.customer)result.sort()print(result)['Ана', 'Димитър', 'Мария']Шест реда, а въпросът е един. Кодът казва как: направи празен списък, обиколи, провери, добави, подреди. За да разбереш какво прави, трябва да го изпълниш наум. Добавиш ли „само първите трима“ или „без повторения“, цикълът ще расте.
Идеята
Списъчен израз: какво, а не как
Същият отчет, на един ред:
result = sorted([o.customer for o in orders if o.total >= 50])print(result)['Ана', 'Димитър', 'Мария'][o.customer for o in orders if o.total >= 50] се казва списъчен израз (на английски list comprehension). Чете се почти като изречение: „клиентът на всяка поръчка o от orders, ако сумата ѝ е поне 50“. Частите са:
for o in orders: откъде идват елементите;if o.total >= 50: кои остават. Това е условие, както вfilter. Може и да липсва;o.customerв началото: в какво се превръща всеки елемент, както вmap;- квадратните скоби: резултатът е нов списък.
sorted(...) подрежда и също връща нов списък. Списъкът orders не се променя.
По-точно, за любопитните
- Същите стъпки ги има и като функции с ламбди от предишния урок:
filterиmap. Резултатът е същият, но се чете отвътре навън, затова в Python обикновено се пише списъчен израз:
result = sorted(map(lambda o: o.customer, filter(lambda o: o.total >= 50, orders)))print(result)['Ана', 'Димитър', 'Мария']- Ако изразът е единственият параметър на функция, квадратните скоби може да се пропуснат:
sorted(o.customer for o in orders if o.total >= 50)дава същото. Тогава изразът е генератор. За него е следващата карта.
Виж го
Какво минава през филтъра
prices = [12, 75, 40, 130, 58]expensive = (p for p in prices if p >= 50)labels = (f"{p} €" for p in expensive)print(list(labels))Стъпка по стъпка, с думи
- Пет цени. Двата генератора само подготвят пътя: още нищо не е минало.
- 12 не минава if p >= 50 и отпада. До превръщането в текст не стига.
- 75 минава if, f"{p} €" го превръща в текста „75 €“, list() го събира.
- 40 също отпада. Всеки елемент минава целия път, преди да тръгне следващият.
- 130 и 58 минават. Списъкът prices не се променя.
- Резултатът е нов списък с три текста.
Петте цени тръгват една по една. 12 стига до проверката if p >= 50 и отпада. 75 минава, вторият генератор го прави на текста „75 €“, и list() го събира. После 40 отпада, а 130 и 58 минават.
Всеки елемент изминава целия път, преди да тръгне следващият. И нищо не тръгва, докато list() не поиска стойностите.
По-точно, за любопитните
Това се казва мързеливо изпълнение. Генераторът само записва какво трябва да се направи. Виж го с отпечатване във всяка стъпка:
def check(p): print("filter", p) return p >= 50
def double(p): print("map", p) return p * 2
stream = (double(p) for p in [12, 75, 40] if check(p))print("Генераторът е готов, нищо не е минало през него.")print(list(stream))Генераторът е готов, нищо не е минало през него.filter 12filter 75map 75filter 40[150]- Редът „Генераторът е готов“ е отпечатан, преди през генератора да е минал и един елемент.
map 75е веднага следfilter 75: елементите не чакат всички да минат проверката.- С квадратни скоби същият израз е списък и се смята веднага, на реда, където е написан:
def check(p): print("filter", p) return p >= 50
def double(p): print("map", p) return p * 2
now = [double(p) for p in [12, 75, 40] if check(p)]print("Списъкът е готов.")print(now)filter 12filter 75map 75filter 40Списъкът е готов.[150]- Затова от генератор може да се вземат само първите няколко стойности, без да се проверяват останалите.
isliceот модулаitertoolsспира, щом вземе колкото трябва:
from itertools import islice
def check(p): print("filter", p) return p >= 50
stream = (p for p in [75, 130, 58, 12, 99] if check(p))print(list(islice(stream, 2)))filter 75filter 130[75, 130]Идеята
Сбор, брой, средно
Резултатът може да е число вместо списък. Сборът на четните числа:
numbers = [1, 2, 3, 4, 5]total = sum(n for n in numbers if n % 2 == 0)print(f"Сума на четните числа: {total}")Сума на четните числа: 6sum събира числата, които получи. Генераторът в нея е без допълнителни скоби, защото е единственият ѝ параметър.Същото за поръчките:
big = len([o for o in orders if o.total >= 50])revenue = sum(o.total for o in orders)average = revenue / len(orders) if orders else 0max_total = max((o.total for o in orders), default=0)
print(f"Поръчки над 50 €: {big}")print(f"Оборот: {revenue:.2f} €")print(f"Средно: {average:.2f} €")print(f"Най-голяма: {max_total:.2f} €")Поръчки над 50 €: 3Оборот: 312.40 €Средно: 62.48 €Най-голяма: 120.00 €lenдава броя. Тук броим елементите на списъчен израз.- Средното е сборът, делено на броя. Ако поръчки няма, делението на 0 спира програмата, затова
if orders else 0. maxна празен списък спира сValueError.default=0казва какво да е, ако стойност няма. Това е следващата идея.
По-точно, за любопитните
С празен списък:
prices = []print(sum(prices))print(max(prices, default=0))try: print(max(prices))except ValueError as e: print("ValueError:", e)try: print(sum(prices) / len(prices))except ZeroDivisionError as e: print("ZeroDivisionError:", e)00ValueError: max() iterable argument is emptyZeroDivisionError: division by zeroИдеята
Подреди, отрежи, без повторения
ratings = [5, 3, 5, 4, 2, 4, 5]print(sorted(set(ratings)))
top = [o.customer for o in sorted(orders, key=lambda o: o.total, reverse=True)[:3]]print(top)
for i in range(1, 4): print(f"Маса {i}")[2, 3, 4, 5]['Димитър', 'Мария', 'Ана']Маса 1Маса 2Маса 3set(...)маха повторенията: в множеството всяка стойност е веднъж. Редът в множеството не е гарантиран, затова после идваsorted.sorted(orders, key=..., reverse=True)подрежда от най-голямата поръчка, сkeyот предишния урок.[:3]взима първите три елемента. Това е отрязък (slice): от началото до номер 3, без него. Заедно съсsortedтова е „топ 3“.range(1, 4)дава числата 1, 2, 3. Краят не се включва.
По-точно, за любопитните
Ако трябва да махнеш повторенията, но да запазиш реда на първата поява, dict.fromkeys прави речник с ключовете в реда на добавяне:
ratings = [5, 3, 5, 4, 2, 4, 5]print(list(dict.fromkeys(ratings)))[5, 3, 4, 2]Пробвай
Кое стига до края?
Какво ще отпечата програмата?
drinks = ["кафе", "чай", "кафе", "вода", "чай", "сок"]result = [d.upper() for d in sorted(set(drinks))[:3]]print(result)set маха повторенията: кафе, чай, вода, сок. sorted ги подрежда по азбука: вода, кафе, сок, чай. [:3] оставя първите три, а d.upper() ги прави с главни букви.Идеята
None: резултат, който може да липсва
Коя е най-голямата поръчка? Ако списъкът е празен, отговор няма. Тогава резултатът е None: „нищо“. Но викащият трябва да помни да провери. Забрави ли, програмата спира с AttributeError някъде по-късно.
Java за това има Optional: кутия, в която има стойност или е празна. В Python такава кутия няма. Липсата е None, а проверката е твоя работа, на мястото, където резултатът се ползва:
biggest = max(orders, key=lambda o: o.total, default=None)if biggest is not None: print(f"Най-голяма: {biggest.customer}")
huge = next((o for o in orders if o.total > 500), None)print(huge is not None)print(huge.customer if huge is not None else "няма такава")Най-голяма: ДимитърFalseняма такаваmax(..., default=None)връщаNone, вместо да спре, ако списъкът е празен.next(генератор, None)дава първата стойност на генератора илиNone, ако такава няма. Това е „първата поръчка над 500 €“, без да се проверяват останалите.is not Noneпроверява дали има стойност. СNoneсе сравнява сis, а не с==: така го иска PEP 8.x if условие else yдава резервата, ако стойност няма.
Твоите функции също връщат None, когато нямат отговор:
def nickname(user): # Връща прякора или None, ако човекът няма. return "Мими" if user == "maria" else Nonemaria = nickname("maria")if maria is not None: print(f"Прякор: {maria}")
georgi = nickname("georgi")print(georgi if georgi is not None else "без прякор")Прякор: Мимибез прякорПо-точно, за любопитните
- Ако забравиш проверката:
huge = next((o for o in orders if o.total > 500), None)print(huge.customer)Traceback (most recent call last): ...AttributeError: 'NoneType' object has no attribute 'customer'x or yсъщо дава резерва, но смята за липса и празния текст, и0. Ползвай го, само ако празният текст наистина е липса:
print(None or "без прякор")print("" or "без прякор")print(0 or 5)без прякорбез прякор5any(...)иall(...)отговарят сTrueилиFalseдали някой или всички елементи отговарят на условието:
print(any(o.total > 100 for o in orders))print(all(o.total >= 10 for o in orders))TrueTrueПробвай
Намери грешката
Избери реда с грешката.
Генераторът минава веднъж. list(expensive) на ред 3 го изчерпва, и на ред 4 в него нищо не е останало. Грешка няма, затова такъв пропуск се хваща трудно:
Брой: 2Цени: []Ако ти трябват два резултата, направи на ред 2 списък, с квадратни скоби: [p for p in prices if p >= 50]. Него може да ползваш колкото пъти искаш: len(expensive) и expensive.
Идеята
Групиране и съединяване
Колко поръчки има от всеки град и какъв е оборотът?
from collections import Counter, defaultdict
count_by_city = Counter(o.city for o in orders)print(dict(sorted(count_by_city.items())))
revenue_by_city = defaultdict(float)for o in orders: revenue_by_city[o.city] += o.totalfor city, total in sorted(revenue_by_city.items()): print(f"{city}: {total:.2f} €")
customers = ", ".join(sorted(o.customer for o in orders))print(customers){'Варна': 2, 'Пловдив': 1, 'София': 2}Варна: 76.90 €Пловдив: 120.00 €София: 115.50 €Ана, Георги, Димитър, Елена, МарияCounterот модулаcollectionsброи: за всеки град колко пъти се среща. Той е речник.defaultdict(float)започва всеки нов град от0.0(това връщаfloat()), а+=добавя сумата.", ".join(...)съединява текстовете в един, със запетая между тях.
Речникът пази ключовете в реда на добавяне. sorted(речник.items()) дава двойките ключ–стойност подредени по ключа, затова отчетът излиза по азбука.
По-точно, за любопитните
- За всеки град списък с имената:
from collections import defaultdict
names_by_city = defaultdict(list)for o in orders: names_by_city[o.city].append(o.customer)print(dict(sorted(names_by_city.items()))){'Варна': ['Георги', 'Ана'], 'Пловдив': ['Димитър'], 'София': ['Мария', 'Елена']}- Речников израз прави речник от списък:
{ключ: стойност for ...}. Ако ключ се повтори, последната стойност печели, без грешка (в JavatoMapхвърля изключение):
last_by_city = {o.city: o.customer for o in orders}print(last_by_city){'София': 'Елена', 'Варна': 'Ана', 'Пловдив': 'Димитър'}Упражнение
Отчети със списъчни изрази
1. Продажби по категория. Клас Sale с продукт, категория, брой и единична цена и метод revenue() (брой по цена). Сумирай оборота по категория с defaultdict(float), отпечатай категориите по азбука и общия оборот. Продажбите: кафе на зърна, напитки, 12 бр. по 18,50 €; чай, напитки, 30 по 4,20 €; шоколад, сладки, 40 по 2,80 €; бисквити, сладки, 25 по 3,10 €; мед, храни, 8 по 12 €; пак кафе на зърна, 5 по 18,50 €.
Напитки: 440.50 €Сладки: 189.50 €Храни: 96.00 €Общо: 726.00 €2. Топ 3 продукта. Със същите продажби намери трите продукта с най-голям оборот. Кафето е в две продажби, затова първо групирай по продукт и сумирай, после подреди записите на речника по стойност. Подсказка: sorted(by_product.items(), key=lambda item: item[1], reverse=True)[:3]: всеки елемент на items() е двойка (продукт, оборот).
Кафе на зърна: 314.50 €Чай: 126.00 €Шоколад: 112.00 €3. Средна оценка. Прочети един ред с оценки, разделени с интервал. Отпечатай средната, най-високата и колко са отличните (5,50 и нагоре). Подсказка: [float(g) for g in input().split()].
4.50 5.75 6.00 3.00 5.50Средно: 4.95Най-висока: 6.00Отлични: 34. Думите в текст. Прочети ред текст и отпечатай думите в него без повторения, по азбучен ред, с малки букви, разделени със запетая. Намери поредиците от букви с re.findall(r"[^\W\d_]+", text): \w е буква, цифра или _, а [^\W\d_] е „буква, но не цифра и не _“.
Кафе с мляко, кафе без захар. Чай с мед!без, захар, кафе, мед, мляко, с, чайОще задачи
- Най-дългата дума. С
max(words, key=len)намери най-дългата дума в изречение. Какво ще стане за празен ред и как помагаdefault? - Средно по клас. Ученици с име, клас и оценка: събери оценките за всеки клас в
defaultdict(list)и отпечатай средната за всеки клас. - Големи и малки поръчки. С
anyпровери дали има поръчка над 1000 €, а сallдали всички са поне 10 €.
Тест