Към съдържанието
wannacode

Ламбди

Проблем

Цяла функция за едно правило

Поръчките трябва да се подредят по сума.

Order
class Order:
def __init__(self, customer, total):
self.customer = customer
self.total = total
orders = [
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.total
total_of_two = lambda a, b: a.total + b.total
guest = 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.8
minus_five = lambda price: price - 5
print(student(50))
print(minus_five(50))
Изход
40.0
45

student е обект от тип function. Викаш го като всяка функция: student(50). Две правила за цена, нито един клас.

Java тук има нужда от функционален интерфейс: интерфейс с един метод, който казва какъв е видът на правилото. В Python такова нещо не трябва. Всяка функция може да се подаде и извика.

student = lambda price: price * 0.8
minus_five = lambda price: price - 5
print(student(50))
print(minus_five(50))
Стъпка по стъпка, с думи
  1. Ламбдата е обект от тип function, а student сочи към него. Тялото ѝ още не се изпълнява.
  2. Второ правило, втори обект. Нито един клас не е написан.
  3. student(50) подава 50 на ламбдата: price е 50, а price * 0.8 дава 40.0.
  4. 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.0
Traceback (most recent call last):
...
TypeError: 'str' object is not callable
  • callable(x) казва дали x може да се извика: callable(student) е True, а callable("студент") е False.

Идеята

Четирите вида правила

Типът на всяко правило е function, но видовете правила са четири. Добре е да ги разпознаваш, защото функциите, които приемат правила, искат определен вид:

  • условие: приема стойност, връща True или False. „Голяма ли е поръчката?“
  • превръщане: приема едно, връща друго. Поръчка в етикет.
  • действие: приема стойност и прави нещо с нея: отпечатва я, записва я. Резултатът му не се ползва.
  • доставчик: не приема нищо, връща нов обект, когато поискаш.
big = lambda o: o.total >= 50
label = 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.2
rounded = 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.67

then приема две функции и връща трета: тя вика първата, а резултатът ѝ подава на втората.

По-точно, за любопитните
  • Действието все пак връща нещо: None, защото това е резултатът на print. print(show("кафе")) отпечатва „-> кафе“ и после None.
  • Модулът operator има готови функции за най-честите превръщания. attrgetter("total") е същото като lambda o: o.total.

Пробвай

Сглобени условия

Какво ще отпечата програмата?

big = lambda o: o.total >= 50
maria = 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))
Отговор

Виж го

Функции, които приемат функции

Много готови функции приемат правило вместо цикъл:
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 = ", ".join
upper = str.upper
parse = int
new_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)
Отговор

Пробвай

Намери грешката

Програмата трябва да отпечата сбора на цените. Спира с грешка още при първата цена. На кой ред е грешката?

Избери реда с грешката.

По-точно, за любопитните

Ламбдата вижда самата променлива, не копие. Ако променливата се смени след ламбдата, при следващото извикване тя вижда новата стойност:

rate = 0.2
discount = lambda price: price * (1 - rate)
print(discount(100))
rate = 0.5
print(discount(100))
Изход
80.0
50.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 €
3. Отстъпки една след друга. Направи три отстъпки като ламбди: студентска (−10%), за Черния петък (−20 €) и „не под нула“. Напиши 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. Пробвай всеки на няколко низа.

Тест

Провери се

1. Какво може да има в тялото на lambda?
2. Кое от тези е условие: приема поръчка и връща True или False?
3. С какво може да се замени lambda s: s.upper()?
4. students.sort(key=lambda s: (s.grade, s.name), reverse=True). Какво обръща reverse=True?
5. Защо total += p във функция, която се вика в цикъл, спира с UnboundLocalError?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·