Skip to content

Latest commit

 

History

History
658 lines (450 loc) · 37.6 KB

File metadata and controls

658 lines (450 loc) · 37.6 KB

Лекция 5. Функции, типизация, lambda. Map, zip, filter.

Оглавление курса

Блок 1 — Python Basic (1–6)
Блок 2 — Git (7–8)
Блок 3 — Python Advanced (9–14)
Блок 4 — SQL (15–17)
Блок 5 — Django (19–26)
Блок 6 — Django Rest Framework (27–30)
Блок 7 — Python async (31–33)
Блок 8 — Deployment (34–35)

drawing

Добрый день, уважаемые студенты! Сегодня мы будем говорить о функциях в Python, одной из самых важных концепций в программировании. Функции позволяют нам организовать код, сделать его более читаемым, модульным и повторно используемым. Давайте начнём с основ.

Что такое функция?

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

def greet(name):
    print("Привет,", name)

Здесь мы объявили функцию greet с одним параметром name. На самом деле параметров может не быть вообще, а может быть больше одного — рассмотрим эти варианты дальше.

Вызов функции

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

greet("Анна")

Этот вызов функции выведет на экран "Привет, Анна".

Вы уже вызывали так называемые встроенные (built-in) функции, например print, input, len, range и т.д. Как вы уже, наверное, поняли, функции можно писать и самим.

Возвращение значений

Функции могут возвращать значения с помощью ключевого слова return. Например:

def add(x, y):
    result = x + y
    return result

Вызов add(3, 5) вернёт результат сложения 3 и 5, который можно сохранить в переменной или использовать в других выражениях.

Если в функции нет оператора return, она неявно возвращает None (так устроено в Python).

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


def print_var(a):
    print(a)


res = count_sum(5, 10)  # Значение будет 15
res2 = print_var(10)  # Значение будет None, потому что функция ничего не возвращает

Область видимости (scope) переменных

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

def multiply(a, b):
    result = a * b
    return result


c = 2
d = 3
product = multiply(c, d)
print(result)  # Ошибка! Переменная result не видна за пределами функции

В этом примере переменная result видна только внутри функции multiply.

Аргументы по умолчанию

Python позволяет указывать значения по умолчанию для аргументов функции. Это позволяет вызывать функцию с меньшим количеством аргументов, если значения по умолчанию заданы. Например:

def power(base, exponent=2):
    result = base ** exponent
    return result


print(power(3))  # Выведет 9, так как exponent по умолчанию равен 2
print(power(2, 3))  # Выведет 8, так как мы явно указали значение exponent

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

# ОШИБКА, ТАК СДЕЛАТЬ НЕЛЬЗЯ
def power(base=5, exponent):
    result = base ** exponent
    return result

Важно: не используйте изменяемые объекты (list/dict/set) как значения по умолчанию — они «накапливаются» между вызовами. Безопасный шаблон — через None.

from typing import Optional

def add_item(item: int, bucket: Optional[list[int]] = None) -> list[int]:
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket

Типизация и аннотации

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

def add(x: int, y: int) -> int:  # Стрелка это два символа "-" и ">"
    result = x + y
    return result

Аннотации типов не обязательны для выполнения кода, но сильно помогают читабельности и качеству. Рантайм Python их не проверяет; для проверки используют статические анализаторы (например, mypy или pyright).

Здесь мы аннотировали аргументы x и y как int, а возвращаемое значение как int.

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

Передача случайного количества параметров

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

  1. *args — позволяет передавать произвольное количество позиционных аргументов в виде кортежа. Имя args — соглашение; можно использовать любое имя.

  2. **kwargs — позволяет передавать произвольное количество именованных аргументов в виде словаря. Имя kwargs — соглашение; можно использовать любое имя после **.

*args — позиционные аргументы; **kwargs — именованные аргументы.

Пример с *args

def print_args(*args):
    for arg in args:
        print(arg)


print_args(1, 2, 3, "hello")  # Выведет все переданные аргументы

В этом примере *args собирает все переданные аргументы в кортеж args, который затем можно перебрать в цикле.

Пример с **kwargs

def print_kwargs(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")


print_kwargs(name="John", age=25, city="New York")  # Выведет все переданные именованные аргументы

Здесь **kwargs собирает все переданные именованные аргументы в словарь kwargs, который можно перебрать в цикле.

Комбинированное использование

Вы также можете комбинировать *args и **kwargs в одной функции, но *args должен идти перед **kwargs:

def print_all_args_and_kwargs(arg1, *args, kwarg1="default", **kwargs):
    print("Обязательный аргумент:", arg1)
    print("Дополнительные аргументы (*args):", args)
    print("Именованный аргумент (kwarg1):", kwarg1)
    print("Дополнительные именованные аргументы (**kwargs):", kwargs)


print_all_args_and_kwargs("first", "second", "third", kwarg1="custom", key1="value1", key2="value2")

В этом примере функция print_all_args_and_kwargs принимает один обязательный аргумент, произвольное количество аргументов *args, один именованный аргумент kwarg1 со значением по умолчанию, и произвольное количество именованных аргументов **kwargs. Это позволяет гибко работать с разными видами аргументов при вызове функции.

Использование *args и **kwargs может быть полезным, когда вам нужно создавать более гибкие функции, способные обрабатывать разное количество и типы аргументов.

Распаковка кортежей с использованием *

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

Оператор * позволяет распаковать элементы кортежа или списка и передать их как отдельные аргументы функции. Это полезно, когда у вас есть кортеж (или список) с переменным количеством элементов, и вы хотите передать их в функцию, которая ожидает отдельные аргументы.

Пример распаковки кортежа

def multiply(a: int, b: int) -> int:
    return a * b


values = (2, 3)
result = multiply(*values)  # Распаковываем кортеж и передаем его элементы как аргументы функции
print(result)  # Выведет 6, так как 2 * 3 = 6

В этом примере мы объявили функцию multiply, которая принимает два аргумента. Затем мы создали кортеж values с двумя элементами и использовали оператор * для распаковки кортежа и передачи его элементов как аргументы функции multiply.

Или можно сделать так:

a, b, *c = 1, 2, 3, 4, 5
print(a, b,
      c)  # Распечатает 1, 2, [3, 4, 5], первые два аргумента будут переданы напрямую, а все остальные будут распакованы как список

Пример распаковки списка

def add(a: int, b: int, c: int) -> int:
    return a + b + c


numbers = [1, 2, 3]
result = add(*numbers)  # Распаковываем список и передаем его элементы как аргументы функции
print(result)  # Выведет 6, так как 1 + 2 + 3 = 6

В этом примере мы используем список numbers и также распаковываем его элементы как аргументы функции add.

Распаковка с использованием * может быть полезной, когда вам нужно передать переменное количество аргументов в функцию или когда вы работаете с данными, хранящимися в кортежах или списках. Это делает ваш код более гибким и читаемым.

Распаковка словарей с использованием **

Оператор ** позволяет распаковать словарь и передать его элементы как именованные аргументы функции. Это полезно, когда у вас есть словарь с переменным количеством ключей и значениями, и вы хотите передать их в функцию, которая ожидает именованные аргументы.

Пример распаковки словаря

def print_person_info(name: str, age: int) -> None:
    print(f"Имя: {name}, Возраст: {age}")


person_info = {"name": "John", "age": 30}
print_person_info(**person_info)  # Распаковываем словарь и передаем его элементы как именованные аргументы функции

В этом примере мы объявили функцию print_person_info, которая принимает два именованных аргумента (name и age). Затем мы создали словарь person_info с ключами "name" и "age" и их соответствующими значениями. С помощью оператора ** мы распаковываем словарь и передаем его элементы как именованные аргументы функции print_person_info.

Комбинированное использование

Вы также можете комбинировать *args и **kwargs в одной функции, чтобы обработать как позиционные, так и именованные аргументы.

def print_info(*args, **kwargs):
    for arg in args:
        print(arg)
    for key, value in kwargs.items():
        print(f"{key}: {value}")


values = (1, 2, 3)
info = {"name": "John", "age": 30}
print_info(*values, **info)  # Распаковываем кортеж и словарь и передаем их элементы как аргументы функции

В этом примере функция print_info принимает как позиционные аргументы, так и именованные аргументы, используя *args и **kwargs.

Распаковка с использованием ** может быть полезной, когда вам нужно передавать переменное количество именованных аргументов в функцию или когда вы работаете с данными, хранящимися в словарях. Это делает ваш код более гибким и удобным для работы с разными видами данных.

Практика

Во всех задачах указывайте аннотации типов!

  1. Простое сложение. Напишите функцию add_numbers, которая принимает два целых числа и возвращает их сумму.

  2. Приветствие. Напишите функцию greet, которая принимает строку name и возвращает приветственное сообщение.

  3. Факториал числа. Напишите функцию factorial, которая принимает целое число и возвращает его факториал.

  4. Среднее значение. Напишите функцию average, которая принимает произвольное количество чисел и возвращает их среднее значение. Подумайте, какие там будут типы данных.

  5. Форматирование строки. Напишите функцию format_string, которая принимает строковый шаблон и произвольное количество именованных аргументов для подстановки в шаблон. Например: format_string("some {value1}, another {value2}", value1="test", value2="something_else").

  6. Объединение словарей. Напишите функцию merge_dicts, которая принимает произвольное количество словарей и объединяет их в один.

  7. Чётные и нечётные числа. Напишите функцию even_odd, которая принимает произвольное количество целых чисел и возвращает кортеж из двух списков: один с чётными числами, другой с нечётными. Добавьте аннотации типов.

  8. Фильтрация списка. Напишите функцию filter_list, которая принимает список целых чисел и пороговое значение и возвращает новый список с числами из оригинального списка, которые больше порога. Добавьте аннотации типов.

  9. Калькулятор. Напишите функцию calculator, которая принимает два числа и строку, представляющую арифметическую операцию ('add', 'subtract', 'multiply', 'divide'), и возвращает результат этой операции. Добавьте аннотации типов. Например, calculator(4, 5, "multiply") вернёт 20.

Передача изменяемых типов данных

Теперь становится очень важно помнить, какие типы данных являются изменяемыми, а какие — неизменяемыми из базовых типов! Неизменяемые: строка (String), число (Number), кортеж (Tuple). Изменяемые: список (List), множество (Set), словарь (Dict).

В Python существуют два типа данных: изменяемые (mutable) и неизменяемые (immutable). Примерами изменяемых типов данных являются списки (list), множества (set) и словари (dict), а неизменяемых — целые числа (int), строки (str) и кортежи (tuple).

В Python используется модель «передача ссылки на объект» (object sharing): имя параметра внутри функции получает ссылку на тот же объект. Если вы мутируете изменяемый объект (список, словарь, множество), изменения видны снаружи. Если вы переназначаете имя параметра (xs = xs + [4] или xs = []), это не влияет на исходный объект.

При работе с изменяемыми типами важно понимать, когда вы мутируете объект, а когда создаёте новый.

Рассмотрим пример:

def modify_list(my_list: list[int]) -> None:
    my_list.append(4)  # изменяет исходный список


original_list = [1, 2, 3]
modify_list(original_list)
print(original_list)  # Выведет [1, 2, 3, 4]

Ещё один пример, показывающий разницу между мутацией и переназначением:

def mutate(xs: list[int]) -> None:
    xs.append(4)  # изменяет исходный список

def rebind(xs: list[int]) -> None:
    xs = xs + [4]  # переназначение локального имени, исходный список не меняется


numbers = [1, 2, 3]
mutate(numbers)
print(numbers)  # [1, 2, 3, 4]

numbers2 = [1, 2, 3]
rebind(numbers2)
print(numbers2)  # [1, 2, 3] — список не изменился!

В этом примере мы передали список original_list в функцию modify_list, и функция добавила элемент 4 в этот список. После вызова функции original_list был изменён и теперь содержит элемент 4.

Чтобы избежать таких побочных эффектов, можно передавать изменяемые объекты в функции с помощью копии объекта или использовать методы копирования, например, copy.copy() или copy.deepcopy() из модуля copy.

import copy


def modify_list_safely(my_list: list[int]) -> list[int]:
    new_list = copy.copy(my_list)
    new_list.append(4)
    return new_list


original_list = [1, 2, 3]
modified_list = modify_list_safely(original_list)
print(original_list)  # Выведет [1, 2, 3]
print(modified_list)  # Выведет [1, 2, 3, 4]

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

Модуль copy и функция copy

Модуль copy предоставляет функцию copy.copy(), которая позволяет создавать поверхностные копии объектов. Это означает, что она создаёт новый объект, который является копией оригинала, но не рекурсивно копирует все вложенные объекты. Вложенные объекты по-прежнему будут ссылаться на одни и те же данные.

import copy

original_list = [1, 2, [3, 4]]
copied_list = copy.copy(original_list)

print(original_list)  # Выведет [1, 2, [3, 4]]
print(copied_list)  # Выведет [1, 2, [3, 4]]

# Изменим вложенный список в копии
copied_list[2][0] = 99

print(original_list)  # Выведет [1, 2, [99, 4]]
print(copied_list)  # Выведет [1, 2, [99, 4]]

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

Функция deepcopy

Для создания глубоких копий объектов, включая все вложенные объекты, используйте функцию copy.deepcopy(). Глубокая копия создаёт новую структуру данных, которая полностью независима от оригинала.

import copy

original_list = [1, 2, [3, 4]]
deep_copied_list = copy.deepcopy(original_list)

print(original_list)  # Выведет [1, 2, [3, 4]]
print(deep_copied_list)  # Выведет [1, 2, [3, 4]]

# Изменим вложенный список в глубокой копии
deep_copied_list[2][0] = 99

print(original_list)  # Выведет [1, 2, [3, 4]]
print(deep_copied_list)  # Выведет [1, 2, [99, 4]]

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

Итак, функции copy.copy() и copy.deepcopy() в модуле copy предоставляют удобные средства для копирования объектов с учётом их изменяемости и вложенности. Выбор между ними зависит от вашего конкретного случая использования.

Ещё встроенные функции

В Python 3 функции map, zip и filter возвращают итераторы; чтобы получить список сразу, оборачивайте результат в list(...). Для простых преобразований часто читаемее использовать list comprehension, но эти функции полезны и важно их знать.

Функция map

Функция map используется для применения определённой функции к каждому элементу в итерируемой последовательности (например, списку) и создания новой последовательности с результатами. Это позволяет применять одну функцию к нескольким элементам без явного использования циклов. Пример:

def square(x: int) -> int:
    return x ** 2


numbers = [1, 2, 3, 4, 5]
squared_numbers = list(map(square, numbers))
print(squared_numbers)  # Выведет [1, 4, 9, 16, 25]

В этом примере мы создали функцию square, которая возводит число в квадрат, и применили её ко всем элементам списка numbers с помощью map.

Функция zip

Функция zip позволяет объединить несколько итерируемых последовательностей в одну последовательность кортежей. Количество элементов в результирующей последовательности равно минимальному количеству элементов среди всех переданных последовательностей. Пример:

names = ["Анна", "Иван", "Мария"]
scores = [90, 85, 88]

zipped_data = list(zip(names, scores))
print(zipped_data)  # Выведет [('Анна', 90), ('Иван', 85), ('Мария', 88)]

Здесь мы объединили список имён и список оценок в список кортежей, создавая пары «имя — оценка».

Функция filter

Функция filter используется для фильтрации элементов в итерируемой последовательности на основе заданного условия ( функции). Она возвращает только те элементы, для которых условие истинно. Пример:

def is_even(x: int) -> bool:
    return x % 2 == 0

numbers = [1, 2, 3, 4, 5, 6]
even_numbers = list(filter(is_even, numbers))
print(even_numbers)  # Выведет [2, 4, 6]

Здесь мы определили функцию is_even, которая проверяет, является ли число чётным, и использовали filter, чтобы отфильтровать только чётные числа из списка numbers.

Использование функций map, zip, filter делает код более читаемым и позволяет выполнять разнообразные операции с данными в более функциональном стиле.

Лямбда-функции

Лямбда-функции (или анонимные функции) — это специальный вид функций, которые могут быть определены в одной строке без использования ключевого слова def.

lambda — это выражение: тело должно быть кратким (без многострочных инструкций). Для нетривиальной логики предпочтительнее обычная функция через def. Они часто используются для создания коротких функций, которые передаются в качестве аргументов другим функциям. Например:

square = lambda x: x ** 2
print(square(5))  # Выведет 25

Лямбда-функции полезны, когда требуется передать небольшую функцию в функцию высшего порядка, такую как map, filter или sorted.

Те же примеры для map и filter в реальности выглядели бы вот так:

map:

numbers = [1, 2, 3, 4, 5]
squared_numbers = list(map(lambda x: x ** 2, numbers))
print(squared_numbers)  # Выведет [1, 4, 9, 16, 25]

filter:

numbers = [1, 2, 3, 4, 5, 6]
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(even_numbers)  # Выведет [2, 4, 6]

Практика

  1. Применение функции ко всем элементам списка. Используя map и лямбда-функцию, напишите код, который принимает список целых чисел и возвращает список их квадратов.

  2. Фильтрация нечётных чисел. Используя filter и лямбда-функцию, напишите код, который принимает список целых чисел и возвращает список только с нечётными числами.

  3. Суммирование элементов списков. Используя zip и лямбда-функцию, напишите код, который принимает два списка одинаковой длины и возвращает список, где каждый элемент — это сумма элементов из входных списков на соответствующих позициях:

list1 = [1, 2, 3]
list2 = [4, 5, 6]
result = [5, 7, 9]
  1. Преобразование строк в числа. Используя map и лямбда-функцию, напишите код, который принимает список строковых представлений чисел и возвращает список этих чисел в виде целых чисел:
string_numbers = ["1", "2", "3", "4", "5"]
result = [1, 2, 3, 4, 5]
  1. Объединение списков словарей. Используя zip, напишите код, который принимает два списка словарей и возвращает список словарей, где каждый словарь — это объединение словарей из входных списков на соответствующих позициях:
list1 = [{'a': 1}, {'b': 2}]
list2 = [{'c': 3}, {'d': 4}]
result = [{'a': 1, 'c': 3}, {'b': 2, 'd': 4}]
  1. Фильтрация строк по длине. Используя filter и лямбда-функцию, напишите код, который принимает список строк и возвращает только те строки, длина которых больше 3 символов.

  2. Вычисление длины строк. Используя map и лямбда-функцию, напишите код, который принимает список строк и возвращает список их длин.


← Лекция 4: Хэш таблицы. Set, frozenset. Dict. Tuple. Немного об импортах. Namedtuple, OrderedDict | Лекция 6: Алгоритмы и структуры данных →