6

What is the proper way in Python to allow the user to extend the types on which a function can operate without altering the original code of the function?

Suppose I have a module with a my_module.foo() function that was originally written to work on float types. Now I would like the same function to be able to work also with, let's say, mpmath arbitrary precision floats - but without altering the code in the original module.

In C++ I would add an extra overload (or, more likely, some template specialisation trickery with an helper struct). How should I structure the original my_module.foo() code so that the user can add her own custom hooks inside?

I can think of a few way to achieve this, but as a novice Python programmer I am sure most of them are going to be horrid :)

EDIT: thanks for all the answers so far, much appreciated.

I should probably clarify that one key requirement is being able to cope with types which I have not defined myself. E.g., if I am trying to code a generic cos function in my module, I want to call math.cos on builtin types, mpmath.cos on mpf types, sympy.cos on sympy symbolic types, etc. And of course I would like the dispatching logic not to be in my module's cos implementation.

bluescarni
  • 3,937
  • 1
  • 22
  • 33
  • 1
    Use duck-typing. Document what methods/special-methods should be supported by the argument and let the user pass in objects that conform to that interface. – Bakuriu Sep 23 '13 at 11:13
  • 1
    I had the exact same problem, but got no satisfactory answers: http://stackoverflow.com/questions/7151776 – sellibitze Sep 23 '13 at 11:34
  • Aren't `mpmath` floats already usable in place of built in `float`s? Or are there some operators that don't work on `mpmath` floats, the availability of which your function requires? – Erik Kaplun Sep 23 '13 at 11:52
  • @ErikAllik: as an example, I would like to define a `cos` function in my module that calls `math.cos` on builtin types, `mpmath.cos` for `mpf` types, maybe `sympy.cos` for sympy symbolic types, etc. And I would like to be able not to fix the number of types my `cos` knows about. – bluescarni Sep 23 '13 at 12:49
  • @Bakuriu: but then I would need to monkey-patch existing types that do not conform to my interface, wouldn't I? – bluescarni Sep 23 '13 at 12:50

3 Answers3

7

There are two ways to do this:

  • Delegation. Already in Python. Most Pythonic. Doesn't work for builtin types. Not exactly what you're looking for.
  • Single Dispatch. Still a PEP. Works for builtin types. Precisely what you're looking for.

Delegation

You usually delegate responsibility to the object you're acting on, and you don't implement the logic in your function.

Here's an example: len. The implementation of len is very staightforward:

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

Different types (str, list, tuple...) have different implementations, but they all work with the same function.

Now, if I want to define my own type, that works with len, I can do:

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

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

In your case, you would be implementing something that looks like len.

(Note: this isn't the actual code for len, but it's more or less equivalent.)

Single Dispatch

Of course, in some cases, that might not be practical. If that's your case, then the "Single Dispatch" PEP 443 is probably what you're looking for.

It suggests a new decorator that would accomplish what you're looking for:

>>> 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)

Once you've defined your function as such, you can call fun(something), and Python will find out the correct implementation (int or list here), of fallback to the default implementation def fun(...): ....

You therefore solely have to decorate your original function, and you're done, your users can add their own types.

Note: as pointed out in the comments, singledispatch is already implemented in Python, it's pkgutil.simplegeneric

Thomas Orozco
  • 53,284
  • 11
  • 113
  • 116
  • Problem is: You can't extend buit-in types (such as float) in that way. – sellibitze Sep 23 '13 at 11:19
  • @sellibitze Hence the link to the single dispatch PEP, I'll clarify that. – Thomas Orozco Sep 23 '13 at 11:20
  • The single dispatch is nothing new. You can find it in `pkgutil` named `simplegeneric` (instead of `functools.singledispatch`). – sloth Sep 23 '13 at 11:24
  • @DominicKexel That's true (and it's listed in the PEP). As far as I know though, `pkgutil.simplegeneric` isn't a documented API - hence my intent to let bluescarni read through the PEP. I'll incorporate your comment though. – Thomas Orozco Sep 23 '13 at 11:27
  • Interesting! Are there any requirements on where the "overloads" can be placed? Ideally one would like to be able to do this from a different module as well. – sellibitze Sep 23 '13 at 11:28
  • @sellibitze As long as `register` has been called before the function is called, things should work as expected. – Thomas Orozco Sep 23 '13 at 11:30
  • @ThomasOrozco: So, "fun" would be a global name w.r.t. the reigster function? – sellibitze Sep 23 '13 at 11:33
  • @sellibitze Basically you create a "generic" function (that would usually raise a `NotImplementedError`), that's `fun`. Then, you register type-specific implementations of the function using `fun.register`. Then, you can call `fun(something)`, and Python will find out what type-specific implementation to use (or fallback to the generic). – Thomas Orozco Sep 23 '13 at 11:34
  • @ThomasOrozco: thanks a lot for the detailed information. The single dispatch mechanism looks really neat, will have to do some reading (why didn't they call it `enable_if`??? /troll mode :) – bluescarni Sep 23 '13 at 12:57
1

It's possible to do what you want without waiting for PEP 443 - Single-dispatch generic functions to be implemented by instead using abstract base classes which were added in Python 2.6. These allow you to create "virtual" meta classes and add arbitrary subclasses to them on the fly without altering existing code or monkey-patching it. Your module can then use types registered with this metaclass to figure out what to do. You (or the authors) of other types can register them as needed.

Here's example code illustrating the concept:

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)
martineau
  • 119,623
  • 25
  • 170
  • 301
0

It depends on a code and expected results. Usually you shouldn't specify data types implicitly. Use Duck Typing.

But if a function expects only floats, you can wrap it. There's a simplest example:

def bar(data):
    """Execute my_module.foo function with data converted to float"""
    return my_module.foo(float(data))
Oleksandr Fedorov
  • 1,213
  • 10
  • 17