Python __call__ ตัวอย่างวิธีการพิเศษ


159

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

ฉันจะขอบคุณมากถ้ามีคนให้ฉันใช้วิธีพิเศษนี้



8
การทำงานของ_call_เป็นเช่นเดียวกับผู้ประกอบการมากเกินไปของ () ใน C ++ หากคุณเพียงสร้างวิธีการใหม่นอกชั้นเรียนคุณอาจไม่สามารถเข้าถึงข้อมูลภายในในชั้นเรียนได้
andy

2
การใช้งานทั่วไปส่วนใหญ่__call__ถูกซ่อนอยู่ในมุมมองธรรมดา มันเป็นวิธีการที่คุณยกตัวอย่างชั้นเรียน: x = Foo()เป็นจริงx = type(Foo).__call__(Foo)ที่__call__ถูกกำหนดโดย metaclass Fooของ
chepner

คำตอบ:


88

โมดูลรูปแบบ Django ใช้__call__วิธีการอย่างดีในการใช้ API ที่สอดคล้องกันสำหรับการตรวจสอบแบบฟอร์ม คุณสามารถเขียน validator ของคุณเองสำหรับแบบฟอร์มใน Django เป็นฟังก์ชั่น

def custom_validator(value):
    #your validation logic

Django มีเครื่องมือตรวจสอบความถูกต้องในตัวที่เป็นค่าเริ่มต้นเช่นเครื่องมือตรวจสอบอีเมล, เครื่องมือตรวจสอบความถูกต้องของ URL เป็นต้นซึ่งอยู่ภายใต้การตรวจสอบของ RegEx เพื่อนำไปใช้อย่างหมดจดเหล่านี้ Django ใช้คลาสที่เรียกได้ (แทนที่จะเป็นฟังก์ชั่น) มันใช้ตรรกะการตรวจสอบ Regex เริ่มต้นใน RegexValidator แล้วขยายชั้นเรียนเหล่านี้สำหรับการตรวจสอบอื่น ๆ

class RegexValidator(object):
    def __call__(self, value):
        # validation logic

class URLValidator(RegexValidator):
    def __call__(self, value):
        super(URLValidator, self).__call__(value)
        #additional logic

class EmailValidator(RegexValidator):
    # some logic

ตอนนี้ทั้งฟังก์ชั่นที่กำหนดเองของคุณและ EmailValidator ในตัวสามารถเรียกใช้ด้วยไวยากรณ์เดียวกัน

for v in [custom_validator, EmailValidator()]:
    v(value) # <-----

อย่างที่คุณเห็นการดำเนินการใน Django นี้คล้ายกับสิ่งที่คนอื่น ๆ อธิบายไว้ในคำตอบด้านล่าง สามารถนำไปใช้ในทางอื่นได้หรือไม่? คุณสามารถทำได้ แต่ IMHO จะไม่สามารถอ่านได้หรือขยายได้ง่ายสำหรับกรอบใหญ่เช่น Django


5
ดังนั้นหากใช้อย่างถูกต้องจะทำให้โค้ดอ่านง่ายขึ้น ฉันคิดว่ามันถูกใช้ผิดที่มันจะทำให้โค้ดอ่านไม่ได้เช่นกัน
mohi666

15
นี่คือตัวอย่างของวิธีการที่จะสามารถนำมาใช้ แต่ไม่ได้เป็นหนึ่งที่ดีในความคิดของฉัน ในกรณีนี้ไม่มีประโยชน์ที่จะมีอินสแตนซ์ที่เรียกได้ มันจะเป็นการดีกว่าถ้ามีคลาสอินเตอร์เฟส / นามธรรมพร้อมเมธอดเช่น .validate (); มันเป็นสิ่งเดียวกันเท่านั้นที่ชัดเจนยิ่งขึ้น มูลค่าที่แท้จริงของ __call__ สามารถใช้อินสแตนซ์ในสถานที่ที่คาดว่าจะสามารถโทรได้ ฉันใช้ __call__ บ่อยที่สุดเมื่อสร้างมัณฑนากร
Daniel

120

ตัวอย่างนี้ใช้การบันทึกช่วยจำโดยทั่วไปเก็บค่าไว้ในตาราง (พจนานุกรมในกรณีนี้) เพื่อให้คุณสามารถค้นหาได้ในภายหลังแทนที่จะคำนวณใหม่

ที่นี่เราใช้คลาสที่เรียบง่ายพร้อม__call__วิธีการคำนวณแฟคทอเรียล (ผ่านวัตถุที่เรียกได้ ) แทนฟังก์ชันแฟกทอเรียลที่มีตัวแปรแบบคงที่

class Factorial:
    def __init__(self):
        self.cache = {}
    def __call__(self, n):
        if n not in self.cache:
            if n == 0:
                self.cache[n] = 1
            else:
                self.cache[n] = n * self.__call__(n-1)
        return self.cache[n]

fact = Factorial()

ตอนนี้คุณมีfactวัตถุที่สามารถเรียกใช้ได้เช่นเดียวกับฟังก์ชั่นอื่น ๆ ตัวอย่างเช่น

for i in xrange(10):                                                             
    print("{}! = {}".format(i, fact(i)))

# output
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880

และมันก็เป็นของรัฐด้วย


2
ฉันอยากมีfactวัตถุที่สามารถจัดทำดัชนีได้เนื่องจาก__call__ฟังก์ชั่นของคุณเป็นดัชนี ก็จะใช้รายการแทน dict แต่นั่นเป็นเพียงฉัน
Chris Lutz

4
@delnan - เกือบทุกอย่างสามารถทำได้หลายวิธีที่แตกต่างกัน คนที่อ่านได้มากขึ้นขึ้นอยู่กับผู้อ่าน
Chris Lutz

1
@Chris Lutz: คุณมีอิสระที่จะพิจารณาการเปลี่ยนแปลงเหล่านั้น สำหรับการบันทึกโดยทั่วไปพจนานุกรมทำงานได้ดีเพราะคุณไม่สามารถรับประกันลำดับที่สิ่งต่าง ๆ เติมเต็มรายการของคุณได้ ในกรณีนี้รายการอาจใช้งานได้ แต่จะไม่เร็วกว่าหรือง่ายกว่า
S.Lott

8
@delnan: นี่ไม่ได้ตั้งใจที่จะสั้นที่สุด ไม่มีใครชนะที่รหัสกอล์ฟ มันตั้งใจที่จะแสดงให้__call__เป็นเรื่องง่ายและไม่มีอะไรเพิ่มเติม
S.Lott

3
แต่มันเป็นตัวอย่างของซากปรักหักพังเมื่อเทคนิคที่แสดงไม่เหมาะกับงานใช่ไหม? (และฉันไม่ได้เกี่ยวกับ "ขอบันทึกบรรทัดสำหรับ heck ของมัน" - สั้นฉันกำลังพูดถึง "เขียนในลักษณะที่ชัดเจนเท่ากันและบันทึกรหัสสำเร็จรูปบาง" - สั้นมั่นใจได้ว่าฉันไม่ได้ หนึ่งในบรรดาคนบ้าที่พยายามเขียนรหัสที่สั้นที่สุดเท่าที่จะทำได้ฉันแค่ต้องการหลีกเลี่ยงรหัสสำเร็จรูปที่ไม่ได้เพิ่มสิ่งใดสำหรับผู้อ่าน)

40

ฉันพบว่ามีประโยชน์เพราะช่วยให้ฉันสามารถสร้าง API ที่ใช้งานง่าย (คุณมีบางวัตถุที่เรียกได้ซึ่งต้องมีอาร์กิวเมนต์เฉพาะ) และใช้งานได้ง่ายเพราะคุณสามารถใช้วิธีปฏิบัติที่มุ่งเน้นวัตถุ

ต่อไปนี้เป็นรหัสที่ฉันเขียนเมื่อวานนี้ที่ทำให้รุ่นของhashlib.fooวิธีการที่แฮชไฟล์ทั้งหมดมากกว่าสตริง:

# filehash.py
import hashlib


class Hasher(object):
    """
    A wrapper around the hashlib hash algorithms that allows an entire file to
    be hashed in a chunked manner.
    """
    def __init__(self, algorithm):
        self.algorithm = algorithm

    def __call__(self, file):
        hash = self.algorithm()
        with open(file, 'rb') as f:
            for chunk in iter(lambda: f.read(4096), ''):
                hash.update(chunk)
        return hash.hexdigest()


md5    = Hasher(hashlib.md5)
sha1   = Hasher(hashlib.sha1)
sha224 = Hasher(hashlib.sha224)
sha256 = Hasher(hashlib.sha256)
sha384 = Hasher(hashlib.sha384)
sha512 = Hasher(hashlib.sha512)

การใช้งานนี้ทำให้ฉันสามารถใช้ฟังก์ชั่นในลักษณะที่คล้ายกับhashlib.fooฟังก์ชั่น:

from filehash import sha1
print sha1('somefile.txt')

แน่นอนว่าฉันสามารถนำไปใช้ในทางที่ต่างออกไป แต่ในกรณีนี้มันดูเหมือนเป็นวิธีที่ง่าย


7
อีกครั้งการปิดทำลายตัวอย่างนี้ pastebin.com/961vU0ayเป็น 80% ของเส้นและชัดเจน

8
ฉันไม่มั่นใจว่ามันจะชัดเจนสำหรับใครบางคนเสมอ (เช่นบางทีคนที่ใช้ Java เท่านั้น) ฟังก์ชันที่ซ้อนกันและการค้นหา / ขอบเขตของตัวแปรอาจทำให้เกิดความสับสน ฉันคิดว่าประเด็นของฉันคือ__call__ให้เครื่องมือที่ช่วยให้คุณใช้เทคนิค OO เพื่อแก้ปัญหา
bradley.ayers

4
ฉันคิดว่าคำถาม "ทำไมต้องใช้ X มากกว่า Y" เมื่อทั้งคู่มีฟังก์ชันการทำงานที่เทียบเท่ากัน สำหรับบางคนวิธีการของ OO นั้นง่ายกว่าที่จะเข้าใจ ไม่มีข้อโต้แย้งที่น่าสนใจที่จะใช้สิ่งใดสิ่งหนึ่งเหนือสิ่งอื่นเว้นแต่ว่าคุณมีสถานการณ์ที่คุณต้องใช้isinstanceหรือสิ่งที่คล้ายกัน
bradley.ayers

2
@delnan ตัวอย่างการปิดของคุณคือบรรทัดของโค้ดที่น้อยลง แต่มันก็ชัดเจนว่าเป็นการยากที่จะโต้แย้ง
เดนนิส

8
ตัวอย่างที่คุณต้องการใช้__call__วิธีการแทนการปิดคือเมื่อคุณกำลังจัดการกับโมดูลหลายตัวประมวลผลซึ่งใช้การดองเพื่อส่งผ่านข้อมูลระหว่างกระบวนการ คุณไม่สามารถเก็บผักตบชวา แต่คุณสามารถเก็บตัวอย่างของชั้นเรียนได้
John Peter Thompson Garcés

21

__call__นอกจากนี้ยังใช้ในการใช้ชั้นเรียนมัณฑนากรในหลาม ในกรณีนี้อินสแตนซ์ของคลาสจะถูกเรียกเมื่อมีการเรียกเมธอดด้วย decorator

class EnterExitParam(object):

    def __init__(self, p1):
        self.p1 = p1

    def __call__(self, f):
        def new_f():
            print("Entering", f.__name__)
            print("p1=", self.p1)
            f()
            print("Leaving", f.__name__)
        return new_f


@EnterExitParam("foo bar")
def hello():
    print("Hello")


if __name__ == "__main__":
    hello()

9

ใช่เมื่อคุณรู้ว่าคุณกำลังเผชิญกับวัตถุเป็นไปได้อย่างสมบูรณ์ (และในหลายกรณีแนะนำให้ใช้) เพื่อใช้การเรียกใช้เมธอดอย่างชัดเจน อย่างไรก็ตามบางครั้งคุณจัดการกับรหัสที่คาดว่าวัตถุที่เรียกได้ - โดยทั่วไปแล้วจะทำงานได้ แต่ต้องขอบคุณที่__call__คุณสามารถสร้างวัตถุที่ซับซ้อนมากขึ้นด้วยข้อมูลตัวอย่างและวิธีการเพิ่มเติมเพื่อมอบหมายงานซ้ำ ๆ ฯลฯ ที่ยังคงเรียกได้

นอกจากนี้บางครั้งคุณกำลังใช้วัตถุทั้งสองสำหรับงานที่ซับซ้อน (ที่เหมาะสมในการเขียนคลาสเฉพาะ) และวัตถุสำหรับงานง่าย ๆ (ที่มีอยู่แล้วในฟังก์ชั่นหรือเขียนได้ง่ายกว่าเป็นฟังก์ชัน) ในการมีอินเทอร์เฟซทั่วไปคุณต้องเขียนคลาสเล็ก ๆ ที่ห่อฟังก์ชั่นเหล่านั้นด้วยอินเทอร์เฟซที่คาดหวังหรือคุณใช้ฟังก์ชั่นฟังก์ชั่นและทำให้วัตถุที่ซับซ้อนมากขึ้นเรียกได้ ลองมาดูตัวอย่างกัน Threadวัตถุจากโมดูลไลบรารีมาตรฐานthreadingต้องการ callable เป็นtargetอาร์กิวเมนต์ (เช่นการกระทำที่จะต้องทำในหัวข้อใหม่) ด้วยวัตถุที่เรียกได้คุณจะไม่ จำกัด ฟังก์ชั่นคุณสามารถผ่านวัตถุอื่นเช่นกันเช่นผู้ทำงานที่ค่อนข้างซับซ้อนที่ได้รับงานที่ต้องทำจากเธรดอื่นและดำเนินการตามลำดับ:

class Worker(object):
    def __init__(self, *args, **kwargs):
        self.queue = queue.Queue()
        self.args = args
        self.kwargs = kwargs

    def add_task(self, task):
        self.queue.put(task)

    def __call__(self):
        while True:
            next_action = self.queue.get()
            success = next_action(*self.args, **self.kwargs)
            if not success:
               self.add_task(next_action)

นี่เป็นเพียงตัวอย่างจากส่วนหัวของฉัน แต่ฉันคิดว่ามันซับซ้อนพอที่จะรับประกันชั้นเรียนได้ การทำสิ่งนี้ด้วยฟังก์ชั่นนั้นทำได้ยากอย่างน้อยมันก็ต้องใช้สองฟังก์ชั่นคืนและมันก็ซับซ้อนขึ้นเรื่อย ๆ หนึ่งสามารถเปลี่ยนชื่อ__call__เป็นอย่างอื่นและผ่านวิธีการที่ถูกผูกไว้ แต่ที่ทำให้รหัสสร้างด้ายชัดเจนน้อยกว่าเล็กน้อยและไม่เพิ่มมูลค่าใด ๆ


3
อาจเป็นประโยชน์ในการใช้วลี "การพิมพ์เป็ด" ( en.wikipedia.org/wiki/Duck_typing#In_Python ) ที่นี่ - คุณสามารถเลียนแบบฟังก์ชันโดยใช้คลาสวัตถุที่ซับซ้อนมากขึ้นด้วยวิธีนี้
Andrew Jaffe

2
เป็นตัวอย่างที่เกี่ยวข้องฉันเคยเห็น__call__ใช้คลาสอินสแตนซ์ (แทนฟังก์ชัน) เป็นโปรแกรม WSGI นี่คือตัวอย่างจาก "คู่มือการแตกหักของเสา": การใช้อินสแตนซ์ของคลาส
Josh Rosen

5

มัณฑนากรตามคลาสใช้__call__เพื่ออ้างอิงฟังก์ชันที่พันไว้ เช่น:

class Deco(object):
    def __init__(self,f):
        self.f = f
    def __call__(self, *args, **kwargs):
        print args
        print kwargs
        self.f(*args, **kwargs)

มีคำอธิบายที่ดีเกี่ยวกับตัวเลือกต่าง ๆ ที่นี่ที่Artima.com


ฉันไม่ค่อยเห็นนักตกแต่งชั้นเรียนเนื่องจากพวกเขาต้องการรหัสสำเร็จรูปที่ไม่มีความชัดเจนในการทำงานกับวิธีการต่างๆ

4

__call__วิธีการและการปิดIMHO ทำให้เรามีวิธีธรรมชาติในการสร้างรูปแบบการออกแบบ STRATEGY ใน Python เรากำหนดตระกูลของอัลกอริธึมห่อหุ้มแต่ละอันทำให้พวกมันแทนกันได้และในที่สุดเราก็สามารถดำเนินการตามขั้นตอนทั่วไปและตัวอย่างเช่นคำนวณแฮชสำหรับไฟล์


4

ฉันสะดุดกับการใช้งาน__call__()คอนเสิร์ต__getattr__()ซึ่งฉันคิดว่าสวยงาม อนุญาตให้คุณซ่อน API JSON / HTTP / (อย่างไรก็ตาม_serialized) API หลายระดับภายในวัตถุ

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

โดยใช้รูปแบบนี้คุณสามารถเช่นโทรออกเช่นapi.v2.volumes.ssd.update(size=20)ซึ่งจะสิ้นสุดลงในคำขอ PUT https://some.tld/api/v2/volumes/ssd/updateให้

รหัสเฉพาะคือไดรเวอร์หน่วยเก็บข้อมูลแบบบล็อกสำหรับไดรฟ์ข้อมูลแบ็คเอนด์ที่แน่นอนใน OpenStack คุณสามารถตรวจสอบได้ที่นี่: https://github.com/openstack/cinder/blob/master/cinder/volume/drivers/nexenta/jsonrpc.py

แก้ไข:อัปเดตลิงก์ให้ชี้ไปที่การแก้ไขหลัก


เยี่ยมมาก ฉันเคยใช้กลไกเดียวกันสำหรับการสำรวจทรี XML โดยใช้การเข้าถึงคุณลักษณะ
Petri

1

ระบุ__metaclass__และแทนที่__call__เมธอดและให้__new__เมธอดคลาสคลาสที่ระบุส่งคืนอินสแตนซ์ของคลาสวิโอลาที่คุณมี "ฟังก์ชัน" พร้อมเมธอด


1

เราสามารถใช้__call__วิธีการเพื่อใช้วิธีการเรียนอื่น ๆ เป็นวิธีการคงที่

    class _Callable:
        def __init__(self, anycallable):
            self.__call__ = anycallable

    class Model:

        def get_instance(conn, table_name):

            """ do something"""

        get_instance = _Callable(get_instance)

    provs_fac = Model.get_instance(connection, "users")             

0

หนึ่งตัวอย่างทั่วไปคือ__call__ในfunctools.partialนี่คือรุ่นที่ง่าย (กับ Python> = 3.5):

class partial:
    """New function with partial application of the given arguments and keywords."""

    def __new__(cls, func, *args, **kwargs):
        if not callable(func):
            raise TypeError("the first argument must be callable")
        self = super().__new__(cls)

        self.func = func
        self.args = args
        self.kwargs = kwargs
        return self

    def __call__(self, *args, **kwargs):
        return self.func(*self.args, *args, **self.kwargs, **kwargs)

การใช้งาน:

def add(x, y):
    return x + y

inc = partial(add, y=1)
print(inc(41))  # 42

0

ผู้ประกอบการเรียกฟังก์ชั่น

class Foo:
    def __call__(self, a, b, c):
        # do something

x = Foo()
x(1, 2, 3)

__call__วิธีสามารถใช้ในการนิยามใหม่ / ใหม่เริ่มต้นวัตถุเดียวกัน นอกจากนี้ยังอำนวยความสะดวกในการใช้อินสแตนซ์ / วัตถุของคลาสเป็นฟังก์ชันโดยส่งอาร์กิวเมนต์ไปยังวัตถุ


มันจะมีประโยชน์เมื่อไหร่? Foo (1, 2, 3) ดูชัดเจนยิ่งขึ้น
Yaroslav Nikitenko

0

ผมพบว่าเป็นสถานที่ที่ดีที่จะใช้วัตถุ callable, ที่กำหนด__call__()คือเมื่อใช้ความสามารถในการเขียนโปรแกรมการทำงานในหลามเช่นmap(), , filter()reduce()

เวลาที่ดีที่สุดในการใช้วัตถุที่เรียกได้ผ่านฟังก์ชั่นธรรมดาหรือฟังก์ชั่นแลมบ์ดาคือเมื่อตรรกะมีความซับซ้อนและจำเป็นต้องรักษาสถานะไว้หรือใช้ข้อมูลอื่นที่ไม่ผ่านไปยัง__call__()ฟังก์ชั่น

นี่คือรหัสบางอย่างที่กรองชื่อไฟล์ขึ้นอยู่กับนามสกุลของไฟล์ของพวกเขาโดยใช้วัตถุที่ callable filter()และ

callable:

import os

class FileAcceptor(object):
    def __init__(self, accepted_extensions):
        self.accepted_extensions = accepted_extensions

    def __call__(self, filename):
        base, ext = os.path.splitext(filename)
        return ext in self.accepted_extensions

class ImageFileAcceptor(FileAcceptor):
    def __init__(self):
        image_extensions = ('.jpg', '.jpeg', '.gif', '.bmp')
        super(ImageFileAcceptor, self).__init__(image_extensions)

การใช้งาน:

filenames = [
    'me.jpg',
    'me.txt',
    'friend1.jpg',
    'friend2.bmp',
    'you.jpeg',
    'you.xml']

acceptor = ImageFileAcceptor()
image_filenames = filter(acceptor, filenames)
print image_filenames

เอาท์พุท:

['me.jpg', 'friend1.jpg', 'friend2.bmp', 'you.jpeg']

0

มันสายเกินไป แต่ฉันยกตัวอย่าง ลองนึกภาพคุณมีVectorชั้นเรียนและPointชั้นเรียน ทั้งสองใช้x, yเป็นตำแหน่ง args ลองจินตนาการว่าคุณต้องการสร้างฟังก์ชั่นที่จะย้ายจุดที่จะใส่เวกเตอร์

4 โซลูชั่น

  • put_point_on_vec(point, vec)

  • ทำให้เป็นวิธีการในชั้นเวกเตอร์ เช่น my_vec.put_point(point)

  • ทำให้เป็นวิธีการในPointชั้นเรียนmy_point.put_on_vec(vec)
  • Vectorใช้งาน__call__ดังนั้นคุณสามารถใช้มันได้my_vec_instance(point)

นี่เป็นส่วนหนึ่งของตัวอย่างบางส่วนที่ฉันกำลังทำอยู่เพื่อเป็นแนวทางสำหรับวิธีการ dunder อธิบายกับคณิตศาสตร์ที่ฉันจะปล่อยไม่ช้าก็เร็ว

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

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