Lambdas
Problem
A whole function for one rule
The orders need to be sorted by total.
class Order: def __init__(self, customer, total): self.customer = customer self.total = totalorders = [ Order("Maria", 75.50), Order("Georgi", 12.90), Order("Elena", 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}")Georgi: €12.90Elena: €40.00Maria: €75.50sort(key=...) takes a function that says, for each element, what to sort it by. Here that is by_total: for an order, it returns its total.
It works. But for one rule ("by total") you write a whole function with a name and blank lines around it, far from the place where you use it. And it is called only here. You want to pass just the rule.
The idea
A lambda: just the rule
The same sort, on one line:
orders.sort(key=lambda o: o.total)lambda o: o.total is called a lambda expression, or a lambda for short. It is a function with no name: after lambda come the parameters, after the colon comes the body. You do not write return: the value of the body is the result.
The ways to write it:
- One parameter:
lambda o: o.total > 50. Several:lambda a, b: a.total + b.total. No parameters:lambda: .... - The body is one expression. If the rule needs several lines, an
ifor a loop, it is a regular function withdef.
by_total = lambda o: o.totaltotal_of_two = lambda a, b: a.total + b.totalguest = lambda: Order("Guest", 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)Georgi: €12.90Elena: €40.00Maria: €75.5052.9GuestA lambda is a value: you can put it in a variable and pass it later. This is also true for a function written with def: by_total without parentheses is the function itself, which is why sort(key=by_total) worked.
More precisely, if you are curious
The PEP 8 style guide advises: if the rule has a name, write it with def, not name = lambda .... A lambda is for the place where the rule is passed directly, as in sort(key=lambda o: o.total). In this lesson some rules are in variables, so you can see that a lambda is a value like any other.
The idea
A function is a value
How does sort know that lambda o: o.total is the key function? It does not need to know anything more. In Python a function is a value, like a number or a string: you can put it in a variable, pass it to another function and call it with parentheses.
student = lambda price: price * 0.8minus_five = lambda price: price - 5
print(student(50))print(minus_five(50))40.045student is an object of type function. You call it like any function: student(50). Two price rules, and not a single class.
Here Java needs a functional interface: an interface with one method that says what kind of rule it is. Python does not need anything like that. Any function can be passed and called.
student = lambda price: price * 0.8minus_five = lambda price: price - 5
print(student(50))print(minus_five(50))Step by step, in words
- The lambda is an object of type function, and student points to it. Its body does not run yet.
- A second rule, a second object. Not a single class is written.
- student(50) passes 50 to the lambda: price is 50, and price * 0.8 gives 40.0.
- minus_five(50): the same number, a different rule, 45.
More precisely, if you are curious
- If you want to write down what rule you expect, Python has a type hint:
Callable[[float], float]means "a function that takes a number and returns a number". Python does not check it. The wrong thing gets to the place where it is called, and only there does the program stop:
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("student", 50)40.0Traceback (most recent call last): ...TypeError: 'str' object is not callablecallable(x)tells you whetherxcan be called:callable(student)isTrue, andcallable("student")isFalse.
The idea
The four kinds of rules
The type of every rule is function, but there are four kinds of rules. It helps to recognize them, because the functions that take rules want a specific kind:
- condition: takes a value, returns
TrueorFalse. "Is the order big?" - transformation: takes one thing, returns another. An order into a label.
- action: takes a value and does something with it: prints it, saves it. Its result is not used.
- supplier: takes nothing, returns a new object when you ask.
big = lambda o: o.total >= 50label = lambda o: o.customer.upper()show = lambda text: print("-> " + text)guest = lambda: Order("Guest", 0)
order = Order("Maria", 75.50)print(big(order))show(label(order))print(guest().customer)True-> MARIAGuestIn Java, rules are objects with methods that combine them: conditions are joined with and, or and negate ("not"), and functions are chained one after another with andThen. Python has no such methods. The new rule is a new function that calls the old ones: lambda o: big(o) and maria(o). Two functions one after another are also combined with a function:
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 takes two functions and returns a third one: it calls the first, and passes its result to the second.
More precisely, if you are curious
- An action still returns something:
None, because that is the result ofprint.print(show("coffee"))prints "-> coffee" and thenNone. - The
operatormodule has ready-made functions for the most common transformations.attrgetter("total")is the same aslambda o: o.total.
Try it
Combined conditions
What will the program print?
big = lambda o: o.total >= 50maria = lambda o: o.customer == "Maria"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("Maria", 30)large = Order("Georgi", 80)
print(big_and_maria(small))print(big_or_maria(small))print(not_big(large))big_and_maria gives False, and big_or_maria gives True, because it is Maria's. Georgi's order is €80, it is big, and not flips the answer: False.See it
Functions that take functions
from collections import defaultdict
cart = ["bread", "", "milk", " ", "coffee"]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["Plovdiv"].append("Maria")by_city["Sofia"].append("Georgi")by_city["Plovdiv"].append("Elena")for city, names in sorted(by_city.items()): print(f"{city}: {names}")breadmilkcoffeecoffeePlovdiv: ['Maria', 'Elena']Sofia: ['Georgi']filter(condition, list)lets through only the elements for which the condition is true. It gives them one at a time, solist(...)collects them into a new list. The old one does not change: herecartjust points to the new one.max(list, key=function)andminfind the largest and the smallest element by the rule.keyis the same as insort: here it is the length, so "coffee" wins.defaultdict(function)is a dictionary that calls the function when a key is missing, puts the result under the key and returns it. This way grouping by city works withoutif city not in by_city. Here the function islambda: [], but it can also belistitself: it also returns a new empty list.- Python has no
forEach. For an action with each element, you usefor.
The dictionary keeps the keys in the order they were added. sorted(by_city.items()) sorts them, which is why Plovdiv comes before Sofia.
The idea
A function by name, without parentheses
lambda s: int(s). Then pass the function itself, by name and without parentheses:names = ["maria", "georgi"]join = ", ".joinupper = str.upperparse = intnew_list = list
print(join(names))print(upper("elena"))print(parse("42") + 1)items = new_list()items.append("coffee")print(items)maria, georgiELENA43['coffee']Java has a separate notation for this, the method reference with ::. In Python the name is enough. The kinds are the same four:
- a method of a specific object:
", ".joinislambda items: ", ".join(items); - a method of the parameter:
str.upperislambda s: s.upper(); - a function:
intislambda s: int(s); - a class:
listislambda: list(). A class is called like a function and returns a new object.
If the lambda does something more, for example lambda s: s.strip().upper(), leave it as a lambda.
See it
Sorting by several fields
key can return a tuple: several values in parentheses. Tuples are compared from left to right: the first values first, and if they are equal, the next ones.
The students are sorted by grade, from the highest, and by name when the grades are equal:
students = [ Student("Ivan", 5.50), Student("Ana", 6.00), Student("Peter", 5.50), Student("Boris", 4.75),]students.sort(key=lambda s: (-s.grade, s.name))Ana 6.0Ivan 5.5Peter 5.5Boris 4.75-s.grade flips the direction of the grade only: a higher grade gives a smaller number and goes first. The name stays alphabetical. Not a single if.
More precisely, if you are curious
- You cannot flip text with a minus. But sorting in Python is stable: equal elements stay in the order they were in. So "by grade in reverse, then by name" also works with two sorts. First by the second rule, then by the first:
students.sort(key=lambda s: s.name)students.sort(key=lambda s: s.grade, reverse=True)Ana 6.0Ivan 5.5Peter 5.5Boris 4.75- A compare function, like
comparein Java (a negative number, 0 or a positive number), is turned into akeywithcmp_to_keyfrom thefunctoolsmodule:
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))Boris 4.75Ivan 5.5Peter 5.5Ana 6.0Try it
What does reverse=True reverse?
(s.grade, s.name), with no minus, and reverse=True flips the direction.What will the program print?
students = [ Student("Ivan", 5.50), Student("Ana", 6.00), Student("Peter", 5.50), Student("Boris", 4.75),]
students.sort(key=lambda s: (s.grade, s.name), reverse=True)for s in students: print(s.name, s.grade)reverse=True reverses the sort by the whole key, and the key is "by grade, then by name". So both are reversed: the grades go from the highest, and Peter comes before Ivan. If you want only the grades in reverse, the minus goes in front of the grade, as in the card before this one.Try it
Find the bug
Pick the line with the bug.
The function add assigns to total:
Traceback (most recent call last): ... total += p ^^^^^UnboundLocalError: cannot access local variable 'total' where it is not associated with a valueAs soon as a function assigns to a name, Python decides it is a local variable of the function. total += p first wants to read the local total, and it has no value yet. The outer total is not even touched.
Reading a variable from outside is allowed. Assigning to it is not, unless the function says global total (or nonlocal total, if the variable is in another function). That is rarely a good idea. For a sum, use a regular loop: for p in prices: total += p at the top level. In the next lesson you will do it on one line too.
In a lambda, += is not possible at all: its body is an expression, and an assignment is not one. lambda p: total += p is a SyntaxError.
More precisely, if you are curious
A lambda sees the variable itself, not a copy. If the variable changes after the lambda, the next call sees the new value:
rate = 0.2discount = lambda price: price * (1 - rate)print(discount(100))
rate = 0.5print(discount(100))80.050.0In Java this is forbidden precisely because of surprises like this. If the rule needs to remember a value, pass it as a parameter.
Exercise
Rules as values
1. Sorting by several fields. A class Product with a name, a category and a price. Sort the products by category, within a category from the most expensive, and by name when the price is equal, with one sort and a tuple in key. The products are: mouse, accessories, €25; laptop, computers, €1299; keyboard, accessories, €49.90; tablet, computers, €399; headphones, accessories, €49.90.
Accessories | Headphones | €49.90Accessories | Keyboard | €49.90Accessories | Mouse | €25.00Computers | Laptop | €1299.00Computers | Tablet | €399.002. Filtering orders. Add a city to Order. Write a function select(orders, rule) that returns a new list with only the orders for which the rule is true. Make two rules, sofia and big (at least €50), and print the orders for which both are true, the ones outside Sofia and the ones "in Sofia or at least €100". Each of the three rules is a lambda that calls sofia and big. The orders: Maria, Sofia, €75.50; Georgi, Varna, €12.90; Elena, Sofia, €40; Dimitar, Plovdiv, €120.
Sofia, over €50: Maria, Sofia: €75.50Outside Sofia: Georgi, Varna: €12.90 Dimitar, Plovdiv: €120.00Free delivery (Sofia or over €100): Maria, Sofia: €75.50 Elena, Sofia: €40.00 Dimitar, Plovdiv: €120.00then(f, g), which returns a function: f first, then g. Use it to chain the discounts in two orders: the student discount first and the Friday discount first. Try it with €120 and with €15. Does the order matter?€120.00 -> student, then Friday: €88.00; Friday, then student: €90.00€15.00 -> student, then Friday: €0.00; Friday, then student: €0.00More tasks
- Cleaning a list. From a list of emails, remove the empty ones and the ones without
@with onefilter, then print the rest withfor. - Students by class. Group students by class (
"10A","10B") in adefaultdict(list)and print the classes in order withsorted. - Validators. Make a dictionary with three validators as lambdas: non-empty text, digits only (
str.isdigit), length up to 10. Try each one on a few strings.
Test