Функции подробно: значения по умолчанию, функции как объекты, *args, **kwargs и lambda

Цели урока
  • Определять параметры со значением по умолчанию и вызывать функцию с именованными аргументами
  • Понимать, что функция — объект — её можно сохранить в переменной и передать другой функции
  • Писать функции, которые принимают переменное число аргументов, с помощью *args и **kwargs
  • Писать простую lambda и использовать её с sorted(key=...)

Что мы изучим?

В теме чистого кода и отладки мы изучили основы: def, параметры, return. Сегодня изучим несколько возможностей, которые делают функции намного гибче:

  1. Значения по умолчанию (default values) и именованные аргументы (keyword arguments).
  2. Функции как объекты (Functions as Objects) — функция — такое же значение, как любое другое.
  3. *args — сколько угодно обычных аргументов.
  4. **kwargs — сколько угодно именованных аргументов.
  5. Lambda — короткая функция без имени.

Зачем это нужно?

  • print("a", "b", "c", sep="-") принимает сколько угодно аргументов плюс именованный аргумент. Как это устроено? Через *args.
  • sorted(students, key=...) принимает функцию как параметр, чтобы знать, по чему сортировать.
  • FastAPI (темы бэкенда и баз данных) целиком построен на идее, что функция — объект: вы пишете функцию, а FastAPI «регистрирует» её как Endpoint и запускает, когда приходит запрос.

Объяснение с основ

Значения по умолчанию

Параметру можно дать значение, которое будет использовано, если аргумент не передан:

def greet(name, greeting="Hello"):
    return greeting + ", " + name

greet("Dana") → "Hello, Dana". greet("Dana", "Hi") → "Hi, Dana".

Правило: параметры со значением по умолчанию идут после параметров без него.

Именованные аргументы

При вызове можно указать имя параметра, и тогда порядок уже не важен:

greet(greeting="Hey", name="Avi")

Это очень читаемо, когда параметров много: create_user("dana", age=24, is_admin=False).

Функции как объекты

В Python функция — это объект, как число или список. Поэтому можно:

  • Сохранить её в переменной: say = greet.
  • Положить её в список или dict.
  • Передать её как аргумент другой функции.

Критическая разница: greet (без скобок) = сама функция. greet("Dana") (со скобками) = её запуск и его результат.

Аналогия: рецепт и торт

greet — это рецепт: его можно сфотографировать и отправить подруге. greet("Dana") — это торт: результат выпечки по рецепту. Когда вы передаёте функцию другой функции, вы отправляете рецепт, чтобы та могла печь по нему, когда понадобится.

*args — переменное число аргументов

Звёздочка перед именем параметра говорит: «собери все обычные аргументы (по позиции) в tuple»:

def total(*numbers):
    return sum(numbers)

total(1, 2)          # numbers = (1, 2)
total(5, 10, 20, 1)  # numbers = (5, 10, 20, 1)

Имя args — соглашение. Работу делает звёздочка.

**kwargs — переменное число именованных аргументов

Две звёздочки: «собери все именованные аргументы в dict»:

def describe(**details):
    for key, value in details.items():
        print(key, "=", value)

describe(name="Dana", age=24, city="Haifa")
# details = {"name": "Dana", "age": 24, "city": "Haifa"}

kwargs = keyword arguments.

Lambda

lambda — способ записать маленькую функцию в одну строку, без имени:

lambda x: x * 2

Равносильно:

def double(x):
    return x * 2
  • После lambda — параметры. После : — одно выражение, которое и есть возвращаемое значение (без return).
  • Главное применение: передать короткую функцию как аргумент, вместо того чтобы определять отдельный def. Чаще всего с sorted, min, max.

Синтаксис

# default value + keyword arguments
def func(required, optional="default"):
    ...
func("a")
func("a", optional="b")

# function as an object
f = func            # no parentheses: the function itself
f("a")              # call it through the new name

def apply(operation, value):
    return operation(value)    # call the function we received

# *args and **kwargs
def func(*args, **kwargs):
    # args   -> tuple of positional arguments
    # kwargs -> dict of keyword arguments
    ...

# lambda
lambda parameters: expression
sorted(items, key=lambda item: item["price"])

Первый пример кода

week-2/functions_deep.py:

def make_greeting(name, greeting="Hello", punctuation="!"):
    return greeting + ", " + name + punctuation

print(make_greeting("Dana"))
print(make_greeting("Avi", "Good morning"))
print(make_greeting("Noa", punctuation="?"))

def shout(text):
    return text.upper()

def whisper(text):
    return text.lower() + "..."

def say(message, style):
    return style(message)

print(say("Hello there", shout))
print(say("Hello there", whisper))

Вывод:

Hello, Dana!
Good morning, Avi!
Hello, Noa?
HELLO THERE
hello there...

Разбор кода построчно

СтрокаОбъяснение
def make_greeting(name, greeting="Hello", punctuation="!")name обязателен. Два других необязательны и имеют значения по умолчанию.
make_greeting("Dana")Оба необязательных параметра берут значение по умолчанию.
make_greeting("Avi", "Good morning")Второй аргумент по позиции идёт в greeting.
make_greeting("Noa", punctuation="?")Именованный аргумент: пропускаем greeting (по умолчанию) и задаём только punctuation.
def say(message, style):style — параметр, который ожидает функцию.
return style(message)Запуск полученной функции. say не знает и не интересуется, что это за функция.
say("Hello there", shout)Передаём shout без скобок, то есть саму функцию.

Ещё один пример

*args, **kwargs и lambda в маленькой настоящей системе. week-2/store_tools.py:

def order_total(*prices, discount=0):
    subtotal = sum(prices)
    return subtotal - subtotal * discount / 100

print(order_total(10, 20, 30))
print(order_total(100, 50, discount=10))

def build_user(username, **extra):
    user = {"username": username}
    for key, value in extra.items():
        user[key] = value
    return user

print(build_user("dana"))
print(build_user("avi", age=30, city="Haifa", is_admin=True))

products = [
    {"name": "Laptop", "price": 3500},
    {"name": "Mouse", "price": 80},
    {"name": "Desk", "price": 900},
]

by_price = sorted(products, key=lambda product: product["price"])
for product in by_price:
    print(product["name"], product["price"])

cheapest = min(products, key=lambda product: product["price"])
print("Cheapest:", cheapest["name"])

Вывод:

60.0
135.0
{'username': 'dana'}
{'username': 'avi', 'age': 30, 'city': 'Haifa', 'is_admin': True}
Mouse 80
Desk 900
Laptop 3500
Cheapest: Mouse

Разбор:

  • def order_total(*prices, discount=0): — *prices собирает любое число цен в tuple. discount идёт после *prices, поэтому его можно передать только по имени (discount=10). Иначе Python не знал бы, где заканчиваются цены.
  • build_user("avi", age=30, ...) — username получает "avi", а всё остальное собирается в extra как dict: {"age": 30, "city": "Haifa", "is_admin": True}.
  • sorted(products, key=lambda product: product["price"]) — sorted не умеет сравнивать словари. key — это функция, которая говорит ему: «для каждого товара используй для сравнения его цену». lambda вызывается по разу для каждого товара.
  • min(..., key=...) — та же идея: «минимум по цене». Возвращается весь товар, а не только цена.

Типичная ошибка

Ошибка 1: передать результат вместо функции.

print(say("Hello", shout()))
TypeError: shout() missing 1 required positional argument: 'text'

shout() со скобками пытается запустить shout сразу, без аргумента. Когда передаёте функцию, пишите только имя: shout.

Ошибка 2: список или dict в качестве значения по умолчанию.

def add_item(item, cart=[]):
    cart.append(item)
    return cart

print(add_item("milk"))
print(add_item("bread"))
['milk']
['milk', 'bread']

Во втором вызове мы ожидали ['bread']! Значение по умолчанию [] создаётся один раз, при определении функции, и все вызовы используют один и тот же список (изменяемый — предыдущий урок!). Принятое исправление:

def add_item(item, cart=None):
    if cart is None:
        cart = []
    cart.append(item)
    return cart

Ошибка 3: слишком сложная lambda.

result = sorted(users, key=lambda u: (0 if u["active"] else 1, -u["score"], u["name"].lower()))

Это работает, но через неделю никто не поймёт (KISS!). Если lambda не читается с первого взгляда, напишите def с понятным именем.

Ошибка 4: параметр со значением по умолчанию перед обязательным параметром.

def greet(greeting="Hello", name):
SyntaxError: parameter without a default follows parameter with a default

Практика

Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.

Упражнение 1: Значения по умолчанию и именованные аргументы

Напишите функцию make_greeting(name, greeting="Hello", punct="!").

print(make_greeting("Dana"))
print(make_greeting("Avi", "Good morning"))
print(make_greeting("Noa", punct="?"))

Ожидаемый вывод:

Hello, Dana!
Good morning, Avi!
Hello, Noa?
Показать решение
def make_greeting(name, greeting="Hello", punct="!"):
    return greeting + ", " + name + punct


print(make_greeting("Dana"))
print(make_greeting("Avi", "Good morning"))
print(make_greeting("Noa", punct="?"))
Hello, Dana!
Good morning, Avi!
Hello, Noa?

В третьем вызове punct="?" передаётся по имени, поэтому можно пропустить greeting и оставить ему значение по умолчанию.

Упражнение 2: *args

Напишите функцию average(*numbers), которая получает любое количество чисел и возвращает среднее. Если не передано ни одного числа — выбрасывает ValueError с сообщением, показанным в ожидаемом выводе.

print(average(80, 90))
print(average(70, 80, 90, 100))
try:
    average()
except ValueError as error:
    print("error:", error)

Ожидаемый вывод:

85.0
85.0
error: need at least one number
Показать решение
def average(*numbers):
    if len(numbers) == 0:
        raise ValueError("need at least one number")
    return sum(numbers) / len(numbers)


print(average(80, 90))
print(average(70, 80, 90, 100))
try:
    average()
except ValueError as error:
    print("error:", error)
85.0
85.0
error: need at least one number

*numbers собирает все аргументы по позиции в tuple.

Упражнение 3: **kwargs

Напишите функцию format_profile(name, **details), которая возвращает строку с именем, за которым следует каждая дополнительная деталь в формате key=value.

print(format_profile("Dana", age=24, city="Haifa"))
print(format_profile("Avi"))

Ожидаемый вывод:

Dana: age=24 city=Haifa
Avi:
Показать решение
def format_profile(name, **details):
    line = name + ":"
    for key, value in details.items():
        line = line + " " + key + "=" + str(value)
    return line


print(format_profile("Dana", age=24, city="Haifa"))
print(format_profile("Avi"))
Dana: age=24 city=Haifa
Avi:

**details собирает все именованные аргументы в dict.

Упражнение 4: lambda в сортировке

products = [("Laptop", 3500), ("Mouse", 80), ("Desk", 900)]
  1. Отсортируйте по цене, от самого дешёвого к самому дорогому, и напечатайте каждый товар.
  2. Напечатайте название самого дорогого товара.

Ожидаемый вывод:

Mouse 80
Desk 900
Laptop 3500
Most expensive: Laptop
Показать решение
products = [("Laptop", 3500), ("Mouse", 80), ("Desk", 900)]

for name, price in sorted(products, key=lambda p: p[1]):
    print(name, price)

most_expensive = max(products, key=lambda p: p[1])
print("Most expensive:", most_expensive[0])
Mouse 80
Desk 900
Laptop 3500
Most expensive: Laptop

lambda p: p[1] — короткая функция, которая возвращает цену, — по ней сравнивают sorted и max.

Упражнение 5: Изменяемое значение по умолчанию

def add_item(item, cart=[]):
    cart.append(item)
    return cart

print(add_item("milk"))
print(add_item("bread"))
  1. Что будет напечатано во втором вызове и почему?
  2. Исправьте функцию.
Показать решение
  1. Напечатается ['milk', 'bread']. Значение по умолчанию [] создаётся один раз, при определении функции, — и все вызовы используют один и тот же список.
  2. Используйте None и создавайте новый список внутри функции:
def add_item(item, cart=None):
    if cart is None:
        cart = []
    cart.append(item)
    return cart


print(add_item("milk"))
print(add_item("bread"))
['milk']
['bread']

Итоги

  • Значение по умолчанию: def f(a, b=10). Параметры со значением по умолчанию — в конце. Никогда [] или {} по умолчанию (используйте None).
  • Именованный аргумент: f(a=1, b=2). Читаемо, и порядок не важен.
  • Функция — объект: f = функция, f() = её запуск. Её можно сохранить в переменной, в dict и передать как аргумент.
  • *args собирает обычные аргументы в tuple. **kwargs собирает именованные аргументы в dict.
  • lambda x: expression — короткая функция без имени. Идеальна для key= в sorted/min/max. Если сложно — лучше def.

Официальная документация