Skip to content

Latest commit

 

History

History
675 lines (572 loc) · 43.7 KB

File metadata and controls

675 lines (572 loc) · 43.7 KB

Функции в Python

  1. Функции и аргументы
  2. Область видимости
  3. Filter, map, reduce
  4. lambda функции
  5. Функции высшего порядка
  6. Homework
  7. Cursed questions
  • Функция - это исполняемый фрагмент кода, который можно переиспользовать множество раз. Функции возвращают какое-либо значение. Синтаксис функции начинается с ключевого слова def, после идет название функции, потом в скобках список аргументов, две точки и на следующей строке с отступом сам код функции. Функция возвращает значение при помощи ключевого слова return:

    def find_lists_intersection(first_list, second_list):
        """Функция для нахождения элементов, которые есть
        и в первом и во втором списках. Возвращает список
        общих элементов.
        """
        intersection = list() # инициализируем пустой список для общих элементов
        for i in first_list: # проходимся по всем элементам первого списка
            if i in second_list: # если элемент есть во втором списке
                intersection.append(i) # то добавляем его в пересечение
        return intersection # возвращаем список общих элементов

    Данная функция находит общие элементы из двух списков(пересечение) и возвращает новый список. Вызывается она следующим образом:

    list_intersect = find_lists_intersection([1, 2, 3, 3], [2, 3, 3, 4])
    print(list_intersect) # [2,3,3]

    Для вызова функции сначала пишется ее имя, потом передается список аргументов(в нашем случае это [1, 2, 3, 3], [2, 3, 3, 4]) в круглых скобках, количество передаваемых аргументов должно быть равно количеству аргументов при объявлении функции. Функции позволяют нам писать код только один раз и переиспользовать его множество раз, например можно найти пересечение сразу 4х списков при помощи уже написанной логики:

    list_1 = [1,2,3,4,5,6,7,8]
    list_2 = [3,4,5,6,7,8,9,0]
    list_3 = [5,6,7,5,5,5,6,7]
    list_4 = [1,2,3,5,7,8,9,5]
    
    list_12_intersect = find_lists_intersection(list_1, list_2) # [3, 4, 5, 6, 7, 8]
    list_34_intersect = find_lists_intersection(list_3, list_4) # [5, 7, 5, 5, 5, 7]
    
    intersect = find_lists_intersection(
        list_12_intersect, list_34_intersect
    )
    
    print(intersect) # [5, 7]

    Функция, при ее вызове возвращает какое-либо значение, как только мы вызвали функцию с переданными ей аргументами(как тут find_lists_intersection(list_1, list_2)), она уже перестает быть функцией и становится значением(в нашем случае она превращается в список), зная это, мы можем переписать предыдущий код:

    list_1 = [1,2,3,4,5,6,7,8]
    list_2 = [3,4,5,6,7,8,9,0]
    list_3 = [5,6,7,5,5,5,6,7]
    list_4 = [1,2,3,5,7,8,9,5]
    
    print(find_lists_intersection(
        find_lists_intersection(list_1, list_2), # здесь у нас уже значение [3, 4, 5, 6, 7, 8]
        find_lists_intersection(list_3, list_4) # и тут тоже уже значение [5, 7, 5, 5, 5, 7]
    )) # [5, 7]
  • Для удобства чтения, к аргументам в функциях стараются добавлять аннотации, а также возвращаемое значение, например:

    # добавили аннотации типов, а также тип возвращаемого значения
    def find_sum(a: int, b: int) -> int:
        return a + b

    Так же можно переписать и функцию по нахождению пересечения 2х листов:

    from typing import List
    
    def find_lists_intersection(first_list: List[any], second_list: List[any]) -> List[any]:
        """Функция для нахождения элементов, которые есть
        и в первом и во втором списках. Возвращает список
        общих элементов.
        """
        intersection = list()
        for i in first_list:
            if i in second_list:
                intersection.append(i)
        return intersection

    В нашем примере аргументами являются коллекции, для их аннотации используются специальные типы из модуля typing. Так как список может содержать элементы любого типа, то указывается List[any], если бы функция принимала только списки целых чисел, то указали бы List[int], если бы списки строк, то List[str]. В модуле typing есть множество различных аннотаций, их нужно подбирать по случаю и лучше пользоваться документацией или гуглить. В версиях python старше 3.8, типы list, tuple, set, dict можно указывать без использования модуля typing, то есть писать просто list[int](с маленькой буквы) в старших версиях можно, однако в младших версиях это приведет к ошибке.

  • Аргументы в функцию можно передавать так как мы передавали и раньше:

    find_lists_intersection([1, 2, 3],[1, "2", 3])

    а можно, используя именованные аргументы, то есть сразу при передачи указывать названия аргументов, которые мы передали:

    def find_lists_intersection(first_list: List[any], second_list: List[any]) -> List[any]:
        ...
    
    find_lists_intersection([1, 2, 3],[1, "2", 3])
    
    find_lists_intersection(first_list=[1, 2, 3],second_list=[1, "2", 3])
    
    find_lists_intersection(second_list=[1, "2", 3], first_list=[1, 2, 3])

    Эти 3 вызова функции абсолютно одинаковы, в первом случае мы просто передали значения, во втором указали их имена, а в третьем сначала передали первый список, а потом второй. Работать с именованными аргументами намного удобнее чем без них, так как разработчик сразу видит, что он передал в функцию. Часто названия аргументов говорят о них больше, чем документация, например именованный аргумент sep в процедуре print(), из его названия следует что мы передаем разделитель(separator).

  • Давайте напишем еще одну небольшую функцию, она должна принимать 2 значения, ставить между ними какой-то разделитель и возвращать одну строку, где 2 значения разделены разделителем:

    def separate_values(first_value: any, second_value: any, separator: any) -> str:
        return f"{first_value}{separator}{second_value}"
    
    string_1 = separate_values(1,2,", ") # "1, 2"
    string_2 = separate_values(separator="!!!", first_value=2, second_value=1) # "2!!!1"
    string_3 = separate_values(1,2) # ошибка!
    string_3 = separate_values(first_value=2, second_value=1) # ошибка!

    Первые два вызова функции корректны, однако два других вызовут ошибку, потому что мы не передали значение аргумента separator. Однако было бы неплохо, если бы, например, по умолчанию проставлялся какой-то separator, например ", ", как в процедуре print(). Для таких целей существуют аргументы по умолчанию, задаются они прямо в объявлении функции, в виде присвоения:

    def separate_values(
        first_value: any,
        second_value: any,
        separator: any = ", ") -> str:
    
        return f"{first_value}{separator}{second_value}"
    
        string_3 = separate_values(1,2) # "1, 2"
        string_3 = separate_values(first_value=2, second_value=1) # "1, 2"

    Аргументы со значением по умолчанию при объявлении функции должны быть только ПОСЛЕ аргументов без значений по умолчанию, то есть, написать так:

    def separate_values(
        separator: any = ", ",
        first_value: any,
        second_value: any
    ) -> str:

    будет неправильно и вызовет ошибку, а так:

    def separate_values(
        first_value: any,
        second_value: any,
        separator: any = ", "
    ) -> str:

    Будет верно. Аргументы со значениями по умолчанию следуют после аргументов без значения по умолчанию.

  • Наша функция separate_values сейчас работает следующим образом: принимает 2 аргумента и разделяет их. На самом деле, такая функция мало чем может помочь в разработке. Намного лучше было бы, если бы количество аргументов, принимаемых функцией было неограниченно. Специально для таких целей в Python есть знак *. Он позволяет обращаться к бесконечным аргументам, как к одному кортежу переменных. Давайте перепишем нашу функцию:

    def separate_values(
        separator: any = ", ",
        *values # добавили бесконечные аргументы
    ) -> str:
        clear_values = []
        for value in values: # превращаем все нестроковые типы в строковые
            clear_values.append(str(value))
        return separator.join(clear_values) # превращаем список в строку и возвращаем
    
    print(separate_values("!!!", 1,2,3,4)) # 1!!!2!!!3!!!4

    работает это следующим образом: при вызове функции первое значение отдается аргументу separator, остальные уходят в кортеж values. Если бы мы написали по-другому:

    def separate_values(
        *values, # бесконечные аргументы переместились до обычных аргументов
        separator: any = ", "
    ) -> str:
        clear_values = []
        for value in values:
            clear_values.append(str(value))
        return separator.join(clear_values)
    
    print(separate_values("!!!", 1,2,3,4)) # !!!, 2, 3, 4, 5, 6
    print(separate_values(separator="!!!", 1,2,3,4)) # 1!!!2!!!3!!!4

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

  • (extra) Кроме того передавать в функции можно и просто словари, но каждый элемент как аргумент, например:

    agrs = [1,2,3,4,5,6]
    
    print(*args) #1, 2, 3, 4, 5, 6
    print(*args, sep="! ") #1! 2! 3! 4! 5! 6

    В данном примере мы передали в процедуру print каждый элемент коллекции args, но в качестве отдельного элемента. Сравните:

    agrs = [1,2,3,4,5,6]
    
    print(args) # [1,2,3,4,5,6]
    print(*args) # 1,2,3,4,5,6

    Знак * написанный перед коллекцией при передаче ее в качестве аргумента распаковывает данную коллекцию и передает каждый ее элемент в качестве отдельного аргумента в функцию.

  • Множественные именованные аргументы. Часто в функциях python можно увидеть следующую запись:

    def separate_values(*values, **format_items):
        ...

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

    def separate_values(*values, **format_items):
        separator = format_items.get("separator", "")
        end = format_items.get("end", "")
        clear_values = []
        for value in values:
            clear_values.append(str(value))
        return separator.join(clear_values) + end
    
    separate_values(1,2,3,4, separator="$$$", end="!") # 1$$$2$$$3$$$4!

    Доступ к переданным именованным аргументам происходит через словарь format_items.

  • (extra) Распаковка словарей. Можно передавать словари в качестве именованных аргументов:

    kwargs = {"sep": " ! ", "end": "?"} # kwargs расшифровывается как key word arguments
    args = [1,2,3,4,5]
    
    print(*args, **kwargs) # можно сделать так и вывод будет: 1 ! 2 ! 3 ! 4 ! 5?
    print(args, kwargs) # вывод будет другим: [1, 2, 3, 4, 5] {'sep': ' ! ', 'end': '?'}

Область видимости

  • Область видимости в программировании - это места в программе, где доступны объявленные переменные. Локальные переменные доступны только в определенных примерах, например, если мы в файле 1.py объявим переменную name = "Alex", то в другом файле 2.py эта переменная доступна не будет. То же самое работает и с функциями, только немного сложнее.

  • Локальные переменные - это переменные, которые доступны только в пределах функции, например:

    def do_smth():
        a = 1
        print(a) # вызывать эту переменную в функции - это нормально 
    
    print(a) # но тут это вызовет ошибку, потому что не видит переменную a
  • Глобальные переменные - видны во всех функциях и вообще во всем файле, например:

    GLOBAL_R = 10
    
    def do_smth():
        print(GLOBAL_R)
    
    do_smth() # 10

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

    GLOBAL_R = 10 
    
    def do_smth():
        GLOBAL_R = 8 # теперь это другая переменная, в пределах функции
        print(GLOBAL_R)
    
    
    print(GLOBAL_R) # 10
    do_smth() # 8
    print(GLOBAL_R) # 10

    Наша глобальная переменная GLOBAL_R после вызова функции никак не поменялись, хотя в функции мы ей присваивали новое значение 8. Суть в том, что при таком присваивании в пределах функции у нас создается новая переменная GLOBAL_R локальная только для функции do_smth, и эта локальная переменная никак не влияет на глобальную. Для того чтобы присвоение в функции влияло на глобальную переменную существует ключевое слово global, используется оно следующим образом:

    GLOBAL_R = 10 
    
    def do_smth():
        global GLOBAL_R # говорим что мы хотим изменить глобальную переменную
        GLOBAL_R = 8 # теперь мы меняем именно глобальную переменную
        print(GLOBAL_R)
    
    print(GLOBAL_R) # 10
    do_smth() # 8
    print(GLOBAL_R) # 8

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

  • Глобальные переменные настолько опасны для программ, что было приняло писать все названия глобальных переменных в верхнем регистре, как в нашем примере. Таким образом вы кричите вашим коллегам о том, что эту переменную лучше не трогать - это опасно.

  • Несмотря на природу области видимости, со сложными структурами данных все еще остаются определенные проблемы, например:

    from typing import List
    
    def do_smth(some_list: List[any]) -> None:
        some_list[0] = 100
        some_list.pop()
    
    int_list = [1,2,3]
    print(int_list) # [1, 2, 3]
    do_smth(int_list)
    print(int_list) # [10, 2]

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

    from typing import List
    
    def do_smth(some_list: List[any]) -> None:
        some_list[0] = 100
        some_list.pop()
    
    int_list = [1,2,3]
    print(int_list) # [1, 2, 3]
    do_smth(int_list)
    print(list(int_list)) # [1, 2, 3]

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

  • Функции могут быть вложены друг в друга, например:

    # объявили функцию
    def my_sum(a, b):
        # и функцию внутри функции
        def to_int(arg):
            return int(arg)
    
        return to_int(a) + to_int(b)
    
    my_sum("4", "5") # так будет работать
    to_int("4") # Так будет ошибка

    Здесь мы объявили функцию внутри функции, самая внешняя функция(my_sum) будет доступна во всем файле, однако функция вложенная(to_int) будет доступна только в пределах функции my_sum.

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

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

    def find_sum(acc: int, current: int) -> int:
        return acc + current

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

    from functools import reduce
    
    int_list = [1,2,3,4,5,6]
    
    int_sum = reduce(find_sum, int_list)
    
    print(int_sum) # 21

    Функция reduce не представлена в стандартном наборе функций Python, потому ее нужно брать из отдельного модуля functools. В данном примере происходит следующее:

    1. Функция reduce принимает функцию обработки find_sum и коллекцию
    2. Далее reduce под капотом заводит аккумулятор, присваивая ему значение первого элемента коллекции
    3. reduce передает в рабочую функцию find_sum аккумулятор и следующее значение в коллекции
    4. Срабатывает рабочая функция
    5. Аккумулятору присваивается результат рабочей функции
    6. Если это был не последний элемент, то возвращаемся к п. 3
    7. reduce возвращает аккумулятор как результат своей работы. Вот еще несколько примеров использования reduce: нахождение произведения всех элементов коллекции:
    from functools import reduce
    
    def find_multiply(acc: int, current: int) -> int:
        return acc * current
    
    int_list = [1,2,3,4,5,6]
    
    int_multiply = reduce(find_multiply, int_list)
    
    print(int_multiply) # 720

    Пример работы с более сложными структурами:

    from functools import reduce
    
    def find_multiply(acc: int, current: int) -> int:
        age = current.get("age")
        return acc * age
    
    users = [
        {
            "name": "Alex",
            "age": 22,
        },
        {
            "name": "Bob",
            "age": 10,
        },
        {
            "name": "Alice",
            "age": 20,
        },
    ]
    
    int_multiply = reduce(find_multiply, users, 1) # последний аргумент - это инициализатор
    # инициализатор передается, если мы хотим, чтобы начальным значением аккумулятора
    # был не первый элемент коллекции, а какое-то другое значение
    # в данном случае это удобно, так как первый элемент коллекции
    # у нас словарь, а сложить его с int нельзя. 
    
    print(int_multiply) # 4400

    Функция reduce является довольно сложной в понимании, потому на практике вместо нее используют обычные циклы. Она довольно редко используется, потому она даже вынесена в отдельный модуль functools. К тому же Python и так предоставляет встроенные понятные функции для агрегации коллекций:

    • sum - нахождение суммы всех элементов коллекции:
      int_list = [1,2,3]
      print(sum(int_list))
    • all - возвращает True, если все элементы коллекции имеют значение True:
      bool_list_1 = [True, False, False]
      bool_list_2 = [True, True, True]
      
      print(all(bool_list_1)) # False
      print(all(bool_list_2)) # True
    • any - возвращает True, если хотя бы один элемент коллекции имеет значение True:
      bool_list_1 = [True, False, False]
      bool_list_2 = [False, False, False]
      
      print(all(bool_list_1)) # True
      print(all(bool_list_2)) # False
    • len - возвращает количество элементов списка. Да, это тоже агрегация.
    • max - находит самый большой элемент в коллекции
    • min - находит самый маленький элемент в коллекции.
  • функция map также принимает функцию и коллекцию, но в отличаи от reduce, возвращает новую коллекцию, а не сводит к одному значению. Применяется для быстрого изменения коллекции или приведения типов в ней, например:

    # как и для reduce, сначала завели функцию 
    def cast_to_int(arg: any) -> int:
        return int(arg)
    
    print(cast_to_int("5")) # 5
    
    trash_list = ["1", 2, "3", 4]
    
    clear_list = list(
        map(cast_to_int, trash_list)
    )
    print(clear_list) # [1,2,3,4]

    Можно и усложнить логику, например умножить каждый элемент на 5, для этого просто нужно переписать рабочую функцию:

    def cast_to_int_and_mul_5(arg: any) -> int:
        mul_arg = int(arg) * 5
        return mul_arg
    
    print(cast_to_int_and_mul_5("5")) # 25
    
    trash_list = ["1", 2, "3", 4]
    
    clear_list = list(
        map(cast_to_int_and_mul_5, trash_list)
    )
    print(clear_list) # [5,10,15,20]

    При помощи map можно обрабатывать более сложные структуры, например получить только список лет всех пользователей:

    def get_age(arg: any) -> int:
        return arg.get("age")
    
    users = [
        {
            "name": "Alex",
            "age": 22,
        },
        {
            "name": "Bob",
            "age": 10,
        },
        {
            "name": "Alice",
            "age": 20,
        },
    ]
    
    ages = map(get_age, users)
    
    print(ages) # [22, 10, 20]

    Функция map на самом деле более часто применяется чем reduce, потому она есть в стандартном пространстве имен Python.

  • Для фильтрации элементов в списке или другой коллекции удобно использовать встроенную функцию filter, она работает как map, то есть принимает сначала функцию фильтрации, а потом коллекцию, которую нужно отфильтровать, возвращает новый список отфильтрованных по определенному признаку элементов. Функция фильтрации должна возвращать булевое значение(True или False) например можно получить все элементы, больше 10:

    def get_more_10(arg: int) -> bool:
        return arg > 10
    
    get_more_10(11) # True
    get_more_10(9) # False
    
    int_list = [7,8,9,10,11,12,4,5,22]
    
    more_10_list = list(filter(get_more_10, int_list))
    
    print(more_10_list) # [11, 12, 22]

    Или работать с более сложными структурами, например получить всех пользователей, которым есть 18:

    def get_18(user):
        return user.get("age") >= 18
    
    users = [
        {
            "name": "Alex",
            "age": 22,
        },
        {
            "name": "Bob",
            "age": 10,
        },
        {
            "name": "Alice",
            "age": 20,
        },
    ]
    
    adult_users = list(filter(get_18, users))
    
    print(adult_users) # [{'name': 'Alex', 'age': 22}, {'name': 'Alice', 'age': 20}]

    Функция filter применяется также довольно часто, потому она есть в стандартном пространстве имен Python.

  • Бывают случаи, когда небольшая функция нужна только один раз. Для таких целей в языке существую специальные конструкции, называемые лямбда-функциями. Они хорошо подходят для функций map и filter. Объявление лямбда-функций происходит следующим образом: сначала пишется ключевое слово lambda, потом список аргументов через запятую, двоеточие и исполняемый код самой функции. Например вот код, который получает из списка словарей значения price каждого элемента:
    # список словарей цен и названий
    mark_price_map = [
        {
            "mark": "cucumber",
            "price": 10,
        },
        {
            "mark": "tomato",
            "price": 8,
        },
        {
            "mark": "banana",
            "price": 10
        }
    ]
    
    # делаем из словаря лист цен с помощью лямбды
    price_list = map(lambda p: p.get("price"), mark_price_map)
    
    amount = sum(price_list) # применяем функцию sum
    
    print(amount)
    Здесь lambda p: p.get("price") является лямбда-функцией. Эта функция получает значение из словаря по ключу, возвращает его и применяется при помощи map к каждому элементу списка словарей.
  • Особенности lambda - присвоение в таких функциях запрещено, выполняют они только небольшие действия, типа получение данных из словаря по ключу, обратиться к ним по имени нельзя, потому иногда их еще называют анонимные функции
  • Хотя обращение к лямбда-функции по имени невозможно, можно присвоить ее в переменную и использовать как обычную функцию, например:
    simple_func = lambda a, b: a + b # объявили лямбду и положили ее в переменную simple_func
    print(simple_func(5, 10)) # можно вызывать как обычную функцию
  • В Python каждая функция является объектом, как int или str. Пока функция не вызвана, она является объектом функции. Это значит, что функции можно передавать другим функциям, например:

    def simple_sum(a, b):
        return a + b
    
    def square(func, a, b):
        return func(a, b) * func(a, b)
    
    print(square, 5, 4) #81

    В данном примере мы объявили функцию simple_sum которая складывает 2 переменные, после мы объявили функцию square, которая принимает функцию и передаваемые ей аргументы. Функция square вызывает внутри себя переданную ей функцию simple_sum и ее результат возводит в квадрат.

  • Напишем еще один пример:

    def apply_to_each(func, collection):
        result = list()
        for element in collection:
            result.append(
                func(element)
            )
        return result
    
    some_list = [1,2,3,"4", 4, "10"]
    int_list = apply_to_each(int, some_list)

    Мы написали свою реализацию встроенной функции map, эта функция так же применяет переданную функцию к каждому элементу последовательности.

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

  • Кроме принятия функций как аргументов, функции высшего порядка могут и отдавать функции как результат, например:

    def get_sum(): # функция
        def simple_sum(a, b): # функция сложения к которой не получить доступ из вне
            return a + b
        return simple_sum # возвращаем как результат внутреннюю функцию
    
    smpl = get_sum() # получаем функцию сложения
    result = smpl(4,5) # вызываем функцию сложения и получаем результат
    print(result) # 9

    Функция get_sum является функцией высшего порядка.

  • Внутренняя функция видит все пространство имен внешней функции, то есть:

    def get_sum():
        b = 5 # объявили здесь b и присвоили 5
        def simple_sum(a): # убрали b как аргумент внутренней функции
            return a + b # имеем доступ к внешней переменной b
        return simple_sum # возвращаем внутреннюю функцию
    
    print(get_sum()(10)) # 15

    В последней строчке мы получаем функцию сложения get_sum(), а после вызываем результат этой функции, передавая значение 10. Внутренняя функция будет всегда слаживать переданный аргумент с 5-кой. Можно это изменить, передавая b как аргумент во внешнюю функцию:

    def get_sum(b): # передаем теперь b как аргумент
        def simple_sum(a):
            return a + b
        return simple_sum
    
    sum_5 = get_sum(5) # предали 5 и получили как результат функцию, которая складывает все с 5-кой
    sum_10 = get_sum(10) # предали 10 и получили как результат функцию, которая складывает все с 10-кой
    
    print(sum_5(30)) # 35
    print(sum_10(30)) # 40

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

  • Как мы убедились, переданные функции можно исполнять внутри других функций, как делает, например, функция map, напишем одну такую функцию:

    def add_separator(func, *args): # функция которая принимает другую функцию, исполняет ее и возвращает результат вместе с разделителями
        separator = "*" * 80
        return separator + "\n" + func(*args) + "\n" + separator # возвращает результат исполнения функции
    
    def replace_spaces(some_string: str, char: str = "_"): # функция удаляет пробелы в строке и заменяет на нижнее подчеркивание по умолчанию
        return some_string.replace(" ", "_")
    
    format_str = add_separator(replace_spaces, "Hello World") # исполняем функцию replace_spaces через add_separator, который добавляет разделители
    
    print(format_str)

    Далее сделаем так, чтобы эта функция возвращала тоже функцию:

    def add_separator(func):
    
        def inner(*args): # объявили внутреннюю функцию, которая добавляет разделитель и возвращает результат
            separator = "*" * 80
            return separator + "\n" + func(*args) + "\n" + separator 
    
        return inner # возвращаем внутреннюю функцию
    
    def replace_spaces(some_string: str, char: str = "_"):
        return some_string.replace(" ", "_")
    
    format_str = add_separator(replace_spaces) # теперь возвращается функция
    result = format_str("Hello World") # Вызываем функцию и получаем результат
    print(result)

    То, что мы сейчас сделали, называется декоратором функции. Он декорирует функционал изначальной функции. Такой прием в Python достаточно распространен, потому для этих целей придуман специальный синтаксис:

    def add_separator(func):
    
        def inner(*args):
            separator = "*" * 80
            return separator + "\n" + func(*args) + "\n" + separator 
    
        return inner
    
    @add_separator # заменяет format_str = add_separator(replace_spaces)
    def replace_spaces(some_string: str, char: str = "_"):
        return some_string.replace(" ", "_")
    
    
    result = replace_spaces("Hello World") # теперь просто вызываем функцию
    print(result)

    Теперь можно кастомизировать разделитель, для этого нужно добавить еще одну функцию-обертку:

    def add_separator(sep = "*"): # еще одна обертка, которая получает разделитель
    
        def separate(func): # функция, которая оборачивает исполнение передаваемой функции
    
            def inner(*args):
                separator = sep * 80 # создаем разделитель, sep взяли из самой внешней функции
                return separator + "\n" + func(*args) + "\n" + separator
    
            return inner
    
        return separate
    
    @add_separator("_") # теперь можно передавать желаемый сепаратор
    def replace_spaces(some_string: str, char: str = "_"):
        return some_string.replace(" ", "_")
    
    result = replace_spaces("Hello World") # Вызываем функцию и получаем результат
    print(result)

    Мы пришли от простой передачи функции в функцию к сложной структуре изменения работы функции, без изменения кода этой функции. @add_separator("_") в примере можно заменить на более сложную структуру:

    func = add_separator("_")(replace_spaces) # заменили @add_separator("_")
    result = func("Hello World")
    
    print(result) # тот же самый результат что и прежде
  • Декораторы являются очень распространенной идеей в Python, многие фреймворки(Flask, FastAPI...) построены на декораторах и их использовании, частым примером использования декораторов в web-приложениях является: логгирование, проверка разрешений, проверка аутентификации, привязка обработчика к url и д.р.

  1. Что такое функция?
  2. Что такое область видимости функции?
  3. Как можно изменить глобальную переменную из функции?
  4. Что такое функции высшего порядка?
  5. Что такое замыкание? Напишите пример.
  6. Что такое декоратор? Напишите пример.
  7. Как сделать из обычного декоратора, декоратор с параметрами?