Цепочка функций в Python

90

На Codewars.com я столкнулся со следующей задачей:

Создайте функцию, addкоторая складывает числа при последовательном вызове. Так что add(1)должен вернуться 1, add(1)(2)должен вернуться 1+2, ...

Хотя я знаком с основами Python, я никогда не встречал функции, которую можно было бы вызывать в такой последовательности, то есть функции, f(x)которую можно было бы вызвать как f(x)(y)(z).... Пока я даже не знаю, как интерпретировать это обозначение.

Как математик я подозреваю, что f(x)(y)это функция, которая назначает каждой xфункции, g_{x}а затем возвращает ее, g_{x}(y)а также для f(x)(y)(z).

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

Как вы называете эту концепцию и где я могу прочитать о ней подробнее?

Стефан Мескен
источник
7
Похоже, вы ищете функции каррирования
OneCricketeer
2
Подсказка: вложенная функция создается динамически, имеет доступ к локальным переменным родительской функции и может быть возвращена как (вызываемый) объект.
Джонатон Рейнхарт
@JonathonReinhart Вот как я думал о проблеме. Но я толком не видел, как это реализовать.
Стефан Мескен
3
В стороне: Python определенно позволит вам динамически создавать функции. Если вам интересно, вот пара связанных понятий, которые стоит прочитать: WP: Первоклассные функции | Как сделать в Python функцию высшего порядка? | functools.partial()| WP: Закрытие
Лукас Граф
@LukasGraf Я посмотрю. Спасибо!
Стефан Мескен

Ответы:

102

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

Подклассификация intи определение __call__:

Первый способ - использовать собственный intподкласс, который определяет, __call__что возвращает новый экземпляр самого себя с обновленным значением:

class CustomInt(int):
    def __call__(self, v):
        return CustomInt(self + v)

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

>>> def add(v):
...    return CustomInt(v)
>>> add(1)
1
>>> add(1)(2)
3
>>> add(1)(2)(3)(44)  # and so on..
50

Кроме того, как intподкласс, возвращаемое значение сохраняет поведение __repr__и s. Однако для более сложных операций вам следует соответствующим образом определить другие dunders .__str__int

Как отметил @Caridorc в комментарии, это addможно также просто записать как:

add = CustomInt 

Переименование класса в addвместо CustomIntтакже работает аналогично.


Определите закрытие, требуется дополнительный вызов для получения значения:

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

def add(v):
    def _inner_adder(val=None):  
        """ 
        if val is None we return _inner_adder.v 
        else we increment and return ourselves
        """
        if val is None:    
            return _inner_adder.v
        _inner_adder.v += val
        return _inner_adder
    _inner_adder.v = v  # save value
    return _inner_adder 

Он постоянно возвращает себя ( _inner_adder), который, если задан a val, увеличивает его ( _inner_adder += val), а если нет, возвращает значение как есть. Как я уже упоминал, ()для возврата увеличенного значения требуется дополнительный вызов:

>>> add(1)(2)()
3
>>> add(1)(2)(3)()  # and so on..
6
Димитрис Фасаракис Хиллиард
источник
6
В интерактивном коде add = CostumIntтоже должно работать и быть попроще.
Caridorc
4
Проблема с подклассом встроенных модулей заключается в том (2*add(1)(2))(3), TypeErrorчто они intне работают из-за невозможности вызова. В основном CustomIntпреобразуется в простой intпри использовании в любом контексте, кроме вызова. Для более надежного решения вам, по сути, придется заново реализовать все __*__методы, включая __r*__версии ...
Бакуриу,
@Caridorc Или не называйте это CustomIntвообще, но addпри определении.
minipif
27

Вы можете меня ненавидеть, но вот резкость :)

add = lambda v: type("", (int,), {"__call__": lambda self, v: self.__class__(self + v)})(v)

Изменить: Хорошо, как это работает? Код идентичен ответу @Jim, но все происходит в одной строке.

  1. typeможет быть использовано для построения новых типов: type(name, bases, dict) -> a new type. Поскольку nameмы предоставляем пустую строку, так как имя в этом случае действительно не нужно. Для bases(кортежа) мы предоставляем (int,), что идентично наследованию int. dictатрибуты класса, к которым мы прикрепляем__call__ лямбду.
  2. self.__class__(self + v) идентичен return CustomInt(self + v)
  3. Новый тип создается и возвращается во внешней лямбде.
Джордан Джамбазов
источник
17
Или даже короче:class add(int):__call__ = lambda self, v: add(self+v)
Бакуриу
3
Код внутри класса выполняется точно так же, как обычный код, поэтому вы можете определять специальные методы с помощью присваиваний. Единственная разница в том, что область видимости класса немного ... своеобразна.
Бакуриу
16

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

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

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

def add():
    current = yield
    while True:
        value = yield current
        current = value + current


it = add()
next(it)
print(it.send(10))
print(it.send(2))
print(it.send(4))

10
12
16
Касравнд
источник
6

Питонический способ сделать это - использовать динамические аргументы:

def add(*args):
    return sum(args)

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

ничочар
источник
1
Я удалил вашу заметку " PS ", ничочар. Все мы знаем, насколько элегантен Python :-) Я не думаю, что он входит в основную часть ответа.
Димитрис Фасаракис Хиллиард
6
Я действительно думаю, что вы могли бы просто сделать, add = sumесли бы собирались пойти по этому пути
codykochmann
6

Просто:

class add(int):
   def __call__(self, n):
      return add(self + n)
Николае
источник
1
Это лучший ответ!
MEMark
6

Если вы согласны принять дополнительные (), чтобы получить результат, вы можете использовать functools.partial:

from functools import partial

def add(*args, result=0):
    return partial(add, result=sum(args)+result) if args else result

Например:

>>> add(1)
functools.partial(<function add at 0x7ffbcf3ff430>, result=1)
>>> add(1)(2)
functools.partial(<function add at 0x7ffbcf3ff430>, result=3)
>>> add(1)(2)()
3

Это также позволяет указать сразу несколько номеров:

>>> add(1, 2, 3)(4, 5)(6)()
21

Если вы хотите ограничить его одним номером, вы можете сделать следующее:

def add(x=None, *, result=0):
    return partial(add, result=x+result) if x is not None else result

Если вы хотите add(x)(y)(z)легко вернуть результат и в дальнейшем быть вызываемым, тогда подклассы - это ваш intпуть.

Гость
источник