Сообщить об ошибке.

Что такое вызываемый объект callable в Python?

Вызываемый объект callable - это все, что можно вызвать, используя круглые скобки. Вызываемые объекты часто принимают аргументы (которые заключаются в круглые скобки).

Вызываемыми объектами callable в Python могут быть:

Вызываемые объекты - очень важная концепция в Python, т.к. они распространены повсеместно.

Изучая документацию Python, часто можно видеть/слышать такие фразы, как функция zip() или функция enumerate(). Все эти термины технически неверны, т.к. это классы Python.

>>> enumerate
# <class 'enumerate'>
>>> zip
# <class 'zip'>

В этом материале объясняется, почему происходит такая путаница между классами и функциями в Python, и почему это различие часто не имеет значения.

Функции являются наиболее очевидными вызываемыми объектами. Функции можно "вызывать" на любом языке программирования. Однако вызываемый класс немного более уникален.

В языке JavaScript "экземпляр" класса создается следующим образом:

> new Date(2023, 1, 1, 0, 0)
// Wed Feb 01 2023 00:00:00 GMT+0300

В JavaScript синтаксис создания экземпляра класса (способ, которым создается "экземпляр") включает ключевое слово new. В Python, при создании экземпляра класса не используется ключевое слово.

В Python можно создать экземпляр класса datetime.datetime следующим образом:

>>> from datetime import datetime
>>> datetime(2020, 1, 1, 0, 0)
# datetime.datetime(2020, 1, 1, 0, 0)

В Python синтаксис создания экземпляра нового класса такой же, как синтаксис вызова функции. Ничего добавлять не нужно: просто вызываем класс и все.

Когда вызывается функция, то всегда возвращается значение, даже если функция ничего не возвращает .

>>> def f(): pass
>>> f
# <function f at 0x7fba774b5510>
>>> f() == None
# True
>>> print(f())
None

Когда вызывается класс, то получаем "экземпляр" этого класса.

В Python используется один и тот же синтаксис для создания объектов из классов и для вызова функций: этот факт является основной причиной того, что слово "вызываемый" (callable) является такой важной частью сленга Python.

Маскировка классов под функции.

Практически все объясняют термин "декоратор" как "функции, которые принимают функции и возвращают функции". Но это не совсем точное объяснение. Существуют также декораторы классов: функции, которые принимают классы и возвращают классы. И есть декораторы, реализованные с использованием классов: классы, которые принимают функции и возвращают объекты.

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

Декоратор property(), также выглядит как функция:

class Circle:
    
    def __init__(self, radius):
        self.radius = radius

    @property
    def diameter(self):
        return self.radius * 2

>>> c = Circle(5)
>>> c.diameter
# 10

Но на самом деле это класс:

>>> property
# <class 'property'>

Декораторы classmethod() и staticmethod() также являются классами:

>>> classmethod
# <class 'classmethod'>
>>> staticmethod
# <class 'staticmethod'>

Декораторы и менеджеры контекста - это всего лишь два места в Python, где часто видим и слышим термин "вызываемые объекты", которые выглядят как функции, но ими не являются. Является ли вызываемый объект классом или функцией, часто это деталь реализации.

Не будет ошибкой ссылаться на property() или redirect_stdout() как на функции, т.к. они также могут быть функциями. При вызове они вернут полезный объект, и это самое главное.

Вызываемые объекты.

Круглые скобки (...) в Python являются синтаксисом "вызова", они могут создавать экземпляр класса или вызывать функцию. Этот синтаксис также может вызывать объект.

Технически все в Python является объектом:

>>> len
# <built-in function len>
>>> isinstance(len, object)
# True

>>> range
# <class 'range'>
>>> isinstance(range, object)
# True

Также часто в Python используется термин "объект", подразумевая, что это экземпляр класса (то, что возвращается в ответ, когда вызывается класс).

В модуле functools есть функция partial(), которая может частичного применить какие-то аргументы к функции, которые будут использоваться при последующем вызове функции. Она часто используется в функциональной парадигме программирования:

>>> from functools import partial
>>> numbers = partial(filter, str.isdigit)
>>> list(numbers(['4', 'hello', '50']))
# ['4', '50']

Вызываемый объект partial не реализуется с помощью функции, при этом фраза "функция partial()" имеет смысл. Разработчики Python могли бы реализовать partial() как функцию, например:

def partial(func, *args, **kwargs):
    def wrapper(*more_args, **more_kwargs):
        all_kwargs = {**kwargs, **more_kwargs}
        return func(*args, *more_args, **all_kwargs)
    return wrapper

Но вместо функции они решили использовать класс, сделав что-то вроде этого:

class partial:
    def __init__(self, func, *args, **kwargs):
        self.func, self.args, self.kwargs = func, args, kwargs
    def __call__(self, *more_args, **more_kwargs):
        all_kwargs = {**self.kwargs, **more_kwargs}
        return self.func(*self.args, *more_args, **all_kwargs)

Обратите внимание на метод __call__(), он и позволяет вызывать объект partial. Таким образом, класс partial создает вызываемый объект.

Добавление метода __call__() к любому классу сделает экземпляры этого класса вызываемыми. Проверка существования метода __call__() - это один из способов проверить, можно ли вызывать этот объект, что и делает встроенная функция callable().

Все функции, классы и вызываемые объекты имеют метод __call__():

>>> hasattr(open, '__call__')
# True
>>> callable(open)
# True
>>> hasattr([], '__call__')
# False
>>> callable([])
# False

Различие между функциями и классами часто не имеет значения. Только 44 из 72 встроенных функций в Python фактически реализованы как функции: 26 - это классы и 1 (help()) - экземпляр вызываемого класса.

Например, встроенная функция sorted() имеет необязательный ключевой аргумент key, про который в документации сказано, что это должна быть функция, принимающая один аргумент (функции min() и max имеют аналогичный ключевой аргумент).

Этим ключевым аргументом может быть функция:

>>> def count(s): return len(s.replace('_', ''))
>>> numbers = ['400', '2_020', '800_000']
>>> sorted(numbers, key=count)
# ['400', '2_020', '800_000']

А может быть и класс:

>>> int
# <class 'int'>
>>> numbers = ['400', '2_020', '800_000']
>>> sorted(numbers, key=int)
# ['400', '2_020', '800_000']

Это подтверждает фразу, что "в Python различие между функциями и классами часто не имеет значения". В Python часто используют слова "функция" и "вызываемый объект" взаимозаменяемо, и это нормально.

Класс defaultdict в модуле collections принимает "фабричный" вызываемый объект, который используется для генерации значений по умолчанию для отсутствующих элементов словаря.

>>> from collections import defaultdict
>>> counts = defaultdict(int)
>>> counts['snakes']
# 0
>>> things = defaultdict(list)
>>> things['newer'].append('Python 3')
>>> things['newer']
# ['Python 3']

Но collections.defaultdict() также может принимать функцию (или любой вызываемый объект):

>>> import random
>>> colors = ['blue', 'yellow', 'purple', 'green']
>>> my_colors = defaultdict(lambda: random.choice(colors))
>>> my_colors['one']
# 'yellow'
>>> my_colors['two']
# 'green'
>>> probabilities = defaultdict(random.random)
>>> probabilities['one']
# 0.6714530824158086
>>> probabilities['two']
# 0.07703364911089605

Практически везде, где принимается "вызываемый объект", функция это, класс или какой-либо другой вызываемый объект, все они будут работать нормально.

Думайте с точки зрения вызываемых объектов, а не классов или функций.

Python практикует утиную типизацию: если это выглядит как утка и крякает как утка, то это утка. Из-за утиной типизации программисты Python склонны использовать общие термины для описания конкретных вещей: списки - это последовательности, генераторы - это итераторы, словари - это сопоставления, а функции - это вызываемые объекты.

Если что-то выглядит как callable и крякает (или, скорее, вызывает) как callable, то это callable. Точно так же, если что-то выглядит как функция и крякает (вызывает) как функция, то можно смело назвать это функцией... даже если оно реализовано с использованием класса!

Вызываемые объекты принимают аргументы и возвращают что-то полезное вызывающей стороне. Когда вызываются классы, то они возвращают экземпляры этого класса. Когда вызывают функции, то они возвращают значение этой функции. Различие между классом и функцией редко бывает важным с точки зрения вызывающей стороны.

Говоря о передаче функций или объектов класса, старайтесь думать с точки зрения вызываемых объектов. То, что происходит, когда что-то вызывается, гораздо важнее, чем то, как оно реализовано.

И еще важный момент, если кто-то ошибочно называет функцию классом или класс функцией, то не поправляйте его, если конечно различие не имеет значения.

Коротко о магическом методе __call__

Метод __call__ вызывается при обращении к экземпляру как к функции. Если класс использует метод __call__, то экземпляр класса может вызывается как функция, передавая ему позиционные и именованные аргументы, которые определены в этом методе:

>>> class CallMe:
        # Реализует вызов экземпляра
        def __call__(self, *args, **kwargs): 
            print('Called:', args, kwargs)

>>> C = CallMe()
>>> C(4,5,6)
# Called: (4,5,6) {}

>>> c(5,6,7, x = 10, y = 20)
# Called: (5,6,7) {'x' : 10, 'y': 20 }

Реализация операции __call__, позволяет экземплярам классов имитировать поведение функций, а также сохранять информацию о состоянии между вызовами. Грубо говоря, упростить написания замыканий.

>>> class Сlosure:
        def __init__(self, value):
             self.value = value
        def __call__(self, other):
             return self.value * other

>>> X = Prod(3) 
>>> X(3)
# 9
>>> X(4)
# 12

Метод __call__ может использоваться для реализации декораторов на основе классов. Например, следующий пример используется для записи количества вызовов функции:

class Counter:
    def __init__(self, func):
        # в качестве аргумента принимаем функцию
        self.func = func
        # начальное значение счетчика
        self.count = 0
 
    def __call__(self, *args, **kwargs):
        # увеличиваем счетчик на 1
        self.count += 1
        # вызываем функцию
        return self.func(*args, **kwargs)
 
@Counter
def foo():
    pass
 
for i in range(10):
    foo()
 
print(foo.count) 
# 10