Расширенный Python Функции

Python Функции

Как вы запутаетесь с Python. Фото от iam_os на Unsplash

После прочтения заголовка вы, вероятно, задаете себе вопросы вроде: “Функции в Python – это продвинутая концепция? Как? Все курсы представляют функции как основной блок в языке.” И вы правы и неправы одновременно.

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

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

Краткие основы

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

def shout(name):    print(f'Привет! Меня зовут {name}.')

В мире программной инженерии мы разделяем части определения функции:

  • def – ключевое слово Python, используемое для определения функции.
  • shout – имя функции.
  • shout(name) – объявление функции.
  • name – аргумент функции.
  • print(...) – часть тела функции или, как мы это называем, определение функции.

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

def break_sentence(sentence):    return sentence.split(' ')

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

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

def shout(name):    return f'Привет! Меня зовут {name}.'# мы будем использовать ранее определенную функцию break_sentence# присвоим функцию другой переменнойanother_breaker = break_sentence another_breaker(shout('Джон'))# ['Привет!', 'Меня', 'зовут', 'Джон.']# Ого! Да, так можно определить функциюname_decorator = lambda x: '-'.join(list(name))name_decorator('Джон')# 'Д-ж-о-н'

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

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

def dash_decorator(name):    return '-'.join(list(name))def no_decorator(name):    return namedef shout(name, decorator=no_decorator):    decorated_name = decorator(name)    return f'Привет! Меня зовут {decorated_name}'shout('Джон')# 'Привет! Меня зовут Джон'shout('Джон', decorator=dash_decorator)# 'Привет! Меня зовут Д-ж-о-н'

Вот так выглядит передача функций в качестве аргументов другой функции. А что насчет функции lambda? Ну, посмотрите на следующий пример:

def shout(name, decorator=lambda x: x):    decorated_name = decorator(name)    return f'Привет! Меня зовут {decorated_name}'print(shout('Джон'))# Привет! Меня зовут Джонprint(shout('Джон', decorator=dash_decorator))# Привет! Меня зовут Д-ж-о-н

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

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

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

Промежуточные или продвинутые части

Это никоим образом не будет ЕДИНСТВЕННЫМ руководством по функциям и продвинутым концепциям вокруг функций в Python. Есть много отличных материалов, которые я оставлю в конце этого поста. Однако я хочу рассказать о нескольких интересных аспектах, которые я нашел очень занимательными.

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

type(shout)# функциятype(type(shout))# type

Когда вы определяете класс в Python, он автоматически наследует класс object. А от какого класса наследует object?

type(object)# type

И должен ли я сказать вам, что классы в Python тоже являются объектами? Действительно, это потрясающе для начинающих. Но, как бы сказал Andrew Ng, это не так важно, не беспокойтесь об этом.

Итак, функции являются объектами. Конечно, у функций должны быть некоторые магические методы, верно?

shout.__class__# функцияshout.__name__# shoutshout.__call__# <method-wrapper '__call__' объекта функции по адресу 0x10d8b69e0># Ого!

Магический метод __call__ определен для объектов, которые можно вызвать. Таким образом, наш объект shout (функция) является вызываемым. Мы можем вызывать его с аргументами или без них. Но это интересно. То, что мы сделали ранее, было определение функции shout и получение объекта, который можно вызвать с помощью магического метода __call__, который является функцией. Вы когда-нибудь смотрели фильм “Начало”?

Итак, наша функция на самом деле не функция, а объект. Объекты являются экземплярами классов и содержат методы и атрибуты, верно? Это то, что вы должны знать из ООП. Как мы можем узнать, какие атрибуты есть у нашего объекта? Есть этот функция Python, называемая vars, которая возвращает словарь атрибутов объекта со значениями. Давайте посмотрим, что произойдет в следующем примере:

vars(shout)# {}shout.name = 'Jimmy'vars(shout)# {'name': 'Jimmy'}

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

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

Сначала определение функции может содержать определение другой функции. Даже больше одной. Вот прекрасный пример:

def shout(name):    def _upper_case(s):        return s.upper()    return _upper_case(name)

Если вы думаете, что это просто запутанная версия name.upper(), то вы правы. Но подождите, мы приходим к этому.

Итак, учитывая предыдущий пример, который является полностью функциональным кодом Python, вы можете экспериментировать с несколькими функциями, определенными внутри вашей функции. Какова ценность этого уловки? Что ж, вы можете оказаться в ситуации, когда ваша функция огромна и содержит повторяющиеся блоки кода. Таким образом, определение подфункции улучшит читаемость. На практике большие функции являются признаком неправильного написания кода, и настоятельно рекомендуется разбить их на несколько более маленьких. Следуя этому совету, вам редко потребуется определять несколько функций внутри друг друга. Одно, на что стоит обратить внимание, это то, что функция _upper_case скрыта и недоступна в области видимости, где функция shout заканчивает свое определение и становится доступной для вызова. Таким образом, вы не можете легко ее протестировать, что является еще одной проблемой при таком подходе.

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

Декораторы функций в Python

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

def my_function():    return sum(range(10))def my_logger(fun):    print(f'{fun.__name__} вызывается!')    return funmy_function()# 45my_logger(my_function)# my_function вызывается!# <function my_function at 0x105afbeb0>my_logger(my_function)()# my_function вызывается!# 45

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

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

def my_function():    return sum(range(10))def my_logger(fun):    print(f'{fun.__name__} вызывается!')    return funmy_function = my_logger(my_function)my_function(10)# my_function вызывается!# 45

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

def my_logger(fun):    print(f'{fun.__name__} вызывается!')    return fun@my_loggerdef my_function():    return sum(range(10))my_function()# my_function вызывается!# 45

Это Зен Python. Посмотрите на выразительность кода и его простоту.

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

def my_logger(fun):    print(f'{fun.__name__} вызывается!')    return fun@my_loggerdef my_function():    return sum(range(10))my_function()# my_function вызывается!# 45my_function()# 45

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

def my_logger(fun):
    def _inner_decorator(*args, **kwargs):
        print(f'{fun.__name__} вызывается!')
        return fun(*args, **kwargs)
    return _inner_fun

@my_logger
def my_function(n):
    return sum(range(n))

print(my_function(5))
# my_function вызывается!
# 10

В этом примере также есть некоторые обновления, поэтому давайте рассмотрим их:
1. Мы хотим иметь возможность передавать аргумент в my_function.
2. Мы хотим иметь возможность декорировать любую функцию, а не только my_function. Поскольку мы не знаем точное количество аргументов для будущих функций, нам нужно оставить все как можно более общим, поэтому мы используем *args и **kwargs.
3. Важно отметить, что мы определили _inner_decorator, который будет вызываться каждый раз при вызове my_function в коде. Он принимает позиционные и ключевые аргументы и передает их в качестве аргументов к декорируемой функции.

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

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

@my_logger
def my_function(n):
    return sum(range(n))

@my_logger
def my_unordinary_function(n, m):
    return sum(range(n)) + m

print(my_function(5))
# my_function вызывается!
# 10

print(my_unordinary_function(5, 1))
# my_unordinary_function вызывается!
# 11

В нашем примере функция-декоратор принимает только декорируемую функцию. Но что, если вы хотите передать дополнительные параметры и динамически изменить поведение декоратора? Допустим, вы хотите настроить уровень подробности декоратора-логгера. Пока наша функция-декоратор принимает один аргумент: декорируемую функцию. Однако, когда у функции-декоратора есть свои аргументы, они передаются ей первыми. Затем функция-декоратор должна вернуть функцию, которая принимает декорируемую функцию. Существенно усложняются вещи. Помните сравнение с фильмом "Начало"?

Вот пример:

from enum import IntEnum, auto
from datetime import datetime
from functools import wraps

class LogVerbosity(IntEnum):
    ZERO = auto()
    LOW = auto()
    MEDIUM = auto()
    HIGH = auto()

def my_logger(verbosity: LogVerbosity):
    def _inner_logger(fun):
        def _inner_decorator(*args, **kwargs):
            if verbosity >= LogVerbosity.LOW:
                print(f'LOG: Уровень подробности: {verbosity}')
                print(f'LOG: {fun.__name__} вызывается!')
            if verbosity >= LogVerbosity.MEDIUM:
                print(f'LOG: Дата и время вызова: {datetime.utcnow()}.')
            if verbosity == LogVerbosity.HIGH:
                print(f'LOG: Область видимости вызывающего: {__name__}.')
                print(f'LOG: Аргументы: {args}, {kwargs}')
            return fun(*args, **kwargs)
        return _inner_decorator
    return _inner_logger

@my_logger(verbosity=LogVerbosity.LOW)
def my_function(n):
    return sum(range(n))

@my_logger(verbosity=LogVerbosity.HIGH)
def my_unordinary_function(n, m):
    return sum(range(n)) + m

print(my_function(10))
# LOG: Уровень подробности: LOW
# LOG: my_function вызывается!
# 45

print(my_unordinary_function(5, 1))
# LOG: Уровень подробности: HIGH
# LOG: my_unordinary_function вызывается!
# LOG: Дата и время вызова: 2023-07-25 19:09:15.954603.
# LOG: Область видимости вызывающего: __main__.
# LOG: Аргументы: (5, 1), {}
# 11

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

Заключение

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

Я надеюсь, что я представил вам что-то новое и что вы теперь уверены в своих способностях писать функции как продвинутый Python программист.

Ссылки

  • Введение в декораторы в Python
  • Внутренние функции в Python: для чего они хороши?
  • Руководство по перечислениям (Enum)