Chuỗi hàm trong Python


90

Trên Codewars.com, tôi gặp phải tác vụ sau:

Tạo một hàm cộng addcác số lại với nhau khi được gọi liên tiếp. Vì vậy add(1)nên trở về 1, add(1)(2)nên trở về 1+2, ...

Mặc dù tôi đã quen thuộc với những điều cơ bản về Python, nhưng tôi chưa bao giờ gặp một hàm có thể được gọi liên tiếp như vậy, tức là một hàm f(x)có thể được gọi như vậy f(x)(y)(z).... Cho đến nay, tôi thậm chí không chắc làm thế nào để giải thích ký hiệu này.

Là một nhà toán học, tôi nghi ngờ rằng đó f(x)(y)là một hàm gán cho mọi xhàm g_{x}rồi trả về g_{x}(y)và tương tự như vậy cho f(x)(y)(z).

Nếu cách diễn giải này đúng, Python sẽ cho phép tôi tạo động các hàm có vẻ rất thú vị đối với tôi. Tôi đã tìm kiếm trên web trong một giờ qua, nhưng không thể tìm thấy khách hàng tiềm năng đi đúng hướng. Vì tôi không biết khái niệm lập trình này được gọi như thế nào, tuy nhiên, điều này có thể không quá ngạc nhiên.

Bạn gọi khái niệm này như thế nào và tôi có thể đọc thêm về nó ở đâu?


7
Trông giống như bạn đang tìm kiếm chức năng currying
OneCricketeer

2
Gợi ý: Một hàm lồng nhau được tạo động, có quyền truy cập vào các địa phương của hàm mẹ của nó và có thể được trả về dưới dạng một đối tượng (có thể gọi).
Jonathon Reinhart

@JonathonReinhart Đó là cách tôi nghĩ về vấn đề. Nhưng tôi không thực sự thấy làm thế nào để thực hiện nó.
Stefan Mesken

3
Ngoài ra: Python chắc chắn sẽ cho phép bạn tạo động các hàm. Nếu bạn quan tâm, đây là một số khái niệm liên quan để đọc thêm: WP: Các hàm hạng nhất | Làm cách nào để bạn tạo một hàm bậc cao hơn trong Python? | functools.partial()| WP: Closures
Lukas Graf

@LukasGraf Tôi sẽ xem qua. Cảm ơn bạn!
Stefan Mesken

Câu trả lời:


100

Tôi không biết liệu đây có phải là chuỗi hàm nhiều như chuỗi có thể gọi hay không , nhưng, vì các hàm chuỗi thể gọi nên tôi đoán không có hại gì. Dù bằng cách nào, có hai cách tôi có thể nghĩ để làm điều này:

Phân loại phụ intvà định nghĩa __call__:

Cách đầu tiên sẽ là với một intlớp con tùy chỉnh xác định lớp __call__này trả về một thể hiện mới của chính nó với giá trị được cập nhật:

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

addBây giờ, hàm có thể được định nghĩa để trả về một CustomIntthể hiện, dưới dạng một thể hiện có thể gọi trả về giá trị cập nhật của chính nó, có thể được gọi liên tiếp:

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

Ngoài ra, là một intlớp con, giá trị trả về vẫn giữ nguyên trạng thái __repr____str__hành vi của ints. Tuy nhiên, đối với các hoạt động phức tạp hơn, bạn nên xác định các lỗi khác một cách thích hợp .

Như @Caridorc đã lưu ý trong một nhận xét, addcũng có thể được viết đơn giản là:

add = CustomInt 

Đổi tên lớp thành addthay vì CustomIntcũng hoạt động tương tự.


Xác định một bao đóng, yêu cầu thêm lệnh gọi để mang lại giá trị:

Cách duy nhất tôi có thể nghĩ đến là liên quan đến một hàm lồng nhau yêu cầu thêm một lệnh gọi đối số trống để trả về kết quả. Tôi không sử dụng nonlocalvà chọn gắn các thuộc tính vào các đối tượng hàm để làm cho nó di động giữa các 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 

Điều này liên tục trả về chính nó ( _inner_adder), nếu a valđược cung cấp, nó sẽ tăng lên (_inner_adder += val ) và nếu không, trả về giá trị như ban đầu. Giống như tôi đã đề cập, nó yêu cầu một ()lệnh gọi bổ sung để trả về giá trị tăng dần:

>>> add(1)(2)()
3
>>> add(1)(2)(3)()  # and so on..
6

6
Trong mã tương tác add = CostumIntcũng nên hoạt động và đơn giản hơn.
Caridorc

4
Vấn đề với tích hợp lớp con là (2*add(1)(2))(3)không thành công với TypeErrorbởi vì intkhông thể gọi được. Về cơ bản, CustomIntđược chuyển đổi thành đơn giản intkhi được sử dụng trong bất kỳ ngữ cảnh nào ngoại trừ khi gọi. Đối với một giải pháp mạnh mẽ hơn về cơ bản bạn phải tái thực hiện tất cả __*__các phương pháp bao gồm các __r*__phiên bản ...
Bakuriu

@Caridorc Hoặc không gọi nó CustomIntnhưng addkhi định nghĩa nó.
minipif

27

Bạn có thể ghét tôi, nhưng đây là một lớp lót :)

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

Chỉnh sửa: Ok, cách này hoạt động? Mã này giống với câu trả lời của @Jim, nhưng mọi thứ diễn ra trên một dòng duy nhất.

  1. typecó thể được sử dụng để xây dựng các loại mới: type(name, bases, dict) -> a new type. Vì namechúng tôi cung cấp chuỗi trống, vì tên không thực sự cần thiết trong trường hợp này. Đối với bases(tuple), chúng tôi cung cấp một (int,), giống như kế thừa int. dictlà các thuộc tính của lớp, nơi chúng tôi đính kèm __call__lambda.
  2. self.__class__(self + v) giống hệt với return CustomInt(self + v)
  3. Kiểu mới được xây dựng và trả về bên trong lambda bên ngoài.

17
Hoặc thậm chí ngắn hơn:class add(int):__call__ = lambda self, v: add(self+v)
Bakuriu

3
Mã bên trong một lớp được thực thi chính xác như mã bình thường để bạn có thể xác định các phương thức đặc biệt bằng các phép gán. Sự khác biệt duy nhất là phạm vi lớp hơi ... đặc biệt.
Bakuriu

16

Nếu bạn muốn xác định một hàm được gọi nhiều lần, trước tiên, bạn cần trả về một đối tượng có thể gọi mỗi lần (ví dụ một hàm), nếu không bạn phải tạo đối tượng của riêng mình bằng cách xác định một __call__thuộc tính, để nó có thể gọi được.

Điểm tiếp theo là bạn cần bảo toàn tất cả các đối số, trong trường hợp này có nghĩa là bạn có thể muốn sử dụng Coroutines hoặc một hàm đệ quy. Nhưng lưu ý rằng Coroutines được tối ưu hóa / linh hoạt hơn nhiều so với các hàm đệ quy , đặc biệt cho các tác vụ như vậy.

Đây là một chức năng mẫu sử dụng Coroutines, chức năng này duy trì trạng thái mới nhất của chính nó. Lưu ý rằng nó không thể được gọi nhiều lần vì giá trị trả về là một giá trị integerkhông thể gọi được, nhưng bạn có thể nghĩ đến việc biến nó thành đối tượng mong đợi của bạn ;-).

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

Cách pythonic để làm điều này là sử dụng các đối số động:

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

Đây không phải là câu trả lời mà bạn đang tìm kiếm, và bạn có thể biết điều này, nhưng tôi nghĩ dù sao thì tôi cũng sẽ đưa ra vì nếu ai đó băn khoăn về việc làm điều này không phải vì tò mò mà vì công việc. Họ có lẽ nên có câu trả lời "việc phải làm".


1
Tôi đã xóa ghi chú ' PS ' của bạn , nichochar. Tất cả chúng ta đều biết Python thanh lịch như thế nào :-) Tôi không nghĩ rằng nó thuộc về phần nội dung của câu trả lời.
Dimitris Fasarakis Hilliard,

6
Tôi nghĩ bạn có thể vừa thực hiện add = sumnếu sẽ đi là con đường
codykochmann

3

Đơn giản:

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

3

Nếu bạn sẵn sàng chấp nhận một bổ sung ()để lấy kết quả, bạn có thể sử dụng functools.partial:

from functools import partial

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

Ví dụ:

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

Điều này cũng cho phép chỉ định nhiều số cùng một lúc:

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

Nếu bạn muốn giới hạn nó ở một số duy nhất, bạn có thể làm như sau:

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

Nếu bạn muốn add(x)(y)(z)trả về kết quả một cách dễ dàng có thể được gọi thêm thì phân loại con intlà cách tốt nhất.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.