Easy📖Теория6 min

Определение функций и аргументы

def, *args/**kwargs, positional-only (/), keyword-only (*), return, type hints

Определение функций и аргументы

Функции -- основной строительный блок программ на Python. Python предоставляет гибкую систему аргументов с позиционными, именованными, positional-only и keyword-only параметрами.

Определение функций

# Basic function definition
def greet(name: str) -> str:
    """Return a greeting message."""
    return f"Привет, {name}!"

print(greet("Иван"))  # Привет, Иван!

# Function without return value (returns None)
def log_message(message: str) -> None:
    """Print a log message with timestamp."""
    from datetime import datetime
    timestamp = datetime.now().strftime("%H:%M:%S")
    print(f"[{timestamp}] {message}")

# Multiple return values (actually returns a tuple)
def divide(a: float, b: float) -> tuple[float, float]:
    """Return quotient and remainder."""
    return a // b, a % b

quotient, remainder = divide(17, 5)
print(f"{quotient=}, {remainder=}")  # quotient=3.0, remainder=2.0

# Early return
def find_first_negative(numbers: list[int]) -> int | None:
    """Find the first negative number in the list."""
    for n in numbers:
        if n < 0:
            return n
    return None

print(find_first_negative([1, 2, -3, 4]))  # -3
print(find_first_negative([1, 2, 3]))       # None

Docstrings (Google Style)

def calculate_bmi(weight: float, height: float) -> float:
    """Calculate Body Mass Index (BMI).

    Args:
        weight: Body weight in kilograms.
        height: Height in meters.

    Returns:
        BMI value as a float.

    Raises:
        ValueError: If weight or height is not positive.

    Examples:
        >>> calculate_bmi(70, 1.75)
        22.857142857142858
    """
    if weight <= 0 or height <= 0:
        raise ValueError("Weight and height must be positive")
    return weight / height ** 2

# Access docstring
print(calculate_bmi.__doc__)
help(calculate_bmi)

Типы аргументов

Позиционные и именованные

def create_user(name: str, age: int, city: str = "Москва") -> dict:
    """Create a user dictionary."""
    return {"name": name, "age": age, "city": city}

# Positional arguments
user1 = create_user("Иван", 25)
user2 = create_user("Мария", 30, "Петербург")

# Keyword arguments (in any order)
user3 = create_user(age=28, name="Пётр", city="Казань")

# Mixed (positional first, then keyword)
user4 = create_user("Анна", age=22)

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

# Default values
def connect(host: str = "localhost", port: int = 5432) -> str:
    return f"Connected to {host}:{port}"

print(connect())                      # localhost:5432
print(connect("db.example.com"))      # db.example.com:5432
print(connect(port=3306))             # localhost:3306

# DANGER: mutable default arguments!
def add_item_bad(item: str, items: list[str] = []) -> list[str]:
    """BAD: mutable default is shared between calls!"""
    items.append(item)
    return items

print(add_item_bad("a"))  # ['a']
print(add_item_bad("b"))  # ['a', 'b'] - BUG! Expected ['b']

# CORRECT: use None as default for mutable arguments
def add_item(item: str, items: list[str] | None = None) -> list[str]:
    """GOOD: create a new list on each call."""
    if items is None:
        items = []
    items.append(item)
    return items

print(add_item("a"))  # ['a']
print(add_item("b"))  # ['b'] - correct!

*args и **kwargs

# *args - variable positional arguments (tuple)
def sum_all(*args: int) -> int:
    """Sum any number of arguments."""
    return sum(args)

print(sum_all(1, 2, 3))       # 6
print(sum_all(1, 2, 3, 4, 5)) # 15

# **kwargs - variable keyword arguments (dict)
def build_html_tag(tag: str, **attrs: str) -> str:
    """Build an HTML tag with attributes."""
    attr_str = " ".join(f'{k}="{v}"' for k, v in attrs.items())
    return f"<{tag} {attr_str}>" if attr_str else f"<{tag}>"

print(build_html_tag("div", id="main", class_="container"))
# <div id="main" class_="container">

# Combined: regular + *args + **kwargs
def log(level: str, *messages: str, **context: str) -> None:
    """Log messages with context."""
    msg = " ".join(messages)
    ctx = ", ".join(f"{k}={v}" for k, v in context.items())
    print(f"[{level}] {msg}" + (f" ({ctx})" if ctx else ""))

log("INFO", "User", "logged", "in", user="ivan", ip="192.168.1.1")
# [INFO] User logged in (user=ivan, ip=192.168.1.1)

# Passing *args and **kwargs to another function
def wrapper(*args, **kwargs):
    """Pass all arguments to the wrapped function."""
    print(f"Calling with args={args}, kwargs={kwargs}")
    return sum_all(*args)

Positional-only (/) и Keyword-only (*)

# Positional-only parameters (before /)
# Cannot be passed as keyword arguments
def pow(base: float, exp: float, /) -> float:
    """Power function with positional-only parameters."""
    return base ** exp

print(pow(2, 10))         # 1024
# pow(base=2, exp=10)     # TypeError!

# Keyword-only parameters (after *)
# Must be passed as keyword arguments
def create_file(path: str, *, mode: str = "w", encoding: str = "utf-8") -> None:
    """Create a file. mode and encoding must be keyword arguments."""
    print(f"Creating {path} with mode={mode}, encoding={encoding}")

create_file("test.txt")                           # OK
create_file("test.txt", mode="a")                  # OK
# create_file("test.txt", "a")                     # TypeError!

# Combined: positional-only + regular + keyword-only
def search(query: str, /, *, limit: int = 10, offset: int = 0) -> str:
    """
    query: positional-only (before /)
    limit, offset: keyword-only (after *)
    """
    return f"Search '{query}' limit={limit} offset={offset}"

print(search("Python", limit=5))  # Search 'Python' limit=5 offset=0
# search(query="Python")           # TypeError! (positional-only)
# search("Python", 5)              # TypeError! (keyword-only)

# Real-world example: Python's built-in sorted
# sorted(iterable, /, *, key=None, reverse=False)
# iterable is positional-only, key and reverse are keyword-only

Type Hints для функций

from typing import Callable, Any
from collections.abc import Sequence, Iterable

# Basic type hints
def greet(name: str, excited: bool = False) -> str:
    suffix = "!!!" if excited else "."
    return f"Привет, {name}{suffix}"

# Optional (Union with None)
def find(items: list[str], target: str) -> int | None:
    try:
        return items.index(target)
    except ValueError:
        return None

# Generic sequences
def first(items: Sequence[Any]) -> Any:
    return items[0]

# Callable type hints
def apply(func: Callable[[int], int], value: int) -> int:
    return func(value)

result = apply(lambda x: x ** 2, 5)
print(result)  # 25

# Complex return types
def parse_config(path: str) -> dict[str, str | int | bool]:
    return {"host": "localhost", "port": 8080, "debug": True}

# TypeVar for generic functions
from typing import TypeVar

T = TypeVar("T")

def first_or_default(items: Sequence[T], default: T) -> T:
    """Return first item or default if empty."""
    return items[0] if items else default

print(first_or_default([1, 2, 3], 0))      # 1
print(first_or_default([], "default"))      # 'default'

Аннотации в Python 3.14

# Python 3.14: deferred evaluation of annotations (PEP 649)
# Forward references work without quotes!

class Node:
    def __init__(self, value: int, children: list[Node] | None = None):
        self.value = value
        self.children = children or []

    def add_child(self, child: Node) -> None:
        self.children.append(child)

# In earlier Python, you'd need:
# from __future__ import annotations
# or use string: children: list['Node'] | None = None

# Type aliases (Python 3.12+ syntax)
type Vector = list[float]
type Matrix = list[Vector]
type Callback = Callable[[str, int], bool]

def transform(matrix: Matrix, callback: Callback) -> Matrix:
    ...

Функции как объекты первого класса

# Functions are objects
def add(a: int, b: int) -> int:
    return a + b

# Assign to variable
operation = add
print(operation(2, 3))  # 5

# Store in data structures
operations = {
    "+": add,
    "-": lambda a, b: a - b,
    "*": lambda a, b: a * b,
}

print(operations["+"](10, 5))   # 15
print(operations["-"](10, 5))   # 5
print(operations["*"](10, 5))   # 50

# Pass as argument
def apply_twice(func: Callable[[int], int], value: int) -> int:
    return func(func(value))

print(apply_twice(lambda x: x * 2, 3))  # 12

# Return from function
def make_multiplier(factor: int) -> Callable[[int], int]:
    def multiplier(x: int) -> int:
        return x * factor
    return multiplier

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5))   # 10
print(triple(5))   # 15

# Function attributes
print(add.__name__)         # 'add'
print(add.__annotations__)  # {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}
print(add.__defaults__)     # None (no defaults)

Итоги

  • def определяет функцию, return возвращает значение (или None)
  • Mutable defaults -- опасная ловушка, используйте None и создавайте объект внутри
  • *args -- переменное число позиционных аргументов (tuple)
  • **kwargs -- переменное число именованных аргументов (dict)
  • / -- positional-only параметры (до /)
  • * -- keyword-only параметры (после *)
  • Type hints обязательны для читаемого и поддерживаемого кода
  • Python 3.14: PEP 649 -- ленивое вычисление аннотаций, forward references без кавычек
  • Функции -- объекты первого класса: можно присваивать, передавать, возвращать

Проверь себя

🧪

Что возвращает функция без оператора return?

🧪

Почему изменяемые значения по умолчанию (list, dict) опасны?

🧪

Что означает символ / в определении функции def f(a, b, /, c, d)?

🧪

Как указать, что функция принимает callable с двумя int-аргументами и возвращает str?