Базовое понятие функций
Определения
Простыми словами — функция это объект, который может принимать входные параметры, и возвращать результат после выполнения определенных инструкций.
Базовый синтаксис для задания функции:
def function_name(parameters):
# тело функции - команды, которые необходимо выполнить с входными параметрами
pass
return result # после оператора return ставится возвращаемое функцией значене
Примечания:
- Функции именуются в snake_case.
- Функция может принимать любое количество параметров (от 0 до произвольного количества).
- Функция всегда возвращает значение — если оператор
return
не указан — функция автоматически вернетNone
.
Вызов функций
function_name(parameters)
— вызов функции осуществляется указанием ее имени, за которым идут скобки, в которые при необходимости передаются параметры. Вы уже встречались с функция — например, print
.
Назначение функций
Функции необходимы для:
- Увеличения индекса переиспользования кода (принцип DRY — don’t repeat yourself).
- Структурирования кода (в процедурных языках именно подпрограммы организуют структуру программы). В Python тажке вполне возможна реализация приложений в процедурном стиле.
- Реализация рекурсивных алгоритмов.
- Функции создают области видимости. (Об этом подробнее в разделе про LEGB).
- С помощью функций реализуются методы в ООП.
- С помощью функций в Python просто реализуется паттерн ‘Декоратор’.
Типы аргументов при задании функций
До версии 3.8
Рассмотрим следующее задание функции:
def all_type_args(a, b=0, *args, c, d=1, **kwargs):
pass
В ней представлены все типы аргументов функции при ее задании:
a
— позиционный аргумент (positional).b
— позиционный аргумент со значением по умолчанию.args
— запакованные в кортеж позиционные аргументы, оставшиеся после определения всех аргументов, указанных до него. Специальный синтаксис —*name
. Традиционное имя —args
. При задании функции может быть только один аргумент такого типа.с
— аргумент, передаваемый только по имени (keyword).d
— аргумент, передаваемый только по имени, со значением по умолчанию.kwargs
— запакованные в словарь аргументы, переданные по имени, оставшиеся после определения всех аргументов, указанных до него. При задании функции может быть только один аргумент такого типа.
Примечания:
- Порядок аргументов при задании функции имеет значение: Сначала задаются все позиционные аргументы, потом все позиционные со значением по умолчанию и т.д.
- В определении функции очевидно могут быть представлены не все типы аргументов.
*args
и**kwargs
позволяют функции принимать неограниченное количество аргументов (и при этом каждый раз разное).- Без веской необходимости не давайте
*
и**
аргументам имена, отличные отargs
иkwargs
. - Позиционные аргументы могут быть переданы как по позиции, так и по имени, поэтому корректное их назнвание, хоть и немного длинное —
positional or keyword
аргумент, т.е. аргумент, передаваемый по позиции или имени. - Функции можно задавать внутри других функций.
args
иkwargs
очевидно являются необязательными параметрами. Если не будет передано ‘лишних’ параметров, они просто останутся пустым кортежем и словарем соответственно.- В случае, если аргументам со значениями по умолчанию, не передано никакое значение, будет использовано значение по умолчанию.
- Значения по умолчанию создаются один раз, при создании объекта функции, а значит необходимо быть аккуратным со значениями по умолчанию изменяемых типов (см. примеры).
В версии Python 3.8 и более поздних
В версии 3.8 был добавлен новый тип аргументов — positional only
аргументы, т.е. передаваемые только по позиции.
Базовый синтаксис:
def name(positional_only_parameters, /, positional_or_keyword_parameters,
*, keyword_only_parameters):
Аргументы, передаваемые только по позиции, отделены от позиционных специальным символом /
.
Данное решение имеет как плюсы так и минусы.
+:
- Позволяет авторам библиотек сильнее контролировать, как используется их API.
- Из-за особенностей реализации, обработка таких аргументов в CPython производится быстрее.
-:
- Схема задания функции стала объективно сложнее. Сейчас, у нас не могут одновременно быть заданы аргументы, передаваемые только по позиции со значением по умолчанию, а после них задать
positional_or_keyword_parameters
, а также еще некоторые особенности. Прочитать про инх можно тут https://deepsource.io/blog/python-positional-only-arguments/.
В целом, PEP на ввод positional only параметров находится тут: https://www.python.org/dev/peps/pep-0570/#rationale.
Это особенность нового Python, и еще неизвестно насколько эти возможности приживутся в языке.
Типы параметров, передаваемых при вызове функций
Рассмотрим следующий вызов функции:
some_function(a, *collection, b=10, **some_dict)
.
В ней представлены все типы аргументов которые могут быть переданы в функцию:
a
— передается по позиции.collection
— любой итерируемый объект. При передаче в функцию с*
— итерируемый обект распаковывается, и его элементы передаются внутрь функции как позиционные параметры.b
— параметр, передаваемый по имени. Слева от знака=
указывается имя аргумента (указанного при задании функции), а слева — значение, которое будет передано этому аргументу.some_dict
— словарь. При передаче в функцию с**
— словарь разворачивается в пары где ключ — имя аргумента функции, а значение — то, что будет передано в этот аргумент. Очевидно, ключами словаря должны быть строки.
Примечания:
- Суть
*
и**
при вызове функций. обратна к заданию функций: в первом случае происходит распаковка, во втором — упаковка параметров. - Эта особенность
*
и**
позволяет например легко пробросить любой набор параметров в вызов функции, осуществляемый внутри другой функции, что мы увидим в декораторах. - В отличие от случая с заданием функции,
*
и**
могут встречаться несколько раз при вызове функции. - В функцию как параметр, может быть передан абсолютно любой Python объект.
Функции как объекты первого класса
Объекты первого класса — это объекты в языке программирования, которые могут быть:
- Переданы как параметр в функцию.
- Возвращены из функции.
- Присвоены переменной.
- Может быть создан в процессе выполнения программы.
В Python абсолютно все объекты являются объектами первого класса. Имеются ввиду все объекты, которые нам уже известны, и все, которые мы изучим позднее (например, классы).
Не являются исключением и функции. Любая функция может быть передана как обычный аргумент в другую функцию, и точно также возвращена из нее.
Code Snippets
Задание и вызов функций
# Задаем простейшую функцию. Она просто складывает переданные ей значения.
# Имена функциям даются в snake_case.
def my_func(a, b):
return a + b
# Вызываем функцию
my_func(1, 2.5)
3.5
# Если в функции не задано возвращаемое значение, она возвращает None .
def func(a, b):
print(a + b)
# Вызываем функцию.
print(func(1, 2.5))
3.5
Типы аргументов при задании функции
[ ]
# Функция, у которой при задании есть все типы аргументов.
def very_big_func(a, b=10, *args, c, d=1, **kwargs):
print(a, b, args, c, d, kwargs)
very_big_func(1, 22, 3, e=7, c=5)
# 3 идет в args, е - в kwargs.
1 22 (3,) 5 1 {'e': 7}
[ ]
# Порядок задания параметров имеет значение. # В случае его нарушения, будет ошибка. # В данном случае позиционный аргумент со значением по умолчанию идет раньше # позиционного аргумента. def func_with_error(a=10, b): pass func_with_error(1, 1) SyntaxError: non-default argument follows default argument
# В случае, если у нас есть позиционные аргументы со значением по умолчанию, # и есть *args, следует быть осторожным и не передавать позиционный аргумент # по имени. def very_big_func(a, b, *args, c, d=1, **kwargs): print(a, b, args, c, d, kwargs) very_big_func(1, 2, 3, b=8, c=5, e=7) # Идея присвоения значений аргументов в Python(и, соответственно, суть ошибки) # Заключается в том, что сначала все, что передано по позиции, присваивается # поочередно всем позиционным аргументам, после чего оставшиеся заворачиваются в # args. В данном случае мы присвоили значения и a, и b, положили 3 в args, # и после этого получаем b, переданный уже по имени. # Очевидно, такого эффекта не будет для аргументов, передаваемых только # по имени. --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-8-437dc6f33e38> in <module>() 5 print(a, b, args, c, d, kwargs) 6 ----> 7 very_big_func(1, 2, 3, b=8, c=5, e=7) 8 9 # Идея присвоения значений аргументов в Python(и, соответственно, суть ошибки) TypeError: very_big_func() got multiple values for argument 'b'
# Задание аргументов, передаваемых только по позиции, в случае, если нет *args.
def func(a, *, c):
print(a, c)
func(3, c=10)
# В случае если мы не хотим принимать неограниченное количество позиционных
# аргументов, мы можем просто указать оператор *, и все переменные, которые
# будут стоять после нее, будут передаваемыми только по имени.
3 10
# Убедимся, что c не может быть передано по позиции. def func(a, *, c): print(a, c) func(3, 10) --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-1-ad4ba2cfc270> in <module> 3 print(a, c) 4 ----> 5 func(3, 10) TypeError: func() takes 1 positional argument but 2 were given
Передача аргументов в функцию
# Распаковка итерируемых объектов и словарей при вызове функций.
def func(a, b, *, c, d):
print(a, b, c, d)
iterable = [1, 2]
dict_ = {'c': 5, 'd': 7}
func(*iterable, **dict_)
1 2 5 7 --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-14-cb0acd49a658> in <module>() 6 iterable = [1, 2, 3] 7 ----> 8 func(*iterable) TypeError: func() takes 2 positional arguments but 3 were given
# Если не указан *args, в распаковываемом объекте количество объектов должно # быть равно количеству аргументов функций. Иначе - ошибка. def func(a, b): print(c, d) iterable = [1, 2, 3] func(*iterable) --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-14-cb0acd49a658> in <module>() 6 iterable = [1, 2, 3] 7 ----> 8 func(*iterable) TypeError: func() takes 2 positional arguments but 3 were given
# То же самое со словарями. def func(*, c, d): print(c, d) dict_ = {'c': 5, 'd': 7, 'e': 10} func(**dict_) NameError Traceback (most recent call last) <ipython-input-1-f1746c35c216> in <module>() 4 print(c, d) 5 ----> 6 print(a) 7 dict_ = {'c': 5, 'd': 7, 'e': 10} 8 NameError: name 'a' is not defined
# Обратите внимание, что словари можно распаковывать не только если аргументы
# с соответствующими именами заданы как передаваемые только по значению.
def func(c, d):
print(c, d)
dict_ = {'c': 5, 'd': 7}
func(**dict_)
5 7
# В отличие от задания функции, * и ** при вызове может встречаться несколько
# раз.
def func(*args, **kwargs): # функция, которая принимает любой набор аргументов
print(args, kwargs)
iterable_1 = (1, )
iterable_2 = [3, 4]
dict_1 = {'c': 5, 'd': 7}
dict_2 = {'e': 5, 'f': 7}
func(*iterable_1, *iterable_2, **dict_1, **dict_2)
# Главное, чтобы вначале передавались позиционные аргументы, а потом аргументы,
# передаваемые по имени. Т.е. Сначала идут все распаковки *, а потом все **.
(1, 3, 4) {'c': 5, 'd': 7, 'e': 5, 'f': 7}
# Не забываем, что строка - тоже итерируемый объект :)
def func(*args): # функция, которая принимает любой набор аргументов
print(args)
str_ = 'string'
func(*str_)
('s', 't', 'r', 'i', 'n', 'g')
Объекты, передаваемые в функции
# попробуем передать в функцию объекты разных типов.
def func(a):
print(a, type(a))
# Передадим некоторые стандартные типы данных.
func(1) # целое число
func([1, 2]) # список
func({3, 4}) # множество
1 <class 'int'> [1, 2] <class 'list'> {3, 4} <class 'set'>
# Пример посложнее - передаем функцию в функцию.
def func(a):
print(a, type(a))
def fun_func():
print('fun')
func(fun_func)
<function fun_func at 0x7ff8133a78c0> <class 'function'>
# Мы можем передать как фнукцию, так и результат ее работы.
# Пример посложнее - передаем функцию в функцию.
def func(a):
print(a, type(a))
def fun_func():
print('fun')
func(fun_func())
# Интерпретируйте полученный результат.
fun None <class 'NoneType'>
Работа с изменяемыми и неизменяемыми значениями по умолчанию
# Неизменяемое значение по умолчанию.
def func(a=1):
a += 1
return a
print(func())
print(func())
2 2
# Изменяемое значение по умолчанию.
def func(a=[]):
a += [1]
return a
print(func())
print(func())
print(func())
[1] [1, 1] [1, 1, 1]
# Стандартный способ обработки таких ситуаций(и заодно пример boilerplate кода).
# Изменяемое значение по умолчанию.
def func(a=None):
if a is None:
a = []
a += [1]
return(a)
print(func())
print(func())
print(func())
# Проверка 'if' a будет в большинстве случаев недостаточна, потому что нам может
# быть передан пустой список, и наш код в таком случае его перезапишет другим.
[1] [1] [1]
Задача для закрепления
- Напишите функцию, которая принимает имя и фамилию, и выводит приветственное сообщение с обращением по инициалам.
- Напишите функцию, которая принимает на входе список из 10 целых чисел (от 0 до 9), а возвращает строку этих чисел в форме номера телефона.
Пример: [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] => «(123) 456-7890» Формат телефона должен совпадать с примером. Не забывайте пробел после закрывающей скобки.
- Написать функцию, на вход получающую массив длиной как минимум 3, содержащий целые числа. Массив либо целиком состоит из четных чисел за исключением одного нечетного, либо полностью состоит из нечетных чисел за исключением одного четного. Найти и вывести аномальное число в массиве.
- Yuliya has a list of numbers. Help her to concatenate these numbers as a strings to get as biggest number as possible. input: [’94’, ’83’, ‘9’] output 99483
def get_full_name(name, surname):
print("Hello {} {}".format(surname, name[0]))
def phone_number(lst):
print('({}{}{}) {}{}{}-{}{}{}{}'.format(*lst))
Встроенные (built-in) функции
Теоретические сведения
В Python присутствует большое количество встроенных функций, необходимых нам для работы и отладки.
Функции, преобразующие типы
dict()
, list()
, int()
, set()
, float()
, …
Так как Python строго типизированный язык, нам необходимо преобразовывать типы вручную. Каждому встроенному типу в Python соответствует своя функция(фактически, конструктор типа, но об этом немного позже), которая позволяет создать необходимый нам объект из другого.
Функции any
, all
Две полезные функции, которые позволяют сделать код более ‘pythonic’:
any(iterable)
— возвращаетTrue
, если в итерируемом объекте находится хотя бы один истинный объект, иFalse
в противном случае.all(iterable)
— возвращаетFalse
, если в итерируемом объекте находится хотя бы один ложный объект, иTrue
в противном случае.
Функции any и all реализуют идею ленивых вычислений (lazy computation), позволяя экономить время.
Кроме того, данные функции часто позволяют писать меньше кода, и избавляют нас от необходимости написания «boiler-plate» кода.
Агрегирующие функции
max(iterable)
— возвращает максимальный элемент итерируемого объекта.min(iterable)
— возвращает минимальный элемент итерируемого объекта.sum(iterable)
— возвращает сумму элементов итерируемого объекта.
Вспомогательные функции
print(object1, [object2, ... ]])
— печатает переданные объекты.range(end)
,range(start, end, [step])
— возвращает итерируемый объект, состоящий из чисел, в зависимости от переданных параметров.type(object)
— возвращает тип переданного объекта.dir(object)
— возвращает перечень атрибутов любого объекта.chr(int)
— возвращает символ(строку длинной 1) по коду этого символа.ord(symbol)
— возвращает код символа(строки длинной 1).
И многие, многие другие встроенные функции.
Code Snippets
# Пример работы агрегирующих функции.
iterable = {1, 2, 3, 4, 1, 2, 3, 4}
print(sum(iterable)) # интерпретируйте пожалуйста резулльтат выполнения
print(max(iterable))
print(min(iterable))
10 4 1
# Важно, чтобы все элементы итерируемого объекта были сравнимы! def func(): pass a = [func, 3] print(max(a)) --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-2-1e0abb6e6b8b> in <module>() 4 5 a = [func, 3] ----> 6 print(max(a)) TypeError: '>' not supported between instances of 'int' and 'function'
# Пример функции преобразования типа и type.
a = tuple('string')
print(type(a), a)
<class 'tuple'> ('s', 't', 'r', 'i', 'n', 'g')
# Пример работы dir. Он пригодится нам когда мы будем изучать классы.
print(dir([1, 2]))
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
"""
В функцию передается итерируемый объект состоящий из словарей. Проверить,
есть ли в этом итерируемом объекте словарь в котором есть непустое (истинное)
значение для ключа key.
"""
def func(iterable):
"""Checks if we have dict with non-empty "key": value pair.
Args:
iterable = [{}, {}, ...] - list of dicts
Return:
True/False
"""
# рассмотрим решение стандартными методами.
for dct in iterable:
if dct.get("key"):
return True
return False
dct1 = [{1: 1}, {"key": None}, ] # False
print(func(dct1))
False
# Решение с помощью any.
def func(iterable):
"""Checks if we have dict with non-empty "key": value pair.
Args:
itr = [{}, {}, ...] - list of dicts
Return:
True/False
"""
return any( [dct.get("key") for dct in iterable] )
dct1 = [{1: 1}, {"key": None}, ] # False
print(func(dct1))
False
# Вызов той же функции но с ожидаемым ответом True.
def func(iterable):
"""Checks if we have dict with non-empty "key": value pair.
Args:
itr = [{}, {}, ...] - list of dicts
Return:
True/False
"""
return any([dct.get("key") for dct in iterable])
dct1 = [{1:1},{"key": 1},]
print(func(dct1))
True
"""
Переформулируем задачу.
Проверить, содержат ли все словари в переданном итерируемом объекте пары
ключ-значение с непустым(истинным) значением
"""
def func(iterable):
return all([dct.get("key") for dct in iterable])
dct = [{1:1},{"key": 1},]
print(func(dct))
False
# Важный момент, напрямую следующий из определения.
print('all:', all([]))
print('any:', any([]))
# Из этого следует, что изменение формулировки в определении критично.
all: True any: False
requires_handling = not all((
any((check(point1_1), check(point1_2)))
any((check(point2_1), check(point2_2)))
any((check(dron1), check(dron2)))
))
Задачи для закрепления
Агрегирующие функции:
- Дан список из трех элементов. Выведите сначала максимальный его элемент, потом минимальный, потом оставшийся. (используйте агрегирующие функции).
Задачи на all и any:
Даны два прямоугольника, стороны которых параллельны осям координат. Известны координаты левого нижнего угла x
, y
, а также ширина и высота прямоугольника w
, h
.
- Определить, принадлежат ли все точки первого прямоугольника второму (
all
). - Определить, пересекаются ли эти прямоугольники (
any
,all
).
Рекурсия
Теоретические сведения
Функция внутри своего тела может вызывать саму себя. В таком случае Она будет называться рекурсивной, а прием называется рекурсией.
Замечания при работе с рекурсивными функциями:
- В случае реализации рекурсивной функции стоит обратить внимание на момент выхода из рекурсии — мы обязательно должны добавить в тело функции блок, который будет останавливать рекурсию при выполнении определенных условий.
- Если функция реализует линейную рекуррентную последовательность (последовательность, в которой следующий член представляет собой линейную комбинацию определенного количества предыдущих членов), всегда есть возможность построить формулу общего члена, избежать расчета всех предыдущих значений. Например, для чисел Фибоначчи это формула Бине.
- Многие рекурсивные функции (которые идут сверху вниз — от последнего искомого члена к предыдущим) могут быть переписаны с использованием цикла который идет снизу вверх, без рекурсии.
- В случае если рекурсия зависит от нескольких предыдущих значений стоит быть осторожным чтобы количество вызовов функции для одного и того же значения не начало резко расти, замедляя работу (см. примеры).
Code snippets
# Простейшая рекурсивная функция - считает факториал.
def factorial(n):
if n != 1:
return n*factorial(n-1)
else: # условия выхода из рекурсии
return 1
print(factorial(4))
# Расчет n-го числа Фибоначчи без использования рекурсии (начиная с второго)
# Замерим время исполнения программы.
def fib(n):
a1 = a2 = 1
for el in range(3, n+1):
a1, a2 = a2, a1 + a2
return a2
import time
start = time.time()
print(fib(100))
print('Execution time:', time.time() - start)
354224848179261915075 Execution time: 0.0001964569091796875
# А сейчас то же самое но для рекурсии написанной в лоб.
def fib(n):
if n in (1, 2): # Условие выхода из рекурсии. Скобки здесь не обязательны
return 1
else:
return fib(n-1) + fib(n-2)
import time
start = time.time()
print(fib(40)) # Обратите внимание, что здесь число в 3 раза меньше!
print('Execution time:', time.time() - start)
102334155 Execution time: 32.334763526916504
# Реализуем кэширование чтобы избежать ситуации из прошлого примера.
cache = {}
def fib(n):
if n in (1, 2):
return 1
else:
if not cache.get(n-1):
cache[n-1] = fib(n-1)
if not cache.get(n-2):
cache[n-2] = fib(n-2)
return cache[n-1] + cache[n-2]
import time
start = time.time()
print(fib(100))
print('Execution time:', time.time() - start)
# Стоит обратить внимание на следующее:
# 1. Мы не можем использовать, например, setdefault к нашему кэшу, потому что
# попадем в похожую ситуацию - ключ в словарь будет добавляться очень поздно.
# 2. На одном запуске сложно оценить производительность первого и второго
# решения, но проведя тест например на 10000 зпусков можно получить более-менее
# релевантные результаты для оценки производительности.
# 3. Рекурсия имеет максимальную глубину, обусловленную стэком вызовов.
# например, на данный момент(декабрь 2020) посчитать 1000 число мы не можем.
# 4. Производительность зависит от реализации интерпретатора, и результаты могут
# разительно отличаться в двух разных реализациях интерпретатора (и даже в
# разных его версиях).
import time
def fib_for(n):
a1 = a2 = 1
for el in range(3, n+1):
a1, a2 = a2, a1 + a2
return a2
start = time.time()
for _ in range(10000):
fib_for(30)
print('Execution time (for loop):', time.time() - start)
cache = {}
def fib_recur(n):
if n in (1, 2):
return 1
else:
if not cache.get(n-1):
cache[n-1] = fib(n-1)
if not cache.get(n-2):
cache[n-2] = fib(n-2)
return cache.get(n-1) + cache.get(n-2)
start = time.time()
for _ in range(10000):
fib_recur(30)
print('Execution time (recursive):', time.time() - start)
Execution time (for loop): 0.022706031799316406 Execution time (recursive): 0.24990200996398926
c = 0
def fib(n):
global c
c += 1
if n in (1, 2):
return 1
else:
return fib(n-1) + fib(n-2)
print(fib(10), c)
Lambda функции
Теоретические сведения
Базовое понятие о лямбда функциях
В целом, существует два подхода в программировании — функциональный подход, основанный на лямбда-исчислении (математическая теория), и императивное программирование, основанное на концепции машины Тьюринга. Обе модели на самом деле являются эквивалентными, для этого существует доказанная теорема Чёрча-Тьюринга, и различие состоит лишь в подходах к программиованию.
Впрочем, от истинных лямбда функций в Python у нас только название, и они в Python являются не более чем сокращенной записью обычных функций. (Если интересно: https://docs.python.org/3/faq/design.html#why-can-t-lambda-expressions-contain-statements, кроме того там еще много интересных ответов на интересные вопросы).
Синтаксис лямбда-функции в Python:
lamdba args: body
Определение лямбда функции состоит из ключевого слова lambda
, перечня связанных аргументов args
(аргументы, которые будет принимать функция), и тела функции body
.
Результат работы функции — значение получившееся после вычисления выражения в теле функции.
Особенности задания лямбда функций:
- Может содержать только выражения и не может включать операторы присвоения в свое тело.
- Пишется как одна строка исполнения.
- Не поддерживает аннотации типов.
- Может быть немедленно вызвана.
Фактически, лямбда в Python — обычная функция, у которой нет имени.
Некоторые функции высшего порядка
map(function_to_apply, list_of_inputs)
— формирует итерируемый объект, применяя функциюfunction_to_apply
к каждому элементу спискаlist_of_inputs
.filter(function_to_check, list_of_inputs)
— формирует итерируемый объект, забирая элементы спискаlist_of_inputs
, удовлетворяющих условиюfunction_to_check
.reduce(reduce_function, list_of_inputs)
— применяет указанную функциюreduce_function
к элементам последовательностиlist_of_inputs
, сводя её к единственному значению.Принцип работы: берет первые два элемента и применяет к ним функциюreduce_function
. Далее применяетreduce_function
к результату предыдущего вычисления и следующему элементу.
Code Snippets
Использование лямбда-функций
# Зададим простейшую лямбда-функцию.
l = lambda x: x+1
print(l(5))
6
# Лямбда функция может быть задана и сразу вызвана. Кроме того ее необязательно
# присваивать какой бы то ни было переменной.
print((lambda x:x+1)(5))
6
# Это считается плохой практикой, но в Python можно записывать несколько
# инструкций на одной строке, используя ;.
print(1); print(2)
1 2
# Но с лямбдой такого фокуса не выйдет. l = (lambda x: print(1); print(2)) print(l(5)) File "<ipython-input-5-6ad75073944d>", line 2 l = (lambda x: print(1); print(2)) ^ SyntaxError: invalid syntax
# Немного более сложное выражение в лямбде.
l = lambda a: [element +1 for element in a]
print(l([2,3]))
[3, 4]
# Лямбда всегда возвращает результат выполнения body.
l = lambda a: print(a)
print(l(a=2))
2 None
# Самый простой способ сделать более сложные вычисления в лямбде - поместить в
# тело функцию. Однако возникает вопрос зачем нам тогда лямбда.
def func(a, b):
print("don't do in this manner")
print("don't do in this manner")
print("don't do in this manner")
print("don't do in this manner")
l = lambda a, b: func(a, b)
l(2,3)
don't do in this manner don't do in this manner don't do in this manner don't do in this manner
# Фактически, одно из главных отличий лямбды в Python от обычной функции.
l = lambda arg: arg + 1
def func():
pass
print(l.__name__)
print(func.__name__)
<lambda> func
# Кстати функция сохраняет свое имя.
def func():
pass
b = func
func = 1
print(b.__name__)
func
# Так как лямбда фактически таже самая функция, в ней могут присутствовать все
# доступные в функции типы аргументов.
l = lambda a, b=0, *args, c=7, **kwargs: print(a, b, args, c, kwargs)
l(1, 2, 3, 4 )
1 2 (3, 4) 7 {}
# Рекурсия на лямбде. Она не совсем честная, потому что мы фактически именуем
# нашу лямбду и используем это имя.
factorial = lambda a: a*factorial(a-1) if a>1 else 1
print(factorial(4))
# Есть возможность реализовать рекурсию и без этого трюка, но тут нужно
# использовать Y-комбинатор и все это выглядит очень плохо. Мое личное мнение -
# забейте. Но если очень любопытно:
# https://en.wikipedia.org/wiki/Fixed-point_combinator - теория.
# Попытка объяснения на русском https://habr.com/ru/post/50354/ (но без лямбд).
# Сам комбинатор:
Y = lambda g: ((lambda f: g(lambda *x: f(f) (*x))) ((lambda f: g(lambda *x: f(f)(*x)))))
# Вычисление факториала рекурсивно с помощью комбинатора.
print(Y(lambda f: (lambda num: num*f(num-1) if num else 1))(4))
# Очевидно можно Y подставить в print и записать все в одну строку, но это
# будет вообще нечитаемо.
24
Некоторые функции высшего порядка
[ ]
# Функция, уменьшающая значение каждого элемента переданного аргумента на 1. ito = (1, 2, 3) dec = lambda x: x-1 result = map(dec, ito) print(result) # обратите внимание, map возвращает итерируемый объект print(list(result)) <map object at 0x7f18d99eac50> --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-17-2416fe0d373e> in <module>() 4 result = map(dec, ito) 5 print(result) # обратите внимание, map возвращает итерируемый объект ----> 6 print(len(result)) 7 print(list(result)) TypeError: object of type 'map' has no len()
# Все то же самое, но изрядно сокращенное.
x = map(lambda x: x-1, [1, 2, 3])
print(list(x))
[0, 1, 2]
# Результат map можно применять, как и любой итерируемый объект, для любых
# наших целей.
x = map(lambda x: x-1, [1, 2, 3])
print({el:el+1 for el in x})
{0: 1, 1: 2, 2: 3}
# Лямбды часто применяются в map, filter, reduce, однако можно применять и
# обычные функции.
def dec(x):
# some operations
return x-1
ito = [1]
x = map(dec, ito)
print(list(x))
[0]
# Однако, перед применением map, стоит понимать, что у нас есть гораздо более
# pythonic альтернатива: list comprehensions.
def f(x):
return x+1
iterable = [1, 2, 3]
print([f(x) for x in iterable])
[2, 3, 4]
# Применение функции filter.
result = filter(lambda x: x if x<0 else None, [-1, 2, 3])
result = list(result)
print('len:', len(result))
print(result)
len: 1 [-1]
# Обратите внимание, filter также возвращает специальный итерируемый объект.
x = filter(lambda x: x<0, [-1, 2, 3])
print(x)
print(list(x))
<filter object at 0x7f18d9a626d0> [-1]
# И еще один важный момент: по результату filter и map можно пройти только
# один раз.
a = map(lambda x: x+1, [1,2,3])
x = filter(lambda x: x<0, [-1, 2, 3])
print('Map:')
print(list(a))
print(list(a))
print('Filter:')
print(list(x))
print(list(x))
Map: [2, 3, 4] [] Filter: [-1] []
# Альтернатива filter - те же самые генераторы.
iterable = (1,)
f = lambda x:x
[x for x in iterable if f(x)]
[1]
# Пример использования reduce.
from functools import reduce
pows = reduce(lambda x, y: x ** y, [1, 2, 3])
print(pows)
1
# Чуть более говорящий пример.
from functools import reduce
pows = reduce((lambda x, y: x ** y), [2, 2, 3])
print(pows)
64
from functools import reduce
print(list(map(lambda x: -x if x > 0 else x, [-1, 0, 2, 3, -4])))
print(list(filter(lambda x: x % 15, [15, 0, 20, 30, 46])))
print(reduce(lambda x, y: int(x) + int(y), '12344567894567890987656780987678098767890098765678987656789098767890987678905678905'))
[-1, 0, -2, -3, -4] [20, 46] 512
# Питонячной альтернативы для reduce у нас нет :).
Задачи для закрепления на занятии
- Поменяйте неотрицательным числам в списке знак используя map.
- С помощью filter извлеките числа, не кратные 15.
- Посчитайте сумму цифр в числе с помощью reduce. можно итерироваться по строке.
Области видимости. Замыкания. Декораторы
Области видимости
Фактически, область видимости задает, где могут быть использованы переменные, заданные в данном месте.
Например, переменные заданные в модуле — могут быть использованы в любом месте данного модуля, включая тело функций, заданных в этом модуле.
Напротив, переменные, заданные в теле функции вне этой функции, не могут быть использованы — это вызовет ошибку интерпретатора.
Глобальная переменная — переменная, доступная для использования во всем модуле.
Локальная переменная — переменная, доступная для использования только внутри функции. Все аргументы, заданные при определении функции, также являются локальными для этой функции.
globals()
— выводит все глобальные переменные.
locals()
— выводит все локальные переменные в данном месте программы.
Порядок поиска значения переменной. LEGB
При обращении к переменной, интерпретатор пытается найти эту переменную в строго определенном порядке.
LEGB
L
— local. Сначала поиск происходит в локальной области видимости.
E
— enclosure. Если переменная не была найдена в локальной области видимости, она проверяется в объемлющей функциии — фактически, ищется в локальных переменных функции, внутри которой определена заданная функция. Это единственный тип областей видимости, которые могут быть вложенными.
G
— global. Глобальная область видимости — т.е. переменные, заданные в модуле.
B
— built-in. Встроенные переменные. Например, функции max, sum и множество других.
Обход областей происходит в строго указанном порядке. Объемлющие функции обходятся от самой ближайшей.
Присвоение значений переменным во внешних областях видимости
Так как в Python при присвоении переменной происходит ее создание, причем создается она в текущей области видимости, представляется проблематичным присвоить значение переменной из внешней области видимости.
Для решения этой проблемы используются специальные операторы.
global var
— позволяет после написания этой строки работать с глобальной переменной (присваивать ей значение, в случае если такой переменной не существует — она будет создана в глобальной области видимости и ей будет присвоено требуемое значение). Важно отметить, что эту инструкцию можно писать только пока не будет создана локальная переменная var
, иначе это вызовет ошибку интерпретатора.
nonlocal var
— позволяет найти переменную var
в объемлющей области видимости, и работать именно с ней, вместо создания новой переменной. В случае, если в объемлющей области видимости данной переменной нет, будет ошибка интерпретатора. Использовать nonlocal
когда уже существует локальная переменная с тем же именем нельзя.
Декораторы
Можно предложить несколько понятийных определения декоратора:
- Декоратор — это обертка вокруг заданной функции, которая позволяет добавлять к функции дополнительный функционал.
- Декоратор — это объект, подменяющий собой задекорированную функцию, и добавляющий к ней дополнительный функционал.
- Декоратор — структурный паттерн проектирования, предназначенный для динамического подключения дополнительного поведения к объекту.
С точки зрения кода, декоратор представляет собой функцию специальной конструкции.
Параметрический декоратор — декоратор, который может кроме декорируемой функции принимать дополнительно определенные параметры, для более гибкой работы с ним.
Шаблон для решения задачи на декораторы:
def my_dec(func):
# используем если необходимо хранить значение между вызовами функции
# var_between_runs = 0
def wrapper(*args, **kwargs):
# используем если необходимо хранить значение между вызовами функции
# nonlocal var_between_runs
# Реализуем блок операций которые должны быть вызваны до вызова функции
# operations_before_run
result = func(*args, **kwargs)
# Реализуем блок операций которые должны быть вызваны после вызова функции
# operations_after_run
return result
return wrapper
Идея заключается в том, что вам необходимо в любом случае написать костяк декоратора, и он всегда одинаковый (все, что не закомментировано), а потом реализовать соответствующие блоки в комментариях, в зависимости от условия задачи. Обычно, эти реализации очень простые, и самая сложная проблема — запомнить структуру декоратора.
Замыкание(closure)
На примере декоратора мы увидели, что wrapper
использует функцию, переданную в декоратор, т.е. находящуюся в объемлющей области видимости. Стоит отметить, что В момент вызова wrapper
‘а функция декоратора уже завершила работу, и ее пространство имен удалилось.
Способность хранить внешние зависимости для дальнейшего использования называется замыканием.
Code Snippets
Области видимости
# На уровне модуля globals и locals будут совпадать - т.к. определенные в # модуле переменные являются для него локальными, но также и глобальны - могут # быть использованы в любом месте модуля. global_var = 10 print(globals()) print(locals())
# В функции локальная область видимости пустая если туда ничего не передается # и не определяется внутри. global_var = 10 def func(): print(globals()) print(locals()) func()
# Локальными для функции являются: аргументы, созданные внутри нее переменные. def func(param): local_var = 10 print(globals()) print(locals()) func(5)
LEGB, global, nonlocal
# Рассмотрим принцип работы поиска аргумента по LEGB. Здесь у нас есть
# локальная, объемлющая и глобальная переменные с одинаковым именем.
var = 'global'
def func():
var = 'enclosure'
def inner_func():
var = 'local'
print(var)
return inner_func
inner = func() # возвращаемое значение - функция
inner() # происходит вызов внутренней функции
local
# Если нет локальной - ищет в объемлющей области видимости.
var = 'global'
def func():
var = 'enclosure'
def inner_func():
print(var)
return inner_func
inner = func()
print(inner.__closure__)
inner()
(<cell at 0x7f9a811bc1d0: str object at 0x7f9a7efae370>,) enclosure
# Объемлющие области могут быть вложенными. В таком случае поиск идет изнутри
# наружу.
var = 'global'
def func():
var = 'enclosure_outer'
def inner_func():
var = 'enclosure_inner'
def last_level_func():
print(var)
return last_level_func
return inner_func
inner = func()
inner()() # так мы вызовем last_level_func
enclosure_inner
# То же самое, но когда в inner_func нет нашей переменной.
var = 'global'
def func():
var = 'enclosure_outer'
def inner_func():
def last_level_func():
print(var)
return last_level_func
return inner_func
inner = func()
inner()() # так мы вызовем last_level_func
enclosure_outer
# Глобальные переменные - последний уровень обороны из того, что реализуем мы.
var = 'global'
def func():
def inner_func():
print(var)
return inner_func
inner = func()
inner()
global
# На последнем этапе мы ищем в built-in объектах.
def func():
def inner_func():
print(max) # нигде не заданная переменная
return inner_func
inner = func()
inner()
<built-in function max>
# Присвоить значение глобальной переменной достаточно проблематично
# внутри функции. Присвоение создает имя переменной в локальной области
# видимости.
var = 10
def func():
var = 'new_value'
func()
print(var)
10
# В такой ситуации нам поможет оператор global.
var = 10
def func():
global var
var = 'new_value'
func()
print(var)
new_value
# Использовать global когда локальная переменная уже существует нельзя. var = 10 def func(): var = 'new_value' print(var) global var func() print(var) File "<ipython-input-15-731d68017c53>", line 7 global var ^ SyntaxError: name 'var' is used prior to global declaration
# Если глобальной переменной не существовало, она будет создана. Но вообще,
# это очень плохая практика.
def func():
global var
var = 'new_value'
func()
print(var)
new_value
# Аналогично будет работать оператор nonlocal.
def outer_func():
var = 10
def func():
nonlocal var
var = 'new_value'
func()
print(var)
return func
result = outer_func()
new_value
# В отличие от global, nonlocal не создает переменную. def outer_func(): def func(): nonlocal var var = 'new_value' return func outer_func()() # вызываем функцию func print(var) File "<ipython-input-26-d5b22a1e93ed>", line 5 nonlocal var ^ SyntaxError: no binding for nonlocal 'var' found
Декораторы
# Создадим простейший декоратор.
def dec(fun):
def wrapper(*args, **kwargs):
print("inside decorator")
return fun(*args, **kwargs)
return wrapper
@dec # декорирование функции
def func():
print("inside function")
func()
inside decorator inside function
# Каждый вызов декоратора создает новый объект wrapper, который в замыкании
# хранит ссылку каждый на свою функцию.
def dec(fun):
def wrapper():
print("inside wrapper")
result = fun()+3
print("after function")
return result
return wrapper
@dec
def func():
print("inside function")
return 1
@dec
def func1():
print("other function")
return 151
print(func())
print()
print(func1())
(<cell at 0x7f4c01edbb90: int object at 0x55765cb41b20>, <cell at 0x7f4c01edb090: function object at 0x7f4c01f04d40>) inside wrapper inside function 10 after function 4 inside wrapper other function 10 after function 154
# Создадим простейший декоратор. def dec(fun): def wrapper(*args, **kwargs): print("inside decorator") return fun(*args, **kwargs) return wrapper def func(): pass func = dec(func)
# Данный прием позволяет хранить значения между вызовами задекорированной
# функции.
def dec(fun):
all_sum = 0
def wrapper():
nonlocal all_sum
all_sum += fun()
return all_sum
return wrapper
@dec
def func():
pass
print(func())
print(func())
print(func())
1 2 3
def dec(fun):
all_sum = 0
def wrapper():
nonlocal all_sum
all_sum = all_sum + fun()
return all_sum
return wrapper
@dec
def func():
pass
print(func())
print(func())
print(func())
[ ]
def dec(fun): cache = 0 def wrapper(): nonlocal cache cache +=1 fun() return all_sum return wrapper @dec def func(): print() print(func()) print(func()) print(func()) NameError Traceback (most recent call last) <ipython-input-38-9ef47aee3537> in <module>() 13 14 ---> 15 print(func()) 16 print(func()) 17 print(func()) 1 frames <ipython-input-38-9ef47aee3537> in func() 10 @dec 11 def func(): ---> 12 print(cache) 13 14 NameError: name 'cache' is not defined
# В случае с изменяемым объектом в enclosure scope, nonlocal ставить не нужно,
# однако часто его все-равно ставят чтобы это было явно. Up to you.
def dec(fun):
result_list = []
def wrapper():
# nonlocal result_list
result_list.append(fun())
return result_list
return wrapper
@dec
def func():
return 1
print(func())
print(func())
print(func())
[1] [1, 1] [1, 1, 1]
globals()
{'In': ['', 'globals()'], 'Out': {}, '_': '', '__': '', '___': '', '__builtin__': <module 'builtins' (built-in)>, '__builtins__': <module 'builtins' (built-in)>, '__doc__': 'Automatically created module for IPython interactive environment', '__loader__': None, '__name__': '__main__', '__package__': None, '__spec__': None, '_dh': ['/content'], '_i': '', '_i1': 'globals()', '_ih': ['', 'globals()'], '_ii': '', '_iii': '', '_oh': {}, '_sh': <module 'IPython.core.shadowns' from '/usr/local/lib/python3.7/dist-packages/IPython/core/shadowns.py'>, 'exit': <IPython.core.autocall.ZMQExitAutocall at 0x7f18e08e1c10>, 'get_ipython': <bound method InteractiveShell.get_ipython of <google.colab._shell.Shell object at 0x7f18e314f8d0>>, 'quit': <IPython.core.autocall.ZMQExitAutocall at 0x7f18e08e1c10>}
[ ]
# Все это время мы декорировали функции которые вызываются без передачи значений
# Однако мы легко можем организовать проброску параметров с помощью операторов
# запаковки и распаковки аргументов * и **.
def dec(fun):
def wrapper(*args, **kwargs): # это повторение самого первого примера
print("inside decorator")
return fun(*args, **kwargs)
return wrapper
@dec
def func(a, b=1, *, c): # но функция другая
print(a, b, c, sep=', ')
func('positional', c='key-word only')
inside decorator positional, 1, key-word only
«»»
Реализовать параметрический декоратор, который прибавляет к результату работы
функции число, переданное параметром
«»»
def my_parametrized_dec(num, *args):
def dec(fun):
print(num)
def wrapper():
return fun() + num
return wrapper
return dec
@my_parametrized_dec(1) # синтаксис декорирования параметрическим декоратором
def func():
return 1
@my_parametrized_dec(2)
def func2():
return 1
print(func())
print(func())
print(func2())
print(func2())
# func = my_parametrized_dec(args)(func)
Задача для закрепления
- Реализуйте декоратор, который при каждом вызове функции печатает количество вызовов функции.
def param_dec(num):
def dec(fun):
counter = 0
def wrapper():
nonlocal counter
counter += 1
if counter < num:
print(‘Counter:’, counter)
return fun()
return wrapper
return dec
Подпишитесь на рассылку