Encadeamento de funções em Python

90

Em Codewars.com , encontrei a seguinte tarefa:

Crie uma função addque some números quando chamada em sucessão. Portanto, add(1)deve retornar 1, add(1)(2)deve retornar 1+2, ...

Embora esteja familiarizado com o básico do Python, nunca encontrei uma função que pudesse ser chamada em tal sucessão, ou seja, uma função f(x)que pudesse ser chamada como f(x)(y)(z).... Até agora, nem tenho certeza de como interpretar essa notação.

Como matemático, suspeito que f(x)(y)seja uma função que atribui a cada xfunção a g_{x}e depois retorna g_{x}(y)e da mesma forma para f(x)(y)(z).

Se essa interpretação estiver correta, Python me permitiria criar funções dinamicamente que me parecem muito interessantes. Pesquisei na web na última hora, mas não consegui encontrar uma pista na direção certa. Como não sei como esse conceito de programação é chamado, no entanto, isso pode não ser muito surpreendente.

Como você chama esse conceito e onde posso ler mais sobre ele?

Stefan Mesken
fonte
7
Parece que você está procurando funções de currying
OneCricketeer
2
Dica: uma função aninhada é criada dinamicamente, tem acesso aos locais de sua função pai e pode ser retornada como um objeto (que pode ser chamado).
Jonathon Reinhart
@JonathonReinhart É assim que eu estava pensando sobre o problema. Mas eu realmente não vi como implementá-lo.
Stefan Mesken
3
Como um aparte: Python definitivamente permitirá que você crie funções dinamicamente. Se você estiver interessado, aqui estão alguns conceitos relacionados para ler sobre: WP: Funções de primeira classe | Como você faz uma função de ordem superior em Python? | functools.partial()| WP: Encerramentos
Lukas Graf
@LukasGraf Vou dar uma olhada nisso. Obrigado!
Stefan Mesken

Respostas:

102

Não sei se isso é encadeamento de funções tanto quanto é encadeamento chamável , mas, como as funções são chamáveis, acho que não houve nenhum problema. De qualquer forma, posso pensar em fazer isso de duas maneiras:

Subclassificando inte definindo __call__:

A primeira forma seria com uma intsubclasse personalizada que define o __call__que retorna uma nova instância de si mesma com o valor atualizado:

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

A função addagora pode ser definida para retornar uma CustomIntinstância, que, como um chamável que retorna um valor atualizado de si mesma, pode ser chamada em sucessão:

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

Além disso, como uma intsubclasse, o valor devolvido retém a __repr__e __str__comportamento de ints. Para operações mais complexas, porém, você deve definir outros dunders apropriadamente .

Como @Caridorc observou em um comentário, addtambém poderia ser escrito simplesmente como:

add = CustomInt 

Renomear a classe para em addvez de CustomInttambém funciona de maneira semelhante.


Defina um fechamento, requer uma chamada extra para gerar o valor:

A única outra maneira que consigo pensar envolve uma função aninhada que requer uma chamada de argumento vazio extra para retornar o resultado. Estou não usando nonlocale opt para anexar atributos para os objetos de função para torná-lo portátil entre 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 

Este retorna continuamente a si mesmo ( _inner_adder) que, se a valfor fornecido, o incrementa ( _inner_adder += val) e, se não, retorna o valor como está. Como mencionei, ele requer uma ()chamada extra para retornar o valor incrementado:

>>> add(1)(2)()
3
>>> add(1)(2)(3)()  # and so on..
6
Dimitris Fasarakis Hilliard
fonte
6
No código interativo add = CostumIntdeve funcionar também e ser mais simples.
Caridorc de
4
O problema com a criação de subclasses de built-ins é que (2*add(1)(2))(3)falha com um TypeErrorporque intnão pode ser chamado. Basicamente, o CustomInté convertido em simples intquando usado em qualquer contexto, exceto ao chamar. Para uma solução mais robusta, você basicamente tem que __*____r*__
reimplementar
@Caridorc Ou não chame de jeito CustomIntnenhum, mas addao defini-lo.
minipif
27

Você pode me odiar, mas aqui está uma linha :)

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

Edit: Ok, como isso funciona? O código é idêntico ao da resposta do @Jim, mas tudo acontece em uma única linha.

  1. typepode ser usado para construir novos tipos: type(name, bases, dict) -> a new type. Pois namefornecemos uma string vazia, pois o nome não é realmente necessário neste caso. Para bases(tupla), fornecemos um (int,), que é idêntico a herdar int. dictsão os atributos da classe, onde anexamos o __call__lambda.
  2. self.__class__(self + v) é idêntico a return CustomInt(self + v)
  3. O novo tipo é construído e retornado dentro do lambda externo.
Jordan Jambazov
fonte
17
Ou ainda mais curto:class add(int):__call__ = lambda self, v: add(self+v)
Bakuriu
3
O código dentro de uma classe é executado exatamente como o código normal para que você possa definir métodos especiais por atribuições. A única diferença é que o escopo da classe é um pouco ... peculiar.
Bakuriu
16

Se você deseja definir uma função a ser chamada várias vezes, primeiro você precisa retornar um objeto que pode ser chamado a cada vez (por exemplo, uma função), caso contrário, você deve criar seu próprio objeto definindo um __call__atributo, para que ele seja chamado.

O próximo ponto é que você precisa preservar todos os argumentos, o que, neste caso, significa que você pode querer usar corrotinas ou uma função recursiva. Mas observe que as corrotinas são muito mais otimizadas / flexíveis do que as funções recursivas , especialmente para tais tarefas.

Aqui está um exemplo de função usando corrotinas, que preserva o estado mais recente de si mesmo. Observe que ele não pode ser chamado várias vezes, pois o valor de retorno é um integerque não pode ser chamado , mas você pode pensar em transformá-lo em seu objeto esperado ;-).

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

A maneira pythônica de fazer isso seria usar argumentos dinâmicos:

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

Esta não é a resposta que você está procurando, e você deve saber disso, mas pensei em respondê-la de qualquer maneira, porque se alguém estivesse pensando em fazer isso não por curiosidade, mas por trabalho. Eles provavelmente devem ter a resposta "a coisa certa a fazer".

nicochar
fonte
1
Removi sua nota ' PS ', nichochar. Todos nós sabemos como o Python é elegante :-) Não acho que ele faça parte do corpo da resposta.
Dimitris Fasarakis Hilliard
6
Eu realmente acho que você poderia ter feito add = sumse fosse por esse caminho
codykochmann,
6

Simplesmente:

class add(int):
   def __call__(self, n):
      return add(self + n)
Nicolae
fonte
1
Esta é facilmente a melhor resposta!
MEMark
6

Se estiver disposto a aceitar um adicional ()para recuperar o resultado, você pode usar functools.partial:

from functools import partial

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

Por exemplo:

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

Isso também permite especificar vários números de uma vez:

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

Se quiser restringir a um único número, você pode fazer o seguinte:

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

Se você deseja add(x)(y)(z)retornar prontamente o resultado e ser posteriormente chamado, a subclasse inté o caminho a percorrer.

um convidado
fonte