Rantai fungsi dengan Python

90

Di Codewars.com saya mengalami tugas berikut:

Buat fungsi addyang menjumlahkan angka saat dipanggil secara berurutan. Jadi add(1)harus kembali 1, add(1)(2)harus kembali 1+2, ...

Meskipun saya terbiasa dengan dasar-dasar Python, saya belum pernah menemukan fungsi yang dapat dipanggil secara berurutan, yaitu fungsi f(x)yang dapat disebut sebagai f(x)(y)(z).... Sejauh ini, saya bahkan tidak yakin bagaimana menafsirkan notasi ini.

Sebagai ahli matematika, saya menduga itu f(x)(y)adalah fungsi yang menetapkan ke setiap xfungsi g_{x}dan kemudian mengembalikan g_{x}(y)dan juga untuk f(x)(y)(z).

Jika interpretasi ini benar, Python akan memungkinkan saya untuk membuat fungsi secara dinamis yang tampaknya sangat menarik bagi saya. Saya telah menelusuri web selama satu jam terakhir, tetapi tidak dapat menemukan petunjuk ke arah yang benar. Karena saya tidak tahu bagaimana konsep pemrograman ini disebut, bagaimanapun, ini mungkin tidak terlalu mengejutkan.

Bagaimana Anda menyebut konsep ini dan di mana saya dapat membaca lebih banyak tentangnya?

Stefan Mesken
sumber
7
Sepertinya Anda mencari acara kari
OneCricketeer
2
Petunjuk: Fungsi bersarang dibuat secara dinamis, memiliki akses ke fungsi induknya, dan dapat dikembalikan sebagai objek (dapat dipanggil).
Jonathon Reinhart
@JonathonReinhart Itulah cara saya memikirkan masalah. Tapi saya tidak begitu paham bagaimana menerapkannya.
Stefan Mesken
3
Sebagai tambahan: Python pasti akan memungkinkan Anda untuk membuat fungsi secara dinamis. Jika Anda tertarik, berikut ini beberapa konsep terkait untuk dibaca: WP: Fungsi kelas satu | Bagaimana Anda membuat fungsi urutan yang lebih tinggi dengan Python? | functools.partial()| WP: Penutupan
Lukas Graf
@LukasGraf Saya akan melihatnya. Terima kasih!
Stefan Mesken

Jawaban:

100

Saya tidak tahu apakah ini function chaining sebanyak callable chaining, tapi, karena function adalah callable, saya rasa tidak ada kerusakan yang terjadi. Bagaimanapun, ada dua cara yang dapat saya pikirkan untuk melakukan ini:

Sub-klasifikasi intdan definisi __call__:

Cara pertama adalah dengan intsubkelas khusus yang menentukan __call__mana yang mengembalikan instance baru dengan nilai yang diperbarui:

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

Fungsi addsekarang dapat didefinisikan untuk mengembalikan sebuah CustomIntinstance, yang, sebagai callable yang mengembalikan nilai yang diperbarui, dapat dipanggil secara berurutan:

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

Selain itu, sebagai intsubclass, nilai yang dikembalikan mempertahankan __repr__dan __str__perilaku ints. Untuk operasi yang lebih kompleks, Anda harus mendefinisikan dunder lain dengan tepat .

Seperti yang dicatat @Caridorc dalam komentar, addbisa juga ditulis sebagai:

add = CustomInt 

Mengganti nama kelas menjadi addbukannya CustomIntjuga berfungsi serupa.


Tentukan closure, membutuhkan panggilan ekstra untuk menghasilkan nilai:

Satu-satunya cara lain yang dapat saya pikirkan melibatkan fungsi bersarang yang membutuhkan panggilan argumen kosong ekstra untuk mengembalikan hasilnya. Saya tidak menggunakan nonlocaldan memilih untuk melampirkan atribut ke objek fungsi agar portabel di antara 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 

Ini terus menerus mengembalikan dirinya sendiri ( _inner_adder) yang, jika a valdiberikan, menambahnya ( _inner_adder += val) dan jika tidak, mengembalikan nilai apa adanya. Seperti yang saya sebutkan, ini membutuhkan ()panggilan ekstra untuk mengembalikan nilai yang bertambah:

>>> add(1)(2)()
3
>>> add(1)(2)(3)()  # and so on..
6
Dimitris Fasarakis Hilliard
sumber
6
Dalam kode interaktif add = CostumIntharus bekerja juga dan lebih sederhana.
Caridorc
4
Masalah dengan subclassing built-in adalah yang (2*add(1)(2))(3)gagal dengan TypeErrorkarena inttidak dapat dipanggil. Pada dasarnya CustomIntdiubah menjadi polos intsaat digunakan dalam konteks apa pun kecuali saat memanggil. Untuk solusi yang lebih kuat pada dasarnya Anda harus menerapkan ulang semua __*__metode termasuk __r*__versi ...
Bakuriu
@Caridorc Atau jangan menyebutnya CustomIntsama sekali kecuali addsaat mendefinisikannya.
minipif
27

Anda bisa membenci saya, tapi ini satu kalimat :)

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

Edit: Ok, bagaimana ini bekerja? Kode tersebut identik dengan jawaban @Jim, tetapi semuanya terjadi dalam satu baris.

  1. typedapat digunakan untuk membangun jenis baru: type(name, bases, dict) -> a new type. Karena namekami menyediakan string kosong, karena nama tidak terlalu dibutuhkan dalam kasus ini. Untuk bases(tuple) kami menyediakan (int,), yang identik dengan mewarisi int. dictadalah atribut kelas, tempat kami melampirkan __call__lambda.
  2. self.__class__(self + v) identik dengan return CustomInt(self + v)
  3. Tipe baru dibangun dan dikembalikan dalam lambda luar.
Jordan Jambazov
sumber
17
Atau bahkan lebih pendek:class add(int):__call__ = lambda self, v: add(self+v)
Bakuriu
3
Kode di dalam kelas dijalankan persis seperti kode normal sehingga Anda dapat menentukan metode khusus berdasarkan tugas. Satu-satunya perbedaan adalah bahwa cakupan kelasnya agak ... aneh.
Bakuriu
16

Jika Anda ingin mendefinisikan suatu fungsi untuk dipanggil beberapa kali, pertama-tama Anda perlu mengembalikan objek yang dapat dipanggil setiap kali (misalnya fungsi) jika tidak Anda harus membuat objek Anda sendiri dengan mendefinisikan __call__atribut, agar dapat dipanggil.

Poin berikutnya adalah Anda perlu mempertahankan semua argumen, yang dalam hal ini berarti Anda mungkin ingin menggunakan Coroutine atau fungsi rekursif. Tetapi perhatikan bahwa Coroutine jauh lebih dioptimalkan / fleksibel daripada fungsi rekursif , khususnya untuk tugas-tugas semacam itu.

Berikut adalah contoh fungsi yang menggunakan Coroutines, yang mempertahankan status terbaru itu sendiri. Perhatikan bahwa itu tidak dapat dipanggil berkali-kali karena nilai yang dikembalikan adalah integeryang tidak dapat dipanggil, tetapi Anda mungkin berpikir untuk mengubahnya menjadi objek yang Anda harapkan ;-).

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

Cara pythonic untuk melakukan ini adalah dengan menggunakan argumen dinamis:

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

Ini bukan jawaban yang Anda cari, dan Anda mungkin tahu ini, tetapi saya pikir saya akan tetap memberikannya karena jika seseorang bertanya-tanya tentang melakukan ini bukan karena penasaran tetapi untuk pekerjaan. Mereka mungkin harus memiliki jawaban "yang benar untuk dilakukan".

nichochar
sumber
1
Saya menghapus catatan ' PS ' Anda, nichochar. Kita semua menyadari betapa elegannya Python :-) Saya rasa itu tidak termasuk dalam isi jawaban.
Dimitris Fasarakis Hilliard
6
Saya pikir Anda bisa melakukannya add = sumjika pergi ke rute itu
codykochmann
4

Jika Anda bersedia menerima tambahan ()untuk mendapatkan hasil, Anda dapat menggunakan functools.partial:

from functools import partial

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

Sebagai contoh:

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

Ini juga memungkinkan menentukan beberapa nomor sekaligus:

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

Jika Anda ingin membatasinya ke satu nomor, Anda dapat melakukan hal berikut:

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

Jika Anda ingin add(x)(y)(z)segera mengembalikan hasilnya dan dapat dipanggil lebih lanjut maka sub-klasifikasi intadalah cara yang harus dilakukan.

seorang tamu
sumber
3

Secara sederhana:

class add(int):
   def __call__(self, n):
      return add(self + n)
Nicolae
sumber