functools — Herramientas para manipular funciones

Propósito:Funciones que operan en otras funciones.

El módulo functools proporciona herramientas para adaptar o ampliar funciones y otros objetos invocables, sin re escribirlos completamente.

Decoradores

La herramienta principal proporcionada por el módulo functools es la clase partial, que se puede usar para «envolver» un objeto invocable con argumentos por defecto. El objeto resultante es en sí mismo invocable y puede ser tratado como si fuera la función original. Accepta todos los mismos argumentos que el original, y se puede invocar con argumentos posicionales extra o también nombrados. Se puede usar una partial en lugar de lambda para proporcionar argumentos predeterminados a una función, mientras deja algunos argumentos no especificados.

Objetos parciales

Este ejemplo muestra dos objetos partial simples para la function myfunc(). La salida de show_details() incluye los atributos func, args, y keywords del objeto parcial.

functools_partial.py
import functools


def myfunc(a, b=2):
    "Docstring for myfunc()."
    print('  called myfunc with:', (a, b))


def show_details(name, f, is_partial=False):
    "Show details of a callable object."
    print('{}:'.format(name))
    print('  object:', f)
    if not is_partial:
        print('  __name__:', f.__name__)
    if is_partial:
        print('  func:', f.func)
        print('  args:', f.args)
        print('  keywords:', f.keywords)
    return


show_details('myfunc', myfunc)
myfunc('a', 3)
print()

# Set a different default value for 'b', but require
# the caller to provide 'a'.
p1 = functools.partial(myfunc, b=4)
show_details('partial with named default', p1, True)
p1('passing a')
p1('override b', b=5)
print()

# Set default values for both 'a' and 'b'.
p2 = functools.partial(myfunc, 'default a', b=99)
show_details('partial with defaults', p2, True)
p2()
p2(b='override b')
print()

print('Insufficient arguments:')
p1()

Al final del ejemplo, el primer partial creado es invocado sin pasar un valor para a, causando una excepción.

$ python3 functools_partial.py

myfunc:
  object: <function myfunc at 0x1007a6a60>
  __name__: myfunc
  called myfunc with: ('a', 3)

partial with named default:
  object: functools.partial(<function myfunc at 0x1007a6a60>,
b=4)
  func: <function myfunc at 0x1007a6a60>
  args: ()
  keywords: {'b': 4}
  called myfunc with: ('passing a', 4)
  called myfunc with: ('override b', 5)

partial with defaults:
  object: functools.partial(<function myfunc at 0x1007a6a60>,
'default a', b=99)
  func: <function myfunc at 0x1007a6a60>
  args: ('default a',)
  keywords: {'b': 99}
  called myfunc with: ('default a', 99)
  called myfunc with: ('default a', 'override b')

Insufficient arguments:
Traceback (most recent call last):
  File "functools_partial.py", line 51, in <module>
    p1()
TypeError: myfunc() missing 1 required positional argument: 'a'

Aduiriendo propiedades de función

El objeto partial por defecto no tiene los atributos __name__ o __doc__ y sin esos atributos, las funciones decoradas son más difíciles de depurar. Usando update_wrapper(), copia o agrega atributos de la función original al objeto partial.

functools_update_wrapper.py
import functools


def myfunc(a, b=2):
    "Docstring for myfunc()."
    print('  called myfunc with:', (a, b))


def show_details(name, f):
    "Show details of a callable object."
    print('{}:'.format(name))
    print('  object:', f)
    print('  __name__:', end=' ')
    try:
        print(f.__name__)
    except AttributeError:
        print('(no __name__)')
    print('  __doc__', repr(f.__doc__))
    print()


show_details('myfunc', myfunc)

p1 = functools.partial(myfunc, b=4)
show_details('raw wrapper', p1)

print('Updating wrapper:')
print('  assign:', functools.WRAPPER_ASSIGNMENTS)
print('  update:', functools.WRAPPER_UPDATES)
print()

functools.update_wrapper(p1, myfunc)
show_details('updated wrapper', p1)

Los atributos agregados al wrapper están definidos en WRAPPER_ASSIGNMENTS, mientras que WRAPPER_UPDATES lista los valores a modificar.

$ python3 functools_update_wrapper.py

myfunc:
  object: <function myfunc at 0x1018a6a60>
  __name__: myfunc
  __doc__ 'Docstring for myfunc().'

raw wrapper:
  object: functools.partial(<function myfunc at 0x1018a6a60>,
b=4)
  __name__: (no __name__)
  __doc__ 'partial(func, *args, **keywords) - new function with
partial application\n    of the given arguments and keywords.\n'

Updating wrapper:
  assign: ('__module__', '__name__', '__qualname__', '__doc__',
'__annotations__')
  update: ('__dict__',)

updated wrapper:
  object: functools.partial(<function myfunc at 0x1018a6a60>,
b=4)
  __name__: myfunc
  __doc__ 'Docstring for myfunc().'

Otros invocables

Los parciales funcionan con cualquier objeto invocable, no solo con funciones independientes.

functools_callable.py
import functools


class MyClass:
    "Demonstration class for functools"

    def __call__(self, e, f=6):
        "Docstring for MyClass.__call__"
        print('  called object with:', (self, e, f))


def show_details(name, f):
    "Show details of a callable object."
    print('{}:'.format(name))
    print('  object:', f)
    print('  __name__:', end=' ')
    try:
        print(f.__name__)
    except AttributeError:
        print('(no __name__)')
    print('  __doc__', repr(f.__doc__))
    return


o = MyClass()

show_details('instance', o)
o('e goes here')
print()

p = functools.partial(o, e='default for e', f=8)
functools.update_wrapper(p, o)
show_details('instance wrapper', p)
p()

Este ejemplo crea parciales de una instancia de una clase con un método __call__().

$ python3 functools_callable.py

instance:
  object: <__main__.MyClass object at 0x1011b1cf8>
  __name__: (no __name__)
  __doc__ 'Demonstration class for functools'
  called object with: (<__main__.MyClass object at 0x1011b1cf8>,
'e goes here', 6)

instance wrapper:
  object: functools.partial(<__main__.MyClass object at
0x1011b1cf8>, f=8, e='default for e')
  __name__: (no __name__)
  __doc__ 'Demonstration class for functools'
  called object with: (<__main__.MyClass object at 0x1011b1cf8>,
'default for e', 8)

Métodos y Funciones

Mientras que partial() devuelve un invocable listo para ser usado directamente, partialmethod() devuelve un invocable listo para ser utilizado como un método sin consolidar de un objeto. En el siguiente ejemplo, la misma función independiente se agrega como un atributo de MyClass dos veces, una vez usando partialmethod() como method1() y otra vez usando partial() como method2().

functools_partialmethod.py
import functools


def standalone(self, a=1, b=2):
    "Standalone function"
    print('  called standalone with:', (self, a, b))
    if self is not None:
        print('  self.attr =', self.attr)


class MyClass:
    "Demonstration class for functools"

    def __init__(self):
        self.attr = 'instance attribute'

    method1 = functools.partialmethod(standalone)
    method2 = functools.partial(standalone)


o = MyClass()

print('standalone')
standalone(None)
print()

print('method1 as partialmethod')
o.method1()
print()

print('method2 as partial')
try:
    o.method2()
except TypeError as err:
    print('ERROR: {}'.format(err))

method1() se puede invocar desde una instancia de MyClass, y la instancia se pasa como el primer argumento al igual que con los métodos definidos normalmente. method2() no está configurado como un método vinculado, por lo que el argumento self debe pasarse explícitamente, o la invocación resultará en un TypeError.

$ python3 functools_partialmethod.py

standalone
  called standalone with: (None, 1, 2)

method1 as partialmethod
  called standalone with: (<__main__.MyClass object at
0x1007b1d30>, 1, 2)
  self.attr = instance attribute

method2 as partial
ERROR: standalone() missing 1 required positional argument:
'self'

Adquiriendo propiedades de funciones para decoradores

Actualizar las propiedades de un invocable envuelto es especialmente útil cuando se usa en un decorador, ya que la función transformada termina con propiedades de la función «desnuda» original.

functools_wraps.py
import functools


def show_details(name, f):
    "Show details of a callable object."
    print('{}:'.format(name))
    print('  object:', f)
    print('  __name__:', end=' ')
    try:
        print(f.__name__)
    except AttributeError:
        print('(no __name__)')
    print('  __doc__', repr(f.__doc__))
    print()


def simple_decorator(f):
    @functools.wraps(f)
    def decorated(a='decorated defaults', b=1):
        print('  decorated:', (a, b))
        print('  ', end=' ')
        return f(a, b=b)
    return decorated


def myfunc(a, b=2):
    "myfunc() is not complicated"
    print('  myfunc:', (a, b))
    return


# The raw function
show_details('myfunc', myfunc)
myfunc('unwrapped, default b')
myfunc('unwrapped, passing b', 3)
print()

# Wrap explicitly
wrapped_myfunc = simple_decorator(myfunc)
show_details('wrapped_myfunc', wrapped_myfunc)
wrapped_myfunc()
wrapped_myfunc('args to wrapped', 4)
print()


# Wrap with decorator syntax
@simple_decorator
def decorated_myfunc(a, b):
    myfunc(a, b)
    return


show_details('decorated_myfunc', decorated_myfunc)
decorated_myfunc()
decorated_myfunc('args to decorated', 4)

functools proporciona un decorador, wraps(), que aplica update_wrapper() a la función decorada.

$ python3 functools_wraps.py

myfunc:
  object: <function myfunc at 0x101241b70>
  __name__: myfunc
  __doc__ 'myfunc() is not complicated'

  myfunc: ('unwrapped, default b', 2)
  myfunc: ('unwrapped, passing b', 3)

wrapped_myfunc:
  object: <function myfunc at 0x1012e62f0>
  __name__: myfunc
  __doc__ 'myfunc() is not complicated'

  decorated: ('decorated defaults', 1)
     myfunc: ('decorated defaults', 1)
  decorated: ('args to wrapped', 4)
     myfunc: ('args to wrapped', 4)

decorated_myfunc:
  object: <function decorated_myfunc at 0x1012e6400>
  __name__: decorated_myfunc
  __doc__ None

  decorated: ('decorated defaults', 1)
     myfunc: ('decorated defaults', 1)
  decorated: ('args to decorated', 4)
     myfunc: ('args to decorated', 4)

Comparación

En Python 2, las clases podían definir un método __cmp __() que devuelve -1, 0, o 1 en función de que si el objeto es menos que, igual o mayor que el elemento que se compara. Python 2.1 introdujo la interfaz de métodos de comparación rica (__lt__(), __le__(), __eq__(), __ne__(), __gt__(), y __ge__()), que realizan una única operación de comparación y regresan un valor booleano. Python 3 desaprobó __cmp__() en favor de estos nuevos métodos y functools proporcionan herramientas para hacer más fácil el escribir clases que cumplan con los nuevos requerimientos de comparación requisitos en Python 3.

Comparación rica

La interfaz de comparación rica está diseñada para permitir clases con comparaciones complejas, implementar cada prueba de la manera más eficiente posible. Sin embargo, para clases donde la comparación es relativamente simple, hay no tiene sentido crear manualmente cada uno de los métodos de comparación rica. El decorador de clases total_ordering() toma una clase que proporciona algunos de los métodos, y agrega el resto de ellos.

functools_total_ordering.py
import functools
import inspect
from pprint import pprint


@functools.total_ordering
class MyObject:

    def __init__(self, val):
        self.val = val

    def __eq__(self, other):
        print('  testing __eq__({}, {})'.format(
            self.val, other.val))
        return self.val == other.val

    def __gt__(self, other):
        print('  testing __gt__({}, {})'.format(
            self.val, other.val))
        return self.val > other.val


print('Methods:\n')
pprint(inspect.getmembers(MyObject, inspect.isfunction))

a = MyObject(1)
b = MyObject(2)

print('\nComparisons:')
for expr in ['a < b', 'a <= b', 'a == b', 'a >= b', 'a > b']:
    print('\n{:<6}:'.format(expr))
    result = eval(expr)
    print('  result of {}: {}'.format(expr, result))

La clase debe proporcionar la implementación de __eq__() y otro método de comparación rica. El decorador agrega implementaciones del resto de los métodos que funcionan usandor las comparaciones proporcionadas. Si no se puede hacer una comparación, el método debería devolver NotImplemented, entonces la comparación puede ser probada usando los operadores de comparación inversa en el otro objeto, antes de fallar por completo.

$ python3 functools_total_ordering.py

Methods:

[('__eq__', <function MyObject.__eq__ at 0x10139a488>),
 ('__ge__', <function _ge_from_gt at 0x1012e2510>),
 ('__gt__', <function MyObject.__gt__ at 0x10139a510>),
 ('__init__', <function MyObject.__init__ at 0x10139a400>),
 ('__le__', <function _le_from_gt at 0x1012e2598>),
 ('__lt__', <function _lt_from_gt at 0x1012e2488>)]

Comparisons:

a < b :
  testing __gt__(1, 2)
  testing __eq__(1, 2)
  result of a < b: True

a <= b:
  testing __gt__(1, 2)
  result of a <= b: True

a == b:
  testing __eq__(1, 2)
  result of a == b: False

a >= b:
  testing __gt__(1, 2)
  testing __eq__(1, 2)
  result of a >= b: False

a > b :
  testing __gt__(1, 2)
  result of a > b: False

Orden de colación

Como las funciones de comparación antiguas están en desuso en Python 3, el argumento cmp para funciones como sort() ya no es soportado. Los programas antiguos que usan funciones de comparación pueden usar cmp_to_key() para convertirlos a una función que devuelve una llave de colación, que se utiliza para determinar la posición en la secuencia final.

functools_cmp_to_key.py
import functools


class MyObject:

    def __init__(self, val):
        self.val = val

    def __str__(self):
        return 'MyObject({})'.format(self.val)


def compare_obj(a, b):
    """Old-style comparison function.
    """
    print('comparing {} and {}'.format(a, b))
    if a.val < b.val:
        return -1
    elif a.val > b.val:
        return 1
    return 0


# Make a key function using cmp_to_key()
get_key = functools.cmp_to_key(compare_obj)

def get_key_wrapper(o):
    "Wrapper function for get_key to allow for print statements."
    new_key = get_key(o)
    print('key_wrapper({}) -> {!r}'.format(o, new_key))
    return new_key


objs = [MyObject(x) for x in range(5, 0, -1)]

for o in sorted(objs, key=get_key_wrapper):
    print(o)

Normalmente cmp_to_key() se usaría directamente, pero en este ejemplo, se introduce una función de envoltura adicional para imprimir más información cuando se está llamando a la función clave.

El resultado muestra que sorted() comienza llamando get_key_wrapper() para cada elemento en la secuencia para producir una llave. Las claves devueltas por cmp_to_key() son instancias de una clase definida en functools que implementa la interfaz de comparación rica utilizando la función de comparación al estilo antiguo. Después de que todas las llaves se crean, la secuencia se ordena comparando las llaves.

$ python3 functools_cmp_to_key.py

key_wrapper(MyObject(5)) -> <functools.KeyWrapper object at
0x1011c5530>
key_wrapper(MyObject(4)) -> <functools.KeyWrapper object at
0x1011c5510>
key_wrapper(MyObject(3)) -> <functools.KeyWrapper object at
0x1011c54f0>
key_wrapper(MyObject(2)) -> <functools.KeyWrapper object at
0x1011c5390>
key_wrapper(MyObject(1)) -> <functools.KeyWrapper object at
0x1011c5710>
comparing MyObject(4) and MyObject(5)
comparing MyObject(3) and MyObject(4)
comparing MyObject(2) and MyObject(3)
comparing MyObject(1) and MyObject(2)
MyObject(1)
MyObject(2)
MyObject(3)
MyObject(4)
MyObject(5)

Almacenamiento en caché

El decorador lru_cache() envuelve una función en un caché de memoria utilizada menos recientemente. Los argumentos a la función se utilizan para construir una llave hash, que luego se asigna al resultado. Llamadas posteriores con los mismos argumentos obtendrán el valor de la memoria caché en lugar de llamar a la función. El decorador también agrega métodos a la función para examinar el estado de la memoria caché (cache_info()) y vaciar el caché (cache_clear()).

functools_lru_cache.py
import functools


@functools.lru_cache()
def expensive(a, b):
    print('expensive({}, {})'.format(a, b))
    return a * b


MAX = 2

print('First set of calls:')
for i in range(MAX):
    for j in range(MAX):
        expensive(i, j)
print(expensive.cache_info())

print('\nSecond set of calls:')
for i in range(MAX + 1):
    for j in range(MAX + 1):
        expensive(i, j)
print(expensive.cache_info())

print('\nClearing cache:')
expensive.cache_clear()
print(expensive.cache_info())

print('\nThird set of calls:')
for i in range(MAX):
    for j in range(MAX):
        expensive(i, j)
print(expensive.cache_info())

Este ejemplo realiza varias llamadas a expensive() en un conjunto de bucles anidados. La segunda vez que esas llamadas se realizan con los mismos valores, los resultados aparecen en el caché. Cuando el caché se borra y los bucles se ejecutan de nuevo los valores deben ser re calculados.

$ python3 functools_lru_cache.py

First set of calls:
expensive(0, 0)
expensive(0, 1)
expensive(1, 0)
expensive(1, 1)
CacheInfo(hits=0, misses=4, maxsize=128, currsize=4)

Second set of calls:
expensive(0, 2)
expensive(1, 2)
expensive(2, 0)
expensive(2, 1)
expensive(2, 2)
CacheInfo(hits=4, misses=9, maxsize=128, currsize=9)

Clearing cache:
CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)

Third set of calls:
expensive(0, 0)
expensive(0, 1)
expensive(1, 0)
expensive(1, 1)
CacheInfo(hits=0, misses=4, maxsize=128, currsize=4)

Para evitar que la memoria caché crezca sin límites en un proceso de larga ejecución, se le da un tamaño máximo. El valor predeterminado es de 128 entradas, pero eso se puede cambiar para cada caché usando el argumento maxsize.

functools_lru_cache_expire.py
import functools


@functools.lru_cache(maxsize=2)
def expensive(a, b):
    print('called expensive({}, {})'.format(a, b))
    return a * b


def make_call(a, b):
    print('({}, {})'.format(a, b), end=' ')
    pre_hits = expensive.cache_info().hits
    expensive(a, b)
    post_hits = expensive.cache_info().hits
    if post_hits > pre_hits:
        print('cache hit')


print('Establish the cache')
make_call(1, 2)
make_call(2, 3)

print('\nUse cached items')
make_call(1, 2)
make_call(2, 3)

print('\nCompute a new value, triggering cache expiration')
make_call(3, 4)

print('\nCache still contains one old item')
make_call(2, 3)

print('\nOldest item needs to be recomputed')
make_call(1, 2)

En este ejemplo, el tamaño de la memoria caché está configurado en 2 entradas. Cuando se utiliza el tercer set de argumentos únicos (3, 4), el elemento más antiguo en la memoria caché es desechado y reemplazado con el nuevo resultado.

$ python3 functools_lru_cache_expire.py

Establish the cache
(1, 2) called expensive(1, 2)
(2, 3) called expensive(2, 3)

Use cached items
(1, 2) cache hit
(2, 3) cache hit

Compute a new value, triggering cache expiration
(3, 4) called expensive(3, 4)

Cache still contains one old item
(2, 3) cache hit

Oldest item needs to be recomputed
(1, 2) called expensive(1, 2)

Las claves para la memoria caché gestionadas por lru_cache() deben ser hashable, por lo que todos los argumentos a la función envuelta con la búsqueda de caché deben ser hashable

functools_lru_cache_arguments.py
import functools


@functools.lru_cache(maxsize=2)
def expensive(a, b):
    print('called expensive({}, {})'.format(a, b))
    return a * b


def make_call(a, b):
    print('({}, {})'.format(a, b), end=' ')
    pre_hits = expensive.cache_info().hits
    expensive(a, b)
    post_hits = expensive.cache_info().hits
    if post_hits > pre_hits:
        print('cache hit')


make_call(1, 2)

try:
    make_call([1], 2)
except TypeError as err:
    print('ERROR: {}'.format(err))

try:
    make_call(1, {'2': 'two'})
except TypeError as err:
    print('ERROR: {}'.format(err))

Si cualquier objeto que no puede ser hash se pasa a la función, se plantea un TypeError.

$ python3 functools_lru_cache_arguments.py

(1, 2) called expensive(1, 2)
([1], 2) ERROR: unhashable type: 'list'
(1, {'2': 'two'}) ERROR: unhashable type: 'dict'

Reduciendo un conjunto de datos

La función reduce() toma un invocable y una secuencia de datos como entrada y produce un solo valor como salida basado en invocar el invocable con los valores de la secuencia y la acumulación de el resultado resultante.

functools_reduce.py
import functools


def do_reduce(a, b):
    print('do_reduce({}, {})'.format(a, b))
    return a + b


data = range(1, 5)
print(data)
result = functools.reduce(do_reduce, data)
print('result: {}'.format(result))

Este ejemplo suma los números en la secuencia de entrada.

$ python3 functools_reduce.py

range(1, 5)
do_reduce(1, 2)
do_reduce(3, 3)
do_reduce(6, 4)
result: 10

El argumento opcional initializer se coloca al frente de la secuencia y es procesado junto con los otros elementos. Esto puede usarse para actualizar un valor previamente calculado con nuevas entradas.

functools_reduce_initializer.py
import functools


def do_reduce(a, b):
    print('do_reduce({}, {})'.format(a, b))
    return a + b


data = range(1, 5)
print(data)
result = functools.reduce(do_reduce, data, 99)
print('result: {}'.format(result))

En este ejemplo, se utiliza una suma previa de 99 para inicializar el valor calculado por reduce().

$ python3 functools_reduce_initializer.py

range(1, 5)
do_reduce(99, 1)
do_reduce(100, 2)
do_reduce(102, 3)
do_reduce(105, 4)
result: 109

Las secuencias con un solo elemento se reducen automáticamente a ese valor cuando no hay inicializador presente. Las listas vacías generan un error, a menos que se proporcione un inicializador.

functools_reduce_short_sequences.py
import functools


def do_reduce(a, b):
    print('do_reduce({}, {})'.format(a, b))
    return a + b


print('Single item in sequence:',
      functools.reduce(do_reduce, [1]))

print('Single item in sequence with initializer:',
      functools.reduce(do_reduce, [1], 99))

print('Empty sequence with initializer:',
      functools.reduce(do_reduce, [], 99))

try:
    print('Empty sequence:', functools.reduce(do_reduce, []))
except TypeError as err:
    print('ERROR: {}'.format(err))

Porque el argumento de inicialización sirve como pre determinado, pero también es combinado con los nuevos valores si la secuencia de entrada no está vacía, es importante considerar cuidadosamente usarlo. Cuando no tiene sentido combinar el valor predeterminado con nuevos valores, es mejor atrapar el TypeError en lugar de pasar un inicializador.

$ python3 functools_reduce_short_sequences.py

Single item in sequence: 1
do_reduce(99, 1)
Single item in sequence with initializer: 100
Empty sequence with initializer: 99
ERROR: reduce() of empty sequence with no initial value

Funciones Genéricas

En un lenguaje de tipado dinámico como Python, es común necesitar realizar una operación ligeramente diferente en función del tipo de argumento, especialmente cuando se trata de la diferencia entre una lista de elementosy un solo elemento. Es lo suficientemente simple verificar el tipo de argumento directamente, pero en casos en que la diferencia de comportamiento pueda ser aislada en funciones separadas functools proporciona el decorador singledispatch() para registrar un conjunto de funciones genéricas para la conmutación automática en función del tipo del primer argumento a una función.

functools_singledispatch.py
import functools


@functools.singledispatch
def myfunc(arg):
    print('default myfunc({!r})'.format(arg))


@myfunc.register(int)
def myfunc_int(arg):
    print('myfunc_int({})'.format(arg))


@myfunc.register(list)
def myfunc_list(arg):
    print('myfunc_list()')
    for item in arg:
        print('  {}'.format(item))


myfunc('string argument')
myfunc(1)
myfunc(2.3)
myfunc(['a', 'b', 'c'])

El atributo register() de la nueva función sirve como otro decorador para registrar implementaciones alternativas. La primera función envuelta con singledispatch() es la implementación predeterminada si no se encuentra otra función específica de tipo, como con el caso float en este ejemplo.

$ python3 functools_singledispatch.py

default myfunc('string argument')
myfunc_int(1)
default myfunc(2.3)
myfunc_list()
  a
  b
  c

Cuando no se encuentra una coincidencia exacta para el tipo, el orden de herencia es evaluado y se usa el tipo de coincidencia más cercano.

functools_singledispatch_mro.py
import functools


class A:
    pass


class B(A):
    pass


class C(A):
    pass


class D(B):
    pass


class E(C, D):
    pass


@functools.singledispatch
def myfunc(arg):
    print('default myfunc({})'.format(arg.__class__.__name__))


@myfunc.register(A)
def myfunc_A(arg):
    print('myfunc_A({})'.format(arg.__class__.__name__))


@myfunc.register(B)
def myfunc_B(arg):
    print('myfunc_B({})'.format(arg.__class__.__name__))


@myfunc.register(C)
def myfunc_C(arg):
    print('myfunc_C({})'.format(arg.__class__.__name__))


myfunc(A())
myfunc(B())
myfunc(C())
myfunc(D())
myfunc(E())

En este ejemplo, las clases D y E no coinciden exactamente con ninguna función genérica registrada, y la función seleccionada depende de la jerarquía de clases.

$ python3 functools_singledispatch_mro.py

myfunc_A(A)
myfunc_B(B)
myfunc_C(C)
myfunc_B(D)
myfunc_C(E)

Ver también