Tworzenie łańcuchów funkcji w Pythonie

90

Na Codewars.com napotkałem następujące zadanie:

Utwórz funkcję, addktóra dodaje liczby do siebie, gdy są wywoływane po kolei. Więc add(1)powinien wrócić 1, add(1)(2)powinien wrócić 1+2, ...

Chociaż znam podstawy Pythona, nigdy nie spotkałem funkcji, którą można wywołać w takiej kolejności, tj. Funkcji, f(x)którą można nazwać as f(x)(y)(z).... Jak dotąd nie jestem nawet pewien, jak zinterpretować ten zapis.

Jako matematyk podejrzewałbym, że f(x)(y)jest to funkcja, która przypisuje każdej xfunkcji, g_{x}a następnie zwraca g_{x}(y)i podobnie dla f(x)(y)(z).

Gdyby ta interpretacja była poprawna, Python pozwoliłby mi dynamicznie tworzyć funkcje, które wydają mi się bardzo interesujące. Szukałem w sieci przez ostatnią godzinę, ale nie mogłem znaleźć tropu we właściwym kierunku. Ponieważ nie wiem, jak nazywa się ta koncepcja programowania, może to nie być zaskakujące.

Jak nazywasz tę koncepcję i gdzie mogę przeczytać o niej więcej?

Stefan Mesken
źródło
7
Wygląda na to, że szukasz funkcji
curry
2
Wskazówka: funkcja zagnieżdżona jest tworzona dynamicznie, ma dostęp do ustawień lokalnych swojej funkcji nadrzędnej i może zostać zwrócona jako obiekt (wywoływalny).
Jonathon Reinhart,
@JonathonReinhart Tak właśnie myślałem o problemie. Ale tak naprawdę nie wiedziałem, jak to wdrożyć.
Stefan Mesken
3
Na marginesie: Python z pewnością pozwoli ci dynamicznie tworzyć funkcje. Jeśli jesteś zainteresowany, oto kilka powiązanych pojęć do przeczytania: WP: Funkcje pierwszej klasy | Jak utworzyć funkcję wyższego rzędu w Pythonie? | functools.partial()| WP: Zamknięcia
Lukas Graf
@LukasGraf Rzucę okiem na to. Dziękuję Ci!
Stefan Mesken,

Odpowiedzi:

100

Nie wiem, czy jest to łańcuch funkcji tak samo, jak wywoływalny łańcuch, ale ponieważ funkcje wywoływane, myślę, że nie wyrządzono żadnej szkody. Tak czy inaczej, mogę pomyśleć o zrobieniu tego na dwa sposoby:

Podklasy inti definiowanie __call__:

Pierwszym sposobem byłaby niestandardowa intpodklasa, która określa, __call__która zwraca nowe wystąpienie samej siebie ze zaktualizowaną wartością:

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

Funkcja addmoże być teraz zdefiniowany zwrócić CustomIntinstancji, jak wywoływalnym która zwraca zaktualizowaną wartość sama w sobie, mogą być wywoływane z rzędu:

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

Ponadto, jako intpodklasa, zwrócona wartość utrzymuje się __repr__i __str__zachowanie ints. Jednak w przypadku bardziej złożonych operacji należy odpowiednio zdefiniować inne dundery .

Jak zauważył @Caridorc w komentarzu, addmożna go również zapisać jako:

add = CustomInt 

Zmiana nazwy klasy na addzamiast CustomIntdziała również podobnie.


Zdefiniuj zamknięcie, wymaga dodatkowego wezwania do uzyskania wartości:

Jedyny inny sposób, jaki przychodzi mi do głowy, dotyczy funkcji zagnieżdżonej, która wymaga dodatkowego wywołania pustego argumentu w celu zwrócenia wyniku. Ja nie używając nonlocali opt do mocowania atrybutów do obiektów funkcyjnych, aby go przenośny między pyton:

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 

To w sposób ciągły zwraca siebie ( _inner_adder), które, jeśli valpodano a, zwiększa je ( _inner_adder += val), a jeśli nie, zwraca wartość taką, jaka jest. Jak wspomniałem, wymaga dodatkowego ()wywołania w celu zwrócenia zwiększonej wartości:

>>> add(1)(2)()
3
>>> add(1)(2)(3)()  # and so on..
6
Dimitris Fasarakis Hilliard
źródło
6
W kodzie interaktywnym też add = CostumIntpowinno działać i być prostsze.
Caridorc,
4
Problem z podklasami wbudowanymi polega na tym, że (2*add(1)(2))(3)nie działa, TypeErrorponieważ intnie można wywołać. Zasadniczo CustomIntjest konwertowany na zwykły, intgdy jest używany w dowolnym kontekście, z wyjątkiem wywoływania. Aby uzyskać bardziej solidne rozwiązanie, musisz w zasadzie ponownie zaimplementować wszystkie __*__metody, w tym __r*__wersje ...
Bakuriu
@Caridorc Lub nie nazywaj tego CustomIntw ogóle, ale addpodczas definiowania.
minipif
27

Możesz mnie nienawidzić, ale tutaj jest jedna linijka :)

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

Edycja: OK, jak to działa? Kod jest identyczny z odpowiedzią @Jim, ale wszystko dzieje się w jednej linii.

  1. typemogą być wykorzystane do skonstruowania nowych typów: type(name, bases, dict) -> a new type. Ponieważ namepodajemy pusty ciąg, ponieważ nazwa nie jest w tym przypadku potrzebna. Dla bases(krotka) podajemy znak (int,), który jest identyczny z dziedziczeniem int. dictto atrybuty klas, do których dołączamy __call__lambdę.
  2. self.__class__(self + v) jest identyczny z return CustomInt(self + v)
  3. Nowy typ jest konstruowany i zwracany w zewnętrznej lambdzie.
Jordan Jambazov
źródło
17
Albo jeszcze krócej:class add(int):__call__ = lambda self, v: add(self+v)
Bakuriu
3
Kod wewnątrz klasy jest wykonywany dokładnie tak, jak normalny kod, dzięki czemu można definiować specjalne metody za pomocą przypisań. Jedyną różnicą jest to, że zakres klasy jest nieco ... osobliwy.
Bakuriu,
16

Jeśli chcesz zdefiniować funkcję, która ma być wywoływana wiele razy, najpierw musisz za każdym razem zwrócić wywoływalny obiekt (na przykład funkcję), w przeciwnym razie musisz utworzyć własny obiekt przez zdefiniowanie __call__atrybutu, aby był wywoływalny.

Następnym punktem jest to, że musisz zachować wszystkie argumenty, co w tym przypadku oznacza, że ​​możesz chcieć użyć Coroutines lub funkcji rekurencyjnej. Należy jednak pamiętać, że programy korekcyjne są znacznie bardziej zoptymalizowane / elastyczne niż funkcje rekurencyjne , szczególnie w przypadku takich zadań.

Oto przykładowa funkcja używająca Coroutines, która zachowuje swój najnowszy stan. Zauważ, że nie można go wywołać wiele razy, ponieważ zwracana wartość jest wartością, integerktórej nie można wywołać, ale możesz pomyśleć o przekształceniu tego w oczekiwany obiekt ;-).

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
Kasravnd
źródło
6

Pythonowym sposobem na to byłoby użycie dynamicznych argumentów:

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

To nie jest odpowiedź, której szukasz i możesz o tym wiedzieć, ale pomyślałem, że i tak jej udzielę, ponieważ gdyby ktoś zastanawiał się nad zrobieniem tego nie z ciekawości, ale z pracy. Prawdopodobnie powinni mieć „właściwą rzecz do zrobienia”.

nichochar
źródło
1
Usunąłem twoją notatkę „ PS ”, Nichochar. Wszyscy zdajemy sobie sprawę, jak elegancki jest Python :-) Nie sądzę, aby należał do treści odpowiedzi.
Dimitris Fasarakis Hilliard
6
Myślę, że mógłbyś po prostu zrobić, add = sumgdybyś jechał tą trasą
codykochmann
3

Po prostu:

class add(int):
   def __call__(self, n):
      return add(self + n)
Nicolae
źródło
3

Jeśli chcesz zaakceptować dodatkowy wynik ()w celu pobrania wyniku, możesz użyć functools.partial:

from functools import partial

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

Na przykład:

>>> 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

Umożliwia to również określenie wielu numerów jednocześnie:

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

Jeśli chcesz ograniczyć to do jednego numeru, możesz wykonać następujące czynności:

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

Jeśli chcesz add(x)(y)(z)łatwo zwrócić wynik i być dalej wywoływanym, najlepszym rozwiązaniem intjest klasyfikowanie podklasy .

gość
źródło