Python __call__ ví dụ thực tế phương pháp đặc biệt


159

Tôi biết rằng __call__phương thức trong một lớp được kích hoạt khi thể hiện của một lớp được gọi. Tuy nhiên, tôi không biết khi nào tôi có thể sử dụng phương thức đặc biệt này, bởi vì người ta có thể chỉ cần tạo một phương thức mới và thực hiện cùng một thao tác được thực hiện trong __call__phương thức và thay vì gọi ví dụ, bạn có thể gọi phương thức.

Tôi thực sự sẽ đánh giá cao nó nếu ai đó cho tôi sử dụng thực tế phương pháp đặc biệt này.



8
chức năng của _call_ giống như toán tử quá tải của () trong C ++ . Nếu bạn chỉ đơn giản tạo một phương thức mới bên ngoài lớp, bạn không thể truy cập dữ liệu nội bộ trong một lớp.
andy

2
Việc sử dụng phổ biến nhất __call__được ẩn trong chế độ xem đơn giản; đó là cách bạn khởi tạo một lớp: x = Foo()thực sự x = type(Foo).__call__(Foo), nơi __call__được định nghĩa bởi siêu dữ liệu của Foo.
chepner

Câu trả lời:


88

Mô-đun biểu mẫu Django sử dụng __call__phương thức độc đáo để triển khai API nhất quán để xác thực mẫu. Bạn có thể viết trình xác nhận của riêng bạn cho một biểu mẫu trong Django dưới dạng hàm.

def custom_validator(value):
    #your validation logic

Django có một số trình xác nhận tích hợp mặc định như trình xác nhận email, trình xác thực url, v.v., nằm trong phạm vi của trình xác thực RegEx. Để thực hiện những điều này một cách sạch sẽ, Django sử dụng các lớp có thể gọi được (thay vì các hàm). Nó triển khai logic xác thực Regex mặc định trong RegexValidator và sau đó mở rộng các lớp này cho các xác nhận khác.

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

Bây giờ cả chức năng tùy chỉnh và EmailValidator tích hợp của bạn đều có thể được gọi với cùng một cú pháp.

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

Như bạn có thể thấy, cách triển khai này trong Django tương tự như những gì người khác đã giải thích trong câu trả lời của họ dưới đây. Điều này có thể được thực hiện theo bất kỳ cách nào khác? Bạn có thể, nhưng IMHO nó sẽ không thể đọc được hoặc dễ dàng mở rộng cho một khung lớn như Django.


5
Vì vậy, nếu được sử dụng đúng cách, nó có thể làm cho mã dễ đọc hơn. Tôi giả sử nếu nó được sử dụng không đúng chỗ, nó sẽ làm cho mã rất khó đọc.
mohi666

15
Đây là một ví dụ về cách nó có thể được sử dụng, nhưng theo tôi thì không tốt. Trong trường hợp này không có lợi thế nào để có một ví dụ có thể gọi được. Sẽ tốt hơn nếu có một lớp giao diện / trừu tượng với một phương thức, như .validate (); đó là điều tương tự chỉ rõ ràng hơn. Giá trị thực của __call__ là có thể sử dụng một thể hiện ở nơi dự kiến ​​có thể gọi được. Tôi sử dụng __call__ thường xuyên nhất khi tạo trang trí chẳng hạn.
Daniel

120

Ví dụ này sử dụng ghi nhớ , về cơ bản lưu trữ các giá trị trong một bảng (từ điển trong trường hợp này) để bạn có thể tra cứu chúng sau này thay vì tính toán lại chúng.

Ở đây, chúng ta sử dụng một lớp đơn giản với một __call__phương thức để tính các giai thừa (thông qua một đối tượng có thể gọi được ) thay vì một hàm giai thừa có chứa một biến tĩnh (như không thể có trong Python).

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

Bây giờ bạn có một factđối tượng có thể gọi được, giống như mọi chức năng khác. Ví dụ

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

nó cũng có trạng thái.


2
Tôi muốn có một factđối tượng có thể lập chỉ mục vì __call__chức năng của bạn về cơ bản là một chỉ mục. Cũng sẽ sử dụng một danh sách thay vì một lệnh, nhưng đó chỉ là tôi.
Chris Lutz

4
@delnan - Hầu như bất cứ điều gì có thể được thực hiện một số cách khác nhau. Cái nào dễ đọc hơn phụ thuộc vào người đọc.
Chris Lutz

1
@Chris Lutz: Bạn có thể tự do chiêm ngưỡng những loại thay đổi đó. Để ghi nhớ nói chung , một từ điển hoạt động tốt vì bạn không thể đảm bảo thứ tự điền vào danh sách của bạn. Trong trường hợp này, một danh sách có thể hoạt động, nhưng nó sẽ không nhanh hơn hay đơn giản hơn.
S.Lott

8
@delnan: Điều này không có ý định ngắn nhất. Không ai thắng tại mã golf. Đó là dự định để hiển thị __call__, đơn giản và không có gì hơn.
S.Lott

3
Nhưng nó làm hỏng ví dụ khi kỹ thuật được chứng minh không lý tưởng cho các nhiệm vụ, phải không? (Và tôi không nói về "hãy lưu các dòng cho sự tàn phá của nó" - tôi đã nói về việc "viết nó theo cách không kém phần rõ ràng này và lưu một số mã soạn sẵn" -bort. Hãy yên tâm rằng tôi không một trong những kẻ điên đang cố gắng viết mã ngắn nhất có thể, tôi chỉ muốn tránh mã soạn sẵn không thêm gì cho người đọc.)

40

Tôi thấy nó hữu ích vì nó cho phép tôi tạo các API dễ sử dụng (bạn có một số đối tượng có thể gọi được yêu cầu một số đối số cụ thể) và dễ thực hiện vì bạn có thể sử dụng các thực tiễn Hướng đối tượng.

Sau đây là mã tôi đã viết ngày hôm qua tạo ra một phiên bản của các hashlib.foophương thức băm toàn bộ tệp chứ không phải là chuỗi:

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

Việc triển khai này cho phép tôi sử dụng các chức năng theo cách tương tự với các hashlib.foochức năng:

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

Tất nhiên tôi có thể thực hiện nó theo một cách khác, nhưng trong trường hợp này có vẻ như là một cách tiếp cận đơn giản.


7
Một lần nữa, đóng cửa làm hỏng ví dụ này. pastebin.com/961vU0ay là 80% các dòng và rõ ràng như vậy.

8
Tôi không tin rằng nó sẽ luôn rõ ràng với một người nào đó (ví dụ như có lẽ ai đó chỉ sử dụng Java). Các hàm lồng nhau và tra cứu / phạm vi biến có thể gây nhầm lẫn. Tôi cho rằng quan điểm của tôi là __call__cung cấp cho bạn một công cụ cho phép bạn sử dụng các kỹ thuật OO để giải quyết vấn đề.
bradley.ayers

4
Tôi nghĩ câu hỏi "tại sao sử dụng X hơn Y" khi cả hai cung cấp chức năng tương đương là chủ quan khủng khiếp. Đối với một số người, cách tiếp cận OO dễ hiểu hơn, đối với những người khác, cách tiếp cận đóng cửa là. Không có đối số thuyết phục để sử dụng cái này hơn cái kia, trừ khi bạn có một tình huống mà bạn phải sử dụng isinstancehoặc một cái gì đó tương tự.
bradley.ayers

2
@delnan Ví dụ đóng cửa của bạn là ít dòng mã hơn, nhưng điều đó càng rõ ràng thì càng khó tranh luận.
Dennis

8
Một ví dụ về nơi bạn muốn sử dụng một __call__phương thức thay vì đóng là khi bạn đang xử lý mô-đun đa xử lý, sử dụng phương pháp tẩy để truyền thông tin giữa các quy trình. Bạn không thể chọn một bao đóng, nhưng bạn có thể chọn một thể hiện của một lớp.
John Peter Thompson Garcés

21

__call__cũng được sử dụng để thực hiện các lớp trang trí trong python. Trong trường hợp này, thể hiện của lớp được gọi khi phương thức với trình trang trí được gọi.

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

Có, khi bạn biết bạn đang xử lý các đối tượng, việc sử dụng một cuộc gọi phương thức rõ ràng là hoàn toàn có thể (và trong nhiều trường hợp). Tuy nhiên, đôi khi bạn xử lý mã mong đợi các đối tượng có thể gọi được - thường là các hàm, nhưng nhờ __call__bạn có thể xây dựng các đối tượng phức tạp hơn, với dữ liệu cá thể và nhiều phương thức hơn để ủy thác các tác vụ lặp đi lặp lại, v.v. vẫn có thể gọi được.

Ngoài ra, đôi khi bạn đang sử dụng cả hai đối tượng cho các tác vụ phức tạp (có ý nghĩa khi viết một lớp dành riêng) và các đối tượng cho các tác vụ đơn giản (đã tồn tại trong các hàm hoặc được viết dễ dàng hơn dưới dạng các hàm). Để có một giao diện chung, bạn phải viết các lớp nhỏ bao bọc các hàm đó với giao diện dự kiến ​​hoặc bạn giữ các hàm chức năng và làm cho các đối tượng phức tạp hơn có thể gọi được. Hãy lấy chủ đề làm ví dụ. Các Threadđối tượng từ mô đun libary tiêu chuẩnthreading muốn có thể gọi là targetđối số (nghĩa là hành động được thực hiện trong luồng mới). Với một đối tượng có thể gọi được, bạn không bị hạn chế đối với các hàm, bạn cũng có thể chuyển các đối tượng khác, chẳng hạn như một công nhân tương đối phức tạp để thực hiện các tác vụ từ các luồng khác và thực hiện chúng một cách tuần tự:

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)

Đây chỉ là một ví dụ ngoài đỉnh đầu của tôi, nhưng tôi nghĩ nó đã đủ phức tạp để đảm bảo cho lớp học. Làm điều này chỉ với các chức năng là khó, ít nhất nó đòi hỏi phải trả lại hai chức năng và điều đó đang dần trở nên phức tạp. Người ta có thể đổi tên __call__thành một thứ khác và vượt qua một phương thức ràng buộc, nhưng điều đó làm cho mã tạo ra luồng hơi ít rõ ràng hơn và không thêm bất kỳ giá trị nào.


3
Có thể hữu ích khi sử dụng cụm từ "gõ vịt" ( en.wikipedia.org/wiki/Duck_typing#In_Python ) ở đây - bạn có thể bắt chước một hàm bằng cách sử dụng một đối tượng lớp phức tạp hơn theo cách này.
Andrew Jaffe

2
Như một ví dụ liên quan, tôi đã thấy __call__được sử dụng để sử dụng các thể hiện của lớp (thay vì các hàm) như các ứng dụng WSGI. Dưới đây là một ví dụ từ "Hướng dẫn dứt khoát về giá treo": Sử dụng thực thể của các lớp học
Josh Rosen

5

Trình trang trí dựa trên lớp sử dụng __call__để tham chiếu chức năng bao bọc. Ví dụ:

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

Có một mô tả tốt về các tùy chọn khác nhau ở đây tại Artima.com


Tôi hiếm khi thấy các trình trang trí lớp mặc dù, vì chúng yêu cầu một số mã soạn sẵn không rõ ràng để làm việc với các phương thức.

4

__call__Phương pháp IMHO và các bao đóng cho chúng ta một cách tự nhiên để tạo ra mẫu thiết kế CHIẾN LƯỢC trong Python. Chúng tôi xác định một họ các thuật toán, gói gọn từng thuật toán, làm cho chúng có thể hoán đổi cho nhau và cuối cùng chúng tôi có thể thực hiện một tập hợp các bước chung và, ví dụ, tính toán một hàm băm cho một tệp.


4

Tôi chỉ vấp ngã khi sử dụng __call__()buổi hòa nhạc với__getattr__() mà tôi nghĩ là đẹp. Nó cho phép bạn ẩn nhiều cấp độ của API JSON / HTTP / (yet_serialized) bên trong một đối tượng.

Phần __getattr__()này quan tâm đến việc lặp lại một thể hiện đã sửa đổi của cùng một lớp, điền vào một thuộc tính nữa tại một thời điểm. Sau đó, sau khi tất cả các thông tin đã cạn kiệt, __call__()tiếp tục với bất kỳ đối số nào bạn đưa vào.

Sử dụng mô hình này, ví dụ bạn có thể thực hiện một cuộc gọi như thế api.v2.volumes.ssd.update(size=20), kết thúc trong yêu cầu PUT tới https://some.tld/api/v2/volumes/ssd/update.

Mã cụ thể là trình điều khiển lưu trữ khối cho một phụ trợ âm lượng nhất định trong OpenStack, bạn có thể kiểm tra nó tại đây: https://github.com/openstack/cinder/blob/master/cinder/volume/drivers/nexenta/jsonrpc.py

EDIT: Cập nhật liên kết để trỏ đến sửa đổi chính.


Điều đó thật tuyệt. Tôi đã từng sử dụng cùng một cơ chế để duyệt qua cây XML tùy ý bằng cách sử dụng quyền truy cập thuộc tính.
Petri

1

Chỉ định một __metaclass__ và ghi đè __call__phương thức và có __new__phương thức của các lớp meta đã chỉ định trả về một thể hiện của lớp, viola bạn có "hàm" với các phương thức.


1

Chúng ta có thể sử dụng __call__phương thức để sử dụng các phương thức lớp khác làm phương thức tĩnh.

    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

Một ví dụ phổ biến là __call__in functools.partial, đây là phiên bản đơn giản hóa (với 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)

Sử dụng:

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

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

0

Toán tử gọi hàm.

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

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

Các __call__ phương pháp có thể được sử dụng để định nghĩa lại / tái khởi cùng một đối tượng. Nó cũng tạo điều kiện cho việc sử dụng các thể hiện / đối tượng của một lớp làm các hàm bằng cách truyền các đối số cho các đối tượng.


Khi nào nó sẽ hữu ích? Foo (1, 2, 3) có vẻ rõ ràng hơn.
Yaroslav Nikitenko

0

Tôi tìm một nơi tốt để sử dụng đối tượng callable, những định nghĩa __call__(), là khi sử dụng các khả năng lập trình chức năng trong Python, chẳng hạn như map(), filter(), reduce().

Thời gian tốt nhất để sử dụng một đối tượng có thể gọi qua hàm đơn giản hoặc hàm lambda là khi logic phức tạp và cần giữ lại một số trạng thái hoặc sử dụng thông tin khác không được truyền cho __call__() hàm.

Đây là một số mã lọc tên tệp dựa trên phần mở rộng tên tệp của chúng bằng cách sử dụng một đối tượng có thể gọi được và filter() .

Có thể gọi

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)

Sử dụng:

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

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

Đầu ra:

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

0

Điều này là quá muộn nhưng tôi đang đưa ra một ví dụ. Hãy tưởng tượng bạn có một Vectorlớp học và một Pointlớp học. Cả hai đều lấy x, ynhư lập luận vị trí. Hãy tưởng tượng bạn muốn tạo một hàm di chuyển điểm cần đặt trên vectơ.

4 giải pháp

  • put_point_on_vec(point, vec)

  • Làm cho nó một phương thức trên lớp vectơ. ví dụ my_vec.put_point(point)

  • Làm cho nó một phương thức trên Pointlớp.my_point.put_on_vec(vec)
  • Vectorthực hiện __call__, vì vậy bạn có thể sử dụng nó nhưmy_vec_instance(point)

Đây thực sự là một phần của một số ví dụ tôi đang làm hướng dẫn cho các phương pháp lừa đảo được giải thích với Toán học mà tôi sẽ sớm phát hành.

Tôi đã để lại logic của việc di chuyển điểm bởi vì đây không phải là câu hỏi này.

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.