Skip to main content

Overloading Python functions

Project description

Ovld

Fast multiple dispatch in Python, with many extra features.

📋 Documentation

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's singledispatch, it works for multiple arguments.

  • ⚡️ Fast: ovld is the fastest multiple dispatch library around, by some margin.
  • 🚀 Variants, mixins and medleys of functions and methods.
  • 🦄 Value-based dispatch: Overloaded functions can depend on more than argument types: they can depend on actual values.
  • 🔑 Extensive: Dispatch on functions, methods, positional arguments and even keyword arguments (with some restrictions).
  • ⚙️ Codegen: (Experimental) For advanced use cases, you can generate custom code for overloads.

Install with pip install ovld

Example

Define one version of your function for each type signature you want to support. ovld supports all basic types, plus literals and value-dependent types such as Regexp.

from ovld import ovld
from ovld.dependent import Regexp
from typing import Literal

@ovld
def f(x: str):
    return f"The string {x!r}"

@ovld
def f(x: int):
    return f"The number {x}"

@ovld
def f(x: int, y: int):
    return "Two numbers!"

@ovld
def f(x: Literal[0]):
    return "zero"

@ovld
def f(x: Regexp[r"^X"]):
    return "A string that starts with X"

assert f("hello") == "The string 'hello'"
assert f(3) == "The number 3"
assert f(1, 2) == "Two numbers!"
assert f(0) == "zero"
assert f("XSECRET") == "A string that starts with X"

Recursive example

ovld shines particularly with recursive definitions, for example tree maps or serialization. Here we define a function that recursively adds lists of lists and integers:

from ovld import ovld, recurse

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

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

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

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

assert add([1, 2], [3, 4]) == [4, 6]
assert add([1, 2, [3]], 7) == [8, 9, [10]]

The recurse function is special: it will recursively call the current ovld object. You may ask: how is it different from simply calling add? The difference is that if you create a variant of add, recurse will automatically call the variant.

For example:

Variants

A variant of an ovld is a copy of the ovld, with some methods added or changed. For example, let's take the definition of add above and make a variant that multiplies numbers instead:

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

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

Simple! This means you can define one ovld that recursively walks generic data structures, and then specialize it in various ways.

Priority and call_next

You can define a numeric priority for each method (the default priority is 0):

from ovld import call_next

@ovld(priority=1000)
def f(x: int):
    return call_next(x + 1)

@ovld
def f(x: int):
    return x * x

assert f(10) == 121

Both definitions above have the same type signature, but since the first has higher priority, that is the one that will be called.

However, that does not mean there is no way to call the second one. Indeed, when the first function calls the special function call_next(x + 1), it will call the next function in line, in order of priority and specificity.

The pattern you see above is how you may wrap each call with some generic behavior. For instance, if you did something like this:

@f.variant(priority=1000)
def f2(x: object)
    print(f"f({x!r})")
    return call_next(x)

The above is effectively a clone of f that traces every call. Useful for debugging.

Dependent types

A dependent type is a type that depends on a value. This enables dispatching based on the actual value of an argument. The simplest example of a dependent type is typing.Literal[value], which matches one single value. ovld also supports Dependent[bound, check] for arbitrary checks. For example, this definition of factorial:

from typing import Literal
from ovld import ovld, recurse, Dependent

@ovld
def fact(n: Literal[0]):
    return 1

@ovld
def fact(n: Dependent[int, lambda n: n > 0]):
    return n * recurse(n - 1)

assert fact(5) == 120
fact(-1)   # Error!

The first argument to Dependent must be a type bound. The bound must match before the logic is called, which also ensures we don't get a performance hit for unrelated types. For type checking purposes, Dependent[T, A] is equivalent to Annotated[T, A].

dependent_check

Define your own types with the @dependent_check decorator:

import torch
from ovld import ovld, dependent_check

@dependent_check
def Shape(tensor: torch.Tensor, *shape):
    return (
        len(tensor.shape) == len(shape)
        and all(s2 is Any or s1 == s2 for s1, s2 in zip(tensor.shape, shape))
    )

@dependent_check
def Dtype(tensor: torch.Tensor, dtype):
    return tensor.dtype == dtype

@ovld
def f(tensor: Shape[3, Any]):
    # Matches 3xN tensors
    ...

@ovld
def f(tensor: Shape[2, 2] & Dtype[torch.float32]):
    # Only matches 2x2 tensors that also have the float32 dtype
    ...

The first parameter is the value to check. The type annotation (e.g. value: torch.Tensor above) is interpreted by ovld to be the bound for this type, so Shape will only be called on parameters of type torch.Tensor.

Methods

Either inherit from OvldBase or use the OvldMC metaclass to use multiple dispatch on methods.

from ovld import OvldBase, OvldMC

# class Cat(OvldBase):  <= Also an option
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"

Subclasses

Subclasses inherit overloaded methods. They may define additional overloads for these methods which will only be valid for the subclass, but they need to use the @extend_super decorator (this is required for clarity):

from ovld import OvldMC, extend_super

class One(metaclass=OvldMC):
    def f(self, x: int):
        return "an integer"

class Two(One):
    @extend_super
    def f(self, x: str):
        return "a string"

assert Two().f(1) == "an integer"
assert Two().f("s") == "a string"

Medleys

Inheriting from ovld.Medley lets you combine functionality in a new way. Classes created that way are free-form medleys that you can (almost) arbitrarily combine together.

All medleys are dataclasses and you must define their data fields as you would for a normal dataclass (using dataclass.field if needed).

from ovld import Medley

class Punctuator(Medley):
    punctuation: str = "."

    def __call__(self, x: str):
        return f"{x}{self.punctuation}"

class Multiplier(Medley):
    factor: int = 3

    def __call__(self, x: int):
        return x * self.factor

# You can add the classes together to merge their methods and fields using ovld
PuMu = Punctuator + Multiplier
f = PuMu(punctuation="!", factor=3)

# You can also combine existing instances!
f2 = Punctuator("!") + Multiplier(3)

assert f("hello") == f2("hello") == "hello!"
assert f(10) == f2(10) == 30

# You can also meld medleys inplace, but only if all new fields have defaults
class Total(Medley):
    pass

Total.extend(Punctuator, Multiplier)
f3 = Total(punctuation="!", factor=3)

Code generation

(Experimental) For advanced use cases, you can generate custom code for type checkers or overloads. See here.

Benchmarks

ovld is pretty fast: the overhead is comparable to isinstance or match, and only 2-3x slower when dispatching on Literal types. Compared to other multiple dispatch libraries, it has 1.5x to 100x less overhead.

Time relative to the fastest implementation (1.00) (lower is better).

Benchmark custom ovld plum multim multid runtype sd
trivial 1.56 1.00 3.38 4.92 2.00 2.38 2.15
multer 1.22 1.00 11.06 4.67 9.22 2.24 3.92
add 1.27 1.00 3.61 4.93 2.24 2.62 x
ast 1.01 1.00 22.98 2.72 1.52 1.70 1.57
calc 1.00 1.28 57.86 29.79 x x x
regexp 1.00 2.28 22.71 x x x x
fib 1.00 3.39 403.38 114.69 x x x
tweaknum 1.00 1.86 x x x x x

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.5.14.tar.gz (105.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ovld-0.5.14-py3-none-any.whl (39.9 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: ovld-0.5.14.tar.gz
  • Upload date:
  • Size: 105.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for ovld-0.5.14.tar.gz
Algorithm Hash digest
SHA256 34bb7c1a3a3ee2656a8e8d7b840fdb9e4828efa27ea91e50d8e93b3f35122179
MD5 e5f2f17102bfa0a70992b79753dd86b6
BLAKE2b-256 da2def4b1de457fb0a5e96fc6cdeaf7cd185186ac4bde6b1d922381fe89c439d

See more details on using hashes here.

Provenance

The following attestation bundles were made for ovld-0.5.14.tar.gz:

Publisher: publish.yml on breuleux/ovld

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

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

File metadata

  • Download URL: ovld-0.5.14-py3-none-any.whl
  • Upload date:
  • Size: 39.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for ovld-0.5.14-py3-none-any.whl
Algorithm Hash digest
SHA256 964474b85a1ecd8d1a163db4d02c1be67ddd87c3e64ee115a46cf7c666828152
MD5 984609b6753fe50628c29031c3699c67
BLAKE2b-256 1745032d3c8008080861085e5bea4d0635fcaf75a362dcf2668780c1bd610f82

See more details on using hashes here.

Provenance

The following attestation bundles were made for ovld-0.5.14-py3-none-any.whl:

Publisher: publish.yml on breuleux/ovld

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

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