Ламбди
Проблем
Цяла функция за едно правило
Поръчките трябва да се подредят по сума.
class Order: def __init__(self, customer, total): self.customer = customer self.total = totalorders = [ Order("Мария", 75.50), Order("Георги", 12.90), Order("Елена", 40.00),]
def by_total(order): return order.total
orders.sort(key=by_total)
for o in orders: print(f"{o.customer}: {o.total:.2f} €")Георги: 12.90 €Елена: 40.00 €Мария: 75.50 €sort(key=...) приема функция, която за всеки елемент казва по какво да се подреди. Тук това е by_total: за поръчката връща сумата ѝ.
Работи. Но за едно правило („по сума“) пишеш цяла функция с име и празни редове около нея, далеч от мястото, където я ползваш. А тя се вика само тук. Искаш да подадеш само правилото.
Идеята
Ламбда: само правилото
Същото подреждане, на един ред:
orders.sort(key=lambda o: o.total)lambda o: o.total се казва ламбда израз, накратко ламбда. Това е функция без име: след lambda са параметрите, след двоеточието е тялото. return не се пише: стойността на тялото е резултатът.
Видовете запис:
- Един параметър:
lambda o: o.total > 50. Няколко:lambda a, b: a.total + b.total. Без параметри:lambda: .... - Тялото е един израз. Ако правилото иска няколко реда,
ifили цикъл, то е обикновена функция сdef.
by_total = lambda o: o.totaltotal_of_two = lambda a, b: a.total + b.totalguest = lambda: Order("Гост", 0)
orders.sort(key=by_total)for o in orders: print(f"{o.customer}: {o.total:.2f} €")print(total_of_two(orders[0], orders[1]))print(guest().customer)Георги: 12.90 €Елена: 40.00 €Мария: 75.50 €52.9ГостЛамбдата е стойност: може да я сложиш в променлива и да я подадеш по-късно. Това важи и за функция, написана с def: by_total без скоби е самата функция, затова sort(key=by_total) работеше.
По-точно, за любопитните
Ръководството PEP 8 съветва: ако правилото има име, пиши го с def, а не име = lambda .... Ламбдата е за мястото, където правилото се подава направо, като в sort(key=lambda o: o.total). В този урок някои правила са в променливи, за да се види, че ламбдата е стойност като всяка друга.
Идеята
Функцията е стойност
Откъде sort знае, че lambda o: o.total е функцията за ключа? Не му трябва да знае нищо повече. В Python функцията е стойност като число или текст: може да я сложиш в променлива, да я подадеш на друга функция и да я извикаш със скоби.
student = lambda price: price * 0.8minus_five = lambda price: price - 5
print(student(50))print(minus_five(50))40.045student е обект от тип function. Викаш го като всяка функция: student(50). Две правила за цена, нито един клас.
Java тук има нужда от функционален интерфейс: интерфейс с един метод, който казва какъв е видът на правилото. В Python такова нещо не трябва. Всяка функция може да се подаде и извика.
student = lambda price: price * 0.8minus_five = lambda price: price - 5
print(student(50))print(minus_five(50))Стъпка по стъпка, с думи
- Ламбдата е обект от тип function, а student сочи към него. Тялото ѝ още не се изпълнява.
- Второ правило, втори обект. Нито един клас не е написан.
- student(50) подава 50 на ламбдата: price е 50, а price * 0.8 дава 40.0.
- minus_five(50): същото число, друго правило, 45.
По-точно, за любопитните
- Ако искаш да запишеш какво правило очакваш, Python има подсказка за типа:
Callable[[float], float]значи „функция, която приема число и връща число“. Python не я проверява. Грешното нещо стига до мястото, където се извиква, и чак там програмата спира:
from collections.abc import Callable
PriceRule = Callable[[float], float]
def show(rule: PriceRule, price: float): print(rule(price))
show(lambda price: price * 0.8, 50)show("студент", 50)40.0Traceback (most recent call last): ...TypeError: 'str' object is not callablecallable(x)казва далиxможе да се извика:callable(student)еTrue, аcallable("студент")еFalse.
Идеята
Четирите вида правила
Типът на всяко правило е function, но видовете правила са четири. Добре е да ги разпознаваш, защото функциите, които приемат правила, искат определен вид:
- условие: приема стойност, връща
TrueилиFalse. „Голяма ли е поръчката?“ - превръщане: приема едно, връща друго. Поръчка в етикет.
- действие: приема стойност и прави нещо с нея: отпечатва я, записва я. Резултатът му не се ползва.
- доставчик: не приема нищо, връща нов обект, когато поискаш.
big = lambda o: o.total >= 50label = lambda o: o.customer.upper()show = lambda text: print("-> " + text)guest = lambda: Order("Гост", 0)
order = Order("Мария", 75.50)print(big(order))show(label(order))print(guest().customer)True-> МАРИЯГостВ Java правилата са обекти с методи, които ги сглобяват: условията се свързват с and, or и negate („не“), а функциите се нареждат една след друга с andThen. В Python такива методи няма. Новото правило е нова функция, която вика старите: lambda o: big(o) and maria(o). Две функции една след друга също се сглобяват с функция:
price_with_vat = lambda price: price * 1.2rounded = lambda price: round(price, 2)then = lambda f, g: lambda x: g(f(x))
final_price = then(price_with_vat, rounded)print(final_price(10.555))12.67then приема две функции и връща трета: тя вика първата, а резултатът ѝ подава на втората.
По-точно, за любопитните
- Действието все пак връща нещо:
None, защото това е резултатът наprint.print(show("кафе"))отпечатва „-> кафе“ и послеNone. - Модулът
operatorима готови функции за най-честите превръщания.attrgetter("total")е същото катоlambda o: o.total.
Пробвай
Сглобени условия
Какво ще отпечата програмата?
big = lambda o: o.total >= 50maria = lambda o: o.customer == "Мария"big_and_maria = lambda o: big(o) and maria(o)big_or_maria = lambda o: big(o) or maria(o)not_big = lambda o: not big(o)
small = Order("Мария", 30)large = Order("Георги", 80)
print(big_and_maria(small))print(big_or_maria(small))print(not_big(large))big_and_maria дава False, а big_or_maria дава True, защото е на Мария. Поръчката на Георги е 80 €, голяма е, а not обръща отговора: False.Виж го
Функции, които приемат функции
from collections import defaultdict
cart = ["хляб", "", "мляко", " ", "кафе"]cart = list(filter(lambda item: item.strip() != "", cart))for item in cart: print(item)print(max(cart, key=lambda item: len(item)))
by_city = defaultdict(lambda: [])by_city["Пловдив"].append("Мария")by_city["София"].append("Георги")by_city["Пловдив"].append("Елена")for city, names in sorted(by_city.items()): print(f"{city}: {names}")хлябмлякокафемлякоПловдив: ['Мария', 'Елена']София: ['Георги']filter(условие, списък)пропуска само елементите, за които условието е вярно. Дава ги един по един, затоваlist(...)ги събира в нов списък. Старият не се променя: тукcartпросто сочи новия.max(списък, key=функция)иminнамират най-големия и най-малкия елемент по правилото.keyе същият като вsort: тук е дължината, затова печели „мляко“.defaultdict(функция)е речник, който вика функцията, когато ключът липсва, слага резултата под ключа и връща него. Така групирането по град става безif city not in by_city. Тук функцията еlambda: [], но може да е и самотоlist: и то връща нов празен списък.forEachв Python няма. За действие с всеки елемент си еfor.
Речникът пази ключовете в реда на добавяне. sorted(by_city.items()) ги подрежда, затова Пловдив е преди София.
Идеята
Функция по име, без скоби
lambda s: int(s). Тогава подай самата функция, по име и без скоби:names = ["мария", "георги"]join = ", ".joinupper = str.upperparse = intnew_list = list
print(join(names))print(upper("елена"))print(parse("42") + 1)items = new_list()items.append("кафе")print(items)мария, георгиЕЛЕНА43['кафе']Java за това има отделен запис, препратка към метод с ::. В Python е достатъчно името. Видовете са същите четири:
- метод на конкретен обект:
", ".joinеlambda items: ", ".join(items); - метод на параметъра:
str.upperеlambda s: s.upper(); - функция:
intеlambda s: int(s); - клас:
listеlambda: list(). Класът се вика като функция и връща нов обект.
Ако ламбдата прави нещо повече, например lambda s: s.strip().upper(), остави я ламбда.
Виж го
Подреждане по няколко полета
key може да върне кортеж (на английски tuple): няколко стойности в скоби. Кортежите се сравняват отляво надясно: първо първите стойности, а при равенство следващите.
Учениците са подредени по оценка, от най-високата, а при равни оценки по име:
students = [ Student("Иван", 5.50), Student("Ана", 6.00), Student("Петър", 5.50), Student("Борис", 4.75),]students.sort(key=lambda s: (-s.grade, s.name))Ана 6.0Иван 5.5Петър 5.5Борис 4.75-s.grade обръща посоката само на оценката: по-високата оценка дава по-малко число и застава отпред. Името остава по азбука. Нито един if.
По-точно, за любопитните
- Текст не може да се обърне с минус. Подреждането в Python обаче е стабилно: равните елементи остават в реда, в който са били. Затова „по оценка наобратно, после по име“ става и с две подреждания. Първо по второто правило, после по първото:
students.sort(key=lambda s: s.name)students.sort(key=lambda s: s.grade, reverse=True)Ана 6.0Иван 5.5Петър 5.5Борис 4.75- Функция за сравнение, като
compareв Java (отрицателно число, 0 или положително), се превръща вkeyсcmp_to_keyот модулаfunctools:
from functools import cmp_to_key
def compare(a, b): return (a.grade > b.grade) - (a.grade < b.grade)
students.sort(key=cmp_to_key(compare))Борис 4.75Иван 5.5Петър 5.5Ана 6.0Пробвай
Какво обръща reverse=True?
(s.grade, s.name), без минус, а посоката обръща reverse=True.Какво ще отпечата програмата?
students = [ Student("Иван", 5.50), Student("Ана", 6.00), Student("Петър", 5.50), Student("Борис", 4.75),]
students.sort(key=lambda s: (s.grade, s.name), reverse=True)for s in students: print(s.name, s.grade)reverse=True обръща подреждането по целия ключ, а той е „по оценка, после по име“. Затова се обръщат и двете: оценките са от най-високата, а Петър е преди Иван. Ако искаш само оценките наобратно, минусът е пред оценката, както в картата преди тази.Пробвай
Намери грешката
Избери реда с грешката.
Функцията add присвоява на total:
Traceback (most recent call last): ... total += p ^^^^^UnboundLocalError: cannot access local variable 'total' where it is not associated with a valueЩом във функция има присвояване на име, Python решава, че това е локална променлива на функцията. total += p иска първо да прочете локалната total, а тя още няма стойност. Външната total дори не се пипа.
Да се чете външна променлива е позволено. Да ѝ се присвоява, не, освен ако функцията не каже global total (или nonlocal total, ако променливата е в друга функция). Това рядко е добра идея. За сбор ползвай обикновен цикъл: for p in prices: total += p на най-горно ниво. В следващия урок ще го направиш и на един ред.
В ламбда пък += изобщо не може: тялото ѝ е израз, а присвояването не е. lambda p: total += p е SyntaxError.
По-точно, за любопитните
Ламбдата вижда самата променлива, не копие. Ако променливата се смени след ламбдата, при следващото извикване тя вижда новата стойност:
rate = 0.2discount = lambda price: price * (1 - rate)print(discount(100))
rate = 0.5print(discount(100))80.050.0В Java това е забранено точно заради такива изненади. Ако правилото трябва да помни стойност, подай му я като параметър.
Упражнение
Правила като стойности
1. Подреждане по няколко полета. Клас Product с име, категория и цена. Подреди продуктите по категория, в една категория от най-скъпия, а при равна цена по име, с едно sort и кортеж в key. Продуктите са: мишка, аксесоари, 25 €; лаптоп, компютри, 1299 €; клавиатура, аксесоари, 49,90 €; таблет, компютри, 399 €; слушалки, аксесоари, 49,90 €.
Аксесоари | Клавиатура | 49.90 €Аксесоари | Слушалки | 49.90 €Аксесоари | Мишка | 25.00 €Компютри | Лаптоп | 1299.00 €Компютри | Таблет | 399.00 €2. Филтър на поръчки. Добави към Order град. Напиши функция select(orders, rule), която връща нов списък само с поръчките, за които правилото е вярно. Направи две правила, sofia и big (поне 50 €), и отпечатай поръчките, за които са верни и двете, тези извън София и тези „в София или поне 100 €“. Всяко от трите правила е ламбда, която вика sofia и big. Поръчките: Мария, София, 75,50 €; Георги, Варна, 12,90 €; Елена, София, 40 €; Димитър, Пловдив, 120 €.
София, над 50 €: Мария, София: 75.50 €Извън София: Георги, Варна: 12.90 € Димитър, Пловдив: 120.00 €Безплатна доставка (София или над 100 €): Мария, София: 75.50 € Елена, София: 40.00 € Димитър, Пловдив: 120.00 €then(f, g), която връща функция: първо f, после g. С нея свържи отстъпките в два реда: студентската първо и петъчната първо. Пробвай с 120 € и с 15 €. Редът има ли значение?120.00 € -> студент, после петък: 88.00 €; петък, после студент: 90.00 €15.00 € -> студент, после петък: 0.00 €; петък, после студент: 0.00 €Още задачи
- Чистене на списък. От списък с имейли махни празните и тези без
@с единfilter, после отпечатай останалите сfor. - Ученици по клас. Групирай ученици по клас (
"10А","10Б") вdefaultdict(list)и отпечатай класовете по ред сsorted. - Валидатори. Направи речник с три валидатора като ламбди: непразен текст, само цифри (
str.isdigit), дължина до 10. Пробвай всеки на няколко низа.
Тест