Введение
Определение
Вот пример простой функции:
def compute_surface(radius):
from math import pi
return pi * radius * radius
Для определения функции нужно всего лишь написать ключевое слово def
перед ее именем, а после — поставить двоеточие. Следом идет блок инструкций.
Последняя строка в блоке инструкций может начинаться с return
, если нужно вернуть какое-то значение. Если инструкции return
нет, тогда по умолчанию функция будет возвращать объект None
. Как в этом примере:
i = 0
def increment():
global i
i += 1
Функция инкрементирует глобальную переменную i
и возвращает None
(по умолчанию).
Вызовы
Для вызова функции, которая возвращает переменную, нужно ввести:
surface = compute_surface(1.)
Для вызова функции, которая ничего не возвращает:
increment()
Еще
Функцию можно записать в одну строку, если блок инструкций представляет собой простое выражение:
def sum(a, b): return a + b
Функции могут быть вложенными:
def func1(a, b):
def inner_func(x):
return x*x*x
return inner_func(a) + inner_func(b)
Функции — это объекты, поэтому их можно присваивать переменным.
Инструкция return
Возврат простого значения
Аргументы можно использовать для изменения ввода и таким образом получать вывод функции. Но куда удобнее использовать инструкцию return
, примеры которой уже встречались ранее. Если ее не написать, функция вернет значение None
.
Возврат нескольких значений
Пока что функция возвращала только одно значение или не возвращала ничего (объект None). А как насчет нескольких значений? Этого можно добиться с помощью массива. Технически, это все еще один объект. Например:
def stats(data):
"""данные должны быть списком"""
_sum = sum(data) # обратите внимание на подчеркивание, чтобы избежать переименования встроенной функции sum
mean = _sum / float(len(data)) # обратите внимание на использование функции float, чтобы избежать деления на целое число
variance = sum([(x-mean)**2/len(data) for x in data])
return mean,variance # возвращаем x,y — кортеж!
m, v = stats([1, 2, 1])
Аргументы и параметры
В функции можно использовать неограниченное количество параметров, но число аргументов должно точно соответствовать параметрам. Эти параметры представляют собой позиционные аргументы. Также Python предоставляет возможность определять значения по умолчанию, которые можно задавать с помощью аргументов-ключевых слов.
Параметр — это имя в списке параметров в первой строке определения функции. Он получает свое значение при вызове. Аргумент — это реальное значение или ссылка на него, переданное функции при вызове. В этой функции:
def sum(x, y):
return x + y
x и y — это параметры, а в этой:
sum(1, 2)
1 и 2 — аргументы.
При определении функции параметры со значениями по умолчанию нужно указывать до позиционных аргументов:
def compute_surface(radius, pi=3.14159):
return pi * radius * radius
Если использовать необязательный параметр, тогда все, что указаны справа, должны быть параметрами по умолчанию.
Выходит, что в следующем примере допущена ошибка:
def compute_surface(radius=1, pi):
return pi * radius * radius
Для вызовов это работает похожим образом. Сначала нужно указывать все позиционные аргументы, а только потом необязательные:
S = compute_surface(10, pi=3.14)
На самом деле, следующий вызов корректен (можно конкретно указывать имя позиционного аргумента), но этот способ не пользуется популярностью:
S = compute_surface(radius=10, pi=3.14)
А этот вызов некорректен:
S = compute_surface(pi=3.14, 10)
При вызове функции с аргументами по умолчанию можно указать один или несколько, и порядок не будет иметь значения:
def compute_surface2(radius=1, pi=3.14159):
return pi * radius * radius
S = compute_surface2(radius=1, pi=3.14)
S = compute_surface2(pi=3.14, radius=10.)
S = compute_surface2(radius=10.)
Можно не указывать ключевые слова, но тогда порядок имеет значение. Он должен соответствовать порядку параметров в определении:
S = compute_surface2(10., 3.14)
S = compute_surface2(10.)
Если ключевые слова не используются, тогда нужно указывать все аргументы:
def f(a=1,b=2, c=3):
return a + b + c
Второй аргумент можно пропустить:
f(1,,3)
Чтобы обойти эту проблему, можно использовать словарь:
params = {'a':10, 'b':20}
S = f(**params)
Значение по умолчанию оценивается и сохраняется только один раз при определении функции (не при вызове). Следовательно, если значение по умолчанию — это изменяемый объект, например, список или словарь, он будет меняться каждый раз при вызове функции. Чтобы избежать такого поведения, инициализацию нужно проводить внутри функции или использовать неизменяемый объект:
def inplace(x, mutable=[]):
mutable.append(x)
return mutable
res = inplace(1)
res = inplace(2)
print(inplace(3))
[1, 2, 3]
def inplace(x, lst=None):
if lst is None: lst=[]
lst.append()
return lst
Еще один пример изменяемого объекта, значение которого поменялось при вызове:
def change_list(seq):
seq[0] = 100
original = [0, 1, 2]
change_list(original)
original
[100, 1, 2]
Дабы не допустить изменения оригинальной последовательности, нужно передать копию изменяемого объекта:
original = [0, 1, 2]
change_list(original[:])
original
[0, 1, 2]
Указание произвольного количества аргументов
Позиционные аргументы
Иногда количество позиционных аргументов может быть переменным. Примерами таких функций могут быть max()
и min()
. Синтаксис для определения таких функций следующий:
def func(pos_params, *args):
block statememt
При вызове функции нужно вводить команду следующим образом:
func(pos_params, arg1, arg2, ...)
Python обрабатывает позиционные аргументы следующим образом: подставляет обычные позиционные аргументы слева направо, а затем помещает остальные позиционные аргументы в кортеж (*args), который можно использовать в функции.
Вот так:
def add_mean(x, *data):
return x + sum(data)/float(len(data))
add_mean(10,0,1,2,-1,0,-1,1,2)
10.5
Если лишние аргументы не указаны, значением по умолчанию будет пустой кортеж.
Произвольное количество аргументов-ключевых слов
Как и в случае с позиционными аргументами можно определять произвольное количество аргументов-ключевых слов следующим образом (в сочетании с произвольным числом необязательных аргументов из прошлого раздела):
def func(pos_params, *args, **kwargs):
block statememt
При вызове функции нужно писать так:
func(pos_params, kw1=arg1, kw2=arg2, ...)
Python обрабатывает аргументы-ключевые слова следующим образом: подставляет обычные позиционные аргументы слева направо, а затем помещает другие позиционные аргументы в кортеж (*args), который можно использовать в функции (см. предыдущий раздел). В конце концов, он добавляет все лишние аргументы в словарь (**kwargs), который сможет использовать функция.
Есть функция:
def print_mean_sequences(**kwargs):
def mean(data):
return sum(data)/float(len(data))
for k, v in kwargs.items():
print k, mean(v)
print_mean_sequences(x=[1,2,3], y=[3,3,0])
y 2.0
x 2.0
Важно, что пользователь также может использовать словарь, но перед ним нужно ставить две звездочки (**):
print_mean_sequences(**{'x':[1,2,3], 'y':[3,3,0]})
y 2.0
x 2.0
Порядок вывода также не определен, потому что словарь не отсортирован.
Документирование функции
Определим функцию:
def sum(s,y): return x + y
Если изучить ее, обнаружатся два скрытых метода (которые начинаются с двух знаков нижнего подчеркивания), среди которых есть __doc__
. Он нужен для настройки документации функции. Документация в Python называется docstring
и может быть объединена с функцией следующим образом:
def sum(x, y):
"""Первая срока - заголовок
Затем следует необязательная пустая строка и текст
документации.
"""
return x+y
Команда docstring
должна быть первой инструкцией после объявления функции. Ее потом можно будет извлекать или дополнять:
print(sum.__doc__)
sum.__doc__ += "some additional text"
Методы, функции и атрибуты, связанные с объектами функции
Если поискать доступные для функции атрибуты, то в списке окажутся следующие методы (в Python все является объектом — даже функция):
sum.func_closure sum.func_defaults sum.func_doc sum.func_name
sum.func_code sum.func_dict sum.func_globals
И несколько скрытых методов, функций и атрибутов. Например, можно получить имя функции или модуля, в котором она определена:
>>> sum.__name__
"sum"
>>> sum.__module
"__main__"
Есть и другие. Вот те, которые не обсуждались:
sum.__call__ sum.__delattr__ sum.__getattribute__ sum.__setattr__
sum.__class__ sum.__dict__ sum.__globals__ sum.__new__ sum.__sizeof__
sum.__closure__ sum.__hash__ sum.__reduce__ sum.__str__
sum.__code__ sum.__format__ sum.__init__ sum.__reduce_ex__ sum.__subclasshook__
sum.__defaults__ sum.__get__ sum.__repr__
Рекурсивные функции
Рекурсия — это не особенность Python. Это общепринятая и часто используемая техника в Computer Science, когда функция вызывает сама себя. Самый известный пример — вычисление факториала n! = n * n — 1 * n -2 * … 2 *1. Зная, что 0! = 1, факториал можно записать следующим образом:
def factorial(n):
if n != 0:
return n * factorial(n-1)
else:
return 1
Другой распространенный пример — определение последовательности Фибоначчи:
f(0) = 1
f(1) = 1
f(n) = f(n-1) + f(n-2)
Рекурсивную функцию можно записать так:
def fibbonacci(n):
if n >= 2:
else:
return 1
Важно, чтобы в ней было была конечная инструкция, иначе она никогда не закончится. Реализация вычисления факториала выше, например, не является надежной. Если указать отрицательное значение, функция будет вызывать себя бесконечно. Нужно написать так:
def factorial(n):
assert n > 0
if n != 0:
return n * factorial(n-1)
else:
return 1
Важно!
Рекурсия позволяет писать простые и элегантные функции, но это не гарантирует эффективность и высокую скорость исполнения.
Если рекурсия содержит баги (например, длится бесконечно), функции может не хватить памяти. Задать максимальное значение рекурсий можно с помощью модуля sys
.
Глобальная переменная
Вот уже знакомый пример с глобальной переменной:
i = 0
def increment():
global i
i += 1
Здесь функция увеличивает на 1 значение глобальной переменной i
. Это способ изменять глобальную переменную, определенную вне функции. Без него функция не будет знать, что такое переменная i
. Ключевое слово global
можно вводить в любом месте, но переменную разрешается использовать только после ее объявления.
За редкими исключениями глобальные переменные лучше вообще не использовать.
Присвоение функции переменной
С существующей функцией func
синтаксис максимально простой:
variable = func
Переменным также можно присваивать встроенные функции. Таким образом позже есть возможность вызывать функцию другим именем. Такой подход называется непрямым вызовом функции.
Менять название переменной также разрешается:
def func(x): return x
a1 = func
a1(10)
10
a2 = a1
a2()
10
В этом примере a1, a2 и func
имеют один и тот же id. Они ссылаются на один объект.
Практический пример — рефакторинг существующего кода. Например, есть функция sq
, которая вычисляет квадрат значения:
def sq(x): return x*x
Позже ее можно переименовать, используя более осмысленное имя. Первый вариант — просто сменить имя. Проблема в том, что если в другом месте кода используется sq
, то этот участок не будет работать. Лучше просто добавить следующее выражение:
square = sq
Последний пример. Предположим, встроенная функция была переназначена:
dir = 3
Теперь к ней нельзя получить доступ, а это может стать проблемой. Чтобы вернуть ее обратно, нужно просто удалить переменную:
del dir
dir()
Анонимная функция: лямбда
Лямбда-функция — это короткая однострочная функция, которой даже не нужно имя давать. Такие выражения содержат лишь одну инструкцию, поэтому, например, if
, for
и while
использовать нельзя. Их также можно присваивать переменным:
product = lambda x,y: x*y
В отличие от функций, здесь не используется ключевое слово return
. Результат работы и так возвращается.
С помощью type()
можно проверить тип:
>>> type(product)
function
На практике эти функции редко используются. Это всего лишь элегантный способ записи, когда она содержит одну инструкцию.
power = lambda x=1, y=2: x**y
square = power
square(5.)
25
power = lambda x,y,pow=2: x**pow + y
[power(x,2, 3) for x in [0,1,2]]
[2, 3, 10]
Изменяемые аргументы по умолчанию
>>> def foo(x=[]):
... x.append(1)
... print x
...
>>> foo()
[1]
>>> foo()
[1, 1]
>>> foo()
[1, 1, 1]
Вместо этого нужно использовать значение «не указано» и заменить на изменяемый объект по умолчанию:
>>> def foo(x=None):
... if x is None:
... x = []
... x.append(1)
... print x
>>> foo()
[1]
>>> foo()
[1]
Порой обучение продвигается с трудом. Сложная теория, непонятные задания… Хочется бросить. Не сдавайтесь, все сложности можно преодолеть. Рассказываем, как
Не понятна формулировка, нашли опечатку?
Выделите текст, нажмите ctrl + enter и опишите проблему, затем отправьте нам. В течение нескольких дней мы улучшим формулировку или исправим опечатку
Что-то не получается в уроке?
Загляните в раздел «Обсуждение»:
- Изучите вопросы, которые задавали по уроку другие студенты — возможно, ответ на ваш уже есть
- Если вопросы остались, задайте свой. Расскажите, что непонятно или сложно, дайте ссылку на ваше решение. Обратите внимание — команда поддержки не отвечает на вопросы по коду, но поможет разобраться с заданием или выводом тестов
- Мы отвечаем на сообщения в течение 2-3 дней. К «Обсуждениям» могут подключаться и другие студенты. Возможно, получится решить вопрос быстрее!
Подробнее о том, как задавать вопросы по уроку
Using wn.synset('dog.n.1').lemma_names
is the correct way to access the synonyms of a sense. It’s because a word has many senses and it’s more appropriate to list synonyms of a particular meaning/sense. To enumerate words with similar meanings, possibly you can also look at the hyponyms.
Sadly, the size of Wordnet is very limited so there are very few lemma_names available for each senses.
Using Wordnet as a dictionary/thesarus is not very apt per se, because it was developed as an inventory of sense/meaning rather than a inventory of words. However you can use access the a particular sense and several (not a lot) related words to the sense. One can use Wordnet as a:
Dictionary: given a word, what are the different meaning of the word
for i,j in enumerate(wn.synsets('dog')):
print "Meaning",i, "NLTK ID:", j.name
print "Definition:",j.definition
Thesarus: given a word, what are the different words for each meaning of the word
for i,j in enumerate(wn.synsets('dog')):
print "Meaning",i, "NLTK ID:", j.name
print "Definition:",j.definition
print "Synonyms:", ", ".join(j.lemma_names)
print
Ontology: given a word, what are the hyponyms (i.e. sub-types) and hypernyms (i.e. super-types).
for i,j in enumerate(wn.synsets('dog')):
print "Meaning",i, "NLTK ID:", j.name
print "Hypernyms:", ", ".join(list(chain(*[l.lemma_names for l in j.hypernyms()])))
print "Hyponyms:", ", ".join(list(chain(*[l.lemma_names for l in j.hyponyms()])))
print
[Ontology Output]
Meaning 0 NLTK ID: dog.n.01
Hypernyms words domestic_animal, domesticated_animal, canine, canid
Hyponyms puppy, Great_Pyrenees, basenji, Newfoundland, Newfoundland_dog, lapdog, poodle, poodle_dog, Leonberg, toy_dog, toy, spitz, pooch, doggie, doggy, barker, bow-wow, cur, mongrel, mutt, Mexican_hairless, hunting_dog, working_dog, dalmatian, coach_dog, carriage_dog, pug, pug-dog, corgi, Welsh_corgi, griffon, Brussels_griffon, Belgian_griffon
Meaning 1 NLTK ID: frump.n.01
Hypernyms: unpleasant_woman, disagreeable_woman
Hyponyms:
Meaning 2 NLTK ID: dog.n.03
Hypernyms: chap, fellow, feller, fella, lad, gent, blighter, cuss, bloke
Hyponyms:
Meaning 3 NLTK ID: cad.n.01
Hypernyms: villain, scoundrel
Hyponyms: perisher
Привет, коллеги!
Я расскажу о библиотеке для Питона с лаконичным названием f
. Это небольшой пакет с функциями и классами для решения задач в функциональном стиле.
— Что, еще одна функциональная либа для Питона? Автор, ты в курсе, что есть fn.py и вообще этих функциональных поделок миллион?
— Да, в курсе.
Причины появления библиотеки
Я занимаюсь Питоном довольно давно, но пару лет назад всерьез увлекся функциональным программированием и Кложей в частности. Некоторые подходы, принятые в ФП, произвели на меня столь сильное впечатление, что мне захотелось перенести их в повседневную разработку.
Подчеркну, что не приемлю подход, когда паттерны одного языка грубо внедряют в другой без учета его принципов и соглашений о кодировании. Как бы я не любил ФП, меня раздражает нагромождение мап и лямбд в попытке выдать это за функциональный стиль.
Поэтому я старался оформить мои функции так, чтобы не встретить сопротивление коллег. Например, использовать внутри стандартные циклы с условиями вместо мапов и редьюсов, чтобы облегчить понимание тем, кто не знаком с ФП.
В результате некоторые из частей библиотеки побывали в боевых проектах и, возможно, все еще в строю. Сперва я копипастил их из проекта в проект, потом завел файлик-свалку функций и сниппетов, и, наконец, оформил все библиотекой, пакетом в Pypi и документаций.
Общие сведения
Библиотека написана на чистом Питоне и работает на любой ОС, в т.ч. на Виндузе. Поддерживаются обе ветки Питона. Конкретно я проверял на версиях 2.6, 2.7 и 3.5. Если возникнут трудности с другими версиями, дайте знать. Единственная зависимость — пакет six
для гибкой разработки сразу под обе ветки.
Библиотека ставится стандартным образом через pip:
pip install f
Все функции и классы доступны в головном модуле. Это значит, не нужно запоминать
пути к сущностям:
import f
f.pcall(...)
f.maybe(...)
f.io_wraps(...)
f.L[1, 2, 3]
Пакет несет на борту следующие подсистемы:
- набор различных функций для удобной работы с данными
- модуль предикатов для быстрой проверки на какие-либо условия
- улучшенные версии коллекций — списка, кортежа, словаря и множества
- реализация дженерика
- монады Maybe, Either, IO, Error
В разделах ниже я приведу примеры кода с комментариями.
Функции
Первой функцией, которую я перенес в Питон из другой экосистемы, стала pcall
из языка Луа. Я программировал на ней несколько лет назад, и хотя язык не функциональный, был от него в восторге.
Функция pcall (protected call, защищенный вызов) принимает другую функцию и возвращает пару (err, result)
, где либо err
— ошибка и result
пуст, либо наоборот. Этот подход знаком нам по другим языкам, например, Джаваскрипту или Гоу.
import f
f.pcall(lambda a, b: a / b, 4, 2)
>>> (None, 2)
f.pcall(lambda a, b: a / b, 4, 0)
>>> (ZeroDivisionError('integer division or modulo by zero'), None)
Функцию удобно использовать как декоратор к уже написанным функциям, которые кидают исключения:
@f.pcall_wraps
def func(a, b):
return a / b
func(4, 2)
>>> (None, 2)
func(4, 0)
>>> (ZeroDivisionError('integer division or modulo by zero'), None)
Используя деструктивный синтаксис, можно распаковать результат на уровне сигнатуры:
def process((err, result)):
if err:
logger.exception(err)
return 0
return result + 42
process(func(4, 2))
К большому сожалению, деструктивный синтаксис выпилен в третьем Питоне. Приходится распаковывать вручную.
Интерсно, что использование пары (err, result)
есть ни что иное, как монада Either
, о которой мы еще поговорим.
Вот более реалистичный пример pcall
. Часто приходится делать ХТТП-запросы и получать структуры данных из джейсона. Во время запроса может произойти масса ошибок:
- кривые хосты, ошибка резолва
- таймаут соединения
- сервер вернул 500
- сервер вернул 200, но парсинг джейсона упал
- сервер вернул 200, но в ответе ошибка
Заворачивать вызов в try с отловом четырех исключений означает сделать код абсолютно нечитаемым. Рано или поздно вы забудете что-то перехватить, и программа упадет. Вот пример почти реального кода. Он извлекает пользователя из локального рест-сервиса. Результат всегда будет парой:
@f.pcall_wraps
def get_user(use_id):
resp = requests.get("http://local.auth.server",
params={"id": user_id}, timeout=3)
if not resp.ok:
raise IOError("<log HTTP code and body here>")
data = resp.json()
if "error" in data:
raise BusinesException("<log here data>")
return data
Рассмотрим другие функции библиотеки. Мне бы хотелось выделить f.achain
и f.ichain
. Обе предназначены для безопасного извлечения данных из объектов по цепочке.
Предположим, у вас Джанго со следующими моделями:
Order => Office => Department => Chief
При этом все поля not null
и вы без страха ходите по смежным полям:
order = Order.objects.get(id=42)
boss_name = order.office.department.chief.name
Да, я в курсе про select_related
, но это роли не играет. Ситуация справедлива не только для ОРМ, но и для любой другой структуры класов.
Так было в нашем проекте, пока один заказчик не попросил сделать некоторые ссылки пустыми, потому что таковы особенности его бизнеса. Мы сделали поля в базе nullable
и были рады, что легко отделались. Конечно, из-за спешки мы не написали юнит-тесты для моделей с пустыми ссылками, а в старых тестах модели были заполнены правильно. Клиент начал работать с обновленными моделями и получил ошибки.
Функция f.achain
безопасно проходит по цепочке атрибутов:
f.achain(model, 'office', 'department', 'chief', 'name')
>>> John
Если цепочка нарушена (поле равно None, не существуте), результат будет None.
Функция-аналог f.ichain
пробегает по цепочке индексов. Она работает со словарями, списками и кортежами. Функция удобна для работы с данными, полученными из джейсона:
data = json.loads('''{"result": [{"kids": [{"age": 7, "name": "Leo"},
{"age": 1, "name": "Ann"}], "name": "Ivan"},
{"kids": null, "name": "Juan"}]}''')
f.ichain(data, 'result', 0, 'kids', 0, 'age')
>>> 7
f.ichain(data, 'result', 0, 'kids', 42, 'dunno')
>> None
Обе функции я забрал из Кложи, где их предок называется get-in
. Удобство в том, что в микросерверной архитектуре структура ответа постоянно меняется и может не соответствовать здравому смыслу.
Например, в ответе есть поле-объект «user» с вложенными полями. Однако, если пользователя по какой-то причине нет, поле будет не пустым объектом, а None. В коде начнут возникать уродливые конструкции типа:
data.get('user', {]}).get('address', {}).get('street', '<unknown>')
Наш вариант читается легче:
f.ichain(data, 'user', 'address', 'street') or '<unknown>'
Из Кложи в библиотеку f
перешли два threading-макроса: ->
и ->>
. В библиотеке они называются f.arr1
и f.arr2
. Оба пропускают исходное значение сквозь функиональные формы. Этот термин в Лиспе означает выражение, которе вычисляется позже.
Другими словами, форма — это либо функция func
, либо кортеж вида (func, arg1, arg2, ...)
. Такую форму можно передать куда-то как замороженное выражение и вычислить позже с изменениями. Получается что-то вроде макросов в Лиспе, только очень убого.
f.arr1
подставляет значение (и дальнейший результат) в качестве первого
аргумента формы:
f.arr1(
-42, # начальное значение
(lambda a, b: a + b, 2), # форма
abs, # форма
str, # форма
)
>>> "40"
f.arr2
делает то же самое, но ставит значение в конец формы:
f.arr2(
-2,
abs,
(lambda a, b: a + b, 2),
str,
("000".replace, "0")
)
>>> "444"
Далее, функция f.comp
возвращает композицию функций:
comp = f.comp(abs, (lambda x: x * 2), str)
comp(-42)
>>> "84"
f.every_pred
строит супер-предикат. Это такой предикат, который истиннен только если все внутренние предикаты истинны.
pred1 = f.p_gt(0) # строго положительный
pred2 = f.p_even # четный
pred3 = f.p_not_eq(666) # не равный 666
every = f.every_pred(pred1, pred2, pred3)
result = filter(every, (-1, 1, -2, 2, 3, 4, 666, -3, 1, 2))
tuple(result)
>>> (2, 4, 2)
Супер-предикат ленив: он обрывает цепочку вычислений на первом же ложном значении. В примере выше использованы предикаты из модуля predicate.py
, о котором мы еще поговорим.
Функция f.transduce
— наивная попытка реализовать паттерн transducer (преобразователь) из Кложи. Короткими словами, transducer
— это комбинация функций map
и reduce
. Их суперпозиция дает преобразование по принципу «из чего угодно во что угодно без промежуточных данных»:
f.transduce(
(lambda x: x + 1),
(lambda res, item: res + str(item)),
(1, 2, 3),
""
)
>>> "234"
Модуль функций замыкет f.nth
и его синонимы: f.first
, f.second
и f.third
для безопасного обращения к элементам коллекций:
f.first((1, 2, 3))
>>> 1
f.second((1, 2, 3))
>>> 2
f.third((1, 2, 3))
>>> 3
f.nth(0, [1, 2, 3])
>>> 1
f.nth(9, [1, 2, 3])
>>> None
Предикаты
Предикат
— это выражение, возвращающие истину или ложь. Предикаты используют в математике, логике и функциональном программировании. Часто предикат передают в качестве переменной в функции высшего порядка.
Я добавил несколько наиболее нужных предикатов в библиотеку. Предикаты могут унарными (без параметров) и бинарными (или параметрическими), когда поведение предиката зависит от первого аргумента.
Рассмотрим примеры с унарными предикатами:
f.p_str("test")
>>> True
f.p_str(0)
>>> False
f.p_str(u"test")
>>> True
# особый предикат, который проверяет на int и float одновременно
f.p_num(1), f.p_num(1.0)
>>> True, True
f.p_list([])
>>> True
f.p_truth(1)
>>> True
f.p_truth(None)
>>> False
f.p_none(None)
>>> True
Теперь бинарные. Создадим новый предикат, который утверждает, что что-то больше нуля. Что именно? Пока неизвесто, это абстракция.
p = f.p_gt(0)
Теперь, имея предикат, проверим любое значение:
p(1), p(100), p(0), p(-1)
>>> True, True, False, False
По аналогии:
# Что-то больше или равно нуля:
p = f.p_gte(0)
p(0), p(1), p(-1)
>>> True, True, False
# Проверка на точное равенство:
p = f.p_eq(42)
p(42), p(False)
>>> True, False
# Проверка на ссылочное равенство:
ob1 = object()
p = f.p_is(ob1)
p(object())
>>> False
p(ob1)
>>> True
# Проверка на вхождение в известную коллекцию:
p = f.p_in((1, 2, 3))
p(1), p(3)
>>> True, True
p(4)
>>> False
Я не буду приводить примеры всех предикатов, это утомительно и долго. Предикаты прекрасно работают с функциями композиции f.comp
, супер-предиката f.every_pred
, встроенной функцией filter
и дженериком, о котором речь ниже.
Дженерики
Дженерик (общий, обобщенный) — вызываемый объект, который имеет несколько стратегий вычисления результата. Выбор стратегии определяется на основании входящий параметров: их состава, типа или значения. Дженерик допускает наличие стратегии по умолчанию, когда не найдено ни одной другой для переданных параметров.
В Питоне нет дженериков из коробки, и особо они не нужны. Питон достаточно гибок, чтобы построить свою систему подбора функции под входящие значения. И все же, мне настолько понравилась реализация дженериков в Коммон-Лиспе, что из спортивного интереса я решил сделать что-то подобное в своей библиотеке.
Выглядит это примерно так. Сначала создадим экземпляр дженерика:
gen = f.Generic()
Теперь расширим его конкретными обработчиками. Декоратор .extend
принимает набор предикатов для этого обработчика, по одному на аргумент.
@gen.extend(f.p_int, f.p_str)
def handler1(x, y):
return str(x) + y
@gen.extend(f.p_int, f.p_int)
def handler2(x, y):
return x + y
@gen.extend(f.p_str, f.p_str)
def handler3(x, y):
return x + y + x + y
@gen.extend(f.p_str)
def handler4(x):
return "-".join(reversed(x))
@gen.extend()
def handler5():
return 42
Логика под капотом проста: декоратор подшивает функцию во внутренний словарь вместе с назначенными ей предикатами. Теперь дженерик можно вызывать с произвольными аргументами. При вызове ищется функция с таким же количеством предикаторв. Если каждый предикат возвращает истину для соответствующего аргумента, считается, что стратегия найдена. Возвращается результат вызова найденной функции:
gen(1, "2")
>>> "12"
gen(1, 2)
>>> 3
gen("fiz", "baz")
>>> "fizbazfizbaz"
gen("hello")
>>> "o-l-l-e-h"
gen()
>>> 42
Что случится, если не подошла ни одна стратегия? Зависит от того, был ли задан обработчик по умолчанию. Такой обработчик должен быть готов встретить произвольное число аргументов:
gen(1, 2, 3, 4)
>>> TypeError exception goes here...
@gen.default
def default_handler(*args):
return "default"
gen(1, 2, 3, 4)
>>> "default"
После декорирования функция становится экземпляром дженерика. Интересный прием — вы можете перебрасывать исполнение одной стратегии в другую. Получаются функции с несколькими телами, почти как в Кложе, Эрланге или Хаскеле.
Обработчик ниже будет вызван, если передать None
. Однако, внутри он перенаправляет нас на другой обработчик с двумя интами, это handler2
. Который, в свою очередь, возвращает сумму аргументов:
@gen.extend(f.p_none)
def handler6(x):
return gen(1, 2)
gen(None)
>>> 3
Коллекции
Библиотека предоставляет «улучшенные» коллекции, основанные на списке, кортеже, словаре и множестве. Под улучшениями я имею в виду дополнительные методы и некоторые особенности в поведении каждой из коллекций.
Улучшенные коллекции создаются или из обычных вызовом класса, или особым синтаксисом с квадратными скобками:
f.L[1, 2, 3] # или f.List([1, 2, 3])
>>> List[1, 2, 3]
f.T[1, 2, 3] # или f.Tuple([1, 2, 3])
>>> Tuple(1, 2, 3)
f.S[1, 2, 3] # или f.Set((1, 2, 3))
>>> Set{1, 2, 3}
f.D[1: 2, 2: 3]
>>> Dict{1: 2, 2: 3} # или f.Dict({1: 2, 2: 3})
Коллекции имеют методы .join
, .foreach
, .map
, .filter
, .reduce
, .sum
.
Список и кортеж дополнительно реализуют .reversed
, .sorted
, .group
, .distinct
и .apply
.
Методы позволяют получить результат вызовом его из коллекции без передачи в функцию:
l1 = f.L[1, 2, 3]
l1.map(str).join("-")
>>> "1-2-3"
result = []
def collect(x, delta=0):
result.append(x + delta)
l1.foreach(collect, delta=1)
result == [2, 3, 4]
>>> True
l1.group(2)
>>> List[List[1, 2], List[3]]
Не буду утомлять листингом на каждый метод, желающие могут посмотреть исходный код с комментариями.
Важно, что методы возвращают новый экземпляр той же коллекции. Это уменьшает вероятность ее случайного измнения. Операция .map
или любая другая на списке вернет список, на кортеже — кортеж и так далее:
f.L[1, 2, 3].filter(f.p_even)
>>> List[2]
f.S[1, 2, 3].filter(f.p_even)
>>> Set{2}
Словарь итерируется по парам (ключ, значение)
, о чем я всегда мечтал:
f.D[1: 1, 2: 2, 0: 2].filter(lambda (k, v): k + v == 2)
>>> Dict{0: 2, 1: 1}
Улучшенные коллекции можно складывать с любой другой коллекцией. Результатом станет новая коллекция этого (левого) типа:
# Слияние словарей
f.D(a=1, b=2, c=3) + {"d": 4, "e": 5, "f": 5}
>>> Dict{'a': 1, 'c': 3, 'b': 2, 'e': 5, 'd': 4, 'f': 5}
# Множество + стандартный спосок
f.S[1, 2, 3] + ["a", 1, "b", 3, "c"]
>>> Set{'a', 1, 2, 3, 'c', 'b'}
# Список и обычный кортеж
f.L[1, 2, 3] + (4, )
List[1, 2, 3, 4]
Любую коллекцию можно переключить в другую:
f.L["a", 1, "b", 2].group(2).D()
>>> Dict{"a": 1, "b": 2}
f.L[1, 2, 3, 3, 2, 1].S().T()
>>> Tuple[1, 2, 3]
Комбо!
f.L("abc").map(ord).map(str).reversed().join("-")
>>> "99-98-97"
def pred(pair):
k, v = pair
return k == "1" and v == "2"
f.L[4, 3, 2, 1].map(str).reversed()
.group(2).Dict().filter(pred)
>>> Dict{"1": "2"}
Монады
Последний и самый сложный раздел в библиотеке. Почитав цикл статей о монадах, я отважился добавить в библиотеку их тоже. При этом позволил себе следующие отклонения:
-
Проверки входных значений основаны не на типах, как в Хаскеле, а на предикатах, что делает монады гибче.
-
Оператор
>>=
в Хаскеле невозможно перенести в Питон, поэтому он фигурирует как>>
(он же__rshift__
, битовый сдвиг вправо). Проблема в том, что в Хаскеле тоже есть оператор>>
, но используется он реже, чем>>=
. В итоге, в Питоне под>>
мы понимаем>>=
из Хаскела, а оригинальный>>
просто не используем. - Не смотря на усилия, я не смог реализовать do-нотацию Хаскелла из-за ограничений синтаксиса в Питоне. Пробовал и цикл, и генератор, и контекстные менеджеры — все мимо.
Maybe
Монада Maybe (возможно) так же известна как Option. Этот класс монад представлен двумя экземплярами: Just (или Some) — хранилище положительного результата, в которм мы заинтересованы. Nothing (в других языках — None) — пустой результат.
Простой пример. Определим монадный конструктор — объект, который будет преобразовывать скалярные (плоские) значения в монадические:
MaybeInt = f.maybe(f.p_int)
По-другому это называется unit, или монадная единица. Теперь получим монадные значения:
MaybeInt(2)
>>> Just[2]
MaybeInt("not an int")
>>> Nothing
Видим, что хорошим результатом будет только то, что проходит проверку на инт. Теперь попробуем в деле монадный конвеер (monadic pipeline):
MaybeInt(2) >> (lambda x: MaybeInt(x + 2))
>>> Just[4]
MaybeInt(2) >> (lambda x: f.Nothing()) >> (lambda x: MaybeInt(x + 2))
>>> Nothing
Из примера видно, что Nothing
прерывает исполнения цепочки. Если быть совсем точным, цепочка не обрывается, а проходит до конца, только на каждом шаге возвращается Nothing
.
Любую функцию можно накрыть монадным декоратором, чтобы получать из нее монадические представления скаляров. В примере ниже декоратор следит за тем, чтобы успехом считался только возрат инта — это значение пойдет в Just
, все остальное — в Nothing
:
@f.maybe_wraps(f.p_num)
def mdiv(a, b):
if b:
return a / b
else:
return None
mdiv(4, 2)
>>> Just[2]
mdiv(4, 0)
>>> Nothing
Оператор >>
по другому называется монадным связыванием или конвеером (monadic binding) и вызывается методом .bind
:
MaybeInt(2).bind(lambda x: MaybeInt(x + 1))
>>> Just[3]
Оба способа >>
и .bind
могут принять не только функцию, но и функциональную форму, о которой я уже писал выше:
MaybeInt(6) >> (mdiv, 2)
>>> Just[3]
MaybeInt(6).bind(mdiv, 2)
>>> Just[3]
Чтобы высвободить скалярное значение из монады, используйте метод .get
. Важно помнить, что он не входит в классическое определение монад и является своего рода поблажкой. Метод .get
должен быть строго на конце конвеера:
m = MaybeInt(2) >> (lambda x: MaybeInt(x + 2))
m.get()
>>> 3
Either
Эта монада расширяет предыдущую. Проблема Maybe в том, что негативный результат отбрасывается, в то время как мы всегда хотим знать причину. Either состоит из подтипов Left и Right, левое и правое значения. Левое значение отвечает за негативный случай, а правое — за позитивный.
Правило легко запомнить по фразе «наше дело правое (то есть верное)». Слово right в английском языке так же значит «верный».
А вот и флешбек из прошлого: согласитесь, напоминает пару (err, result)
из начала статьи? Коллбеки в Джаваскрипте? Результаты вызовов в Гоу (только в другом порядке)?
То-то же. Все это монады, только не оформленные в контейнеры и без математического аппарата.
Монада Either
используется в основном для отлова ошибок. Ошибочное значение уходит влево и становится результатом конвеера. Корректный результат пробрысывается вправо к следующим вычислениям.
Монадический конструктор Either
принимает два предиката: для левого значения и для правого. В примере ниже строковые значения пойдут в левое значение, числовые — в правое.
EitherStrNum = f.either(f.p_str, f.p_num)
EitherStrNum("error")
>>> Left[error]
EitherStrNum(42)
>>> Right[42]
Проверим конвеер:
EitherStrNum(1) >> (lambda x: EitherStrNum(x + 1))
>>> Right[2]
EitherStrNum(1) >> (lambda x: EitherStrNum("error"))
>> (lambda x: EitherStrNum(x + 1))
>>> Left[error]
Декоратор f.either_wraps
делает из функции монадный конструктор:
@f.either_wraps(f.p_str, f.p_num)
def ediv(a, b):
if b == 0:
return "Div by zero: %s / %s" % (a, b)
else:
return a / b
@f.either_wraps(f.p_str, f.p_num)
def esqrt(a):
if a < 0:
return "Negative number: %s" % a
else:
return math.sqrt(a)
EitherStrNum(16) >> (ediv, 4) >> esqrt
>>> Right[2.0]
EitherStrNum(16) >> (ediv, 0) >> esqrt
>>> Left[Div by zero: 16 / 0]
IO
Монада IO (ввод-вывод) изолирует ввод-вывод данных, например, чтение файла, ввод с клавиатуры, печать на экран. Например, нам нужно спросить имя пользователя. Без монады мы бы просто вызвали raw_input
, однако это снижает абстракцию и засоряет код побочным эффектом.
Вот как можно изолировать ввод с клавиатуры:
IoPrompt = f.io(lambda prompt: raw_input(prompt))
IoPrompt("Your name: ") # Спросит имя. Я ввел "Ivan" и нажал RET
>>> IO[Ivan]
Поскольку мы получили монаду, ее можно пробросить дальше по конвееру. В примере ниже мы введем имя, а затем выведем его на экран. Декоратор f.io_wraps
превращает функцию в монадический конструктор:
import sys
@f.io_wraps
def input(msg):
return raw_input(msg)
@f.io_wraps
def write(text, chan):
chan.write(text)
input("name: ") >> (write, sys.stdout)
>>> name: Ivan # ввод имени
>>> Ivan # печать имени
>>> IO[None] # результат
Error
Монада Error, она же Try (Ошибка, Попытка) крайне полезна с практической точки зрения. Она изолирует исключения, гарантируя, что результатом вычисления станет либо экземпляр Success
с правильным значением внутри, либо Failture
с зашитым исключением.
Как и в случае с Maybe и Either, монадный конвеер исполняется только для положительного результата.
Монадический конструктор принимает функцию, поведение которой считается небезопасным. Дальнейшие вызовы дают либо Success
, либо Failture
:
Error = f.error(lambda a, b: a / b)
Error(4, 2)
>>> Success[2]
Error(4, 0)
>>> Failture[integer division or modulo by zero]
Вызов метода .get
у экземпляра Failture
повторно вызовет исключение. Как же до него добраться? Поможет метод .recover
:
Error(4, 0).get()
ZeroDivisionError: integer division or modulo by zero
# value variant
Error(4, 0).recover(ZeroDivisionError, 42)
Success[2]
Этот метод принимает класс исключения (или кортеж классов), а так же новое значение. Результатом становится монада Success
с переданным значением внутри. Значение может быть и функцией. Тогда в нее передается экземпляр исключения, а результат тоже уходит в Success
. В этом месте появляется шанс залогировать исключение:
def handler(e):
logger.exception(e)
return 0
Error(4, 0).recover((ZeroDivisionError, TypeError), handler)
>>> Success[0]
Вариант с декоратором. Функции деления и извлечения корня небезопасны:
@f.error_wraps
def tdiv(a, b):
return a / b
@f.error_wraps
def tsqrt(a):
return math.sqrt(a)
tdiv(16, 4) >> tsqrt
>>> Success[2.0]
tsqrt(16).bind(tdiv, 2)
>>> Success[2.0]
Конвеер с расширенным контекстом
Хорошо, когда функции из конвеера требуют данные только из предыдущей монады. А что делать, если нужно значение, полученное два шага назад? Где хранить контекст?
В Хаскеле это проблему решает та самая do-нотация, которую не удалось повторить в Питоне. Придется воспользоваться вложенными функциями:
def mfunc1(a):
return f.Just(a)
def mfunc2(a):
return f.Just(a + 1)
def mfunc3(a, b):
return f.Just(a + b)
mfunc1(1) >> (lambda x: mfunc2(x) >> (lambda y: mfunc3(x, y)))
# 1 2 1 2
>>> Just[3]
В примере выше затруднения в том, что функции mfunc3
нужно сразу два значения, полученных из других монад. Сохранить контекст пересенных x
и y
удается благодаря замыканиям. После выхода из замыкания цепочку можно продолжить дальше.
Заключение
Итак, мы рассмотрели возможности библиотеки f
. Напомню, проект не ставит цель вытеснить другие пакеты с функциональным уклоном. Это всего лишь попытка обобщить разрозненную практику автора, желание попробовать себя в роли мейнтейнера проекта с открытым исходным кодом. А еще — привлечь интерес начинающих разработчиков к функциональному подходу.
Ссылка на Гитхаб. Документация и тесты — там же. Пакет в Pypi.
Я надеюсь, специалисты по ФП простят неточности в формулировках.
Буду рад замечаниям в комментариях. Спасибо за внимание.
Синонимы и антонимы доступны как часть wordnet, которая является лексической базой данных для английского языка. Он доступен как часть доступа корпорации NLTK. В wordnet синонимами являются слова, которые обозначают одну и ту же концепцию и являются взаимозаменяемыми во многих контекстах, так что они сгруппированы в неупорядоченные множества (synsets). Мы используем эти наборы для получения синонимов и антонимов, как показано в следующих программах.
from nltk.corpus import wordnet synonyms = [] for syn in wordnet.synsets("Soil"): for lm in syn.lemmas(): synonyms.append(lm.name()) print (set(synonyms))
Когда мы запускаем вышеуказанную программу, мы получаем следующий вывод:
set([grease', filth', dirt', begrime', soil', grime', land', bemire', dirty', grunge', stain', territory', colly', ground'])
Чтобы получить антонимы, мы просто используем функцию антонима.
from nltk.corpus import wordnet antonyms = [] for syn in wordnet.synsets("ahead"): for lm in syn.lemmas(): if lm.antonyms(): antonyms.append(lm.antonyms()[0].name()) print(set(antonyms))
Когда мы запускаем вышеуказанную программу, мы получаем следующий вывод: