ฟังก์ชัน Chaining ใน 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
ดูเหมือนว่าคุณกำลังมองหาฟังก์ชั่น
แกงกะหรี่

2
คำแนะนำ: ฟังก์ชันที่ซ้อนกันจะถูกสร้างขึ้นแบบไดนามิกเข้าถึงท้องถิ่นของฟังก์ชันหลักและสามารถส่งคืนเป็นอ็อบเจ็กต์ (เรียกได้)
Jonathon Reinhart

@JonathonReinhart นั่นคือวิธีที่ฉันคิดเกี่ยวกับปัญหา แต่ฉันไม่เห็นวิธีการใช้งานจริงๆ
Stefan Mesken

3
นอกจากนี้ Python จะช่วยให้คุณสร้างฟังก์ชันแบบไดนามิกได้อย่างแน่นอน หากคุณสนใจโปรดอ่านแนวคิดที่เกี่ยวข้องสองสามข้อต่อไปนี้: WP: First-class functions | คุณสร้างฟังก์ชันลำดับที่สูงขึ้นใน Python ได้อย่างไร? | functools.partial()| WP: Closures
Lukas Graf

@LukasGraf ฉันจะดูมัน ขอขอบคุณ!
Stefan Mesken

คำตอบ:


102

ผมไม่ทราบว่านี้เป็นฟังก์ชั่นการผูกมัดมากที่สุดเท่าที่มันเป็นcallableผูกมัด แต่เนื่องจากฟังก์ชั่นที่มี callables ผมคิดว่ามีอันตรายทำไม่มี ไม่ว่าจะด้วยวิธีใดมีสองวิธีที่ฉันคิดได้:

การจำแนกย่อย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__และ__str__พฤติกรรมของints สำหรับการดำเนินงานที่ซับซ้อนมากขึ้น แต่คุณควรกำหนด dunders อื่น ๆ อย่างเหมาะสม

ดังที่ @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*__เวอร์ชัน ...
Bakuriu

@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(tuple) เราให้ซึ่งเป็นเหมือนการสืบทอด(int,) เป็นแอตทริบิวต์คลาสที่เราแนบแลมด้าintdict__call__
  2. self.__class__(self + v) เหมือนกับ return CustomInt(self + v)
  3. ชนิดใหม่ถูกสร้างและส่งคืนภายในแลมด้าภายนอก

17
หรือสั้นกว่านั้น:class add(int):__call__ = lambda self, v: add(self+v)
Bakuriu

3
โค้ดภายในคลาสจะถูกดำเนินการเหมือนกับโค้ดปกติทุกประการดังนั้นคุณสามารถกำหนดวิธีการพิเศษโดยการกำหนด ความแตกต่างเพียงอย่างเดียวคือขอบเขตของคลาสนั้นค่อนข้าง ... แปลก
Bakuriu

16

หากคุณต้องการกำหนดฟังก์ชันที่จะเรียกใช้หลาย ๆ ครั้งก่อนอื่นคุณต้องส่งคืนอ็อบเจ็กต์ที่เรียกได้ทุกครั้ง (เช่นฟังก์ชัน) มิฉะนั้นคุณต้องสร้างอ็อบเจ็กต์ของคุณเองโดยกำหนด__call__แอ็ตทริบิวต์เพื่อให้สามารถเรียกใช้ได้

ประเด็นต่อไปคือคุณต้องสงวนอาร์กิวเมนต์ทั้งหมดไว้ซึ่งในกรณีนี้หมายความว่าคุณอาจต้องการใช้Coroutinesหรือฟังก์ชันเรียกซ้ำ แต่โปรดทราบว่าCoroutines ได้รับการปรับให้เหมาะสม / ยืดหยุ่นกว่าฟังก์ชันเรียกซ้ำโดยเฉพาะสำหรับงานดังกล่าว

นี่คือฟังก์ชั่นตัวอย่างที่ใช้ Coroutines ซึ่งจะรักษาสถานะล่าสุดของตัวมันเอง โปรดทราบว่าไม่สามารถเรียกได้หลายครั้งเนื่องจากค่าที่ส่งคืนเป็นค่า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

วิธี pythonic ในการทำเช่นนี้คือการใช้อาร์กิวเมนต์แบบไดนามิก:

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

นี่ไม่ใช่คำตอบที่คุณกำลังมองหาและคุณอาจจะรู้ แต่ฉันคิดว่าฉันจะให้มันอยู่ดีเพราะถ้ามีคนสงสัยเกี่ยวกับการทำสิ่งนี้ไม่ใช่เพราะความอยากรู้อยากเห็น แต่เพื่อการทำงาน พวกเขาน่าจะมีคำตอบ "สิ่งที่ควรทำ"


1
ฉันลบโน้ต ' PS ' ของคุณ nichochar เราทุกคนตระหนักดีว่า Python นั้นสง่างามแค่ไหน :-) ฉันไม่คิดว่ามันอยู่ในเนื้อหาของคำตอบ
Dimitris Fasarakis Hilliard

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เป็นวิธีที่จะไป

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.