Skip to content
wannacode

Lambdas

Problem

A whole function for one rule

The orders need to be sorted by total.

Order
class Order:
def __init__(self, customer, total):
self.customer = customer
self.total = total
orders = [
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}")
Output
Georgi: €12.90
Elena: €40.00
Maria: €75.50

sort(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 if or a loop, it is a regular function with def.
by_total = lambda o: o.total
total_of_two = lambda a, b: a.total + b.total
guest = 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)
Output
Georgi: €12.90
Elena: €40.00
Maria: €75.50
52.9
Guest

A 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.8
minus_five = lambda price: price - 5
print(student(50))
print(minus_five(50))
Output
40.0
45

student 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.8
minus_five = lambda price: price - 5
print(student(50))
print(minus_five(50))
Step by step, in words
  1. The lambda is an object of type function, and student points to it. Its body does not run yet.
  2. A second rule, a second object. Not a single class is written.
  3. student(50) passes 50 to the lambda: price is 50, and price * 0.8 gives 40.0.
  4. 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)
Output
40.0
Traceback (most recent call last):
...
TypeError: 'str' object is not callable
  • callable(x) tells you whether x can be called: callable(student) is True, and callable("student") is False.

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 True or False. "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 >= 50
label = 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)
Output
True
-> MARIA
Guest

In 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.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))
Output
12.67

then 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 of print. print(show("coffee")) prints "-> coffee" and then None.
  • The operator module has ready-made functions for the most common transformations. attrgetter("total") is the same as lambda o: o.total.

Try it

Combined conditions

What will the program print?

big = lambda o: o.total >= 50
maria = 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))
Answer

See it

Functions that take functions

Many built-in functions take a rule instead of a loop:
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}")
Output
bread
milk
coffee
coffee
Plovdiv: ['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, so list(...) collects them into a new list. The old one does not change: here cart just points to the new one.
  • max(list, key=function) and min find the largest and the smallest element by the rule. key is the same as in sort: 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 without if city not in by_city. Here the function is lambda: [], but it can also be list itself: it also returns a new empty list.
  • Python has no forEach. For an action with each element, you use for.

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

Often a lambda only passes its parameter to an existing function: lambda s: int(s). Then pass the function itself, by name and without parentheses:
names = ["maria", "georgi"]
join = ", ".join
upper = str.upper
parse = int
new_list = list
print(join(names))
print(upper("elena"))
print(parse("42") + 1)
items = new_list()
items.append("coffee")
print(items)
Output
maria, georgi
ELENA
43
['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: ", ".join is lambda items: ", ".join(items);
  • a method of the parameter: str.upper is lambda s: s.upper();
  • a function: int is lambda s: int(s);
  • a class: list is lambda: 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))
Output
Ana 6.0
Ivan 5.5
Peter 5.5
Boris 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)
Output
Ana 6.0
Ivan 5.5
Peter 5.5
Boris 4.75
  • A compare function, like compare in Java (a negative number, 0 or a positive number), is turned into a key with cmp_to_key from the functools module:
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))
Output
Boris 4.75
Ivan 5.5
Peter 5.5
Ana 6.0

Try it

What does reverse=True reverse?

Here the key is (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)
Answer

Try it

Find the bug

The program should print the sum of the prices. It stops with an error at the very first price. Which line is the bug on?

Pick the line with the bug.

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.2
discount = lambda price: price * (1 - rate)
print(discount(100))
rate = 0.5
print(discount(100))
Output
80.0
50.0

In 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

Solve the tasks in your editor.

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.

Output
Accessories | Headphones | €49.90
Accessories | Keyboard | €49.90
Accessories | Mouse | €25.00
Computers | Laptop | €1299.00
Computers | Tablet | €399.00

2. 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.

Output
Sofia, over €50:
Maria, Sofia: €75.50
Outside Sofia:
Georgi, Varna: €12.90
Dimitar, Plovdiv: €120.00
Free delivery (Sofia or over €100):
Maria, Sofia: €75.50
Elena, Sofia: €40.00
Dimitar, Plovdiv: €120.00
3. Discounts one after another. Make three discounts as lambdas: a student discount (−10%), a Black Friday discount (−€20) and "not below zero". Write then(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?
Output
€120.00 -> student, then Friday: €88.00; Friday, then student: €90.00
€15.00 -> student, then Friday: €0.00; Friday, then student: €0.00

More tasks

  • Cleaning a list. From a list of emails, remove the empty ones and the ones without @ with one filter, then print the rest with for.
  • Students by class. Group students by class ("10A", "10B") in a defaultdict(list) and print the classes in order with sorted.
  • 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

Check yourself

1. What can the body of a lambda contain?
2. Which of these is a condition: it takes an order and returns True or False?
3. What can replace lambda s: s.upper()?
4. students.sort(key=lambda s: (s.grade, s.name), reverse=True). What does reverse=True reverse?
5. Why does total += p in a function that is called in a loop stop with UnboundLocalError?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·