5.02. Функции
Функции
Что такое функция?
Функция — это именованный блок кода, предназначенный для выполнения определённой задачи. Она может принимать входные данные (аргументы), обрабатывать их и возвращать результат. Функции позволяют организовать код по принципу модульности: повторное использование, изоляция логики, упрощение отладки и тестирования.
В отличие от некоторых других языков программирования, таких как Java или C#, где методы строго связаны с классами и требуют явного указания типов возвращаемого значения и параметров, в Python функции являются автономными конструкциями, не привязанными к классам по умолчанию, а система типов — динамической. Это не означает, что типы отсутствуют, но они проверяются во время выполнения, а не на этапе компиляции.
Особенности функций в Python
Функции в Python обладают рядом характерных черт, отличающих их от аналогичных конструкций в строго типизированных языках:
- Динамическая типизация параметров и возвращаемых значений. Типы аргументов и возвращаемых значений не указываются в сигнатуре функции. Проверка корректности типов происходит только во время выполнения.
- Гибкая передача аргументов. Поддерживается комбинированное использование позиционных, именованных аргументов, а также распаковка коллекций через
*argsи словарей через**kwargs. - Функции — объекты первого класса. Функции можно присваивать переменным, передавать как аргументы, возвращать из других функций. Эта особенность фундаментальна для понимания поведения функций в Python, даже если она будет рассмотрена подробнее позже.
- Отсутствие необходимости объявления типа возврата. Ключевое слово return используется для возврата значения, но тип возвращаемого результата не декларируется. Функция может возвращать разные типы в разных ветвях исполнения.
- Автоматическое создание локального пространства имён. При вызове функции создаётся новая область видимости, в которой существуют локальные переменные. Управление доступом к глобальным и нелокальным переменным осуществляется через явные инструкции.
Объявление функции
Функция в Python объявляется с помощью ключевого слова def, за которым следует имя функции, список параметров в круглых скобках и двоеточие.
Тело функции представляет собой блок кода, выделенный отступом (обычно 4 пробела).
def greet(name):
return f"Hello, {name}!"
Синтаксис:
def <имя_функции>(<список_параметров>):
<тело функции>
Имя функции должно следовать правилам идентификаторов: начинаться с буквы или подчёркивания, содержать только буквы, цифры и подчёркивания. Рекомендуется использовать змеиный_регистр (snake_case) согласно PEP 8.
Структура функции
Тело функции форматируется с соблюдением отступов. Отступ определяет принадлежность строк к блоку. Все строки с одинаковым отступом считаются частью одного блока. Завершение отступа означает выход из области действия функции.
Параметр — переменная, указанная в сигнатуре функции.
Аргумент — фактическое значение, переданное функции при вызове.
Например, в вызове greet("Alice"), "Alice" — аргумент, а name в def greet(name): — параметр.
Позиционные параметры. Аргументы передаются в порядке, соответствующем порядку параметров в определении функции.
def add(a, b):
return a + b
result = add(3, 5) # a=3, b=5
Именованные параметры (ключевые аргументы). Аргументы передаются с указанием имени параметра. Это позволяет задавать аргументы в произвольном порядке.
result = add(b=5, a=3)
Именованные аргументы могут использоваться только после позиционных.
Часто можно встретить такой подход:
print(4, end="")
Он означает, буквально "выведи 4" и добавляет в конец строки пустую строку "".
Параметр end="" — это именованный аргумент функции print() в Python, который определяет, чем завершается вывод строки.
По умолчанию:
print(x) # эквивалентно print(x, end='\n')
— после вывода x добавляется символ перевода строки (\n), и следующий print() начнётся с новой строки.
Если задать end="":
print(x, end="")
— после x ничего не выводится, в том числе нет перевода строки. Следующий print() (или другой вывод) продолжит писать в той же строке.
Без end (по умолчанию):
print("A")
print("B")
Вывод:
A
B
С end="":
print("A", end="")
print("B", end="")
print("C")
Вывод:
ABC
Параметры со значениями по умолчанию. Параметру может быть присвоено значение по умолчанию. Такой параметр становится необязательным при вызове.
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet("Bob")) # Hello, Bob!
print(greet("Bob", "Hi")) # Hi, Bob!
Параметры со значениями по умолчанию должны следовать после всех обязательных параметров. Значения по умолчанию вычисляются один раз при определении функции. Не рекомендуется использовать изменяемые объекты (например, списки или словари) в качестве значений по умолчанию.
Параметры до / — только позиционные.
Параметры после * — только именованные.
def func(pos_only, /, standard, *, kwd_only):
pass
Здесь:
pos_onlyможно передать только позиционно.standard— и позиционно, и по имени.kwd_only— только по имени.
Произвольное число аргументов
Python предоставляет механизм для приёма произвольного числа аргументов.
• *args — собирает все позиционные аргументы в кортеж.
• **kwargs — собирает все именованные аргументы в словарь.
def log_call(*args, **kwargs):
print(f"Позиционные аргументы: {args}")
print(f"Именованные аргументы: {kwargs}")
log_call(1, 2, action="save", user="admin")
# Вывод:
# Позиционные аргументы: (1, 2)
# Именованные аргументы: {'action': 'save', 'user': 'admin'}
Эти механизмы часто используются при создании декораторов, базовых классов, функций-обёрток.
Распаковка аргументов при вызове:
args = [1, 2]
kwargs = {"c": 3, "d": 4}
def func(a, b, c, d):
return a + b + c + d
func(*args, **kwargs) # эквивалентно func(1, 2, c=3, d=4)
Возврат значения
Оператор return завершает выполнение функции и возвращает указанное значение вызывающему коду. Если return отсутствует или не содержит значения, функция возвращает None.
def square(x):
return x ** 2
def do_nothing():
pass # вернёт None
result = do_nothing() # result == None
Кортеж
Функция может возвращать несколько значений — на практике это кортеж:
def divide_remainder(a, b):
return a // b, a % b
quotient, remainder = divide_remainder(10, 3)
Это синтаксический сахар для создания и распаковки кортежа.
Области видимости
В Python действует правило LEGB:
- L — Local (локальная область — внутри функции),
- E — Enclosing (объемлющая — вложенная функция),
- G — Global (глобальная — модуль),
- B — Built-in (встроенная — вроде
print,len).
Переменные, объявленные внутри функции, по умолчанию являются локальными.
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # local
inner()
print(x) # enclosing
outer()
print(x) # global
Для изменения глобальной переменной внутри функции используется ключевое слово global:
counter = 0
def increment():
global counter
counter += 1
Для изменения переменной из объемлющей области — nonlocal:
def outer():
x = 1
def inner():
nonlocal x
x += 1
inner()
print(x) # 2
Без nonlocal интерпретатор создаст новую локальную переменную x, не затрагивая внешнюю.
Анонимные функции
Кроме именованных функций, объявляемых через def, Python поддерживает анонимные функции с помощью выражения lambda.
Синтаксис:
lambda <параметры>: <выражение>
Тело lambda — одно выражение (не блок кода). Не может содержать операторы (return, if, for и т.п.), кроме тернарного оператора. Не может иметь аннотаций типов (хотя это технически возможно, но не поддерживается стандартом).
Примеры:
square = lambda x: x ** 2
add = lambda a, b: a + b
is_even = lambda n: n % 2 == 0
lambda часто используется как аргумент для функций высшего порядка:
data = [(1, 'b'), (2, 'a'), (3, 'c')]
sorted(data, key=lambda x: x[1]) # сортировка по второму элементу
Хотя lambda удобна для кратких случаев, её использование не обязательно. Любая lambda-функция может быть заменена на обычную, определённую через def. Выбор зависит от читаемости и контекста.
Рекурсия
Функция называется рекурсивной, если она вызывает саму себя.
Рекурсия — естественный способ решения задач, имеющих рекурсивную структуру (например, обход деревьев, вычисление факториала, последовательности Фибоначчи). Пример:
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
Базовый случай — условие завершения рекурсии. Без него возникает бесконечная рекурсия и переполнение стека вызовов.
Глубина рекурсии в Python ограничена (по умолчанию ~1000). Её можно изменить с помощью sys.setrecursionlimit(), но это не рекомендуется из-за риска аварийного завершения.
Рекурсия может быть менее эффективной, чем итерация, из-за накладных расходов на вызовы. Однако она часто упрощает реализацию алгоритмов на основе разделяй-и-властвуй.
Иногда требуется объявить функцию, тело которой пока не реализовано. Для этого используется оператор-заглушка pass.
def todo():
pass # заглушка, ничего не делает
Также можно использовать ... (Ellipsis), хотя это менее распространено:
def stub():
...
Благодаря динамической типизации, гибкой системе аргументов и возможности возвращать функции, они служат основой для многих парадигм программирования — от процедурной до функциональной.
Функции первого класса
Термин «объект первого класса» (first-class object) происходит из теории языков программирования и означает сущность, которая:
- Может быть присвоена переменной.
- Может быть передана как аргумент другой функции.
- Может быть возвращена из функции.
- Может быть сохранена в структурах данных (списках, словарях, множествах и т.п.).
Если такой объект — функция, то говорят: функция является объектом первого класса. В Python все функции удовлетворяют этим критериям. Это не просто синтаксическая конструкция — это полноценные объекты, существующие в runtime, обладающие типом, атрибутами и поведением, как и любые другие данные. Важно: термин «первого класса» не означает «лучше» или «высокопроизводительнее». Он указывает на степень интеграции функций в систему типов и выполнения программы.
Например, в C функции нельзя напрямую хранить в списках или возвращать из других функций без использования указателей (что выходит за рамки базовой модели языка). В Java до версии 8 методы не были объектами первого класса — их можно было использовать только через интерфейсы или рефлексию.
В Python же функция — полноценный объект, создаваемый во время исполнения.
Поскольку функции в Python — объекты, они могут использоваться так же, как числа, строки или списки.
Присваивание функции переменной:
def greet(name):
return f"Hello, {name}!"
say_hello = greet # Присваиваем функцию переменной
print(say_hello("Alice")) # Вызов через новое имя
Здесь greet и say_hello — два имени, ссылающиеся на один и тот же объект-функцию.
Функция, объявленная через ключевое слово def, создаёт объект и связывает его с именем. Это имя можно использовать для вызова функции, но сам объект функции существует независимо от имени:
def calculate_total(items):
return sum(items)
processor = calculate_total
result = processor([10, 20, 30]) # 60
Имена calculate_total и processor ссылаются на один и тот же объект. Изменение одного имени не влияет на доступность функции через другое имя. Удаление исходного имени через del calculate_total не уничтожает объект, пока на него существует хотя бы одна активная ссылка.
Функция может принимать другую функцию в качестве параметра. Такие функции называются функциями высшего порядка:
def transform_data(data, transformer):
return [transformer(item) for item in data]
def to_upper(text):
return text.upper()
words = ["python", "java", "csharp"]
result = transform_data(words, to_upper) # ['PYTHON', 'JAVA', 'CSHARP']
Стандартная библиотека Python активно использует этот подход. Функции map(), filter(), sorted() принимают функции для преобразования или сравнения элементов:
numbers = [3, 1, 4, 1, 5]
sorted_numbers = sorted(numbers, key=lambda x: -x) # [5, 4, 3, 1, 1]
Передача функции как аргумента:
def apply_operation(func, x):
return func(x)
def square(n):
return n ** 2
result = apply_operation(square, 5) # 25
Такой подход лежит в основе функций высшего порядка — функций, которые принимают или возвращают другие функции.
Хранение функций в структурах данных:
operations = {
'square': lambda x: x ** 2,
'double': lambda x: x * 2,
'negate': lambda x: -x
}
result = operations['square'](4) # 16
Это позволяет динамически выбирать поведение программы.
Атрибуты функций
Функция как объект имеет атрибуты, доступные во время выполнения:
def fetch_user(user_id):
"""Получает данные пользователя по идентификатору"""
return {"id": user_id, "name": "User"}
print(fetch_user.__name__) # 'fetch_user'
print(fetch_user.__doc__) # 'Получает данные пользователя по идентификатору'
print(fetch_user.__module__) # имя модуля, где определена функция
Пользовательские атрибуты можно добавлять динамически:
fetch_user.version = "1.2"
fetch_user.cache_ttl = 300
greet.author = "John Doe"
print(greet.author) # John Doe
Эти атрибуты используются фреймворками для хранения метаданных: права доступа, кэш-политики, версии API.
Функция — это объект типа function. Его можно исследовать:
print(type(greet)) # <class 'function'>
print(callable(greet)) # True
print(greet.__name__) # 'greet'
print(greet.__module__) # имя модуля
Замыкание
Замыкание — это функция, которая «захватывает» переменные из своей объемлющей области видимости и продолжает иметь к ним доступ даже после завершения этой области.
def create_formatter(prefix):
def format_value(value):
return f"{prefix}: {value}"
return format_value
error_formatter = create_formatter("ERROR")
info_formatter = create_formatter("INFO")
print(error_formatter("File not found")) # ERROR: File not found
print(info_formatter("Process started")) # INFO: Process started
Функция format_value захватывает переменную prefix из области видимости create_formatter. После возврата format_value из create_formatter, переменная prefix сохраняется в специальной структуре, называемой ячейкой окружения (cell). Объект функции хранит ссылку на это окружение через атрибут __closure__:
print(error_formatter.__closure__[0].cell_contents) # 'ERROR'
Условия возникновения замыкания - наличие вложенной функции; вложенная функция ссылается на переменную из внешней функции; внешняя функция возвращает внутреннюю.
def make_multiplier(factor):
def multiply(x):
return x * factor # захватывает factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
Здесь multiply — замыкание. Оно «помнит» значение factor, даже когда make_multiplier уже завершилась.
Когда функция возвращается, вместе с ней возвращается и ссылка на её окружение (cell objects), в котором хранятся захваченные переменные. Это обеспечивает инкапсуляцию состояния без использования классов. Проверить наличие замыкания можно через атрибут __closure__:
print(double.__closure__) # (<cell at 0x...: int object at 0x...>,)
print(double.__closure__[0].cell_contents) # 2
Замыкания позволяют инкапсулировать состояние без использования классов. Пример счётчика:
def create_counter(start=0):
count = start
def increment(step=1):
nonlocal count
count += step
return count
return increment
counter = create_counter(10)
print(counter()) # 11
print(counter(5)) # 16
Переменная count существует в окружении функции increment и сохраняет своё значение между вызовами. Ключевое слово nonlocal указывает, что переменная count берётся из ближайшей объемлющей области, а не создаётся как новая локальная переменная.
Другой пример — фабрика обработчиков событий с контекстом:
def make_event_handler(user_id):
def handle_event(event_type, data):
log_entry = {
"user_id": user_id,
"event_type": event_type,
"timestamp": get_current_time(),
"payload": data
}
save_to_audit_log(log_entry)
return handle_event
admin_handler = make_event_handler(1)
admin_handler("login", {"ip": "192.168.1.100"})
Каждый обработчик сохраняет свой user_id в замыкании, обеспечивая изоляцию контекста без передачи идентификатора при каждом вызове.
Декоратор
Декоратор — это функция, которая принимает другую функцию и возвращает новую функцию, расширяя или изменяя её поведение без модификации исходного кода. Декораторы реализуют принцип открытости/закрытости: объект (функция) открыт для расширения, но закрыт для изменения. Они позволяют вынести побочные эффекты (логирование, проверку прав, кэширование) в отдельные модули, улучшая читаемость и повторное использование.
Синтаксис @decorator. Без декоратора:
def my_function():
print("Основная логика")
def logger(func):
def wrapper():
print(f"Вызов: {func.__name__}")
func()
print("Вызов завершён")
return wrapper
my_function = logger(my_function)
С синтаксисом @:
@logger
def my_function():
print("Основная логика")
Оба варианта эквивалентны. Декоратор применяется при определении функции.
Декоратор с параметром — это функция, возвращающая декоратор. То есть трёхуровневая структура:
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def say_hello():
print("Привет!")
Здесь repeat(3) возвращает декоратор. Декоратор применяется к say_hello.
Примеры декораторов
Логирование вызовов:
def log_execution(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
try:
result = func(*args, **kwargs)
duration = time.time() - start
logger.info(f"{func.__name__} завершена за {duration:.4f} сек")
return result
except Exception as e:
duration = time.time() - start
logger.error(f"{func.__name__} завершена с ошибкой за {duration:.4f} сек: {e}")
raise
return wrapper
Кэширование результатов:
def memoize(func):
cache = {}
@wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
Проверка прав доступа:
def require_role(role):
def decorator(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if user.get('role') != role:
raise PermissionError(f"Требуется роль {role}")
return func(user, *args, **kwargs)
return wrapper
return decorator
@require_role('admin')
def delete_user(user, target_id):
database.delete(target_id)
Декораторы с параметрами
Декоратор с параметрами представляет собой функцию, которая возвращает декоратор. Структура имеет три уровня вложенности:
def retry(max_attempts=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
attempts = 0
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except Exception as e:
attempts += 1
if attempts == max_attempts:
raise
time.sleep(delay)
logger.warning(f"Попытка {attempts} не удалась: {e}")
return wrapper
return decorator
@retry(max_attempts=5, delay=2)
def fetch_data(url):
response = requests.get(url)
response.raise_for_status()
return response.json()
Параметры max_attempts и delay передаются при применении декоратора. Функция retry возвращает конкретный декоратор, настроенный под заданные параметры. Этот декоратор затем применяется к функции fetch_data.
Сохранение метаданных функции
Декоратор заменяет оригинальную функцию обёрткой. Без дополнительных мер метаданные теряются:
print(compute.__name__) # 'wrapper', а не 'compute'
Модуль functools предоставляет декоратор @wraps, который копирует метаданные из оригинальной функции в обёртку:
from functools import wraps
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Вызов {func.__name__}")
return func(*args, **kwargs)
return wrapper
После применения @wraps атрибуты __name__, __doc__, __module__ сохраняют значения оригинальной функции.
Хранение функций в структурах данных
Функции можно помещать в списки, кортежи, словари и множества. Это позволяет создавать динамические наборы поведений:
operations = {
'add': lambda a, b: a + b,
'subtract': lambda a, b: a - b,
'multiply': lambda a, b: a * b
}
def execute(op_name, x, y):
return operations[op_name](x, y)
result = execute('multiply', 6, 7) # 42
Такой подход применяется в маршрутизаторах веб-фреймворков, обработчиках событий и системах плагинов. Каждый маршрут или событие связывается с функцией-обработчиком, которая вызывается при наступлении соответствующего условия.