Приклад спеціального методу 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 у 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__ найчастіше, наприклад, для створення декораторів.
Даніель

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__функція по суті є індексом. Також використовував би список замість дикта, але це тільки я.
Кріс Луц

4
@delnan - Практично все можна зробити декількома різними способами. Хто з них читає, залежить від читача.
Кріс Лутц

1
@Chris Lutz: Ви можете розглянути ці зміни. Для запам'ятовування в цілому словник добре працює, оскільки ви не можете гарантувати порядок, коли речі заповнюють ваш список. У цьому випадку список може працювати, але він не буде швидшим чи простішим.
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__також використовується для реалізації класів декораторів у пітоні. У цьому випадку екземпляр класу викликається, коли викликається метод з декоратором.

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


4

Я просто натрапив на використання __call__()концерту з__getattr__() яким я думаю, що це красиво. Це дозволяє приховати декілька рівнів API JSON / HTTP / (Однак_серіалізований) всередині об'єкта.

__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

EDIT: оновлено посилання, щоб вказати на головну версію.


Це мило. Колись я використовував той самий механізм для переміщення довільного дерева 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() .

Дзвінки:

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)

Це насправді частина деяких прикладів, над якими я працюю над керівництвом по методах "Дандер", поясненим з Maths, що я вийду рано чи пізно.

Я залишив логіку переміщення точки, тому що це питання не в цьому

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.