Skip to content

Latest commit

 

History

History
546 lines (367 loc) · 31.7 KB

lesson7.md

File metadata and controls

546 lines (367 loc) · 31.7 KB

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

Добрый день, уважаемые студенты! Сегодня мы будем говорить о функциях в 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 будут интерпретироваться питоном как функция в которой последней инструкцией написано return None, потому что у функции всегда должно быть возвращаемое значение.

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

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

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

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

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

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

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

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

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

  1. *args: Этот синтаксис позволяет передавать произвольное количество аргументов в виде кортежа (tuple). Имя args является соглашением (хоть матом напишите, работать будет, но принято использовать именно это слово).

  2. **kwargs: Этот синтаксис позволяет передавать произвольное количество именованных аргументов в виде словаря ( dictionary). Имя 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.

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

Давайте добавим информацию о распаковке словарей с использованием оператора двойной звездочки ** в Python.

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

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

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

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) и словари (dict), а неизменяемых - целые числа (int), строки (str) и кортежи (tuple).

И бывает два вида передачи данных в функцию (и не только) по ссылке и по значению.

Передача по ссылке предполагает, что вы передаете не сами данные, а только адрес нахождения этого объекта в памяти!

Передача по значению предполагает, что вы передаете копию переменной!

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

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

def modify_list(my_list: list) -> list:
    my_list.append(4)


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

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

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

import copy


def modify_list_safely(my_list: list) -> list:
    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 предоставляют удобные средства для копирования объектов с учетом их изменяемости и вложенности. Выбор между ними зависит от вашего конкретного случая использования.

Еще built-in функции

Функция 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. Они часто используются для создания коротких функций, которые передаются в качестве аргументов другим функциям. Например:

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 и лямбда-функцию, напишите код, который принимает список строк и возвращает список их длин.