Закрытие функции по сравнению с вызываемым классом - PullRequest
11 голосов
/ 23 января 2012

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

class F:
  def __init__(self, op):
    self.op = op
  def __call__(self, arg1, arg2):
    if (self.op == 'mult'):
      return arg1 * arg2
    if (self.op == 'add'):
      return arg1 + arg2
    raise InvalidOp(op)

f = F('add')

или

def F(op):
  if op == 'or':
    def f_(arg1, arg2):
      return arg1 | arg2
    return f_
  if op == 'and':
    def g_(arg1, arg2):
      return arg1 & arg2
    return g_
  raise InvalidOp(op)

f = F('add')

Какие факторы следует учитывать при выборе в любом направлении?

Я могу думать о двух:

  • Кажется, что замыкание всегда будет иметь лучшую производительность (не могу вспомнить контрпример).

  • Я думаю, что бывают случаи, когда замыкание не можетвыполнить задание (например, если его состояние меняется со временем).

Правильно ли я в этом?Что еще можно добавить?

Ответы [ 5 ]

10 голосов
/ 23 января 2012

Закрытия быстрее.Классы более гибкие (т.е. доступно больше методов, чем просто __call __).

3 голосов
/ 27 декабря 2013

Я понимаю, что это более старая публикация, но один фактор, который я не увидел в списке, это то, что в Python (до нелокальной) вы не можете изменять локальную переменную, содержащуюся в ссылочной среде. (В вашем примере такая модификация не важна, но технически говоря, отсутствие возможности изменить такую ​​переменную означает, что это не настоящее закрытие.)

Например, следующий код не работает:

def counter():
    i = 0
    def f():
        i += 1
        return i
    return f

c = counter()
c()

Вызов c выше вызовет исключение UnboundLocalError.

Это легко обойти, используя изменяемый, такой как словарь:

def counter():
    d = {'i': 0}
    def f():
        d['i'] += 1
        return d['i']
    return f

c = counter()
c()     # 1
c()     # 2

но, конечно, это просто обходной путь.

1 голос
/ 14 января 2017

Обратите внимание, что из-за ошибки, ранее обнаруженной в моем коде тестирования, мой первоначальный ответ был неверным.Ниже приведена пересмотренная версия.

Я сделал небольшую программу для измерения времени работы и потребления памяти.Я создал следующий вызываемый класс и замыкание:

class CallMe:
    def __init__(self, context):
        self.context = context

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

def call_me(func):
    return lambda *args, **kwargs: func(*args, **kwargs)

Я синхронизировал вызовы простых функций, принимающих разное количество аргументов (math.sqrt() с 1 аргументом, math.pow() с 2 и max() с 12),

Я использовал CPython 2.7.10 и 3.4.3+ в Linux x64.Я смог выполнить профилирование памяти только на Python 2. Использованный мной исходный код доступен здесь .

Мои выводы таковы:

  • Замыкания работают быстрее, чемэквивалентные вызываемые классы: примерно в 3 раза быстрее на Python 2, но только в 1,5 раза быстрее на Python 3. Сужение происходит потому, что закрытие стало медленнее, а вызываемые классы медленнее.
  • Замыкания занимают меньше памяти, чем эквивалентные вызываемые классы: примерно2/3 памяти (проверено только на Python 2).
  • Хотя это и не является частью исходного вопроса, интересно отметить, что накладные расходы времени выполнения для вызовов, сделанных с помощью замыкания, примерно такие же, как вызовдо math.pow(), тогда как через вызываемый класс это примерно вдвое больше.

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

Таким образом, это подтверждает (наоборот, то, что я написал ранее), что принятый ответ @RaymondHettinger является правильным, иЗакрытия должны быть предпочтительнее для косвенных вызовов, по крайней мере, до тех пор, пока они не ухудшают читабельность.Также спасибо @AXO за указание на ошибку в моем исходном коде.

1 голос
/ 23 января 2012

Я считаю, что классовый подход легче понять с первого взгляда и, следовательно, более легко обслуживать. Поскольку это одна из предпосылок хорошего кода на Python, я думаю, что при прочих равных условиях лучше использовать класс, а не вложенную функцию. Это один из случаев, когда гибкая природа Python заставляет язык нарушать предикат «должен быть один, а предпочтительно только один, очевидный способ сделать что-то» для кодирования в Python.

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

Но да, если был жесткий цикл с использованием переменных состояния, оценка переменных замыкания должна быть немного быстрее, чем оценка атрибутов класса. Конечно, это можно было бы преодолеть, просто вставив строку типа op = self.op внутри метода класса перед входом в цикл, сделав доступ к переменной внутри цикла к локальной переменной - это позволит избежать поиска атрибутов и выборка для каждого доступа. Опять же, различия в производительности должны быть незначительными, и у вас возникнет более серьезная проблема, если вам нужно немного больше производительности и вы пишете код на Python.

0 голосов
/ 23 января 2012

Я бы переписал class пример с чем-то вроде:

class F(object):
    __slots__ = ('__call__')
    def __init__(self, op):
        if op == 'mult':
            self.__call__ = lambda a, b: a * b
        elif op == 'add':
            self.__call__ = lambda a, b: a + b
        else:
            raise InvalidOp(op)

Это дает 0,40 usec / pass (функция 0,31, поэтому она на 29% медленнее) на моей машине с Python 3.2.2. Без использования object в качестве базового класса он дает 0,65% использования / проход (т.е. на 55% медленнее, чем на основе object). И по какой-то причине код с проверкой op в __call__ дает почти такие же результаты, как если бы это было сделано в __init__. С object в качестве базы и проверки внутри __call__ дает 0,61 usec / pass.

Причиной использования классов может быть полиморфизм.

class UserFunctions(object):
    __slots__ = ('__call__')
    def __init__(self, name):
        f = getattr(self, '_func_' + name, None)
        if f is None: raise InvalidOp(name)
        else: self.__call__ = f

class MyOps(UserFunctions):
    @classmethod
    def _func_mult(cls, a, b): return a * b
    @classmethod
    def _func_add(cls, a, b): return a + b
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...