Skip to main content

Overloading Python functions

Project description

Ovld

Multiple dispatch in Python, with some extra features.

With ovld, you can write a version of the same function for every type signature using annotations instead of writing an awkward sequence of isinstance statements. Unlike Python singledispatch, it works for multiple arguments.

Other features of ovld:

  • Multiple dispatch for methods (with metaclass=ovld.OvldMC)
  • Create variants of functions
  • Built-in support for extensible, stateful recursion
  • Function wrappers
  • Function postprocessors
  • Nice stack traces

Example

Here's a function that adds lists, tuples and dictionaries:

from ovld import ovld

@ovld
def add(x: list, y: list):
    return [add(a, b) for a, b in zip(x, y)]

@ovld
def add(x: tuple, y: tuple):
    return tuple(add(a, b) for a, b in zip(x, y))

@ovld
def add(x: dict, y: dict):
    return {k: add(v, y[k]) for k, v in x.items()}

@ovld
def add(x: object, y: object):
    return x + y

Bootstrapping and variants

Now, there is another way to do this using ovld's auto-bootstrapping. Simply list self as the first argument to the function, and self will be bound to the function itself, so you can call self(x, y) for the recursion instead of add(x, y):

@ovld
def add(self, x: list, y: list):
    return [self(a, b) for a, b in zip(x, y)]

@ovld
def add(self, x: tuple, y: tuple):
    return tuple(self(a, b) for a, b in zip(x, y))

@ovld
def add(self, x: dict, y: dict):
    return {k: self(v, y[k]) for k, v in x.items()}

@ovld
def add(self, x: object, y: object):
    return x + y

Why is this useful, though? Observe:

@add.variant
def mul(self, x: object, y: object):
    return x * y

assert add([1, 2], [3, 4]) == [4, 6]
assert mul([1, 2], [3, 4]) == [3, 8]

A variant of a function is a copy which inherits all of the original's implementations but may define new ones. And because self is bound to the function that's called at the top level, the implementations for list, tuple and dict will bind self to add or mul depending on which one was called.

State

You can pass initial_state to @ovld or variant. The initial state must be a function that takes no arguments. Its return value will be available in self.state. The state is initialized at the top level call, but recursive calls to self will preserve it.

In other words, you can do something like this:

@add.variant(initial_state=lambda: 0)
def count(self, x, y):
    self.state += 1
    return (f"#{self.state}", x + y)

assert count([1, 2, 3], [4, 5, 6]) == [("#1", 5), ("#2", 7), ("#3", 9)]

The initial_state function can return any object and you can use the state to any purpose (e.g. cache or memoization).

Custom dispatch

You can define your own dispatching function. The dispatcher's first argument is always self.

  • self.resolve(x, y) to get the right function for the types of x and y
  • self[type(x), type(y)] will also return the right function for these types, but it works directly with the types.

For example, here is how you might define a function such that f(x) <=> f(x, x):

@ovld.dispatch
def add_default(self, x, y=None):
    if y is None:
        y = x
    return self.resolve(x, y)(x, y)

@ovld
def add_default(x: int, y: int):
    return x + y

@ovld
def add_default(x: str, y: str):
    return x + y

@ovld
def add_default(xs: list, ys: list):
    return [add_default(x, y) for x, y in zip(xs, ys)]

assert add_default([1, 2, "alouette"]) == [2, 4, "alouettealouette"]

There are other uses for this feature, e.g. memoization.

The normal functions may also have a self, which works the same as bootstrapping, and you can give an initial_state to @ovld.dispatch as well.

Postprocess

@ovld, @ovld.dispatch, etc. take a postprocess argument which should be a function of one argument. That function will be called with the result of the call and must return the final result of the call.

Note that intermediate, bootstrapped recursive calls (recursive calls using self()) will not be postprocessed (if you want to wrap these calls, you can do so otherwise, like defining a custom dispatch). Only the result of the top level call is postprocessed.

Methods

Use the OvldMC metaclass to use multiple dispatch on methods. In this case there is no bootstrapping as described above and self is simply bound to the class instance.

from ovld import OvldMC

class Cat(metaclass=OvldMC):
    def interact(self, x: Mouse):
        return "catch"

    def interact(self, x: Food):
        return "devour"

    def interact(self, x: PricelessVase):
        return "destroy"

Ambiguous calls

The following definitions will cause a TypeError at runtime when called with two ints, because it is unclear which function is the right match:

@ovld
def ambig(x: int, y: object):
    print("io")

@ovld
def ambig(x: object, y: int):
    print("oi")

ambig(8, 8)  # ???

You may define an additional function with signature (int, int) to disambiguate:

@ovld
def ambig(x: int, y: int):
    print("ii")

Other features

Tracebacks

ovld automagically renames functions so that the stack trace is more informative:

@add.variant
def bad(self, x: object, y: object):
    raise Exception("Bad.")

bad([1], [2])

"""
  File "/Users/breuleuo/code/ovld/ovld/core.py", line 148, in bad.entry
    res = ovc(*args, **kwargs)
  File "/Users/breuleuo/code/ovld/ovld/core.py", line 182, in bad.dispatch
    return method(self.bind_to, *args, **kwargs)
  File "example.py", line 6, in bad[list, list]
    return [self(a, b) for a, b in zip(x, y)]
  File "example.py", line 6, in <listcomp>
    return [self(a, b) for a, b in zip(x, y)]
  File "/Users/breuleuo/code/ovld/ovld/core.py", line 182, in bad.dispatch
    return method(self.bind_to, *args, **kwargs)
  File "example.py", line 26, in bad[*, *]
    raise Exception("Bad.")
  Exception: Bad.
"""

The functions on the stack have names like bad.entry, bad.dispatch, bad[list, list] and bad[*, *] (* stands for object), which lets you better understand what happened just from the stack trace.

This also means profilers will be able to differentiate between these paths and between variants, even if they share code paths.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ovld-0.2.0.tar.gz (14.1 kB view details)

Uploaded Source

Built Distribution

ovld-0.2.0-py3-none-any.whl (13.3 kB view details)

Uploaded Python 3

File details

Details for the file ovld-0.2.0.tar.gz.

File metadata

  • Download URL: ovld-0.2.0.tar.gz
  • Upload date:
  • Size: 14.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.0.0b3 CPython/3.7.7 Darwin/19.6.0

File hashes

Hashes for ovld-0.2.0.tar.gz
Algorithm Hash digest
SHA256 6d672eedb46c30556defd1f8df6f949a84b418160d3cd54a64ee6dd7fddd9277
MD5 1b0b8e08c49e26b784f13c606786a0c2
BLAKE2b-256 3b6f3eef1e70f03441d5e7714fca4c7a8d4a6bca53d1b47c79f49f71ade68b89

See more details on using hashes here.

File details

Details for the file ovld-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: ovld-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 13.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.0.0b3 CPython/3.7.7 Darwin/19.6.0

File hashes

Hashes for ovld-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 00216046e492cec56897dd25243ce798258549c0648a30d73a0b71c692992802
MD5 472338f94e95dcbe9654789998d745be
BLAKE2b-256 65561c5d2fcc545078eaf5608c8dc20bc53e06712852395c875ec1fb857c67ef

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page