Синоним функции python

Введение

Определение

Вот пример простой функции:

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

Что-то не получается в уроке?

Загляните в раздел «Обсуждение»:

  1. Изучите вопросы, которые задавали по уроку другие студенты — возможно, ответ на ваш уже есть
  2. Если вопросы остались, задайте свой. Расскажите, что непонятно или сложно, дайте ссылку на ваше решение. Обратите внимание — команда поддержки не отвечает на вопросы по коду, но поможет разобраться с заданием или выводом тестов
  3. Мы отвечаем на сообщения в течение 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))

Когда мы запускаем вышеуказанную программу, мы получаем следующий вывод:

Понравилась статья? Поделить с друзьями:
  • Синоним фундука
  • Синоним фундаментальный анализ
  • Синоним фундаментальные знания
  • Синоним фундамент здания
  • Синоним фудкорта