Python-фичи, которые стоит знать

Содержание

1. Введение

Когда только начинаешь изучать Python, функции выглядят довольно просто: передал несколько аргументов, получил результат. Но постепенно появляются значения по умолчанию, произвольное количество параметров, вложенные функции и аннотации типов. В этот момент становится понятно, что с функциями в Python связано гораздо больше механик, чем кажется на первый взгляд.

Большая часть вещей из этой статьи не относится к каким-то редким трюкам языка. Наоборот, их можно встретить практически в любом достаточно крупном Python-проекте. Поэтому полезно понимать не только синтаксис, но и то, что происходит за ним.

2. Mutable default arguments

Начнем с одной из самых известных особенностей Python — изменяемых объектов в аргументах функции по умолчанию.

Представим, что нам нужна функция, которая добавляет переданное значение в список. На первый взгляд следующий код выглядит вполне логично:

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


print(add_item("Python"))
print(add_item("Java"))

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

['Python']
['Java']

Но на самом деле получится:

['Python']
['Python', 'Java']

Причина в том, что значение аргумента по умолчанию создается не при каждом вызове функции. Оно создается один раз — в момент определения самой функции. После этого следующие вызовы продолжают работать с тем же объектом.

Список является mutable, то есть изменяемым объектом. Когда мы вызываем append(), изменяется уже существующий список, и это изменение сохраняется для следующего вызова функции.

Обычно такую функцию лучше написать через None:

def add_item(item, items=None):
    if items is None:
        items = []

    items.append(item)
    return items


print(add_item("Python"))
print(add_item("Java"))

Теперь при каждом вызове без второго аргумента внутри функции создается новый список:

['Python']
['Java']

То же самое касается словарей, множеств и других изменяемых объектов. Например, конструкция config={} в параметрах функции может привести к точно такому же поведению.

При этом само поведение Python иногда можно использовать специально. Например, таким способом можно хранить некоторое состояние между вызовами функции. Но такой код обычно не самый очевидный для человека, который будет читать его позже, поэтому для хранения состояния лучше использовать более явные решения.

3. *args

Обычно количество аргументов функции известно заранее:

def add(a, b):
    return a + b

Но иногда заранее неизвестно, сколько значений будет передано. Для этого в Python существует конструкция *args.

def add(*args):
    print(args)


add(1, 2, 3, 4)

В результате получим:

(1, 2, 3, 4)

args внутри функции является обычным кортежем. Поэтому с ним можно работать так же, как с любым другим tuple.

Например, можно написать функцию, которая складывает любое количество чисел:

def add(*args):
    result = 0

    for number in args:
        result += number

    return result


print(add(1, 2))
print(add(1, 2, 3, 4, 5))

Название args само по себе не является специальным словом Python. Важна именно звездочка перед названием параметра.

Технически функцию можно написать и так:

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

Однако вариант *args стал стандартным соглашением и хорошо знаком большинству Python-разработчиков.

Звездочка используется не только при объявлении функции. С ее помощью можно также распаковать последовательность при вызове.

def print_user(name, age):
    print(name, age)


user = ["Alex", 25]

print_user(*user)

Этот вызов фактически будет эквивалентен следующему:

print_user("Alex", 25)

Такую распаковку удобно использовать, когда аргументы уже находятся внутри списка или кортежа.

4. **kwargs

Если *args позволяет принимать произвольное количество позиционных аргументов, то **kwargs делает похожую вещь с именованными аргументами.

def show_user(**kwargs):
    print(kwargs)


show_user(name="Alex", age=25, city="Madrid")

Внутри функции получим обычный словарь:

{'name': 'Alex', 'age': 25, 'city': 'Madrid'}

Поэтому значения можно получать привычным способом:

def show_user(**kwargs):
    print(kwargs.get("name"))
    print(kwargs.get("age"))


show_user(name="Alex", age=25)

Как и в случае с args, название kwargs является соглашением, а не обязательной частью синтаксиса. Важны две звездочки.

*args и **kwargs часто используются вместе:

def example(*args, **kwargs):
    print("args:", args)
    print("kwargs:", kwargs)


example(1, 2, 3, language="Python", version=3)

Получим:

args: (1, 2, 3)
kwargs: {'language': 'Python', 'version': 3}

Через две звездочки можно также распаковывать словари при вызове функции.

def create_user(name, age):
    print(f"{name}: {age}")


user = {
    "name": "Alex",
    "age": 25
}

create_user(**user)

Python возьмет ключи словаря как имена аргументов, а соответствующие значения — как значения этих аргументов.

Такая возможность особенно полезна при работе с конфигурациями, декораторами, библиотеками и функциями-обертками, когда заранее неизвестно, какие именно параметры придется передавать дальше.

5. Scope и правило LEGB

Еще одна важная тема в Python — области видимости переменных, или scope. Если в программе существует несколько переменных с одинаковым названием, интерпретатору нужно определить, какую именно из них использовать.

Для этого обычно используют правило LEGB:

  • L — Local
  • E — Enclosing
  • G — Global
  • B — Built-in

Python ищет имя именно в таком порядке.

Local

Local — локальная область текущей функции.

def example():
    language = "Python"
    print(language)


example()

Переменная language существует внутри функции. Если попробовать обратиться к ней снаружи, Python ее не найдет.

Enclosing

Enclosing появляется, когда одна функция находится внутри другой.

def outer():
    language = "Python"

    def inner():
        print(language)

    inner()


outer()

В функции inner() переменной language нет. Поэтому Python переходит на следующий уровень и ищет ее в области функции outer().

Если нам нужно не просто прочитать такую переменную, а изменить ее, используется ключевое слово nonlocal.

def counter():
    value = 0

    def increment():
        nonlocal value
        value += 1
        return value

    print(increment())
    print(increment())


counter()

Здесь nonlocal сообщает Python, что нужно использовать переменную из внешней функции, а не создавать новую локальную.

Global

Global — уровень текущего модуля.

language = "Python"


def show_language():
    print(language)


show_language()

Функция не находит language локально и во внешней функции, поэтому доходит до глобальной области.

Чтобы изменить глобальную переменную внутри функции, существует ключевое слово global:

counter = 0


def increment():
    global counter
    counter += 1


increment()
print(counter)

После вызова значение counter будет равно 1.

При этом злоупотреблять глобальными переменными обычно не стоит. Когда разные функции напрямую изменяют общее глобальное состояние, становится сложнее понимать, откуда именно взялось текущее значение.

Built-in

Последний уровень — Built-in. Здесь находятся встроенные имена Python: например, print, len, sum, str и многие другие.

numbers = [1, 2, 3]

print(len(numbers))

Если создать собственную переменную с таким же именем, можно случайно перекрыть встроенную функцию:

len = 10

numbers = [1, 2, 3]

print(len(numbers))

Теперь len содержит число, а не встроенную функцию, поэтому попытка вызвать его закончится ошибкой.

В итоге правило можно запомнить довольно просто: Local → Enclosing → Global → Built-in. Python идет от самой близкой области видимости к самой общей и останавливается, как только находит нужное имя.

6. Lambda-функции

Lambda позволяет создавать небольшие функции без обычного объявления через def.

Например, обычная функция:

def square(number):
    return number ** 2

С помощью lambda ее можно записать так:

square = lambda number: number ** 2

print(square(5))

Результат в обоих случаях будет одинаковым:

25

Общий синтаксис выглядит следующим образом:

lambda arguments: expression

Главное ограничение состоит в том, что после двоеточия находится одно выражение. Поэтому lambda подходит именно для небольших операций, а не для полноценной функции на десять строк.

Особенно удобно использовать ее там, где функция нужна только один раз. Например, при сортировке:

users = [
    {"name": "Alex", "age": 27},
    {"name": "Max", "age": 21},
    {"name": "Kate", "age": 24},
]

users.sort(key=lambda user: user["age"])

print(users)

Здесь функция нужна только для того, чтобы сообщить методу sort(), по какому значению сортировать элементы.

Еще один распространенный пример — использование вместе с map() или filter():

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

squares = list(map(lambda number: number ** 2, numbers))

print(squares)

Результат:

[1, 4, 9, 16, 25]

Но сокращать код любой ценой не стоит. Если lambda становится слишком длинной или внутри появляется сложная логика, обычная функция через def почти всегда будет читаться лучше.

7. Type hints

Python относится к языкам с динамической типизацией. Нам не нужно заранее объявлять тип каждой переменной, и одна и та же переменная в разное время может ссылаться на объекты разных типов.

value = 10
value = "Python"

Это удобно, но в больших проектах иногда становится сложно понять, какие данные функция ожидает получить и что она должна вернуть. Для этого используются type hints — аннотации типов.

Например, обычная функция может выглядеть так:

def greet(name):
    return f"Hello, {name}"

Добавим информацию о типах:

def greet(name: str) -> str:
    return f"Hello, {name}"

Теперь из объявления функции сразу понятно, что аргумент name ожидается как строка и функция должна возвращать строку.

При этом Python сам по себе обычно не запрещает передать значение другого типа только из-за аннотации.

def greet(name: str) -> str:
    return f"Hello, {name}"


print(greet(123))

Аннотации в первую очередь дают дополнительную информацию разработчику и инструментам анализа кода. IDE и статические анализаторы могут заметить несоответствие типов еще до запуска программы.

Типизировать можно и коллекции:

def get_names(users: list[str]) -> list[str]:
    return [user.upper() for user in users]

Если значение может иметь несколько типов, в современных версиях Python можно использовать оператор |:

def find_user(user_id: int) -> str | None:
    if user_id == 1:
        return "Alex"

    return None

Запись str | None показывает, что функция может вернуть либо строку, либо None.

Для более сложных случаев существует модуль typing. Например, с помощью Callable можно описывать функции, передаваемые как аргументы.

from typing import Callable


def calculate(
    a: int,
    b: int,
    operation: Callable[[int, int], int]
) -> int:
    return operation(a, b)


result = calculate(10, 5, lambda a, b: a + b)

print(result)

Type hints особенно полезны не потому, что делают Python похожим на строго типизированные языки, а потому, что выступают своеобразной документацией прямо внутри кода. Когда функция имеет пять аргументов и возвращает сложную структуру, аннотации сильно упрощают ее чтение.

8. Заключение

На первый взгляд рассмотренные возможности почти никак не связаны между собой. *args и **kwargs отвечают за передачу аргументов, LEGB — за поиск имен, lambda — за создание небольших функций, а type hints помогают описывать ожидаемые типы данных.

Но все эти механики объединяет одна вещь: они постоянно встречаются в обычном Python-коде. Без понимания mutable default arguments легко получить неожиданное состояние между вызовами функции. Без *args и **kwargs сложнее писать универсальные функции и обертки. А понимание областей видимости помогает разобраться, откуда Python вообще берет значение переменной.

Lambda и type hints скорее относятся к удобству написания и чтения программы. Первая позволяет компактно описывать небольшие операции, а вторые делают интерфейс функций значительно понятнее.

Это далеко не все особенности Python, которые стоит знать. У языка есть декораторы, генераторы, контекстные менеджеры, comprehensions, распаковка, property, dataclasses и множество других интересных механизмов. Но это уже темы для отдельного продолжения.

Долганов Ян Львович
Долганов Ян Львович
Junior Python Developer

Мои научные интересы сосредоточены на разработке на Python, программной инженерии, backend-разработке и современных подходах к проектированию и созданию надёжных программных систем.