Разное

Функциональное программирование на python: Python/Функциональное программирование на python — Викиучебник

Содержание

Функциональное программирование в Python. Генераторы, как питонячий декларативный стиль / Хабр

  • Общее введение
  • ФП
    • Введение в ФП
    • Основные принципы ФП
    • Основные термины
    • Встроенное ФП поведение в Python
    • Библиотека Xoltar Toolkit
    • Библиотека returns
    • Литература
  • Генераторы
    • Введение в итераторы
    • Введение в генераторы
    • Генераторы vs итераторы
    • Генераторы как пайплайн
    • Концепт yield from
    • Маршрутизация данных на генераторах (мультиплексирование, броадкастинг)
    • Пример трейсинга генератора
    • Стандартные инструменты генераторы
    • Выводы
    • Литература
  • Итоги

Говоря о Python, обычно используется процедурный и ООП стиль программирования, однако это не значит, что другие стили невозможны. В презентации ниже мы рассмотрим ещё пару вариантов — Функциональное программирование и программирование с помощью генераторов. Последние, в том числе, привели к появлению сопрограмм, которые позднее помогли создать асинхронность в Python. Сопрограммы и асинхронность выходят за рамки текущего доклада, поэтому, если интересно, можете ознакомиться об этом самостоятельно. Лично я рекомендую книгу «Fluent Python», в которой разговор начинается от итераторов, плавно переходит в темы о генераторах, сопрограммах и асинхронности.

Введение в ФП

Говоря о ФП сразу следует подчеркнуть, что программирование через функции далеко не всегда ФП, чаще всего это всего лишь процедурный стиль программирования. Чтобы попробовать понять ФП — необходимо разобраться, что это такое, и помогут нам в этом теоретические знания.

Выделяют две крупные парадигмы программирования: императивная и декларативная.

Императивное программирование предполагает ответ на вопрос “Как?”. В рамках этой парадигмы вы задаете последовательность действий, которые нужно выполнить, для того чтобы получить результат. Результат выполнения сохраняется в ячейках памяти, к которым можно обратиться впоследствии.

Декларативное программирование предполагает ответ на вопрос “Что?”. Здесь вы описываете задачу, даете спецификацию, говорите, что вы хотите получить в результате выполнения программы, но не определяете, как этот ответ будет получен. Каждая из этих парадигм включает в себя более специфические модели.

В продуктовой разработке наибольшее распространение получили процедурное и объектно-ориентированное программирование из группы “императивное программирование” и функциональное программирование из группы “декларативное программирование”.

В рамках процедурного подхода к программированию основное внимание сосредоточено на декомпозиции – разбиении программы / задачи на отдельные блоки / подзадачи. Разработка ведётся пошагово, методом “сверху вниз”. Наиболее распространенным языком, который предполагает использование процедурного подхода к программирования является язык C, в нем, основными строительными блоками являются функции.

В рамках объектно-ориентированного (ООП) подхода программа представляется в виде совокупности объектов, каждый из которых является экземпляром определенного класса, классы образуют иерархию наследования. ООП базируется на следующих принципах: инкапсуляция, наследование, полиморфизм, абстракция. Примерами языков, которые позволяют вести разработку в этой парадигме являются C#, Java.

В рамках функционального программирования выполнение программы – процесс вычисления, который трактуется как вычисление значений функций в математическом понимании последних (в отличие от функций как подпрограмм в процедурном программировании). Языки, которые реализуют эту парадигму – Haskell, Lisp.

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

Основные принципы ФП

  • Функции являются объектами первого класса (First Class Object).

    Это означает, что с функциями вы можете работать, также как и с данными — передавать их в качестве аргументов другим функциям, присваивать переменным и т.п.
  • Использование рекурсии в качестве основной структуры контроля потока управления. В некоторых языках не существует иной конструкции цикла, кроме рекурсии.
  • Акцент на обработке списков (lists, отсюда название Lisp — LISt Processing). Списки с рекурсивным обходом подсписков часто используются в качестве замены циклов.
  • Используются функции высшего порядка (High Order Functions). Функции высшего порядка – функции, которые могут в качестве аргументов принимать другие функции.

    Функции высшего порядка принимают в качестве аргументов другие функции. В стандартную библиотеку Python входит достаточно много таких функций, в качестве примера приведем функцию map. Она принимает функцию и Iterable объект, применяет функцию к каждому элементу Iterable объекта и возвращает Iterator объект, который итеративно возвращает все модифицированные после функции элементы.

  • Функции являются “чистыми” (Pure Functions) – т.е. не имеют побочных эффектов (иногда говорят: не имеют сайд-эффектов).

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

  • Акцент на том, что должно быть вычислено, а не на том, как вычислять.

Основные термины

Не все термины ниже необходимы для понимания доклада, но необходимы для понимания ФП (спасибо одному другу за их подборку)

  • Ссылочная прозрачность.

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

  • Функции

    • Детерминированные.

      Детерминированная функция возвращает тот же результат для одних и тех же входных данных.

    • Чистые.

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

    • Тотальные.

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

      Тотальные функции всегда завершаются и никогда не вызывают исключений.

  • Композиция функций — применение одной функции к результату другой

  • Сайд эффект.

    Сайд эффект возникает, когда выполнение выражения делает что-то большее, чем вычисление значения. Сайд эффекты — взаимодействия, которые можно наблюдать за пределами функции или выражения. Распространённые сайд эффекты включают в себя доступ к базе данных, доступ к файлам, доступ к сети, системные вызовы, изменение изменяемой памяти или вызов функций, которые выполняют любое из вышеперечисленных действий.

  • Полиморфизм.

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

    • Параметрический полиморфизм.

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

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

  • Замыкание (closure)

    Замыкание — процедура вместе с привязанной к ней совокупностью данных. © Steve Majewski

    Замыкание — функция, которая ссылается на свободные переменные в своей области видимости.

Встроенное ФП поведение в Python

Базовые элементы ФП в Python — функции map(), reduce(), filter() и оператор lambda. В Python 1.x введена также функция apply(), удобная для прямого применения функции к списку, возвращаемому другой. Python 2.0 предоставляет для этого улучшенный синтаксис. Начиная с Python 2.3 считается устаревшей, удалена в Python 3.0

Несколько неожиданно, но этих функций и всего нескольких базовых операторов почти достаточно для написания любой программы на Python; в частности, все управляющие утверждения (if, elif, else, assert, try, except, finally, for, break, continue, while, def) можно представить в функциональном стиле, используя исключительно функции и операторы. Несмотря на то, что задача реального удаления всех команд управления потоком, возможно, полезна только для представления на конкурс «невразумительный Python» (с кодом, выглядящим как программа на Lisp’е), стоит уяснить, как ФП выражает управляющие структуры через вызовы функций и рекурсию.

В соответствии с вышесказанным, попробуем сделать несколько хаков, чтобы наш код был более ФПшный. Избавимся от if/elif/else в Python

# Normal statement-based flow control
if <cond1>: 
    func1() 
elif <cond2>: 
    func2() 
else: 
    func3() 

    # Equivalent "short circuit" expression
(<cond1> and func1()) or (<cond2> and func2()) or (func3()) 

Примечание. Как заметил skymorp, эквивалентность соблюдается лишь в том случае, если func1, func2 и (необязательно) func3 возвращают non falsy значения. Например, если в качестве func будут принты, то последнее выражение (func3) будет выполняться всегда.

Используем lambda для присваивания таких условных выражений

pr = lambda s:s 
namenum = lambda x: (x==1 and pr("one")) or (x==2 and pr("two")) or (pr("other"))
assert namenum(1) == 'one' 
assert namenum(2) == 'two' 
assert namenum(3) == 'other'

Замена циклов на выражения так же проста, как и замена условных блоков. for может быть переписана с помощью map().

for e in lst:  
    func(e)      # statement-based loop

map(func,lst)    # map-based loop

То же самое мы можем сделать и с функциями.

do_it = lambda f: f()

# let f1, f2, f3 (etc) be functions that perform actions

map(do_it, [f1,f2,f3])

Перевести while впрямую немного сложнее, но вполне получается.

# statement-based while loop
while <cond>: 
    <pre-suite> 
    if <break_condition>: 
        break
    else: 
        <suite> 

# FP-style recursive while loop
def while_block(): 
    <pre-suite> 
    if <break_condition>: 
        return 1 
    else: 
        <suite> 
        return 0 

while_FP = lambda: (<cond> and while_block()) or while_FP() 
while_FP()

ФП вариант while все еще требует функцию while_block(), которая сама по себе может содержать не только выражения, но и утверждения (statements). Но мы могли бы продолжить дальнейшее исключение утверждений в этой функции (как, например, замену блока if/else в вышеописанном шаблоне).

К тому же, обычная проверка на месте (наподобие while myvar == 7) вряд ли окажется полезной, поскольку тело цикла (в представленном виде) не может изменить какие-либо переменные (хотя глобальные переменные могут быть изменены в while_block()). Один из способов применить более полезное условие — заставить while_block() возвращать более осмысленное значение и сравнивать его с условием завершения.

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

# imperative version of "echo()"
def echo_IMP():
    while 1: 
        x = input("IMP -- ") 
        if x == 'quit': 
            break
        else:
            print(x) 

echo_IMP() 

# utility function for "identity with side-effect"
def monadic_print(x):
    print(x) 
    return x 
    # FP version of "echo()" 

echo_FP = lambda: monadic_print(input("FP -- ")) == 'quit' or echo_FP() 
echo_FP()

Примечание. Как заметил skymorp, в случае с императивным стилем, print не вызывается, если input("IMP -- ") == 'quit'. В примере с функциональным программированием print вызывается всегда.

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

Мы все еще используем служебную функцию monadic_print(), но эта функция совершенно общая и может использоваться в любых функциональных выражениях, которые мы создадим позже. Заметим, что любое выражение, содержащее monadic_print(x) вычисляется так же, как если бы оно содержало просто x.

После всей проделанной работы по избавлению от совершенно осмысленных конструкций и замене их на невразумительные вложенные выражения, возникает естественный вопрос — «Зачем?!». Перечитывая описания характеристик ФП, мы можем видеть, что все они достигнуты в Python. Но важнейшая (и, скорее всего, в наибольшей степени реально используемая) характеристика — исключение побочных эффектов или, по крайней мере, ограничение их применения специальными областями наподобие монад. Огромный процент программных ошибок и главная проблема, требующая применения отладчиков, случается из-за того, что переменные получают неверные значения в процессе выполнения программы.

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

# Nested loop procedural style for finding big products 
xs = (1,2,3,4) 
ys = (10,15,3,22) 
bigmuls = [] 
# ...more stuff...
for x in xs: 
    for y in ys: 
        # ...more stuff...
        if x*y > 25: 
            bigmuls.append((x,y)) 
        # ...more stuff...
    # ...more stuff...
    print(bigmuls)

Секции, комментированные как #...more stuff... — места, где побочные эффекты с наибольшей вероятностью могут привести к ошибкам.

В любой из этих точек переменные xs, ys, bigmuls, x, y могут приобрести неожиданные значения в гипотетическом коде. Далее, после завершения этого куска кода все переменные могут иметь значения, которые могут ожидаться, а могут и не ожидаться посдедующим кодом.

Очевидно, что инкапсуляция в функциях/объектах и тщательное управление областью видимости могут использоваться, чтобы защититься от этого рода проблем. Вы также можете всегда удалять (del) ваши переменные после использования.

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

bigmuls = lambda xs,ys: filter(lambda (x,y):x*y > 25, combine(xs,ys))
combine = lambda xs,ys: map(None, xs*len(ys), dupelms(ys,len(xs)))
dupelms = lambda lst,n: reduce(lambda s,t:s+t, map(lambda l,n=n: [l]*n, lst))
print(bigmuls((1,2,3,4),(10,15,3,22)))

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

print([(x,y) for x in (1,2,3,4) for y in (10,15,3,22) if x*y > 25])

Да, всё верно, list, tuple, set, dict comprehensions и generator expressions — изначально ФП техники, которые, как и многое другое, перекочевало в другие не-ФП языки

Сразу оговоримся, что библиотека достаточно старая и подходит лишь для Python 2, однако для ознакомления её достаточно. Библиотека Xoltar Toolkit Брина Келлера (Bryn Keller) покажет нам больше возможностей ФП.

Основные возможности ФП Келлер представил в виде небольшого эффективного модуля на чистом Python. Помимо модуля functional, в Xoltar Toolkit входит модуль lazy, поддерживающий структуры, вычисляемые «только когда это необходимо». Множество функциональных языков программирования поддерживают отложенное вычисление, поэтому эти компоненты Xoltar Toolkit предоставят вам многое из того, что вы можете найти в функциональном языке наподобие Haskell.

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

>>> car = lambda lst: lst[0] 
>>> cdr = lambda lst: lst[1:] 
>>> sum2 = lambda lst: car(lst)+car(cdr(lst)) 
>>> sum2(range(10))
1 
>>> car = lambda lst: lst[2] 
>>> sum2(range(10))
5

К несчастью, одно и то же выражение sum2(range(10)) вычисляется к разным результатам в двух местах программы, несмотря на то, что аргументы выражении не являются изменяемыми переменными.

К счастью, модуль functional предоставляет класс Bindings, предотвращающий такое переприсваивание.

>>> from functional import * 
>>> let = Bindings() 
>>> let.car = lambda lst: lst[0] 
>>> let.car = lambda lst: lst[2] 
Traceback (innermost last): 
    File "<stdin>", 
        line 1, in ? File "d:\tools\functional.py", 
        line 976, in __setattr__ raise BindingError, "Binding '%s' cannot be modified." % name 
        functional.BindingError: Binding 'car' cannot be modified. >>> car(range(10)) 0

Разумеется, реальная программа должна перехватить и обработать исключение BindingError, однако сам факт его возбуждения позволяет избежать целого класса проблем.

Библиотека returns

Уже более современная библиотека, предлагаемая Никитой Соболевым, нашим соотечественником, также позволяет использовать возможности ФП в Python. Декоратор maybe позволяет переходить к следующей итерации только при успешном завершении предыдущей

from returns.maybe import Maybe, maybe
@maybe  # decorator to convert existing Optional[int] to Maybe[int]
def bad_function() -> Optional[int]:
    ...
    maybe_number: Maybe[float] = bad_function().map(
    lambda number: number / 2,
    )
# => Maybe will return Some[float] only if there's a non-None value
#    Otherwise, will return Nothing

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

# Imperative style
user: Optional[User]
discount_program: Optional['DiscountProgram'] = None
if user is not None:
     balance = user.get_balance()
     if balance is not None:
         credit = balance.credit_amount()
         if credit is not None and credit > 0:
            discount_program = choose_discount(credit)

# same with returns

user: Optional[User]
# Type hint here is optional, it only helps the reader here:
discount_program: Maybe['DiscountProgram'] = Maybe.from_value(
    user,
    ).map(  # This won't be called if `user is None`
    lambda real_user: real_user.get_balance(),
    ).map(  # This won't be called if `real_user.get_balance()` returns None
    lambda balance: balance.credit_amount(),
    ).map(  # And so on!
    lambda credit: choose_discount(credit) if credit > 0 else None,
    )

Или, например, позволяет обходить возможные подводные камни напрямую через пайплайн

# Imperative style
def fetch_user_profile(user_id: int) -> 'UserProfile':
    """Fetches UserProfile dict from foreign API."""
    response = requests.get('/api/users/{0}'.format(user_id))
    # What if we try to find user that does not exist?
    # Or network will go down? Or the server will return 500?
    # In this case the next line will fail with an exception.
    # We need to handle all possible errors in this function
    # and do not return corrupt data to consumers.
    response.raise_for_status()
    # What if we have received invalid JSON?
    # Next line will raise an exception!
    return response.json()

И то же самое, только с помощью returns

import requests
from returns.result import Result, safe
from returns.pipeline import flow
from returns.pointfree import bind
def fetch_user_profile(user_id: int) -> Result['UserProfile', Exception]:
    """Fetches `UserProfile` TypedDict from foreign API."""
    return flow(
        user_id,
        _make_request,
        bind(_parse_json),
    )

@safe
def _make_request(user_id: int) -> requests.Response:
    response = requests.get('/api/users/{0}'.format(user_id))
    response.raise_for_status()
    return response

@safe
def _parse_json(response: requests.Response) -> 'UserProfile':
    return response.json()

Теперь у нас есть чистый, безопасный и декларативный способ выразить потребности нашего бизнеса: Мы начинаем с запроса, который может потерпеть неудачу в любой момент, затем анализирем ответ, если запрос был успешным, а потом возвращаем результат.

Вместо обычных значений возвращаются значения, заключенные в специальный контейнер, благодаря декоратору @safe. Он вернет Success [YourType] или Failure [Exception]. И никогда не бросит нам исключение!

Подробнее об этой библиотеке можно уточнить из её достаточно большой и подробной документации, указанной в литературе.

Прим. Я, как автор этой статьи, не использовал returns в проде и не думаю, что буду когда-либо использовать, но не упомянуть об этой библиотеке в рамках данной статьи просто нельзя.

Литература

Введение в итераторы

Итерация — по сути является перебором значений. Вот обычный пример, который встречается повсюду.

>>> for x in [1,4,5,10]:
... print(x, end=' ')
...
1 4 5 10

Также, как мы знаем, итерация может происходить по многим типам объектов (не только спискам). Причина, почему мы можем итерироваться по объектам — реализация специального протокола

>>> items = [1, 4, 5]
>>> it = iter(items)
>>> it.__next__()
1
>>> it.__next__()
4
>>> it.__next__()
5
>>> it.__next__()

Внутри под капотом обычной итерации.

for x in obj:
    # statements

Происходит примерно следующее:

_iter = iter(obj) # Get iterator object
while 1:
    try:
        x = _iter.__next__() # Get next item
    except StopIteration: # No more items
        break
    # statements

Фактически, любой объект, который поддерживает конструкцию iter() называется итерируемым. Чтобы добавить его поддержку в своём классе, нам необходимо имплементировать методы __iter__() и __next__().

Например, посмотрим, как имплементировать такое:

>>> for x in Countdown(10):
... print(x, end=' ')
...
10 9 8 7 6 5 4 3 2 1

Его реализация будет такова:

class Countdown(object):
    def __init__(self,start):
        self.start = start
    def __iter__(self):
        return CountdownIter(self.start)

class CountdownIter(object):
    def __init__(self, count):
        self.count = count
    def __next__(self):
        if self.count <= 0:
            raise StopIteration
        r = self.count
        self.count -= 1
        return r

Введение в генераторы

Генератор — функция, которая генерирует последовательность результатов вместо одного значения

def countdown(n):
    while n > 0:
        yield n
        n -= 1

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

def countdown(n):
    print("Counting down from", n)
    while n > 0:
        yield n
        n -= 1
>>> x = countdown(10)
>>> x
<generator object at 0x58490>
>>>

Функция генератор выполняется только при вызове __next__().

>>> x = countdown(10)
>>> x
<generator object at 0x58490>
>>> x.__next__()
Counting down from 10
10
>>>

yield возвращает значение, но приостанавливает выполнение функции. Функция генератор возобновляется при следующем вызове __next__(). При завершении итератора возбуждается исключение StopIteration.

>>> x.__next__()
9
>>> x.__next__()
8
>>>
...
>>> x.__next__()
1
>>> x.__next__()
Traceback (most recent call last):
    File "<stdin>", line 1, in ?
        StopIteration
>>>

Небольшие выводы:

  • Функция генератор — более удобный способ написания итератора
  • Вам не нужно беспокоиться о реализации протокола итератора (__next__, __iter__ и т. д.), т.к. при создании функции с yield магия Python уже добавляет в объект функции нужные методы.
>>> def x():
...     return 1
... 
>>> def y():
...     yield 1
... 
>>> [i for i in dir(y()) if i not in dir(x())]
['__del__', '__iter__', '__name__', '__next__', '__qualname__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']

Помимо функции генератора возможно также использовать генераторное выражение, которое также возвращает generator object.

>>> a = [1,2,3,4]
>>> b = (2*x for x in a)
>>> b
<generator object at 0x58760>
>>> for i in b: print(b, end=' ')
...
2 4 6 8

Синтаксис генераторного выражения также прост

(expression for i in s if condition)
# the same with
for i in s:
    if condition:
        yield expression

Генераторы vs итераторы

Функция генератор немного отличается от объекта, поддерживающего итерацию. Генератор — разовая операция. Мы можем перебирать сгенерированные данные один раз, но если мы хотим сделать это снова, мы должны снова вызвать функцию генератор. Это отличается, например, от списка (который мы можем перебирать столько раз, сколько хотим)

Генераторы как пайплайн

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

Узнайте, сколько байтов данных было передано, суммируя последний столбец данных в журнале веб-сервера Apache. И да, размер лог файла может измеряться в гигабайтах

Каждая строка в логах выглядит примерно так:

81.107.39.38 - ... "GET /ply/ply.html HTTP/1.1" 200 97238

Число байтов находится в последней колонке:

bytes_sent = line.rsplit(None,1)[1]

Это может быть число или отсутствующее значение.

81.107.39.38 - ... "GET /ply/ HTTP/1.1" 304 -

Сконвертируем полученный результат в число

if bytes_sent != '-':
    bytes_sent = int(bytes_sent)

В итоге, может получиться примерно такой код

with open("access-log") as wwwlog:
    total = 0
    for line in wwwlog:
        bytes_sent = line.rsplit(None,1)[1]
        if bytes_sent != '-':
            total += int(bytes_sent)
    print("Total", total)

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

with open("access-log") as wwwlog:
    bytecolumn = (line.rsplit(None,1)[1] for line in wwwlog)
    bytes_sent = (int(x) for x in bytecolumn if x != '-')
    print("Total", sum(bytes_sent))

Этот подход отличается от предыдущего, меньше строк, напоминает функциональный стиль Мы получили пайплайн

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

Безусловно, этот генераторный подход имеет разновидности причудливой медленной магии. Файл из 1.3 ГБ код в старом стиле выполнил за 18.6 секунд, код на генераторах был выполнен за 16,7 секунд.

AWK с той же задачей справился гораздо медленее, за 70.5 секунд

awk '{ total += $NF } END { print total }' big-access-log

Небольшие выводы:

  • Это не только не медленно, но и на 10% быстрее, чем старый стиль
  • Меньше кода
  • Код относительно легко читается
  • И, честно говоря, мне он нравится в целом больше
  • Ни разу в нашем решении с генератором мы не создавали большие временные списки
  • Таким образом, это решение не только быстрее, но и может применяться к огромным файлам с данными
  • Подход конкурентоспособен с традиционными инструментами

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

Концепт yield from

‘yield from’ может использоваться для делегирования итерации

def countdown(n):
    while n > 0:
        yield n
        n -= 1

def countup(stop):
    n = 1
    while n < stop:
        yield n
        n += 1

def up_and_down(n):
    yield from countup(n)
    yield from countdown(n)

>>> for x in up_and_down(3):
... print(x)
...
1
2
3
2
1
>>>

Также стоит упомянуть, что в более ранних версиях python (3.5 и ниже) yield from также использовался вместо await, пока await не стал новым ключевым зарезервированными словом, т.к. await — по сути, просто передача контекста управления внутри другой корутины. Чтобы не возлагать много ответственности на yield from и было придумано отдельное зарезервированное слово — await.

Маршрутизация данных на генераторах (мультиплексирование, броадкастинг)

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

На данной диаграмме видно, что нам потребуется мультиплексирование (всё в одно) и броадкастинг (одно во всё). Что ж, не будем томить и напишем сразу решение.


# same with `tail -f`

def follow(thefile):
    thefile.seek(0, os.SEEK_END) # End-of-file
    while True:
        line = thefile.readline()
        if not line:
            time.sleep(0.1) # Sleep briefly
            continue
        yield line

def gen_cat(sources):
    # В данном конкретном случае используется для распаковки вложенного списка в плоский
    for src in sources:
        yield from src

def genfrom_queue(thequeue):
    while True:
        item = thequeue.get()
        if item is StopIteration:
            break
        yield item

def sendto_queue(source, thequeue):
    for item in source:
        thequeue.put(item)
    thequeue.put(StopIteration)

def multiplex(sources):
    in_q = queue.Queue()
    consumers = []
    for src in sources:
        thr = threading.Thread(target=sendto_queue, args=(src, in_q))
        thr.start()
        consumers.append(genfrom_queue(in_q))
    return gen_cat(consumers)

def broadcast(source, consumers):
    for item in source:
        for c in consumers:
            c.send(item)

class Consumer(object):
    def send(self,item):
        print(self, "got", item)

if __name__ == '__main__':
    c1 = Consumer()
    c2 = Consumer()
    c3 = Consumer()

    log1 = follow(open("foo/access-log"))
    log2 = follow(open("bar/access-log"))
    log3 = follow(open("baz/access-log"))

    lines = multiplex([log1, log2, log3])

    broadcast(lines,[c1,c2,c3])

Как мы видим из этого примера — ничего сложного в этом нет, вполне легко можно решать самые разнообразные задачи.

Пример трейсинга генератора

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

def trace(source):
    for item in source:
        print(item)
        yield item

lines = follow(open("access-log"))
log = trace(apache_log(lines))
r404 = trace(r for r in log if r['status'] == 404)

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

Стандартные инструменты генераторы

Генераторы встречаются повсюду в стандартной библиотеке. Начиная с 3.0 их стараются внедрять повсеместно. Например, pathlib.Path.rglob, glob.iglob, os.walk, range, map, filter. Есть даже целиком библиотека на генераторах — itertools.

Выводы

Плюсы:

  • Генераторы — невероятно полезный инструмент для решения многообразных проблем
  • Сила исходит от способности настраивать пайплайны
  • Можно создавать компоненты, которые можно переиспользовать в разных пайплайнах
  • Небольшие компоненты, которые просто обрабатывают поток данных
  • Намного проще, чем это может быть сделано с помощью ООП шаблонов
  • Можно расширить идею пайплайнов во многих направлениях (сеть, потоки, корутины)

Минусы

  • Использование этого стиля программирования у непосвященных может привести к взрыву головы
  • Обработка ошибок сложна, потому что у нас много компонентов, связанных вместе
  • Необходимо уделять особое внимание отладке, надежности и другим вопросам.

Литература

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

Как можно заметить, если мы пишем код на генераторах, то он получается вполне себе декларативным. Можно сказать, что код на генераторах — в некоторой степени ФП, так элегантно вошедший в Python.

Наша главная задача — писать ясный, понятный, красивый, тестируемый код и выбирать для этого подходящие инструменты. ФП — не самоцель, а лишь средство, как и всегда, чтобы мочь написать ещё лучший код!

Если нашли ошибки, пишите в телеграмме Niccolum или на почту [email protected] Буду рад конструктивной критике.

Функциональное программирование на языке Python

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

Python — свободно распространяемый, очень высокоуровневый интерпретируемый язык, разработанный Гвидо ван Россумом (Guido van Rossum). Он сочетает прозрачный синтаксис с мощной (но необязательной) объектно-ориентированной семантикой. Python доступен почти на всех существующих ныне платформах и обладает очень высокой переносимостью между платформами.

Лучше всего начать с труднейшего вопроса — а что, собственно, такое «функциональное программирование (FP)»? Один из возможных ответов — «это когда вы пишете на языке наподобие Lisp, Scheme, Haskell, ML, OCAML, Clean, Mercury или Erlang (или еще на некоторых других)». Этот ответ, безусловно, верен, но не сильно проясняет суть. К сожалению, получить четкое мнение о том, что же такое FP, оказывается очень трудно даже среди собственно функциональных программистов. Вспоминается притча о трех слепцах и слоне. Возможно также определить FP, противопоставив его «императивному программированию» (тому, что вы делаете на языках наподобие C, Pascal, C++, Java, Perl, Awk, TCL и на многих других — по крайнее мере, большей частью).

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

* Функции — объекты первого класса. Т.е., все, что можно делать с «данными», можно делать и с функциями (вроде передачи функции другой функции в качестве параметра).
* Использование рекурсии в качестве основной структуры контроля потока управления. В некоторых языках не существует иной конструкции цикла, кроме рекурсии.
* Акцент на обработке списков (lists, отсюда название Lisp — LISt Processing). Списки с рекурсивным обходом подсписков часто используются в качестве замены циклов.
* «Чистые» функциональные языки избегают побочных эффектов. Это исключает почти повсеместно распространенный в императивных языках подход, при котором одной и той же переменной последовательно присваиваются различные значения для отслеживания состояния программы.
* FP не одобряет или совершенно запрещает утверждения (statements), используя вместо этого вычисление выражений (т.е. функций с аргументами). В предельном случае, одна программа есть одно выражение (плюс дополнительные определения).
* FP акцентируется на том, что должно быть вычислено, а не как.
* Большая часть FP использует функции «высокого порядка» (функции, оперирующие функциями, оперирующими функциями).

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

Python поддерживает большую часть характеристик функционального программирования, начиная с версии Python 1.0. Но, как большинство возможностей Python, они присутствуют в очень смешанном языке. Так же как и с объектно-ориентированными возможностями Python, вы можете использовать то, что вам нужно, и игнорировать все остальное (пока оно вам не понадобится). В Python 2.0 было добавлено очень удачное «синтаксическое украшение» — списочные встраивания (list comprehensions). Хотя и не добавляя принципиально новых возможностей, списочные встраивания делают использование многих старых возможностей значительно приятнее.

Базовые элементы FP в Python — функции map(), reduce(), filter() и оператор lambda. В Python 1.x введена также функция apply(), удобная для прямого применения функции к списку, возвращаемому другой. Python 2.0 предоставляет для этого улучшенный синтаксис. Несколько неожиданно, но этих функций и всего нескольких базовых операторов почти достаточно для написания любой программы на Python; в частности, все управляющие утверждения (‘if’, ‘elif’, ‘else’, ‘assert’, ‘try’, ‘except’, ‘finally’, ‘for’, ‘break’, ‘continue’, ‘while’, ‘def’) можно представить в функциональном стиле, используя исключительно функции и операторы. Несмотря на то, что задача реального удаления всех команд управления потоком, возможно, полезна только для представления на конкурс «невразумительный Python» (с кодом, выглядящим как программа на Lisp’е), стоит уяснить, как FP выражает управляющие структуры через вызовы функций и рекурсию.

Первое, о чем стоит вспомнить в нашем упражнении — то, что Python «замыкает накоротко» вычисление логических выражений.1 Оказывается, это предоставляет эквивалент блока ‘if’/’elif’/’else’ в виде выражения. Итак:

#------ "Короткозамкнутые" условные вызовы в Python -----#
# Обычные управляющие конструкции 
if <cond1>: func1()
elif <cond2>: func2()
else: func3()

# Эквивалентное "накоротко замкнутое" выражение
(<cond1> and func1()) or (<cond2> and func2()) or (func3())

# Пример "накоротко замкнутого" выражения
>>> x = 3
>>> def pr(s): return s
>>> (x==1 and pr('one')) or (x==2 and pr('two')) or (pr('other'))
'other'
>>> x = 2
>>> (x==1 and pr('one')) or (x==2 and pr('two')) or (pr('other'))
'two'

Казалось бы, наша версия условных вызовов с помощью выражений — не более, чем салонный фокус; однако все становится гораздо интересней, если учесть, что оператор lambda может содержать только выражения! Раз, как мы только что показали, выражения могут содержать условные блоки, используя короткое замыкание, выражение lambda позволяет в общей форме представить условные возвращаемые значения. Базируясь на предыдущем примере:

#--------- Lambda с короткозамкнутыми условными выражениями в Python -------#
>>> pr = lambda s:s
>>> namenum = lambda x: (x==1 and pr("one")) 
... or (x==2 and pr("two")) 
... or (pr("other"))
>>> namenum(1)
'one'
>>> namenum(2)
'two'
>>> namenum(3)
'other'

Приведенные примеры уже засвидетельствовали, хотя и неочевидным образом, статус функций как объектов первого класса в Python. Дело в том, что, создав объект функции оператором lambda, мы произвели чрезвычайно общее действие. Мы имели возможность привязать наш объект к именам pr и namenum в точности тем же способом, как могли бы привязать к этим именам число 23 или строку «spam». Но точно так же, как число 23 можно использовать, не привязывая ни к какому имени (например, как аргумент функции), мы можем использовать объект функции, созданный lambda, не привязывая ни к какому имени. Функция в Python — всего лишь еще одно значение, с которым можно что-то сделать.

Главное, что мы делаем с нашими объектами первого класса — передаем их во встроенные функции map(), reduce() и filter(). Каждая из этих функций принимает объект функции в качестве первого аргумента. map() применяет переданную функцию к каждому элементу в переданном списке (списках) и возвращает список результатов. reduce() применяет переданную функцию к каждому значению в списке и ко внутреннему накопителю результата; например, reduce(lambda n,m:n*m, range(1,10)) означает 10! (факториал 10 — умножить каждый элемент на результат предыдущего умножения). filter() применяет переданную функцию к каждому элементу списка и возвращает список тех элементов исходного списка, для которых переданная функция вернула значение истинности. Мы также часто передаем функциональные объекты нашим собственным функциям, но чаще некоторым комбинациям вышеупомянутых встроенных функций.

Комбинируя три этих встроенных FP-функции, можно реализовать неожиданно широкий диапазон операций потока управления, не прибегая к утверждениям (statements), а используя лишь выражения.

Замена циклов на выражения так же проста, как и замена условных блоков. ‘for’ может быть впрямую переведено в map(). Так же, как и с условным выполнением, нам понадобится упростить блок утверждений до одного вызова функции (мы близки к тому, чтобы научиться делать это в общем случае):

#---------- Функциональный цикл 'for' в Python ----------#
for e in lst: func(e) # цикл на утверждении 'for'
map(func,lst) # цикл, основанный на map()

Кстати, похожая техника применяется для реализации последовательного выполнения программы, используя функциональный подход. Т.е., императивное программирование по большей части состоит из утверждений, требующих «сделать это, затем сделать то, затем сделать что-то еще». ‘map()’ позволяет это выразить так:

#----- Функциональное последовательное выполнение в Python ----------#
# создадим вспомогательную функцию вызова функции
do_it = lambda f: f()

# Пусть f1, f2, f3 (etc) - функции, выполняющие полезные действия
map(do_it, [f1,f2,f3]) # последовательное выполнение, реализованное на map()

В общем случае, вся главная программа может быть вызовом ‘map()’ со списком функций, которые надо последовательно вызвать, чтобы выполнить программу. Еще одно удобное свойство функций как объектов — то, что вы можете поместить их в список.

Перевести ‘while’ впрямую немного сложнее, но вполне получается :

#-------- Функциональный цикл 'while' в Python ----------#
# Обычный (основаный на утверждении 'while') цикл
while <cond>:
 <pre-suite>
 if <break_condition>:
 break
 else:
 <suite>

# Рекурсивный цикл в функциональном стиле
def while_block():
 <pre-suite>
 if <break_condition>:
 return 1
 else:
 <suite>
 return 0

while_FP = lambda: (<cond> and while_block()) or while_FP()
while_FP()

Наш вариант ‘while’ все еще требует функцию while_block(), которая сама по себе может содержать не только выражения, но и утверждения (statements). Но мы могли бы продолжить дальнейшее исключение утверждений в этой функции (как, например, замену блока ‘if/else’ в вышеописанном шаблоне на короткозамкнутое выражение). К тому же, обычная проверка на месте <cond> (наподобие ‘while myvar==7’) вряд ли окажется полезной, поскольку тело цикла (в представленном виде) не может изменить какие-либо переменные (хотя глобальные переменные могут быть изменены в while_block()). Один из способов применить более полезное условие — заставить while_block() возвращать более осмысленное значение и сравнивать его с условием завершения. Стоит взглянуть на реальный пример исключения утверждений:

#---------- Функциональный цикл 'echo' в Python ------------#
# Императивная версия "echo()"
def echo_IMP():
 while 1:
 x = raw_input("IMP -- ")
 if x == 'quit':
 break
 else
 print x
echo_IMP()

# Служебная функция, реализующая "тождество с побочным эффектом"
def monadic_print(x):
 print x
 return x

# FP версия "echo()"
echo_FP = lambda: monadic_print(raw_input("FP -- "))=='quit' or echo_FP()
echo_FP()

Мы достигли того, что выразили небольшую программу, включающую ввод/вывод, циклы и условия в виде чистого выражения с рекурсией (фактически — в виде функционального объекта, который при необходимости может быть передан куда угодно). Мы все еще используем служебную функцию monadic_print(), но эта функция совершенно общая и может использоваться в любых функциональных выражениях , которые мы создадим позже (это однократные затраты).2 3 Заметьте, что любое выражение, содержащее monadic_print(x) вычисляется так же, как если бы оно содержало просто x. В FP (в частности, в Haskell) есть понятие «монады» для функции, которая «не делает ничего, и вызывает побочный эффект при выполнении».

После всей проделанной работы по избавлению от совершенно осмысленных конструкций и замене их на невразумительные вложенные выражения, возникает естественный вопрос — «Зачем?!». Перечитывая мои описания характеристик FP, мы можем видеть, что все они достигнуты в Python. Но важнейшая (и, скорее всего, в наибольшей степени реально используемая) характеристика — исключение побочных эффектов или, по крайней мере, ограничение их применения специальными областями наподобие монад. Огромный процент программных ошибок и главная проблема, требующая применения отладчиков, случается из-за того, что переменные получают неверные значения в процессе выполнения программы. Функциональное программирование обходит эту проблему, просто вовсе не присваивая значения переменным.

Взглянем на совершенно обычный участок императивного кода. Его цель — распечатать список пар чисел, чье произведение больше 25. Числа, составляющие пары, сами берутся из двух других списков. Все это весьма напоминает то, что программисты реально делают во многих участках своих программ. Императивный подход к этой задаче мог бы выглядеть так:

#--- Императивный код для "печати произведений" ----#
# Процедурный стиль - поиск больших произведений с помощью вложенных циклов
xs = (1,2,3,4)
ys = (10,15,3,22)
bigmuls = []
#...прочий код...
for x in xs:
 for y in ys:
 #...прочий код...
 if x*y > 25:
 bigmuls.append((x,y))
 #...прочий код...
#...прочий код...
print bigmuls

Этот проект слишком мал для того, чтобы что-нибудь пошло не так. Но, возможно, он встроен в код, предназначенный для достижения множества других целей в то же самое время. Секции, комментированные как «#…прочий код…» — места, где побочные эффекты с наибольшей вероятностью могут привести к ошибкам. В любой из этих точек переменные xs, ys, bigmuls, x, y могут приобрести неожиданные значения в гипотетическом коде. Далее, после завершения этого куска кода все переменные могут иметь значения, которые могут ожидаются, а могут и не ожидаться посдедующим кодом. Очевидно, что инкапсуляция в функциях/объектах и тщательное управление областью видимости могут использоваться, чтобы защититься от этого рода проблем. Вы также можете всегда удалять (‘del’) ваши переменные после использования. Но, на практике, указанный тип ошибок весьма обычен.

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

#--- Функциональный код для поиска/печати больших произведений на Python ----#
bigmuls = lambda xs,ys: filter(lambda (x,y):x*y > 25, combine(xs,ys))
combine = lambda xs,ys: map(None, xs*len(ys), dupelms(ys,len(xs)))
dupelms = lambda lst,n: reduce(lambda s,t:s+t, map(lambda l,n=n: [l]*n, lst))
print bigmuls((1,2,3,4),(10,15,3,22))

Мы связываем в примере анонимные (‘lambda’) функции с именами, но это не необходимо. Вместо этого мы могли просто вложить определения. Мы использовали имена как ради большей читаемости, так и потому, что combine() — в любом случае отличная служебная функция (генерирует список всех возможных пар элементов из двух списков). В свою очередь, dupelms() в основном лишь вспомогательная часть combine(). Хотя этот функциональный пример более многословен, чем императивный, при повторном использовании служебных функций код в собственно bigmuls() окажется, вероятно, более лаконичным, чем в императивном варианте.

Реальное преимущество этого функционального примера в том, что в нем абсолютно ни одна переменная не меняет своего значения. Какое-либо неожиданное побочное влияние на последующий код (или со стороны предыдущего кода) просто невозможно. Конечно, само по себе отсутствие побочных эффектов не гарантирует безошибочность кода, но в любом случае это преимущество. Однако заметьте, что Python, в отличие от многих функциональных языков, не предотвращает повторное привязывание имен bigmuls, combine и dupelms. Если дальше в процессе выполнения программы combine() начнет значить что-нибудь другое — увы! Можно было бы разработать класс-одиночку (Singleton) для поддержки однократного связывания такого типа (напр. ‘s.bigmuls’, etc.), но это выходит за рамки настоящей статьи.

Еще стоит отметить, что задача, которую мы только что решили, скроена в точности под новые возможности Python 2.0. Вместо вышеприведенных примеров — императивного или функционального — наилучшая (и функциональная) техника выглядит следующим образом:

#----- Код Python для "bigmuls" с использованием списочных встраиваний (list comprehensions) -----#
print [(x,y) for x in (1,2,3,4) for y in (10,15,3,22) if x*y > 25]

Эта статья продемонстрировала способы замены практически любой конструкции управления потоком в Python на функциональный эквивалент (избавляясь при этом от побочных эффектов). Эффективный перевод конкретной программы требует дополнительного обдумывания, но мы увидели, что встроенные функциональные примитивы являются полными и общими. В последующих статьях мы рассмотрим более мощные подходы к функциональному программированию; и, я надеюсь, сможем подробнее рассмотреть «pro» и «contra» функционального подхода.

телеграм канал. Подпишись, будет полезно!

Python Функциональное программирование — один из 4-рёх основных стилей программирования. Экопарк Z

Функциональное программирование на Python

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

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

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

Содержание

  • 1 Возможности
  • 1.1 Определение и использование функции
  • 1.2 Списковые включения
  • 1.3 Встроенные функции высших порядков
  • 1.3.1 map()
  • 1.3.2 filter()
  • 1.3.3 reduce()
  • 1.3.4 apply()
  • 1.4 Замыкания
  • 1.5 Итераторы
  • 1.6 Модуль functools
  • 1.7 Ленивые вычисления
  • 1.8 Функторы
  • 2 Примечания
  • 3 Ссылки
  • 4 Литература

Возможности

Определение и использование функции

Функция в Python может быть определена с помощью оператора def или лямбда-выражением. Следующие операторы эквивалентны:

def  func(x, y):
return x**2 + y**2

func = lambda x, y: x**2 + y**2

В определении функции фигурируют формальные аргументы. Некоторые из них могут иметь значения по умолчанию. Все аргументы со значениями по умолчанию следуют после аргументов без значений по умолчанию.

При вызове функции задаются фактические аргументы. Например:

func(2, y=7)

В начале идут позиционные аргументы. Они сопоставляются с именами формальных аргументов по порядку. Затем следуют именованные аргументы. Они сопоставляются по именам и могут быть заданы в вызове функции в любом порядке.

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

Функция всегда возвращает только одно значение (или None, если значение не задано в операторе return или этот оператор не встречен по достижении конца определения функции). Однако, это незначительное ограничение, так как возвращаемым значением может быть кортеж.

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

>>> (lambda x: x+2)(5)
7

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

Функции в Python являются объектами первого класса, то есть, они могут употребляться в программе наравне с объектами других типов данных.

Списковые включения

Списковое включение (англ. list comprehension) — наиболее выразительное из функциональных средств Python. Например, для вычисления списка квадратов положительных целых чисел, меньших 10, можно использовать выражение:

l = [x**2 for x in range(1,10)]

Встроенные функции высших порядков

В Python есть функции, одним из аргументов которых являются другие функции: map()filter()reduce()apply().

map()

Функция map() позволяет обрабатывать одну или несколько последовательностей с помощью заданной функции:

>>> list1 = [7, 2, 3, 10, 12]
>>> list2 = [-1, 1, -5, 4, 6]
>>> map(lambda x, y: x*y, list1, list2)
[-7, 2, -15, 40, 72]

Аналогичного (только при одинаковой длине списков) результата можно добиться с помощью списочных выражений:

>>> [x*y for x, y in zip(list1, list2)]
[-7, 2, -15, 40, 72]
filter()

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

>>> numbers = [10, 4, 2, -1, 6]
>>> filter(lambda x: x < 5, numbers)  # В результат попадают только те элементы x, для которых x < 5 истинно
[4, 2, -1]

То же самое с помощью списковых выражений:

>>> numbers = [10, 4, 2, -1, 6]
>>> [x for x in numbers if x < 5]
[4, 2, -1]
reduce()

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

>>> numbers = [2, 3, 4, 5, 6]
>>> reduce(lambda res, x: res*x, numbers, 1)
720

Вычисления происходят в следующем порядке:

Цепочка вызовов связывается с помощью промежуточного результата (res). Если список пустой, просто используется третий параметр (в случае произведения нуля множителей это 1):

>>> reduce(lambda res, x: res*x, [], 1)
1

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

>>> reduce(lambda res, x: [x]+res, [1, 2, 3, 4], [])
[4, 3, 2, 1]

Для наиболее распространенных операций в Python есть встроенные функции:

>>> numbers = [1, 2, 3, 4, 5]
>>> sum(numbers)
15
>>> list(reversed(numbers))
[5, 4, 3, 2, 1]

В Python 3 встроенной функции reduce() нет, но её можно найти в модуле functools.

apply()

Функция для применения другой функции к позиционным и именованным аргументам, заданным списком и словарем соответственно (Python 2):

>>> def f(x, y, z, a=None, b=None):
...     print x, y, z, a, b
...
>>> apply(f, [1, 2, 3], {'a': 4, 'b': 5})
1 2 3 4 5

В Python 3 вместо функции apply() следует использовать специальный синтаксис:

>>> def f(x, y, z, a=None, b=None):
...     print(x, y, z, a, b)
...
>>> f(*[1, 2, 3], **{'a': 4, 'b': 5})
1 2 3 4 5

Замыкания

Функции, определяемые внутри других функций, представляют собой полноценные замыкания (англ. closures):

def multiplier(n):
    "multiplier(n) возвращает функцию, умножающую на n"
    def mul(k):
        return n*k
    return mul
# того же эффекта можно добиться выражением
# multiplier = lambda n: lambda k: n*k
mul2 = multiplier(2) # mul2 - функция, умножающая на 2, например, mul2(5) == 10

Итераторы

Другие средства функционального программирования доступны из стандартной библиотеки (например, модуль itertools) и других библиотек.

Следующий пример иллюстрирует применение перечисляющего и сортирующего итераторов (итератор не может быть напечатан оператором print, поэтому оставшиеся в нем значения были помещены в список):

>>> it = enumerate(sorted("PYTHON"))  # итератор для перечисленных отсортированных букв слова
>>> it.next()                         # следующее значение
(0, 'H')
>>> print list(it)                    # оставшиеся значения в виде списка
[(1, 'N'), (2, 'O'), (3, 'P'), (4, 'T'), (5, 'Y')]

Следующий пример иллюстрирует использование модуля itertools:

>>> from itertools import chain
>>> print list(chain(iter("ABC"), iter("DEF")))
['A', 'B', 'C', 'D', 'E', 'F']

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

from math import cos
from itertools import groupby
lst = [cos(x*.4) for x in range(30)]                       # косинусоида
[list(y) for k, y in groupby(lst, lambda x: x > 0)]        # группы положительных и отрицательных чисел

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

Модуль functools

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

>>> from  functools import partial
>>> def myfun(a, b): return a + b
...
>>> myfun1 = partial(myfun, 1)
>>> print myfun1(2)
3

(Частичное применение функций также можно реализовать с помощью замыканий или функторов)

Ленивые вычисления

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

  • простейшие логические операции or и and не вычисляют второй операнд, если результат определяется первым операндом
  • лямбда-выражения
  • определенные пользователем классы с ленивой логикой вычислений или функторы
  • Генераторы и генераторные выражения
  • (Python 2.5) if-выражение имеет «ленивую» семантику (вычисляется только тот операнд, который нужен)

Пример, который иллюстрирует работу if-выражения. С помощью оператора print можно проследить, какие функции реально вызывались:

>>> def f():
...     print "f"
...     return "f"
...
>>> def g():
...     print "g"
...     return "g"
...
>>> f() if True else g()
f
'f'
>>> f() if False else g()
g
'g'

Некоторые примеры из книги рецептов:

  • Ленивая сортировка
  • Ленивый обход графа
  • Ленивое вычисление свойств
  • Карринг

Функторы

Функторами называют объекты, синтаксически подобные функциям, то есть поддерживающие операцию вызова. Для определения функтора нужно перегрузить оператор () с помощью метода __call__.

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

Функторы позволяют заменить некоторые приёмы, связанные с использованием замыкания, статических переменных и т. п.

Ниже представлено замыкание и эквивалентный ему функтор:

def addClosure(val1):
    def closure(val2):
        return val1 + val2
    return closure
    
class AddFunctor(object):
    def __init__(self, val1):
        self.val1 = val1
    def __call__(self, val2):
        return self.val1 + val2

cl = addClosure(2)
fn = AddFunctor(2)

print cl(1), fn(1)  # напечатает "3 3"

Следует отметить, что код, использующий замыкание, будет исполняться быстрее, чем код с функтором. Это связано с необходимостью получения атрибута val у переменной self (то есть функтор проделывает на одну Python операцию больше).

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

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

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

class SlowFunctor(object):
	def __init__(self,func):
		self.func = func
	def __add__(self,val):                  # сложение функтора с чем-то
		if isinstance(val,SlowFunctor): # если это функтор
			new_func = lambda *dt,**mp : self(*dt,**mp) + val(*dt,**mp)
		else:                           # если что-то другое
			new_func = lambda *dt,**mp : self(*dt,**mp) + val
		return SlowFunctor( new_func )
	def __call__(self,*dt):
		return self.func(*dt)

import math
def test1(x):
    return x + 1
def test2(x):
    return math.sin(x)

func = SlowFunctor(test1)                # создаем функтор
func = func + SlowFunctor(test2)         # этот функтор можно складывать с функторами
func = (lambda x : x + 2)(func)          # и числами, передавать в качестве параметра в функции
                                         # как будто это число

def func2(x):                            # Эквивалентная функция
    return test1(x) + test2(x) + 2

print func(math.pi)                      # печатает 3.14159265359
print func(math.pi) - func2(math.pi)     # печатает 0.0

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

Определенный подобным образом функтор создает значительные накладные расходы, так как при каждом вызове проходит по вызовам всех вложенных lambda.

Можно оптимизировать функтор, применив технику генерирования байт-кода во время исполнения. Соответствующий пример и тесты на скорость есть в Примерах Python программ.

При использовании этой техники скорость исполнения не будет отличаться от «статического» кода (если не считать времени, требующегося на однократное конструирование результирующей функции).

Вместо байт-кода Python можно генерировать на выходе, например, код на языке программирования C, других языках программирования или XML-файлы.

Несмотря на накладные расходы, ленивое вычисление может дать заметный выигрыш в скорости в случаях, когда действия, оборачиваемые ленивым функтором, достаточно дороги — например, включают объёмные вычисления или доступ к диску.

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

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

В другой ветке, где X требуется для вычисления конечного результата функции, цепочка функторов произведёт его вычисление.

При этом программисту не нужно отслеживать, в какой из веток алгоритма значение может не потребоваться: он может рассчитывать, что дорогостоящее вычисление X произойдёт только тогда, когда его результат не будет отброшен.

Приглашаю всех высказываться в Комментариях. Критику и обмен опытом одобряю и приветствую. В хороших комментариях сохраняю ссылку на сайт автора!

И не забывайте, пожалуйста, нажимать на кнопки социальных сетей, которые расположены под текстом каждой страницы сайта.
Продолжение тут…

Функциональное программирование на Python — Википедия

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

Возможности

Определение и использование функции

Функция в Python может быть определена с помощью оператора def или лямбда-выражением. Следующие операторы эквивалентны:

def func(x, y):
    return x**2 + y**2

func = lambda x, y: x**2 + y**2

В определении функции фигурируют формальные аргументы. Некоторые из них могут иметь значения по умолчанию. Все аргументы со значениями по умолчанию следуют после аргументов без значений по умолчанию.

При вызове функции задаются фактические аргументы. Например:

В начале идут позиционные аргументы. Они сопоставляются с именами формальных аргументов по порядку. Затем следуют именованные аргументы. Они сопоставляются по именам и могут быть заданы в вызове функции в любом порядке. Разумеется, все аргументы, для которых в описании функции не указаны значения по умолчанию, должны присутствовать в вызове функции. Повторы в именах аргументов недопустимы.

Функция всегда возвращает только одно значение (или None, если значение не задано в операторе return или этот оператор не встречен по достижении конца определения функции). Однако, это незначительное ограничение, так как возвращаемым значением может быть кортеж.

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

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

Функции в Python являются объектами первого класса, то есть, они могут употребляться в программе наравне с объектами других типов данных.

Списковые включения

Списковое включение[1] (англ. list comprehension) — наиболее выразительное из функциональных средств Python. Например, для вычисления списка квадратов положительных целых чисел, меньших 10, можно использовать выражение:

l = [x**2 for x in range(1,10)]

Встроенные функции высших порядков

В Python есть функции, одним из аргументов которых являются другие функции: map(), filter(), reduce(), apply().

map()

Функция map() позволяет обрабатывать одну или несколько последовательностей с помощью заданной функции:

>>> list1 = [7, 2, 3, 10, 12]
>>> list2 = [-1, 1, -5, 4, 6]
>>> map(lambda x, y: x*y, list1, list2)
[-7, 2, -15, 40, 72]

Аналогичного (только при одинаковой длине списков) результата можно добиться с помощью списочных выражений:

>>> [x*y for x, y in zip(list1, list2)]
[-7, 2, -15, 40, 72]
filter()

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

>>> numbers = [10, 4, 2, -1, 6]
>>> filter(lambda x: x < 5, numbers)     # В результат попадают только те элементы x, для которых x < 5 истинно
[4, 2, -1]

То же самое с помощью списковых выражений:

>>> numbers = [10, 4, 2, -1, 6]
>>> [x for x in numbers if x < 5]
[4, 2, -1]
reduce()

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

>>>from functools import reduce
>>>numbers = [2, 3, 4, 5, 6]
>>> reduce(lambda res, x: res*x, numbers, 1)
720

Вычисления происходят в следующем порядке:

Цепочка вызовов связывается с помощью промежуточного результата (res). Если список пустой, просто используется третий параметр (в случае произведения нуля множителей это 1):

>>> reduce(lambda res, x: res*x, [], 1)
1

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

>>> reduce(lambda res, x: [x]+res, [1, 2, 3, 4], [])
[4, 3, 2, 1]

Для наиболее распространенных операций в Python есть встроенные функции:

>>> numbers = [1, 2, 3, 4, 5]
>>> sum(numbers)
15
>>> list(reversed(numbers))
[5, 4, 3, 2, 1]

В Python 3 встроенной функции reduce() нет, но её можно найти в модуле functools.

apply()

Функция для применения другой функции к позиционным и именованным аргументам, заданным списком и словарем соответственно (Python 2):

>>> def f(x, y, z, a=None, b=None):
...     print x, y, z, a, b
...
>>> apply(f, [1, 2, 3], {'a': 4, 'b': 5})
1 2 3 4 5

В Python 3 вместо функции apply() следует использовать специальный синтаксис:

>>> def f(x, y, z, a=None, b=None):
...     print(x, y, z, a, b)
...
>>> f(*[1, 2, 3], **{'a': 4, 'b': 5})
1 2 3 4 5

Замыкания

Функции, определяемые внутри других функций, представляют собой полноценные замыкания (англ. closures)[2]:

def multiplier(n):
    "multiplier(n) возвращает функцию, умножающую на n"
    def mul(k):
        return n*k
    return mul
# того же эффекта можно добиться выражением
# multiplier = lambda n: lambda k: n*k
mul2 = multiplier(2) # mul2 - функция, умножающая на 2, например, mul2(5) == 10

Итераторы

Другие средства функционального программирования доступны из стандартной библиотеки (например, модуль itertools) и других библиотек.

Следующий пример иллюстрирует применение перечисляющего и сортирующего итераторов (итератор не может быть напечатан оператором print, поэтому оставшиеся в нем значения были помещены в список):

>>> it = enumerate(sorted("PYTHON"))  # итератор для перечисленных отсортированных букв слова
>>> it.next()                         # следующее значение
(0, 'H')
>>> print list(it)                    # оставшиеся значения в виде списка
[(1, 'N'), (2, 'O'), (3, 'P'), (4, 'T'), (5, 'Y')]

Следующий пример иллюстрирует использование модуля itertools:

>>> from itertools import chain
>>> print list(chain(iter("ABC"), iter("DEF")))
['A', 'B', 'C', 'D', 'E', 'F']

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

from math import cos
from itertools import groupby
lst = [cos(x*.4) for x in range(30)]                       # косинусоида
[list(y) for k, y in groupby(lst, lambda x: x > 0)]        # группы положительных и отрицательных чисел

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

Модуль functools

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

>>> from  functools import partial
>>> def myfun(a, b): return a + b
...
>>> myfun1 = partial(myfun, 1)
>>> print myfun1(2)
3

(Частичное применение функций также можно реализовать с помощью
замыканий или
функторов)

Ленивые вычисления

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

  • простейшие логические операции or и and не вычисляют второй операнд, если результат определяется первым операндом
  • лямбда-выражения
  • определенные пользователем классы с ленивой логикой вычислений[3] или функторы
  • Генераторы и генераторные выражения
  • (Python 2.5) if-выражение имеет «ленивую» семантику (вычисляется только тот операнд, который нужен)

Пример, который иллюстрирует работу if-выражения. С помощью оператора print можно проследить, какие функции реально вызывались:

>>> def f():
...     print "f"
...     return "f"
...
>>> def g():
...     print "g"
...     return "g"
...
>>> f() if True else g()
f
'f'
>>> f() if False else g()
g
'g'

Некоторые примеры из книги рецептов:

  • Ленивая сортировка[4]
  • Ленивый обход графа[5]
  • Ленивое вычисление свойств[6]
  • Карринг[7]

Функторы

Функторами называют объекты, синтаксически подобные функциям, то есть поддерживающие операцию вызова.
Для определения функтора нужно перегрузить оператор () с помощью метода __call__. В
Python функторы полностью аналогичны функциям, за исключением специальных атрибутов
(func_code и некоторых других). Например, функторы можно передавать в качестве функций
обратного вызова (callback) в С-код. Функторы позволяют заменить некоторые приёмы, связанные с использованием замыкания, статических переменных и т. п.

Ниже представлено замыкание и эквивалентный ему функтор:

def addClosure(val1):
    def closure(val2):
        return val1 + val2
    return closure
    
class AddFunctor(object):
    def __init__(self, val1):
        self.val1 = val1
    def __call__(self, val2):
        return self.val1 + val2

cl = addClosure(2)
fn = AddFunctor(2)

print cl(1), fn(1)  # напечатает "3 3"

Следует отметить, что код, использующий замыкание, будет исполняться быстрее, чем код с функтором. Это связанно с необходимостью получения атрибута val у переменной self (то есть функтор проделывает на одну Python операцию больше).
Также функторы нельзя использовать для создания декораторов с параметрами.
С другой стороны, функторам доступны все возможности ООП в Python, что делает их очень полезными для функционального программирования. Например, можно написать функтор, который будет «запоминать» исполняемые над ним операции и затем повторять их.
Для этого достаточно соответствующим образом перегрузить специальные методы.

class SlowFunctor(object):
	def __init__(self,func):
		self.func = func
	def __add__(self,val):                  # сложение функтора с чем-то
		if isinstance(val,SlowFunctor): # если это функтор
			new_func = lambda *dt,**mp : self(*dt,**mp) + val(*dt,**mp)
		else:                           # если что-то другое
			new_func = lambda *dt,**mp : self(*dt,**mp) + val
		return SlowFunctor( new_func )
	def __call__(self,*dt):
		return self.func(*dt)

import math
def test1(x):
    return x + 1
def test2(x):
    return math.sin(x)

func = SlowFunctor(test1)                # создаем функтор
func = func + SlowFunctor(test2)         # этот функтор можно складывать с функторами
func = (lambda x : x + 2)(func)          # и числами, передавать в качестве параметра в функции
                                         # как будто это число

def func2(x):                            # Эквивалентная функция
    return test1(x) + test2(x) + 2

print func(math.pi)                      # печатает 3.14159265359
print func(math.pi) - func2(math.pi)     # печатает 0.0

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

Определенный подобным образом функтор создает значительные накладные расходы, так как при каждом вызове
проходит по вызовам всех вложенных lambda. Можно оптимизировать функтор,
применив технику генерирования байткода во время исполнения. Соответствующий пример и тесты на скорость
есть в Примерах Python программ.
При использовании этой техники скорость исполнения не будет отличаться от «статического» кода (если не считать времени, требующегося на однократное конструирование результирующей функции).
Вместо байтокода Python можно генерировать на выходе, например, код на языке программирования
C, других языках программирования или XML-файлы.

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

Примечания

Ссылки

Литература

ФП на Python. Часть 7. Функции высшего порядка

Функции высшего порядка – это один из мощнейших инструментов функционального подхода к программированию. Эта идея прекрасно реализована в Python, как на уровне самого языка, так и в его экосистеме: в модулях functools, operator и itertools.

Что такое функция высшего порядка?

На протяжении этого цикла статей мы неоднократно сталкивались с функциями высшего порядка (High Order Functions или HOF), явно про это написано в статье “ФП на Python. Часть 2. Абстракция и композиция. Функции“. Функция высшего порядка – это функция, которая может принимать в качестве аргумента другую функцию и/или возвращать функцию как результат работы. Так как в Python функции – это объекты первого класса, то они являются HOF, это свойство активно используется при разработке программного обеспечения. 

Встроенные функции высшего порядка

К встроенным функциям высшего порядка, которые можно использовать без импорта каких-либо библиотек, относятся map и filter.

Функция map принимает функцию и итератор, возвращает итератор, элементами которого являются результаты применения функции к элементам входного итератора.

Примеры:

>>> a = [1, 2, 3, 4, 5]
>>> list(map(lambda x: x**2, a))
[1, 4, 9, 16, 25]

>>> def fun(x):
        if x % 2 == 0:
            return 0
        else:
            return x*2

>>> list(map(fun, a))
[2, 0, 6, 0, 10]

Функция filter принимает функцию предикат и итератор, возвращает итератор, элементами которого являются данные из исходного итератора, для которых предикат возвращает True.

Примеры:

>>> list(filter(lambda x: x > 0, [-1, 1, -2, 2, 0]))
[1, 2]

>>> list(filter(lambda x: len(x) == 2, ["a", "aa", "b", "bb"]))
['aa', 'bb']

Если придерживаться “питонического” стиля программирования, то вместо map и filter лучше использовать списковое включение (list comprehensions) с круглыми скобками  (в этом случае будет создан генератор, более подробно см. “Python. Урок 7. Работа со списками (list)“):

Вариант с функцией map:

map(lambda x: x**2, [1,2,3])

можно заменить на:

(v**2 for v in [1,2,3])

Для варианта с  функцией filter:

filter(lambda x: x > 0, [-1, 1, -2, 2, 0])

аналог будет выглядеть так:

(v for v in [-1, 1, -2, 2, 0] if v > 0)

В экосистеме Python есть два модуля: functools и operator, которые развивают идею использования HOF и предоставляют инструменты для разработки программ в функциональном стиле.

Модуль functools

Модуль functools предоставляет набор декораторов и HOF функций, которые либо принимают другие функции в качестве аргумента, либо возвращают функции как результат работы.

HOF функции

partial

Функция partial создает частично примененные функции (см. “Функция как возвращаемое значение. Каррирование, замыкание, частичное применение”). Суть идеи в том, что если у функции есть несколько аргументов, то можно создать на базе нее другую, у которой часть аргументов будут иметь заранее заданные значения.

Прототип:

partial(func, /, *args, **keywords)

Параметры:

  • func
    • Функция, для которой нужно построить частично примененный вариант.
  • args
    • Позиционные аргументы функции.
  • keywords
    • Именованные аргументы функции.

Примеры:

В статье “ФП на Python. Часть 2. Абстракция и композиция. Функции” мы приводили пример функции, которая складывает три переданных в нее аргумента и строили для нее частично примененный вариант, немного модифицируем ее:

def faddr(x, y, z):
   return x + 2 * y + 3 * z

Построим частично примененную функцию с помощью partial():

p_faddr = partial(faddr, 1, 2)
>>> p_faddr(3)
14

Построим функцию с заранее заданными значениями для параметров y и z:

>>> p_kw_faddr = partial(faddr, y=3, z=5)
>>> p_kw_faddr(2)
23
partialmethod

Инструмент, аналогичный по своему назначению функции partial(), применяется для методов классов.

Прототип:

class partialmethod(func, /, *args, **keywords)

Параметры:

  • func
    • Метод класса, для которого нужно построить частично примененный вариант.
  • args
    • Позиционные аргументы метода.
  • keywords
    • Именованные аргументы метода.

Примеры:

class Math:
   def mul(self, a, b):
       return a * b

   x10 = partialmethod(mul, 10)

>>> m = Math()
>>> m.mul(2,3)
6

>>> m.x10(5)
50
reduce

Сворачивает переданную последовательность с помощью заданной функции. Прототип функции:

reduce(function, iterable[, initializer])

Параметры:

  • function
    • Функция для свертки исходной последовательности, должна принимать два аргумента.
  • iterable
    • Последовательность для свертки (итератор).
  • initializer
    • Начальное значение, которое будет использоваться для сверки. Если значение не задано, то в качестве начального будет выбран первый элемент из итератора.

Примеры:

>>> from operator import add

>>> add(1,2)
3

>>> reduce(add, [1,2,3,4,5])
15
update_wrapper

Оборачивает исходную функцию так, чтобы она выглядела как функция-обертка.

Прототип:

update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)

Параметры:

  • wrapper
    • Исходная функция.
  • wrapped
    • Функция обертка.
  • assigned
    • Кортеж с атрибутами, которые необходимо заместить у оборачиваемой функции атрибутами функции-обертки. Значение по умолчанию: WRAPPER_ASSIGNMENTS, в этом случае будут замещены атрибуты: __module__, __name__, __qualname__, __annotations__,  __doc__.
  • updated
    • Кортеж с атрибутами, которые необходимо заместить у функции-обертки атрибутами оборачиваемой функции. Значение по умолчанию: WRAPPER_UPDATES, в этом случае будут замещен атрибут: __dict__.

Примеры:

def x10(a):   
   return a * 10

def some_mul(a: int) -> int:
   """a * some value"""
   return a * 1

wrapped_mul = update_wrapper(x10, some_mul)

>>> wrapped_mul(3)
30

>>> wrapped_mul.__name__
'some_mul'

>>> wrapped_mul.__annotations__
{'a': <class 'int'>, 'return': <class 'int'>}

>>> wrapped_mul.__doc__
'a * some value'

Декораторы

@cached_property

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

Прототип:

@cached_property(func)

Параметры:

  • func
    • Декорируемая функция.

Примеры:

Напишем реализацию класса, который будет хранить набор данных и предоставлять свойство mean – среднее арифметическое. Для того, чтобы каждый раз не вычислять это значение при обращении к свойству, его можно объявить с декоратором @cached_property:

import functools

class DataProc:
   def __init__(self, data_set):
      self._data_set = data_set

   @functools.cached_property
   def mean(self):
      return sum(self._data_set) / len(self._data_set)

>>> d = DataProc([1,2,3,4,5])
>>> d.mean
5
@lru_cache

Декоратор для создания кэшированной версии функции (метода класса). 

Прототип:

@lru_cache(user_function)

@lru_cache(maxsize=128, typed=False)

Параметры:

  • maxsize
    • Количество запоминаемых результатов работы функции для разных наборов значений аргументов. Значение по умолчанию: 128.
  • typed
    • Если параметр равен True, то при кэшировании также будет учитываться тип аргументов.

Подробное описание:

Декоратор обеспечивает хранение результатов работы функции на различных аргументах в количестве до maxsize штук. Декоратор @lru_cache создает функцию cache_info(), с помощью которой можно оценить насколько эффективно работает LRU кэш, она возвращает именованный кортеж с четырьмя полями: hits (количество попаданий), misses (количество промахов), maxsize (максимальный размер кэша) и currsize (текущий размер кэша). Для очистки кэша используйте функцию cache_clear(). Если параметр typed=True, то при работе с кэшем будет учитываться тип аргумента, в этом случае func(10) и func(10.0) будут распознаваться как вызовы на разных аргументах (будут закэшированны по отдельности).

Примеры:

@lru_cache(maxsize=16)
def square(value):
   return value**2

>>> for v in [1, 2, 3, 4, 2, 3, 4, 5, 5, 6]:
...     square(v)
... 
1
4
9
16
4
9
16
25
25
36

>>> square.cache_info()
CacheInfo(hits=4, misses=6, maxsize=16, currsize=6)
@total_ordering

Декоратор класса, который автоматически добавляет методы сравнения, если задан одни из методов  __lt__(), __le__(), __gt__(), __ge__() и метод __eq__().

Прототип:

@total_ordering

Примеры:

Создадим класс для работы с рациональными (дробными) числами:

class Rational:
   def __init__(self, a, b):
       self.num = a
       self.den = b

   def __lt__(self, other):
       return (self.num / self.den) < (other.num / other.den)

   def __eq__(self, other):
       return (self.num == other.num) and (self.den == other.den)

В представленном варианте реализации можно использовать операции сравнения <, >, ==:

>>> a = Rational(1, 2)
>>> b = Rational(3, 4)

>>> a < b
True

>>> a == b
False

>>> a > b
False

Но использовать операции <=, >= нельзя:

>>> a <= b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<=' not supported between instances of 'Rational' and 'Rational'

Если при объявлении класса добавить декоратор @total_ordering, то мы получим в распоряжение весь набор операторов сравнения:

@total_ordering
class Rational:
   def __init__(self, a, b):
       self.num = a
       self.den = b

   def __lt__(self, other):
       return (self.num / self.den) < (other.num / other.den)

   def __eq__(self, other):
       return (self.num == other.num) and (self.den == other.den)

Продемонстрируем это:

>>> a = Rational(1, 2)
>>> b = Rational(3, 4)

>>> a < b
True

>>> a <= b
True

>>> a == b
False

>>> a >= b
False

>>> a > b
False
@singledispatch

Декоратор трансформирует функцию в single-dispatch функцию. Особенность такой функции состоит в том, что выбор ее реализации определяется типом первого аргумента. Аналогичным по функционалу является singledispatchmethod.

Прототип:

@singledispatch

Примеры:

Создадим generic-фукцию с декоратором @singledispatch:

@singledispatch
def test(arg):
   print(f"Generic, arg: {arg}")

Пример добавления реализации с использованием аннотации типов:

@test.register
def _(arg: int):
   print(f"Int, arg: {arg}")

Пример явного задания типа через функцию register().

@test.register(float)
def _(arg):
   print(f"Float, arg: {arg}")

Пример работы с лямбда выражением:

test.register(str, lambda x: f"Str, arg: {x}")

Добавление уже созданной функции:

def list_printer_already_exist(arg):
   print(f"List, arg {arg}")

test.register(list, lambda x: f"List, arg: {x}")

Демонстрация работы функции test() на разных типах аргументов:

>>> test(None)
Generic, arg: None

>>> test(1)
Int, arg: 1

>>> test(2.0)
Float, arg: 2.0

>>> test("hello")
'Str, arg: hello'

>>> test([1,2,3])
'List, arg: [1, 2, 3]'

@wraps

Декоратор для упрощения работы с функцией update_wrapper().

Прототип:

@wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)

Описание параметров функции аналогично приведенным для update_wrapper.

Примеры:

def some_mul(a: int) -> int:
   """a * some value"""
   return a * 1

@wraps(some_mul)
def x20(a):
   return a * 20

>>> x20(3)
60

>>> x20.__name__
'some_mul'

>>> x20.__annotations__
{'a': <class 'int'>, 'return': <class 'int'>}

>>> x20.__doc__
'a * some value'

Модуль operator

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

Например, функция reduce из модуля functools, рассмотренного выше, первым аргументом принимает функцию, которую будет использовать для свертки. Ее можно создать где-то предварительно или по месту (в виде лямбды) либо, если она есть в составе модуля operator, воспользоваться им:

>>> from operator import mul
>>> from functools import reduce

>>> reduce(lambda a, b: a * b, [1,2,3,4,5])
120

>>> reduce(mul, [1,2,3,4,5])
120

Ниже представлена таблица с функциями из модуля operator.

Операция Синтаксис Функция
Сложение a + b add(a, b)
Конкатенация seq1 + seq2 concat(seq1, seq2)
Тест на вхождение obj in seq contains(seq, obj)
Деление a / b truediv(a, b)
Деление a // b floordiv(a, b)
Побитовое И a & b and_(a, b)
Побитовое исключающее ИЛИ a ^ b xor(a, b)
Битовая инверсия ~ a invert(a)
Побитовое ИЛИ a | b or_(a, b)
Возведение в степень a ** b pow(a, b)
Проверка того, что a есть b a is b is_(a, b)
Проверка того, что a не есть b a is not b is_not(a, b)
Присвоение значения элементу  по его индексу obj[k] = v setitem(obj, k, v)
Удаление элемента по его индексу del obj[k] delitem(obj, k)
Получение элемента по его индексу obj[k] getitem(obj, k)
Сдвиг влево a << b lshift(a, b)
Остаток от деления a % b mod(a, b)
Умножение a * b mul(a, b)
Умножение матриц a @ b matmul(a, b)
Получение отрицательной версии числа – a neg(a)
Логическое НЕ not a not_(a)
Получение положительной версии числа + a pos(a)
Сдвиг вправо a >> b rshift(a, b)
Присвоение значений срезу последовательности seq[i:j] = values setitem(seq, slice(i, j), values)
Удаление среза элементов del seq[i:j] delitem(seq, slice(i, j))
Получение среза seq[i:j] getitem(seq, slice(i, j))
Форматирование строки s % obj mod(s, obj)
Вычитание a – b sub(a, b)
Проверка истинности obj truth(obj)
Операция порядка a < b lt(a, b)
Операция порядка a <= b le(a, b)
Проверка равенства a == b eq(a, b)
Проверка неравенства a != b ne(a, b)
Операция порядка a >= b ge(a, b)
Операция порядка a > b gt(a, b)

Модуль operator содержит версии функций, модифицирующие значение аргументов, в рамках данной статьи они рассматриваться не будут.

P.S.

Вводные уроки по “Линейной алгебре на Python” вы можете найти соответствующей странице нашего сайта. Все уроки по этой теме собраны в книге “Линейная алгебра на Python”.

Если вам интересна тема анализа данных, то мы рекомендуем ознакомиться с библиотекой Pandas.  Для начала вы можете познакомиться с вводными уроками. Все уроки по библиотеке Pandas собраны в книге “Pandas. Работа с данными”.

Функциональное программирование на Python. Часть 2. Абстракция и композиция. Функции

Если попытаться выделить наиболее фундаментальные концепции, которые используются в программировании, то это будут абстракция и композиция (Category: The Essence of Composition). Рассмотрим их через призму понятия функции с примерами на Python.

Для начал обратимся к википедии за определениями указанных выше терминов: 

Абстракция (лат. abstractio — отвлечение) — теоретическое обобщение как результат абстрагирования. Абстрагирование отвлечение в процессе познания от несущественных сторон, свойств, связей объекта (предмета или явления) с целью выделения их существенных, закономерных признаков. Результат абстрагирования — абстрактные понятия, например: цвет, кривизна, красота и т. д. 

Композиция (лат. compositio — составление, связывание, сложение, соединение) — составление целого из частей. 

Число

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

Начнем с базовой вещи – с понятия числа. То, как мы работаем с числами, это достаточно новое изобретение человечества. Для нас, когда мы говорим: пять яблок или пять стульев — это одни и те же пятерки. Число пять, в данном случае, это некоторая абстракция, которая позволяет не думать о содержании (стулья, яблоки и т.п.), а сосредоточиться на их количестве. Но понадобились тысячи лет эволюции, чтобы это произошло, чтобы мы (люди) могли отвлеченно использовать числа. Об этом интересно пишет Алексей Савватеев в книге “Математика для гуманитариев”, и приводит там такой пример: “… в русском языке до сих пор говорят “сорок” вместо “четырьдесят”, хотя раньше можно было сказать “сорок собольих шкурок”, но не “сорок деревьев”. То есть сорок означало не количество каких-либо предметов вообще, а только вполне определенных. Сейчас мы довольно свободно оперируем числами для счета предметов (и не только), а сами вычисления выполняем уже в отрыве от связи числа с сущностью. 

Функция

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

1+2=3 

4+7=11 

121+144=265 

… 

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

a + b = c 

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

>>> def add(a, b): 
        return a + b 

>>> add(1, 3) 
4 
>>> add(4, 7) 
11

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

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

Введем понятие lambda-функции (в программировании): lambda-функция – это безымянная функция с произвольным числом аргументов, вычисляющая одно выражение. В Python она определяется так: вначале записывается ключевое слово lambda, потом перечисляются через запятую аргументы, с которыми будет проводиться работа, далее ставится двоеточие, а после него тело функции. Более подробно про нее можете прочитать в статье Python. Урок 10. Функции в Python

Перейдем к решению задачи. Точку с координатами будем задавать как кортеж (см Python. Урок 8. Кортежи (tuple)). Напишем функцию, которая возводит в квадрат разность двух чисел: 

>>> sq_sub = lambda x1, x2: (x2 - x1)**2

Реализуем непосредственно саму функцию dist

>>> dist = lambda p1, p2: (sq_sub(p1[0], p2[0])+sq_sub(p1[1], p2[1]))**0.5

Проверим ее работу: 

>>> point1 = (1, 1) 
>>> point2 = (4, 5) 
>>> dist(point1, point2) 
5.0

Композиция функций

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

Математически она записывается вот так:

h = g ∘ f

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

Решим нашу задачу, задав все вычисления явно – через функции, для этого нам понадобятся: 

>>> sub = lambda a, b: a - b # функция вычитания 
>>> sq = lambda x: x**2      # функция возведения в квадрат 
>>> sm = lambda a, b: a + b  # функция сложения 
>>> sqrt = lambda x: x**0.5  # функция извлечения квадратного корня

Благодаря тому, что Python умеет распаковывать кортежи, мы может этим воспользоваться и реализовать dist следующим образом: 

>>> dist = lambda x1, y1, x2, y2: sqrt(sm(sq(sub(x1, x2)), sq(sub(y1, y2)))) 
>>> dist(*point1, *point2) 
5.0

Как видите, у нас получилось написать функцию, которая вызывает функцию и т.д., то есть мы построили композицию ряда простых функций для решения более сложной вычислительной задачи. Естественно, с практической точки зрения первый вариант функции dist более предпочтителен, второй был приведен для иллюстрации принципа. 

Рекурсия

Используя приведенный выше подход создания функций и объединения их в композиции, можно решить довольно большое количество задач (и не только вычислительных): посчитать логарифм, вычислить синус или косинус заданного угла, определить квадрат числа и т.д. Все эти задачи объединяет то, что нам достаточно подставить какое-то число (или набор чисел), в качестве аргумента, в функцию, и мы получаем решение. Но бывают задачи, где этого недостаточно, например, определение факториала числа или элемента ряда Фибоначчи. Ключевое отличие этих задач заключается в том, что там присутствует некоторый вычислительный процесс, протекание которого определяется переданным аргументом. Другими словами, для того чтобы вычислить расстояния между точками по уже известной формуле, мы всегда будем делать одно и тоже количество операций: два вычитания, два возведения в степень, одно сложение и один раз извлечем корень. Количество умножений, которое придется выполнить, для того чтобы посчитать факториал, определяется числом, факториал которого мы хотим найти: для факториала пяти – пять умножений, для факториала десяти – десять.

Для построения такого типа (и не только) вычислительных процессов используется рекурсия. Суть рекурсии заключается в том, что функция вызывается из нее же самой. О рекурсии уже было достаточно много сказано на devpractice (https://devpractice.ru/fp-python-part1-general/#p32, https://devpractice.ru/about-rec-and-iter/), поэтому мы не будем на ней подробно останавливаться. 

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

Следующий шаг на пути к абстракции заключается в том, что если внимательно посмотреть на те программы, которые мы разрабатываем, то можно заметить, что у внешне, казалось бы, различных решений, будут находиться некоторые общие черты. Например, вам может понадобиться найти квадраты чисел заданного массива, или из данного набора чисел построить новый, элементами которого будут абсолютные значения элементов из исходного массива. Эти примеры наталкивают на идею построения некоторого внешнего интерфейса, принимающего в качестве аргументов набор данных и функцию, которая будет применяться к элементам этого набора. Идея, лежащая в этом типе абстракции та же, что использовалась нами, когда мы переходили от конкретных выражений 2+3=5, 7+8=15 и т.п. к самой операции сложения: мы абстрагировались от чисел, с которыми работали и сосредоточились на выполняемой операции. Так и здесь, мы можем абстрагироваться от частности: возведения всех элементов в квадрат или взятие абсолютного значения, и сосредоточиться на сути процесса: применение заданной функции к каждому элементу набора данных для построения нового набора. 

Так, мы приходим к идее, что нам нужны функции, которые могут использовать другие функции, переданные им в качестве аргументов. Такие конструкции носят название: функции высшего порядка (higher-order function). Так как функции, которые передаются в качестве аргументов, как правило, небольшие по размеру, то для удобства, их конструируют в виде lambd’ы прямо в месте вызова. Приведем несколько полезных свойств lambda-функций: 

Их можно вызвать в месте объявления: 

>>> (lambda x: x*2)(3)

Lambd’у можно сохранить в переменную и использовать ее в дальнейшем: 

>>> mul2 = lambda x: x*2 
>>> mul2(3) 
6

Функция, реализующая процесс модификации заданного набора данных, называется map. В качестве первого аргумента (если говорить про Python), ей передается функция, вторым – набор данных. Продемонстрируем работу с ней на примере: возведем все элементы заданного списка в квадрат: 

>>> list(map(lambda x: x**2, [1, 2, 3, 4, 5])) 
[1, 4, 9, 16, 25]

Функция list нужна для построения списка из результата работы map

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

>>> def is_config_valid(validator, data_struct): 
        return validator(data_struct)

Создадим новую конфигурацию: 

>>> tmp_conf = {"test1": 123}

Для нее подготовим валидатор: 

>>> is_tmp_valid = lambda x: True if "test1" in x else False

Проверим корректность конфигурации: 

>>> is_config_valid(is_tmp_valid, tmp_conf) 
True

В случае, если конфигурация имеет ошибки: 

>>> tmp2_conf = {"test2": 456}

Валидатор нам об этом сообщит: 

>>> is_config_valid(is_tmp_valid, tmp2_conf) 
False

Функция как возвращаемое значение. Каррирование, замыкание, частичное применение

Помимо возможности передачи функции в качестве аргумента, иногда возникает задача возвращения функции как результата работы. Идея возврата функции как результата нашла применение в построении замыкания и каррирования. Разберем эти понятия более подробно. 

Замыкание— функция, в теле которой присутствуют ссылки на переменные, объявленные вне тела этой функции в окружающем коде и не являющиеся её параметрами. Поясним эту идею на примере: построим конструктор линейных функции вида y = k*x + b с заранее заданными коэффициентами: 

>>> def linear_builder(k, b): 
        def helper(x): 
            return k * x + b 
    return helper

Создадим линейную функцию со следующими параметрами 3 * x + 9 

>>> linf = linear_builder(3, 9) 
>>> linf(5) 
24

Если вас заинтересовала тема замыкания, рекомендуем вам обратиться к статье Замыкания в Python

 Каррирование — преобразование функции от многих аргументов в набор функций, каждая из которых является функцией от одного аргумента. Суть в том, чтобы перейти от вида f(x, y, z) к виду f(x)(y)(z). Каррирование позволяет строить частично примененные функции. 

Создадим функцию, которая складывает три числа: 

>>> def faddr(x, y, z): 
        return x + y + z 

>>> faddr(1, 2, 3) 
6

Каррированный вариант этой функции будет выглядеть так: 

>>> def curr_faddr(x): 
    def tmp_a(y): 
        def tmp_b(z): 
            return x+y+z 
        return tmp_b 
    return tmp_a 

>>> curr_faddr(1)(2)(3) 
6

На базе нее можно строить частично примененные функции: 

>>> p_c_faddr = curr_faddr(1)(2) 
>>> p_c_faddr(3) 
6

В этом примере p_c_faddr делает следующее 1+2+x, неизвестное значение x принимает в качестве аргумента. 

Функции – объекты первого класса 

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

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

Сущность, удовлетворяющая перечисленным выше требованиям, называется объектом первого класса (или объектом первого порядка). Как было отмечено в первой части, наличие таких свойств, является отличительной чертой функциональных языков программирования. В Python функции являются объектами первого класса, что позволяет придерживаться функционального стиля, когда это необходимо, но при этом создатель языка Гвидо ван Россум отмечает: “… хотя сделал функции полноправными объектами, никогда не рассматривал Python как язык функционального программирования”. 

P.S.

Вводные уроки по “Линейной алгебре на Python” вы можете найти соответствующей странице нашего сайта. Все уроки по этой теме собраны в книге “Линейная алгебра на Python”.

Если вам интересна тема анализа данных, то мы рекомендуем ознакомиться с библиотекой Pandas.  Для начала вы можете познакомиться с вводными уроками. Все уроки по библиотеке Pandas собраны в книге “Pandas. Работа с данными”.

Функциональное программирование на Python — Википедия

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

Возможности

Определение и использование функции

Функция в Python может быть определена с помощью оператора def или лямбда-выражением. Следующие операторы эквивалентны:

def func(x, y):
    return x**2 + y**2

func = lambda x, y: x**2 + y**2

В определении функции фигурируют формальные аргументы. Некоторые из них могут иметь значения по умолчанию. Все аргументы со значениями по умолчанию следуют после аргументов без значений по умолчанию.

При вызове функции задаются фактические аргументы. Например:

В начале идут позиционные аргументы. Они сопоставляются с именами формальных аргументов по порядку. Затем следуют именованные аргументы. Они сопоставляются по именам и могут быть заданы в вызове функции в любом порядке. Разумеется, все аргументы, для которых в описании функции не указаны значения по умолчанию, должны присутствовать в вызове функции. Повторы в именах аргументов недопустимы.

Функция всегда возвращает только одно значение (или None, если значение не задано в операторе return или этот оператор не встречен по достижении конца определения функции). Однако, это незначительное ограничение, так как возвращаемым значением может быть кортеж.

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

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

Функции в Python являются объектами первого класса, то есть, они могут употребляться в программе наравне с объектами других типов данных.

Списковые включения

Списковое включение[1] (англ. list comprehension) — наиболее выразительное из функциональных средств Python. Например, для вычисления списка квадратов положительных целых чисел, меньших 10, можно использовать выражение:

l = [x**2 for x in range(1,10)]

Встроенные функции высших порядков

В Python есть функции, одним из аргументов которых являются другие функции: map(), filter(), reduce(), apply().

map()

Функция map() позволяет обрабатывать одну или несколько последовательностей с помощью заданной функции:

>>> list1 = [7, 2, 3, 10, 12]
>>> list2 = [-1, 1, -5, 4, 6]
>>> map(lambda x, y: x*y, list1, list2)
[-7, 2, -15, 40, 72]

Аналогичного (только при одинаковой длине списков) результата можно добиться с помощью списочных выражений:

>>> [x*y for x, y in zip(list1, list2)]
[-7, 2, -15, 40, 72]
filter()

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

>>> numbers = [10, 4, 2, -1, 6]
>>> filter(lambda x: x < 5, numbers)     # В результат попадают только те элементы x, для которых x < 5 истинно
[4, 2, -1]

То же самое с помощью списковых выражений:

>>> numbers = [10, 4, 2, -1, 6]
>>> [x for x in numbers if x < 5]
[4, 2, -1]
reduce()

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

>>>from functools import reduce
>>>numbers = [2, 3, 4, 5, 6]
>>> reduce(lambda res, x: res*x, numbers, 1)
720

Вычисления происходят в следующем порядке:

Цепочка вызовов связывается с помощью промежуточного результата (res). Если список пустой, просто используется третий параметр (в случае произведения нуля множителей это 1):

>>> reduce(lambda res, x: res*x, [], 1)
1

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

>>> reduce(lambda res, x: [x]+res, [1, 2, 3, 4], [])
[4, 3, 2, 1]

Для наиболее распространенных операций в Python есть встроенные функции:

>>> numbers = [1, 2, 3, 4, 5]
>>> sum(numbers)
15
>>> list(reversed(numbers))
[5, 4, 3, 2, 1]

В Python 3 встроенной функции reduce() нет, но её можно найти в модуле functools.

apply()

Функция для применения другой функции к позиционным и именованным аргументам, заданным списком и словарем соответственно (Python 2):

>>> def f(x, y, z, a=None, b=None):
...     print x, y, z, a, b
...
>>> apply(f, [1, 2, 3], {'a': 4, 'b': 5})
1 2 3 4 5

В Python 3 вместо функции apply() следует использовать специальный синтаксис:

>>> def f(x, y, z, a=None, b=None):
...     print(x, y, z, a, b)
...
>>> f(*[1, 2, 3], **{'a': 4, 'b': 5})
1 2 3 4 5

Замыкания

Функции, определяемые внутри других функций, представляют собой полноценные замыкания (англ. closures)[2]:

def multiplier(n):
    "multiplier(n) возвращает функцию, умножающую на n"
    def mul(k):
        return n*k
    return mul
# того же эффекта можно добиться выражением
# multiplier = lambda n: lambda k: n*k
mul2 = multiplier(2) # mul2 - функция, умножающая на 2, например, mul2(5) == 10

Итераторы

Другие средства функционального программирования доступны из стандартной библиотеки (например, модуль itertools) и других библиотек.

Следующий пример иллюстрирует применение перечисляющего и сортирующего итераторов (итератор не может быть напечатан оператором print, поэтому оставшиеся в нем значения были помещены в список):

>>> it = enumerate(sorted("PYTHON"))  # итератор для перечисленных отсортированных букв слова
>>> it.next()                         # следующее значение
(0, 'H')
>>> print list(it)                    # оставшиеся значения в виде списка
[(1, 'N'), (2, 'O'), (3, 'P'), (4, 'T'), (5, 'Y')]

Следующий пример иллюстрирует использование модуля itertools:

>>> from itertools import chain
>>> print list(chain(iter("ABC"), iter("DEF")))
['A', 'B', 'C', 'D', 'E', 'F']

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

from math import cos
from itertools import groupby
lst = [cos(x*.4) for x in range(30)]                       # косинусоида
[list(y) for k, y in groupby(lst, lambda x: x > 0)]        # группы положительных и отрицательных чисел

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

Модуль functools

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

>>> from  functools import partial
>>> def myfun(a, b): return a + b
...
>>> myfun1 = partial(myfun, 1)
>>> print myfun1(2)
3

(Частичное применение функций также можно реализовать с помощью
замыканий или
функторов)

Ленивые вычисления

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

  • простейшие логические операции or и and не вычисляют второй операнд, если результат определяется первым операндом
  • лямбда-выражения
  • определенные пользователем классы с ленивой логикой вычислений[3] или функторы
  • Генераторы и генераторные выражения
  • (Python 2.5) if-выражение имеет «ленивую» семантику (вычисляется только тот операнд, который нужен)

Пример, который иллюстрирует работу if-выражения. С помощью оператора print можно проследить, какие функции реально вызывались:

>>> def f():
...     print "f"
...     return "f"
...
>>> def g():
...     print "g"
...     return "g"
...
>>> f() if True else g()
f
'f'
>>> f() if False else g()
g
'g'

Некоторые примеры из книги рецептов:

  • Ленивая сортировка[4]
  • Ленивый обход графа[5]
  • Ленивое вычисление свойств[6]
  • Карринг[7]

Функторы

Функторами называют объекты, синтаксически подобные функциям, то есть поддерживающие операцию вызова.
Для определения функтора нужно перегрузить оператор () с помощью метода __call__. В
Python функторы полностью аналогичны функциям, за исключением специальных атрибутов
(func_code и некоторых других). Например, функторы можно передавать в качестве функций
обратного вызова (callback) в С-код. Функторы позволяют заменить некоторые приёмы, связанные с использованием замыкания, статических переменных и т. п.

Ниже представлено замыкание и эквивалентный ему функтор:

def addClosure(val1):
    def closure(val2):
        return val1 + val2
    return closure
    
class AddFunctor(object):
    def __init__(self, val1):
        self.val1 = val1
    def __call__(self, val2):
        return self.val1 + val2

cl = addClosure(2)
fn = AddFunctor(2)

print cl(1), fn(1)  # напечатает "3 3"

Следует отметить, что код, использующий замыкание, будет исполняться быстрее, чем код с функтором. Это связанно с необходимостью получения атрибута val у переменной self (то есть функтор проделывает на одну Python операцию больше).
Также функторы нельзя использовать для создания декораторов с параметрами.
С другой стороны, функторам доступны все возможности ООП в Python, что делает их очень полезными для функционального программирования. Например, можно написать функтор, который будет «запоминать» исполняемые над ним операции и затем повторять их.
Для этого достаточно соответствующим образом перегрузить специальные методы.

class SlowFunctor(object):
	def __init__(self,func):
		self.func = func
	def __add__(self,val):                  # сложение функтора с чем-то
		if isinstance(val,SlowFunctor): # если это функтор
			new_func = lambda *dt,**mp : self(*dt,**mp) + val(*dt,**mp)
		else:                           # если что-то другое
			new_func = lambda *dt,**mp : self(*dt,**mp) + val
		return SlowFunctor( new_func )
	def __call__(self,*dt):
		return self.func(*dt)

import math
def test1(x):
    return x + 1
def test2(x):
    return math.sin(x)

func = SlowFunctor(test1)                # создаем функтор
func = func + SlowFunctor(test2)         # этот функтор можно складывать с функторами
func = (lambda x : x + 2)(func)          # и числами, передавать в качестве параметра в функции
                                         # как будто это число

def func2(x):                            # Эквивалентная функция
    return test1(x) + test2(x) + 2

print func(math.pi)                      # печатает 3.14159265359
print func(math.pi) - func2(math.pi)     # печатает 0.0

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

Определенный подобным образом функтор создает значительные накладные расходы, так как при каждом вызове
проходит по вызовам всех вложенных lambda. Можно оптимизировать функтор,
применив технику генерирования байткода во время исполнения. Соответствующий пример и тесты на скорость
есть в Примерах Python программ.
При использовании этой техники скорость исполнения не будет отличаться от «статического» кода (если не считать времени, требующегося на однократное конструирование результирующей функции).
Вместо байтокода Python можно генерировать на выходе, например, код на языке программирования
C, других языках программирования или XML-файлы.

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

Примечания

Ссылки

Литература

лучших практик использования функционального программирования в Python

Содержание

Введение

Python — очень универсальный язык программирования высокого уровня. Он имеет обширную стандартную библиотеку, поддержку нескольких парадигм программирования и большую внутреннюю прозрачность. Если вы выберете, вы можете заглянуть в нижние уровни Python и изменить их — и даже изменить среду выполнения на лету по мере выполнения программы.

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

С учетом сказанного, другие функции существуют не просто так, и они являются важными инструментами для понимания. «Как писать хороший код» — очевидно, обширная тема, и на нее нет единственного правильного ответа! Вместо этого моя цель в этом сообщении в блоге — сосредоточиться на конкретном аспекте: функциональном программировании применительно к Python. Я расскажу, что это такое, как его можно использовать в Python и как, по моему опыту, лучше всего использовать.

Чтобы узнать больше о том, как писать хороший код, посетите наш канал YouTube!

Что такое функциональное программирование?

Функциональное программирование или FP — это парадигма кодирования, в которой строительными блоками являются неизменяемые значения и «чистые функции», которые не имеют общего состояния с другими функциями.Каждый раз, когда чистая функция получает заданный вход, она будет возвращать один и тот же результат — без изменения данных и без побочных эффектов. В этом смысле чистые функции часто сравнивают с математическими операциями. Например, 3 плюс 4 всегда будет равняться 7, независимо от того, какие еще математические операции выполняются или сколько раз вы складывали что-то раньше.

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

Несмотря на то, что функциональное программирование существует с 1950-х годов и реализуется многочисленными языками, оно не полностью описывает язык программирования. Clojure, Common Lisp, Haskell и OCaml — это языки, ориентированные на функциональность, с разными позициями в отношении других концепций языка программирования, таких как система типов и строгое или ленивое вычисление.Большинство из них также поддерживают побочные эффекты, такие как запись в файлы и чтение из них тем или иным образом — обычно все они очень тщательно помечаются как нечистые.

Функциональное программирование может иметь репутацию заумного, в котором элегантность или лаконичность отдается предпочтению практичности. Крупные компании редко полагаются на функционально-ориентированные языки в широком масштабе или, по крайней мере, делают это на меньшем уровне, чем другие языки, такие как C ++, Java или Python. Однако FP — это на самом деле просто основа для размышлений о логических потоках с их достоинствами и недостатками, и ее можно комбинировать с другими парадигмами.

Хотите кодировать быстрее ?

Kite — это плагин для PyCharm, Atom, Vim, VSCode, Sublime Text и IntelliJ, который использует машинное обучение, чтобы предоставить вам завершение кода в реальном времени, отсортированное по релевантности. Начните кодирование быстрее сегодня .

Что поддерживает Python?

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

  def add (a, b): 
return a + b

plus = add

plus (3, 4) # возвращает 7

Лямбда

Синтаксис «лямбда» позволяет декларативно создавать определения функций.Ключевое слово лямбда происходит от греческой буквы, используемой в формальной математической логике для абстрактного описания функций и привязок переменных, «лямбда-исчисление», которое существует даже дольше, чем функциональное программирование. Другой термин для этой концепции — «анонимная функция», поскольку лямбда-функции могут использоваться в режиме реального времени без необходимости в имени. Если вы решите назначить анонимную функцию переменной, они будут работать точно так же, как и любые другие функции.

  (lambda a, b: a + b) (3, 4) # возвращает 7 

сложение = lambda a, b: a + b
сложение (3, 4) # возвращает 7

Чаще всего лямбда-функции я вижу «в дикой природе» для функций, которые принимают вызываемый объект.«Вызываемый» — это все, что может быть вызвано в круглых скобках — практически говоря, классы, функции и методы. Среди них наиболее распространенное использование — объявление относительного приоритета с помощью ключа аргумента при сортировке структур данных.

  авторы = [Октавия Батлер, Исаак Азимов, Нил Стивенсон, Маргарет Этвуд, Usula K Le Guin, Рэй Брэдбери] 
sorted (авторы, key = len) # Возвращает список, отсортированный по длине имени автора
отсортировано (авторы, ключ = лямбда-имя: имя.split () [- 1]) # Возвращает список, отсортированный в алфавитном порядке по фамилии.

Обратной стороной встроенных лямбда-функций является то, что они отображаются без имени в трассировке стека, что может усложнить отладку.

Functools

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

  val = [1, 2, 3, 4, 5, 6] 

# Умножить каждый элемент на два
list (map (lambda x: x * 2, val)) # [2, 4, 6, 8, 10, 12]
# Возьмите факториал, умножив полученное значение на следующий элемент
уменьшить (лямбда: x, y: x * y, val, 1) # 1 * 1 * 2 * 3 * 4 * 5 * 6

Существует куча других функций более высокого порядка, которые манипулируют функциями другими способами, в частности частичными, что блокирует некоторые параметры функции.Это также известно как «каррирование» — термин, названный в честь первопроходца FP Хаскелла Карри.

  мощность деф (базовая, эксп): 
возвратная база ** ехр
куб = частичный (степень, ехр = 3)
cube (5) # возвращает 125

Для подробного ознакомления с вводными концепциями FP в Python, написанными так, как их использует функционально-ориентированный язык, я рекомендую здесь статью Мэри Роуз Кук.

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

Хотите кодировать быстрее ?

Kite — это плагин для PyCharm, Atom, Vim, VSCode, Sublime Text и IntelliJ, который использует машинное обучение, чтобы предоставить вам завершение кода в реальном времени, отсортированное по релевантности. Начните кодирование быстрее сегодня .

Декораторы

Функции высшего порядка также встроены в повседневный Python через декораторы.Один из способов объявления декораторов отражает это, а символ @ — это в основном синтаксический сахар для передачи декорированной функции в качестве аргумента декоратору. Вот простой декоратор, который устанавливает повторные попытки для фрагмента кода и возвращает первое успешное значение или отказывается и вызывает самое последнее исключение после 3 попыток.

  def retry (func): 
def retried_function (* аргументы, ** kwargs):
exc = нет
для _ в диапазоне (3):
попробуйте:
return func (* args, ** kwargs)
кроме исключения как exc:
print ("Исключение возникает при вызове% s с аргументами:% s, kwargs:% s.Повторная попытка "% (func, args, kwargs).

поднять exc
return retried_function

@retry
def do_something_risky ():
...

retried_function = retry (do_something_risky) # Нет необходимости использовать `@`

Этот декоратор оставляет входные и выходные типы и значения в точности такими же, но это не является обязательным требованием. Декораторы могут добавлять или удалять аргументы или изменять их тип.Их также можно настроить с помощью самих параметров. Я хочу подчеркнуть, что сами декораторы не обязательно являются «чисто функциональными»; они могут (и часто имеют, как в примере выше) иметь побочные эффекты — они просто используют функции более высокого порядка.

Как и многие другие техники Python среднего или продвинутого уровня, это очень мощный и часто сбивающий с толку. Имя вызванной вами функции будет отличаться от имени в трассировке стека, если вы не используете декоратор functools.wraps для аннотирования.Я видел, как декораторы делают очень сложные или важные вещи, например, анализируют значения из json blobs или обрабатывают аутентификацию. Я также видел несколько уровней декораторов для одного и того же определения функции или метода, что требует знания порядка приложения декоратора для понимания. Я думаю, что может быть полезно использовать встроенные декораторы, такие как `staticmethod`, или писать простые, четко названные декораторы, которые экономят много шаблонов, но особенно если вы хотите сделать свой код совместимым с проверкой типов, всем, что изменяет ввод или вывод. типы могут легко превратиться в «слишком умных».

Мои рекомендации

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

  • Чтобы начать использовать Python, не обязательно понимать FP.Скорее всего, вы запутаете других читателей или себя в будущем.
  • У вас нет гарантии, что любой код, на который вы полагаетесь (модули pip или код ваших соавторов), является функциональным и чистым. Вы также не знаете, настолько ли чист ваш собственный код, насколько вы надеетесь, — в отличие от языков, ориентированных на функциональность, синтаксис или компилятор не помогают обеспечить чистоту и помогают устранить некоторые типы ошибок. Смешивание побочных эффектов и функций более высокого уровня может быть чрезвычайно запутанным, потому что вы в конечном итоге получаете два вида сложности для рассуждений, а затем мультипликативный эффект обоих вместе.
  • Использование функций высшего порядка с комментариями типа — продвинутый навык. Типовые подписи часто становятся длинными и громоздкими гнездами Callable . Например, правильный способ ввести простой декоратор более высокого порядка, который возвращает функцию ввода, — это объявить F = TypeVar ['F', bound = Callable [..., Any]] , а затем аннотировать как def transparent (func : F) -> F: вернуть функцию . Или у вас может возникнуть соблазн внести залог и использовать Any вместо попытки выяснить правильную подпись.

Итак, какие части функционального программирования следует использовать?

Чистые функции

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

Вот нефункциональный пример.

  dictionary = ['лиса', 'босс', 'апельсин', 'пальцы ног', 'фея', 'чашка'] 
def puralize (слова):
для i в диапазоне (len (слова)):
word = words [i]
если слово.заканчивается на ('s') или word.endswith ('x'):
слово + = 'es'
если word.endswith ('y'):
word = word [: - 1] + 'ies'
еще:
слово + = 's'
words [i] = word

def test_pluralize ():
множественное число (словарь)
assert dictionary == ['лисы', 'боссы', 'апельсины', 'пальцы ног', 'феи', 'чашки']

При первом запуске test_pluralize он пройдет, но каждый раз после сбоя, так как s и es добавляются до бесконечности.Чтобы сделать его чистой функцией, мы могли бы переписать его как:

  dictionary = ['лиса', 'босс', 'апельсин', 'пальцы ног', 'фея', 'чашка'] 
def puralize (слова):
результат = []
словом прописью:
word = words [i]
если word.endswith ('s') или word.endswith ('x'):
множественное число = слово + 'es')
если word.endswith ('y'):
множественное число = слово [: - 1] + 'ies'
еще:
множественное число = + 's'
результат.добавить (множественное число)
вернуть результат

def test_pluralize ():
результат = множественное число (словарь)
assert result == ['лисы', 'боссы', 'апельсины', 'пальцы', 'феи', 'чашки']

Обратите внимание, что я на самом деле не использую концепции, специфичные для FP, а просто создаю и возвращаю новый объект вместо изменения и повторного использования старого. Таким образом, если у кого-то останется ссылка на список ввода, он не удивится.

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

Понимание (и предотвращение) изменчивости

Популярная викторина, какие из следующих структур данных являются изменяемыми?

  1. список
  2. кортеж
  3. набор
  4. дикт
  5. строка

Почему это важно? Иногда списки и кортежи кажутся взаимозаменяемыми, и возникает соблазн написать код, использующий их случайную комбинацию.Затем кортежи ошибаются, как только вы пытаетесь выполнить операцию мутации, такую ​​как присвоение элементу. Или вы пытаетесь использовать список в качестве ключа словаря и видите ошибку TypeError , которая возникает именно потому, что списки изменяемы. Кортежи и строки могут использоваться в качестве ключей словаря, потому что они неизменяемы и могут детерминированно хешироваться, а все другие структуры данных — нет, потому что они могут меняться по значению, даже если идентичность объекта одинакова.

Самое главное, что когда вы передаете dicts / lists / sets, они могут неожиданно измениться в каком-то другом контексте.Это беспорядок для отладки. Изменяемый параметр по умолчанию — классический случай этого:

  def add_bar (items = []): 
items.append ('бар')
вернуть элементы

l = add_bar () # l is ['bar']
l.append ('фу')
add_bar () # возвращает ['bar', 'foo', 'bar']

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

Хотите кодировать быстрее ?

Kite — это плагин для PyCharm, Atom, Vim, VSCode, Sublime Text и IntelliJ, который использует машинное обучение, чтобы предоставить вам завершение кода в реальном времени, отсортированное по релевантности. Начните кодирование быстрее сегодня .

Ограничение использования классов

Часто классы (и их экземпляры) обладают обоюдоострым мечом изменчивости. Чем больше я программирую на Python, тем больше откладываю создание классов до тех пор, пока они не станут очевидными, и я почти никогда не использую изменяемые атрибуты классов. Это может быть сложно для тех, кто работает с высокообъектно-ориентированными языками, такими как Java, но многие вещи, которые обычно или всегда выполняются через класс на другом языке, можно сохранить на уровне модуля в Python.Например, если вам нужно сгруппировать функции, константы или пространство имен, их можно вместе поместить в отдельный файл .py.

Часто я вижу классы, используемые для хранения небольшого набора имен переменных со значениями, когда namedtuple (или typing.NamedTuple для специфичности типа) будет работать так же хорошо и быть неизменным.

  из коллекции import namedtuple 
VerbTenses = namedtuple ('VerbTenses', ['прошлое', 'настоящее', 'будущее'])
# против
class VerbTenses (объект):
def __init __ (я, прошлое, настоящее, будущее):
я.прошлое = прошлое,
self.present = присутствует
self.future = будущее

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

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

  Класс Автобус (объект): 
пассажиры = набор ()
def add_passenger (я, человек):
я.Passenger.add (человек)

bus1 = Автобус ()
bus2 = Автобус ()
bus1.add_passenger ('abe')
bus2.add_passenger ('bertha')
bus1.passengers # возвращает ['abe', 'bertha']
bus2.passengers # также ['abe', 'bertha']

Идемпотентность

В любой реалистичной, большой и сложной системе бывают случаи, когда она должна дать сбой и повторить попытку. Концепция «идемпотентности» существует также в дизайне API и матричной алгебре, но в функциональном программировании идемпотентная функция возвращает то же самое, когда вы передаете предыдущий результат.Следовательно, повторение чего-либо всегда приводит к одному и тому же значению. Более полезная версия вышеупомянутой функции «множественное число» проверяет, было ли что-то уже во множественном числе, прежде чем, например, пытаться вычислить, как сделать это множественным числом.

Экономное использование лямбда-выражений и функций высшего порядка

Я считаю, что часто быстрее и понятнее использовать лямбды в случае коротких операций, например, в ключе упорядочивания для sort . Однако, если лямбда становится длиннее одной строки, вероятно, лучше использовать обычное определение функции.И передача функций в целом может быть полезна для избежания повторения, но я стараюсь иметь в виду, не слишком ли затрудняет ясность дополнительная структура. Часто бывает проще разбить на более мелких составных помощников.

Генераторы и функции более высокого уровня, при необходимости

Иногда вы встретите абстрактный генератор или итератор, возможно, тот, который возвращает большую или даже бесконечную последовательность значений. Хорошим примером этого является диапазон. В Python 3 теперь это генератор по умолчанию (эквивалент xrange в Python 2), отчасти для того, чтобы уберечь вас от ошибок нехватки памяти, когда вы пытаетесь перебирать большое число, например range (10 ** 10).Если вы хотите выполнить какую-либо операцию с каждым элементом в потенциально большом генераторе, то лучшим вариантом может быть использование таких инструментов, как карта и фильтр.

Точно так же, если вы не знаете, сколько значений может вернуть ваш недавно написанный итератор — а оно, вероятно, велико, — определение генератора может быть правильным решением. Однако не все будут разбираться в его использовании и могут решить собрать результат в виде списка, что приведет к ошибке OOM, которую вы пытались избежать в первую очередь.Генераторы, реализация потокового программирования в Python, также не обязательно являются чисто функциональными, поэтому все те же предостережения в отношении безопасности применимы, как и любой другой стиль программирования на Python.

Заключительные мысли

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

.

Введение в функциональное программирование с помощью Python

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

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

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

Начнем с примера обычной не чистой функции, которая удаляет последний элемент в списке:

  def remove_last_item (mylist):
    "" "Удаляет последний элемент из списка."" "
    mylist.pop (-1) # Это изменяет mylist
  

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

  def butlast (mylist):
    "" "Подобно butlast в Лиспе; возвращает список без последнего элемента." ""
    return mylist [: - 1] # Это возвращает копию mylist
  

Мы определяем функцию butlast () (например, butlast в Lisp), которая возвращает список без последнего элемента без изменения исходного списка.Вместо этого он возвращает копию списка, в котором есть изменения, что позволяет нам сохранить оригинал. К практическим преимуществам использования функционального программирования можно отнести следующие:

  • Модульность. Функциональный стиль написания требует определенной степени разделения
    при решении ваших индивидуальных проблем и упрощает повторное использование частей кода
    в других контекстах. Поскольку функция не зависит от какой-либо внешней переменной или состояния
    , вызвать ее из другого фрагмента кода
    просто.
  • Краткость. Функциональное программирование часто менее многословно, чем другие парадигмы.
  • Параллелизм. Чисто функциональные функции являются поточно-ориентированными и могут выполняться одновременно с
    . Некоторые функциональные языки делают это автоматически, что может быть большим подспорьем в
    , если вам когда-нибудь понадобится масштабировать приложение, хотя в Python это еще не так.
  • Тестируемость. Тестировать функциональную программу невероятно просто: все, что вам нужно,
    — это набор входных данных и ожидаемый набор выходных данных.Они идемпотентны,
    означает, что многократный вызов одной и той же функции с одними и теми же аргументами
    всегда будет возвращать один и тот же результат.

Обратите внимание, что c

.

Почему Python не очень хорош для функционального программирования?

Переполнение стека

  1. Около
  2. Продукты

  3. Для команд
  1. Переполнение стека
    Общественные вопросы и ответы

  2. Переполнение стека для команд
    Где разработчики и технологи делятся частными знаниями с коллегами

  3. Вакансии
    Программирование и связанные с ним технические возможности карьерного роста

  4. Талант
    Нанимайте технических специалистов и создавайте свой бренд работодателя

  5. Реклама
    Обратитесь к разработчикам и технологам со всего мира

.Учебники и примечания по функциональному программированию

| Python

Python и функциональное программирование:

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

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

В этом уроке вы посмотрите на:

  1. Какие особенности функционального программирования
  2. Как добиться этих характеристик.
  3. Что означает использование функций как объектов первого класса.
  4. Что такое функциональная чистота.
  5. Как преобразовать процедурный код в функциональный.

Характеристики функционального программирования

Функционально чистый язык должен поддерживать следующие конструкции:

  • Функции как объекты первого класса, что означает, что вы должны иметь возможность применять все конструкции использования данных также и к функциям.
  • Чистые функции; в них не должно быть побочных эффектов
  • Способы и конструкции для ограничения использования циклов for
  • Хорошая поддержка рекурсии

Функции как объекты первого класса в Python:

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

  >>> список (map (int, ["1", "2", "3"]))
[1, 2, 3]
  

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

  >>> def hello_world (h):
... def world (w):
... печать (ч, ш)
... return world # возвращающие функции
...
>>> h = hello_world # назначение
>>> x = h ("hello") # присваиваем
>>> х
<функциональный мир по адресу 0x7fec47afc668>
>>> x ("мир")
('Привет мир')
  

Вы можете хранить функции в различных структурах данных.Например, в следующем коде вы можете хранить несколько функций в списке.

  >>> function_list = [h, x]
>>> список_функций
[, ]
  

Функциональная чистота Python:

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

  def naive_sum (список):
    s = 0
    для l в списке:
        s + = l
    вернуть s
  

можно заменить следующей конструкцией:

  сум (список)
  

Точно так же встроенные функции, такие как map, reduce и модуль itertools в Python, могут использоваться, чтобы избежать побочных эффектов в вашем коде.

Уменьшение использования циклов в Python:

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

  для x в л:
    func (x)
  

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

  карта (func, l)
  

Это читается как «сопоставить функцию со списком», что соответствует нашей идее определения вопроса «что».

Если вы возьмете эту идею и примените ее к последовательному выполнению функций, вы получите следующую конструкцию.

  def func1 ():
    проходить

def func2 ():
    проходить

def func3 ():
    проходить

выполнение = лямбда f: f ()
карта (выполнение, [func1, func2, func3])
  

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

Рекурсия Python:

Что такое рекурсия

Рекурсия — это метод разбиения проблемы на подзадачи, которые по сути имеют тот же тип, что и исходная проблема. Вы решаете базовые задачи, а затем объединяете результаты. Обычно это включает вызов самой функции.

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

съесть пельмени:
1.проверь сколько вареников на тарелке
2. если не осталось пельменей, прекратите есть
3. еще съесть один пельмень
4. «съесть пельмени»

Как реализовать рекурсию в вашем коде

Функции Python

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

Пример:
Следующий код генерирует числа Фибоначчи посредством рекурсии.

  def fib (n):
    если n == 0: вернуть 0
    elif n == 1: вернуть 1
    иначе: вернуть fib (n-1) + fib (n-2)
  

Небольшой пример, показывающий, как преобразовать процедурный код в функциональный код:

Давайте рассмотрим небольшой пример, в котором вы пытаетесь преобразовать процедурный код в функциональный. В приведенном ниже примере у вас есть начальное число, которое возводится в квадрат, результат увеличивается на 1, а результат возводится в степень 3, затем код принимает уменьшение.Обратите внимание, что код работает поэтапно.

  # процедурный код
start_number = 96

# получаем квадрат числа
квадрат = начальное_число ** 2

# увеличить число на 1
приращение = квадрат + 1

# куб числа
куб = приращение ** 3

# уменьшаем куб на 1
декремент = куб - 1

# получить окончательный результат
результат = печать (уменьшение) # вывод 783012621312
  

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

  # определить функцию `call`, в которой вы указываете функцию и аргументы
def call (x, f):
    вернуть f (x)

# определяем функцию, которая возвращает квадрат
квадрат = лямбда x: x * x

# определяем функцию, которая возвращает приращение
приращение = лямбда x: x + 1

# определяем функцию, которая возвращает куб
куб = лямбда x: x * x * x

# определить функцию, которая возвращает декремент
декремент = лямбда x: x-1

# поместите все функции в список в том порядке, в котором вы хотите их выполнять
funcs = [квадрат, приращение, куб, декремент]

# собрать все вместе.Ниже находится нефункциональная часть.
# в функциональном программировании вы разделяете функциональную и нефункциональную части.
from functools import reduce # reduce находится в библиотеке functools
print (reduce (call, funcs, 96)) # output 783012621312
  

Предоставил: Джойдип Бхаттачарджи

.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

2021 © Все права защищены. Карта сайта