Python __call__ специальный метод практический пример


159

Я знаю, что __call__метод в классе запускается, когда вызывается экземпляр класса. Однако я понятия не имею, когда я смогу использовать этот специальный метод, потому что можно просто создать новый метод и выполнить ту же операцию, что и в __call__методе, и вместо вызова экземпляра вы можете вызвать метод.

Я был бы очень признателен, если бы кто-нибудь дал мне практическое применение этого специального метода.



8
функциональность _call_ аналогична перегруженному оператору () в C ++ . Если вы просто создадите новый метод за пределами класса, вы не сможете получить доступ к внутренним данным в классе.
Энди

2
Наиболее распространенное использование __call__скрыто в простом виде; это как вы создаете экземпляр класса: x = Foo()действительно x = type(Foo).__call__(Foo), где __call__определяется метаклассом Foo.
Чепнер

Ответы:


88

Модуль форм Django __call__прекрасно использует метод для реализации согласованного API для проверки формы. Вы можете написать свой собственный валидатор для формы в Django как функцию.

def custom_validator(value):
    #your validation logic

В Django есть некоторые встроенные валидаторы по умолчанию, такие как валидаторы электронной почты, валидаторы URL и т. Д., Которые в общем подпадают под валидаторы RegEx. Чтобы реализовать их чисто, Django прибегает к вызываемым классам (вместо функций). Он реализует логику Regex Validation по умолчанию в 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 похожа на то, что другие объяснили в своих ответах ниже. Может ли это быть реализовано любым другим способом? Вы могли бы, но ИМХО, это не будет так же легко читаемо или так легко расширяемо для больших фреймворков, как Django.


5
Так что при правильном использовании он может сделать код более читабельным. Я предполагаю, что если он будет использован не в том месте, это также сделает код очень нечитаемым.
mohi666

15
Это пример того , как он может быть использован, но не является хорошим , на мой взгляд. В этом случае нет никакого преимущества иметь вызываемый экземпляр. Было бы лучше иметь интерфейс / абстрактный класс с методом, например .validate (); это то же самое, только более явно. Реальное значение __call__ - это возможность использовать экземпляр в месте, где ожидается вызов. Например, я чаще всего использую __call__ при создании декораторов.
Даниил

120

В этом примере используется запоминание , в основном хранение значений в таблице (словарь в данном случае) , так что вы можете посмотреть их позже вместо перерасчета их.

Здесь мы используем простой класс с __call__методом для вычисления факториалов (через вызываемый объект ) вместо функции факториала, которая содержит статическую переменную (как это невозможно в 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()

Теперь у вас есть 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, но это только я.
Крис Латс

4
@delnan - Почти все можно сделать несколькими разными способами. Какой из них более читабелен, зависит от читателя.
Крис Латс

1
@ Крис Лутц: Вы можете созерцать такие изменения. Для запоминания в целом словарь работает хорошо, потому что вы не можете гарантировать порядок, в котором вещи заполняют ваш список. В этом случае список может работать, но он не будет быстрее или проще.
S.Lott

8
@delnan: это не должно быть самым коротким. Никто не выигрывает в коде гольф. Он призван показать __call__, быть простым и не более того.
S.Lott

3
Но это как бы рушит пример, когда продемонстрированная техника не идеальна для задач, не так ли? (И я не имел в виду «давайте сохраним строки, черт побери», я говорил о «напишите это столь же ясным способом и сохраните некоторый шаблонный код»). Будьте уверены, что я не один из тех безумцев, пытающихся написать самый короткий код, я просто хочу избежать стандартного кода, который ничего не добавляет для читателя.)

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__дать вам инструмент, позволяющий вам использовать ОО-методы для решения проблем.
bradley.ayers

4
Я думаю, что вопрос «зачем использовать X над Y», когда оба обеспечивают эквивалентную функциональность, ужасно субъективен. Для некоторых людей подход ОО легче понять, для других - подход закрытия. Там нет убедительных аргументов, чтобы использовать один над другим, если у вас не было ситуации, где вы должны были использовать isinstanceили что-то подобное.
bradley.ayers

2
@delnan Ваш пример замыкания состоит из меньшего количества строк кода, но с этим так же ясно, что спорить труднее.
Деннис

8
Пример того, где вы предпочитаете использовать __call__метод вместо замыкания, - это когда вы имеете дело с многопроцессорным модулем, который использует травление для передачи информации между процессами. Вы не можете засолить замыкание, но вы можете засолить экземпляр класса.
Джон Питер Томпсон Гарсес

21

__call__также используется для реализации классов декоратора в python. В этом случае экземпляр класса вызывается при вызове метода с декоратором.

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объекты из стандартного модуля libarythreading хотят вызываемые в качестве 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 ) - таким образом вы можете имитировать функцию, используя более сложный объект класса.
Эндрю Джаффе

2
В качестве связанного примера я видел __call__использование экземпляров классов (вместо функций) в качестве приложений WSGI. Вот пример из «Полного руководства по пилонам»: Использование экземпляров классов
Джош Розен,

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__метод и замыкания дают нам естественный способ создания шаблона проектирования STRATEGY в Python. Мы определяем семейство алгоритмов, инкапсулируем каждый из них, делаем их взаимозаменяемыми и, в конце концов, мы можем выполнить общий набор шагов и, например, вычислить хеш для файла.


4

Я просто наткнулся на использование __call__()совместно с __getattr__()которой я думаю , что это красиво. Это позволяет скрывать несколько уровней API JSON / HTTP / (однако_serialized) внутри объекта.

Эта __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 с использованием доступа к атрибутам.
Петри

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__in 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) кажется более понятным.
Ярослав Никитенко

0

Я нахожу хорошее место для использования вызываемых объектов, которые определяют __call__() , когда с помощью функциональных возможностей программирования в Python, такие как map(), filter(), reduce().

Лучшее время для использования вызываемого объекта над простой функцией или лямбда-функцией - когда логика сложна и требует сохранения какого-то состояния или использует другую информацию, которая не передана __call__()функции.

Вот некоторый код, который фильтрует имена файлов по их расширению, используя вызываемый объект и 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как позиционные аргументы. Представим, что вы хотите создать функцию, которая перемещает точку, которая будет помещена в вектор.

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)

На самом деле, это часть некоторых примеров, над которыми я работаю для руководства по более сложным методам, объясненным с помощью математики, которое я собираюсь выпустить рано или поздно.

Я оставил логику перемещения самой точки, потому что это не то, о чем этот вопрос

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.