Правильный способ сделать функции расширяемыми пользователем

Как правильно в Python разрешить пользователю расширять типы, с которыми может работать функция, не изменяя исходный код функции?

Предположим, у меня есть модуль с my_module.foo() функция, которая была изначально написана для работы над float типы. Теперь я хотел бы, чтобы эта функция работала также с, скажем, mpmath произвольная точность плавает - но без изменения кода в исходном модуле.

В C++ я бы добавил дополнительную перегрузку (или, что более вероятно, некоторую хитрость специализации шаблона с помощью вспомогательной структуры). Как я должен структурировать оригинал my_module.foo() код, чтобы пользователь мог добавить свои собственные пользовательские хуки внутри?

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

РЕДАКТИРОВАТЬ: спасибо за все ответы до сих пор, высоко ценится.

Я, вероятно, должен пояснить, что одним из ключевых требований является способность справляться с типами, которые я сам не определил. Например, если я пытаюсь кодировать общий cos функция в моем модуле, я хочу позвонить math.cos на встроенных типах, mpmath.cos на mpf типы, sympy.cos о символьных типах и т. д. И, конечно, я бы хотел, чтобы логика диспетчеризации не присутствовала в моём модуле. cos реализация.

3 ответа

Решение

Есть два способа сделать это:

  • Делегация Уже в Python. Самый Питонический. Не работает для встроенных типов. Не совсем то, что вы ищете.
  • Однократная отправка. Все еще ПКП. Работает для встроенных типов. Именно то, что вы ищете.

Делегация

Вы обычно делегируете ответственность объекту, с которым работаете, и не реализуете логику в своей функции.

Вот пример: len, Реализация len очень просто:

def len(obj):
    return obj.__len__()

Различные типы (str, list, tuple...) имеют разные реализации, но все они работают с одной и той же функцией.

Теперь, если я хочу определить свой собственный тип, который работает с len, Я могу сделать:

class MyTypeOfLength3(object):
    def __len__(self):
        return 3

o = MyTypeOfLength3()
print len(o) 
# 3

В вашем случае вы бы реализовали что-то похожее на len,

(Примечание: это не фактический код для len, но это более или менее эквивалентно.)

Одиночная отправка

Конечно, в некоторых случаях это может быть непрактично. Если это ваш случай, то PEP 443 "Single Dispatch", вероятно, то, что вы ищете.

Он предлагает новый декоратор, который будет выполнять то, что вы ищете:

>>> from functools import singledispatch
>>> @singledispatch
... def fun(arg, verbose=False):
...     if verbose:
...         print("Let me just say,", end=" ")
...     print(arg)
...
>>> @fun.register(int)
... def _(arg, verbose=False):
...     if verbose:
...         print("Strength in numbers, eh?", end=" ")
...     print(arg)
...
>>> @fun.register(list)
... def _(arg, verbose=False):
...     if verbose:
...         print("Enumerate this:")
...     for i, elem in enumerate(arg):
...         print(i, elem)

Как только вы определили свою функцию как таковую, вы можете вызвать fun(something) и Python найдет правильную реализацию (int или же list здесь), откат к реализации по умолчанию def fun(...): ...,

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

Примечание: как указано в комментариях, singledispatch уже реализовано в Python, это pkgutil.simplegeneric

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

Вот пример кода, иллюстрирующего концепцию:

import abc

class Trigonometric(object):
    __metaclass__ = abc.ABCMeta
    _registry = {}

    @classmethod
    def register(cls, subclass, cos_func, sin_func):
        cls.__metaclass__.register(cls, subclass)
        if subclass not in cls._registry:  # may or may not want this check...
            cls._registry[subclass] = {'cos': cos_func, 'sin': sin_func}

    @classmethod
    def call_func(cls, func_name, n):
        try:
            return cls._registry[n.__class__][func_name](n)
        except KeyError:
            raise RuntimeError(
                "Either type {} isn't registered or function {}() "
                "isn't known.".format(n.__class__.__name__, func_name))

# module-level functions
def cos(n):
    return Trigonometric.call_func('cos', n)

def sin(n):
    return Trigonometric.call_func('sin', n)

if __name__ == '__main__':
    # avoid hardcoding this module's filename into the source
    import sys
    my_module = sys.modules[__name__]  # replaces import my_module

    # register the built-in float type
    import math
    print 'calling Trigonometric.register(float)'
    Trigonometric.register(float, math.cos, math.sin)

    # register mpmath's arbitrary-precision mpf float type
    from mpmath import mp
    print 'calling Trigonometric.register(mp.mpf)'
    Trigonometric.register(mp.mpf, mp.cos, mp.sin)

    f = 1.0
    print 'isinstance(f, Trigonometric):', isinstance(f, Trigonometric)
    print 'my_module.cos(f):', my_module.cos(f), my_module.sin(f)

    v = mp.mpf(1)
    print 'isinstance(v, Trigonometric):', isinstance(v, Trigonometric)
    print 'my_module.cos(v):', my_module.cos(v), my_module.sin(v)

Это зависит от кода и ожидаемых результатов. Обычно вы не должны указывать типы данных неявно. Используйте Duck Typing.

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

def bar(data):
    """Execute my_module.foo function with data converted to float"""
    return my_module.foo(float(data))
Другие вопросы по тегам