MidТеория6 min

Lambda и функциональное программирование

lambda, map/filter/reduce, functools.partial, operator, itertools и ФП-паттерны

Python -- мультипарадигменный язык с хорошей поддержкой функционального программирования. Разберём анонимные функции, функции высшего порядка и инструменты из functools и itertools.

Lambda-функции

Lambda -- анонимная функция в одно выражение:

# Basic lambda
square = lambda x: x ** 2
print(square(5))  # 25

# Multiple arguments
add = lambda a, b: a + b
print(add(3, 4))  # 7

# With default arguments
greet = lambda name, greeting="Привет": f"{greeting}, {name}!"
print(greet("Иван"))               # Привет, Иван!
print(greet("Мария", "Здравствуй")) # Здравствуй, Мария!

# Lambda as key function (most common use case)
names = ["Иван", "Мария", "Александр", "Пётр", "Ли"]
print(sorted(names, key=lambda n: len(n)))
# ['Ли', 'Иван', 'Пётр', 'Мария', 'Александр']

# Sort by multiple criteria
students = [("Иван", 85), ("Мария", 92), ("Пётр", 85)]
print(sorted(students, key=lambda s: (-s[1], s[0])))
# [('Мария', 92), ('Иван', 85), ('Пётр', 85)]

# Lambda with conditional expression
classify = lambda x: "положительное" if x > 0 else "отрицательное" if x < 0 else "ноль"
print(classify(5))    # положительное
print(classify(-3))   # отрицательное
print(classify(0))    # ноль

Когда использовать lambda

# GOOD: short, one-time use as key/callback
sorted(data, key=lambda x: x["name"])
button.on_click(lambda: print("Clicked"))
max(users, key=lambda u: u.age)

# BAD: complex logic, reuse needed, hard to read
# Don't do this:
process = lambda x: (x.strip().lower().replace(" ", "_") if x else "default")

# Instead, define a named function:
def normalize_name(name: str) -> str:
    """Normalize a name for use as an identifier."""
    if not name:
        return "default"
    return name.strip().lower().replace(" ", "_")

# BAD: assigning lambda to a variable (PEP 8 violation)
# square = lambda x: x ** 2  # Use def instead

# GOOD: inline in an expression
print(sorted([3, 1, 4], key=lambda x: -x))  # [4, 3, 1]

map(), filter(), reduce()

map()

# map(func, iterable) - apply function to each element
numbers = [1, 2, 3, 4, 5]
squares = list(map(lambda x: x**2, numbers))
print(squares)  # [1, 4, 9, 16, 25]

# map with built-in functions
strings = ["1", "2", "3", "4"]
ints = list(map(int, strings))
print(ints)  # [1, 2, 3, 4]

# map with multiple iterables
a = [1, 2, 3]
b = [10, 20, 30]
sums = list(map(lambda x, y: x + y, a, b))
print(sums)  # [11, 22, 33]

# Usually, list comprehension is more Pythonic
squares = [x**2 for x in numbers]  # preferred over map()

filter()

# filter(func, iterable) - keep elements where func returns True
numbers = range(20)
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)  # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# Filter with None removes falsy values
data = [0, 1, "", "hello", None, False, True, [], [1, 2]]
truthy = list(filter(None, data))
print(truthy)  # [1, 'hello', True, [1, 2]]

# List comprehension equivalent (preferred)
evens = [x for x in range(20) if x % 2 == 0]

reduce()

from functools import reduce

# reduce(func, iterable, initial) - accumulate result
numbers = [1, 2, 3, 4, 5]
total = reduce(lambda acc, x: acc + x, numbers, 0)
print(total)  # 15

# Product of all numbers
product = reduce(lambda acc, x: acc * x, numbers, 1)
print(product)  # 120

# Find maximum (reduce is rarely the best choice)
maximum = reduce(lambda a, b: a if a > b else b, numbers)
print(maximum)  # 5
# Better: max(numbers)

# Flatten nested lists
nested = [[1, 2], [3, 4], [5, 6]]
flat = reduce(lambda acc, lst: acc + lst, nested, [])
print(flat)  # [1, 2, 3, 4, 5, 6]
# Better: [x for sublist in nested for x in sublist]

# Build a pipeline
from functools import reduce

def pipeline(*functions):
    """Compose functions into a pipeline."""
    def execute(data):
        return reduce(lambda result, func: func(result), functions, data)
    return execute

process = pipeline(
    str.strip,
    str.lower,
    lambda s: s.replace(" ", "_"),
)
print(process("  Hello World  "))  # 'hello_world'

functools: partial и другие инструменты

functools.partial

from functools import partial

# Freeze some arguments of a function
def power(base: int, exponent: int) -> int:
    return base ** exponent

square = partial(power, exponent=2)
cube = partial(power, exponent=3)

print(square(5))  # 25
print(cube(5))    # 125

# Practical: configure HTTP requests
import urllib.request

def fetch(url: str, timeout: int = 30, headers: dict | None = None) -> str:
    """Simplified fetch function."""
    return f"GET {url} timeout={timeout}"

# Create specialized fetchers
fast_fetch = partial(fetch, timeout=5)
api_fetch = partial(fetch, headers={"Authorization": "Bearer token123"})

print(fast_fetch("https://api.example.com"))
# GET https://api.example.com timeout=5

# partial vs lambda
# These are equivalent:
add_ten = partial(lambda a, b: a + b, 10)
add_ten_lambda = lambda b: 10 + b  # noqa

# partial preserves function metadata better
print(add_ten.func)     # <lambda>
print(add_ten.args)     # (10,)
print(add_ten.keywords) # {}

functools.reduce и accumulate

from functools import reduce
from itertools import accumulate

numbers = [1, 2, 3, 4, 5]

# reduce gives final result only
total = reduce(lambda a, b: a + b, numbers)
print(total)  # 15

# accumulate gives intermediate results (lazy)
running_sum = list(accumulate(numbers))
print(running_sum)  # [1, 3, 6, 10, 15]

# Custom accumulator
import operator
running_product = list(accumulate(numbers, operator.mul))
print(running_product)  # [1, 2, 6, 24, 120]

# Running maximum
data = [3, 1, 4, 1, 5, 9, 2, 6]
running_max = list(accumulate(data, max))
print(running_max)  # [3, 3, 4, 4, 5, 9, 9, 9]

Модуль operator

Модуль operator предоставляет функции для стандартных операторов:

import operator

# Arithmetic
print(operator.add(2, 3))      # 5
print(operator.mul(4, 5))      # 20
print(operator.pow(2, 10))     # 1024

# Comparison
print(operator.lt(3, 5))       # True (3 < 5)
print(operator.eq("a", "a"))   # True

# Item access
getter = operator.itemgetter(1)
print(getter([10, 20, 30]))    # 20

# Multiple items
multi_getter = operator.itemgetter(0, 2)
print(multi_getter([10, 20, 30]))  # (10, 30)

# Practical: sort by dictionary key
users = [
    {"name": "Иван", "age": 25},
    {"name": "Мария", "age": 30},
    {"name": "Пётр", "age": 22},
]

by_age = sorted(users, key=operator.itemgetter("age"))
print([u["name"] for u in by_age])  # ['Пётр', 'Иван', 'Мария']

# attrgetter for object attributes
from dataclasses import dataclass

@dataclass
class Student:
    name: str
    grade: int

students = [Student("Иван", 85), Student("Мария", 92), Student("Пётр", 78)]
by_grade = sorted(students, key=operator.attrgetter("grade"), reverse=True)
print([s.name for s in by_grade])  # ['Мария', 'Иван', 'Пётр']

# methodcaller
names = ["иван", "мария", "пётр"]
upper_names = list(map(operator.methodcaller("upper"), names))
print(upper_names)  # ['ИВАН', 'МАРИЯ', 'ПЁТР']

itertools -- инструменты для итерации

from itertools import (
    chain, islice, cycle, repeat,
    takewhile, dropwhile,
    product, permutations, combinations,
    groupby, starmap, zip_longest
)

# chain - combine iterables
combined = list(chain([1, 2], [3, 4], [5, 6]))
print(combined)  # [1, 2, 3, 4, 5, 6]

# chain.from_iterable - flatten one level
matrix = [[1, 2], [3, 4], [5, 6]]
flat = list(chain.from_iterable(matrix))
print(flat)  # [1, 2, 3, 4, 5, 6]

# islice - lazy slicing of any iterable
from itertools import count
first_5_squares = list(islice((x**2 for x in count(1)), 5))
print(first_5_squares)  # [1, 4, 9, 16, 25]

# cycle - infinite repetition
from itertools import cycle
colors = cycle(["red", "green", "blue"])
print([next(colors) for _ in range(7)])
# ['red', 'green', 'blue', 'red', 'green', 'blue', 'red']

# takewhile / dropwhile
data = [1, 3, 5, 7, 2, 4, 6, 8]
before_even = list(takewhile(lambda x: x % 2 != 0, data))
print(before_even)  # [1, 3, 5, 7]

after_odd = list(dropwhile(lambda x: x % 2 != 0, data))
print(after_odd)  # [2, 4, 6, 8]

# product - Cartesian product
sizes = ["S", "M", "L"]
colors = ["red", "blue"]
print(list(product(sizes, colors)))
# [('S', 'red'), ('S', 'blue'), ('M', 'red'), ('M', 'blue'), ('L', 'red'), ('L', 'blue')]

# permutations and combinations
print(list(permutations("ABC", 2)))
# [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]

print(list(combinations("ABCD", 2)))
# [('A', 'B'), ('A', 'C'), ('A', 'D'), ('B', 'C'), ('B', 'D'), ('C', 'D')]

# groupby (requires sorted input!)
data = sorted([
    ("А", "Иван"), ("Б", "Мария"), ("А", "Пётр"), ("Б", "Анна")
])
for key, group in groupby(data, key=lambda x: x[0]):
    print(f"Группа {key}: {[name for _, name in group]}")
# Группа А: ['Иван', 'Пётр']
# Группа Б: ['Анна', 'Мария']

# starmap - map with unpacking
pairs = [(2, 5), (3, 2), (10, 3)]
results = list(starmap(pow, pairs))
print(results)  # [32, 9, 1000]

Композиция функций

from functools import reduce
from typing import Callable, TypeVar

T = TypeVar("T")

def compose(*functions: Callable) -> Callable:
    """Compose functions: compose(f, g, h)(x) = f(g(h(x)))."""
    def composed(x):
        return reduce(lambda result, func: func(result), reversed(functions), x)
    return composed

# Usage
add_one = lambda x: x + 1
double = lambda x: x * 2
to_str = lambda x: f"Result: {x}"

transform = compose(to_str, double, add_one)
print(transform(5))  # 'Result: 12'  (5 -> 6 -> 12 -> 'Result: 12')

# Pipe (left-to-right composition)
def pipe(*functions: Callable) -> Callable:
    """Pipe functions: pipe(f, g, h)(x) = h(g(f(x)))."""
    def piped(x):
        return reduce(lambda result, func: func(result), functions, x)
    return piped

process = pipe(
    str.strip,
    str.lower,
    lambda s: s.split(),
    lambda words: [w.capitalize() for w in words],
    " ".join,
)

print(process("  hello BEAUTIFUL world  "))  # 'Hello Beautiful World'

Итоги

  • lambda -- анонимные функции для коротких выражений (ключи сортировки, колбэки)
  • map/filter -- ФП-альтернативы циклам, но list comprehension обычно предпочтительнее
  • reduce -- свёртка последовательности в одно значение
  • functools.partial -- фиксация аргументов функции
  • operator -- itemgetter, attrgetter, methodcaller для чистого ФП-стиля
  • itertools -- мощный набор ленивых итераторов (chain, product, groupby, combinations)
  • Композиция функций (compose/pipe) -- создание конвейеров обработки данных
  • Python предпочитает comprehensions над map/filter, но ФП-инструменты незаменимы в сложных случаях

Проверь себя

Когда уместно использовать lambda вместо def?

Какой из подходов предпочтительнее в Python: map(func, iterable) или list comprehension?

Что делает operator.itemgetter('name')?

Что делает itertools.chain([1,2], [3,4], [5,6])?