ovld


Nameovld JSON
Version 0.5.0 PyPI version JSON
download
home_pageNone
SummaryOverloading Python functions
upload_time2025-04-09 03:52:22
maintainerNone
docs_urlNone
authorNone
requires_python>=3.9
licenseNone
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            
# Ovld

Fast multiple dispatch in Python, with many extra features.

[📋 Documentation](https://ovld.readthedocs.io/en/latest/)

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](https://ovld.readthedocs.io/en/latest/compare/#results):** ovld is the fastest multiple dispatch library around, by some margin.
* 🚀 [**Variants**](https://ovld.readthedocs.io/en/latest/usage/#variants), [**mixins**](https://ovld.readthedocs.io/en/latest/usage/#mixins) and [**medleys**](https://ovld.readthedocs.io/en/latest/medley) of functions and methods.
* 🦄 **[Dependent types](https://ovld.readthedocs.io/en/latest/dependent/):** Overloaded functions can depend on more than argument types: they can depend on actual values.
* 🔑 **[Extensive](https://ovld.readthedocs.io/en/latest/usage/#keyword-arguments):** Dispatch on functions, methods, positional arguments and even keyword arguments (with some restrictions).
* ⚙️ **[Codegen](https://ovld.readthedocs.io/en/latest/codegen/):** (Experimental) For advanced use cases, you can generate custom code for overloads.

## Example

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

```python
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: tuple, y: tuple):
    return tuple(recurse(a, b) for a, b in zip(x, y))

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

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

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

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:

```python
@add.variant
def mul(x: object, y: object):
    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):

```python
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 the list below itself.

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

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

You would effectively be creating a clone of `f` that traces every call.


## Dependent types

A dependent type is a type that depends on a value. `ovld` supports this, either through `Literal[value]` or `Dependent[bound, check]`. For example, this definition of factorial:

```python
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:

```python
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.

```python
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):


```python
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`](https://ovld.readthedocs.io/en/latest/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).

```python
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](https://ovld.readthedocs.io/en/latest/codegen/).


# 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](https://github.com/breuleux/ovld) | [plum](https://github.com/beartype/plum) | [multim](https://github.com/coady/multimethod) | [multid](https://github.com/mrocklin/multipledispatch) | [runtype](https://github.com/erezsh/runtype) | [sd](https://docs.python.org/3/library/functools.html#functools.singledispatch) |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|[trivial](https://github.com/breuleux/ovld/tree/master/benchmarks/test_trivial.py)|1.45|1.00|3.32|4.63|2.04|2.41|1.91|
|[multer](https://github.com/breuleux/ovld/tree/master/benchmarks/test_multer.py)|1.13|1.00|11.05|4.53|8.31|2.19|7.32|
|[add](https://github.com/breuleux/ovld/tree/master/benchmarks/test_add.py)|1.08|1.00|3.73|5.21|2.37|2.79|x|
|[ast](https://github.com/breuleux/ovld/tree/master/benchmarks/test_ast.py)|1.00|1.08|23.14|3.09|1.68|1.91|1.66|
|[calc](https://github.com/breuleux/ovld/tree/master/benchmarks/test_calc.py)|1.00|1.23|54.61|29.32|x|x|x|
|[regexp](https://github.com/breuleux/ovld/tree/master/benchmarks/test_regexp.py)|1.00|1.87|19.18|x|x|x|x|
|[fib](https://github.com/breuleux/ovld/tree/master/benchmarks/test_fib.py)|1.00|3.30|444.31|125.77|x|x|x|
|[tweaknum](https://github.com/breuleux/ovld/tree/master/benchmarks/test_tweaknum.py)|1.00|2.09|x|x|x|x|x|

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "ovld",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.9",
    "maintainer_email": null,
    "keywords": null,
    "author": null,
    "author_email": "Olivier Breuleux <breuleux@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/d1/5d/f6969bd85d70b7558e6ee09d6f13d7b19e501b5748670adb978790b0e47c/ovld-0.5.0.tar.gz",
    "platform": null,
    "description": "\n# Ovld\n\nFast multiple dispatch in Python, with many extra features.\n\n[\ud83d\udccb Documentation](https://ovld.readthedocs.io/en/latest/)\n\nWith 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.\n\n* \u26a1\ufe0f **[Fast](https://ovld.readthedocs.io/en/latest/compare/#results):** ovld is the fastest multiple dispatch library around, by some margin.\n* \ud83d\ude80 [**Variants**](https://ovld.readthedocs.io/en/latest/usage/#variants), [**mixins**](https://ovld.readthedocs.io/en/latest/usage/#mixins) and [**medleys**](https://ovld.readthedocs.io/en/latest/medley) of functions and methods.\n* \ud83e\udd84 **[Dependent types](https://ovld.readthedocs.io/en/latest/dependent/):** Overloaded functions can depend on more than argument types: they can depend on actual values.\n* \ud83d\udd11 **[Extensive](https://ovld.readthedocs.io/en/latest/usage/#keyword-arguments):** Dispatch on functions, methods, positional arguments and even keyword arguments (with some restrictions).\n* \u2699\ufe0f **[Codegen](https://ovld.readthedocs.io/en/latest/codegen/):** (Experimental) For advanced use cases, you can generate custom code for overloads.\n\n## Example\n\nHere's a function that recursively adds lists, tuples and dictionaries:\n\n```python\nfrom ovld import ovld, recurse\n\n@ovld\ndef add(x: list, y: list):\n    return [recurse(a, b) for a, b in zip(x, y)]\n\n@ovld\ndef add(x: tuple, y: tuple):\n    return tuple(recurse(a, b) for a, b in zip(x, y))\n\n@ovld\ndef add(x: dict, y: dict):\n    return {k: recurse(v, y[k]) for k, v in x.items()}\n\n@ovld\ndef add(x: object, y: object):\n    return x + y\n\nassert add([1, 2], [3, 4]) == [4, 6]\n```\n\nThe `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.\n\nFor example:\n\n\n## Variants\n\nA *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:\n\n```python\n@add.variant\ndef mul(x: object, y: object):\n    return x * y\n\nassert mul([1, 2], [3, 4]) == [3, 8]\n```\n\nSimple! This means you can define one `ovld` that recursively walks generic data structures, and then specialize it in various ways.\n\n\n## Priority and call_next\n\nYou can define a numeric priority for each method (the default priority is 0):\n\n```python\nfrom ovld import call_next\n\n@ovld(priority=1000)\ndef f(x: int):\n    return call_next(x + 1)\n\n@ovld\ndef f(x: int):\n    return x * x\n\nassert f(10) == 121\n```\n\nBoth definitions above have the same type signature, but since the first has higher priority, that is the one that will be called.\n\nHowever, 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 the list below itself.\n\nThe pattern you see above is how you may wrap each call with some generic behavior. For instance, if you did something like that:\n\n```python\n@f.variant(priority=1000)\ndef f2(x: object)\n    print(f\"f({x!r})\")\n    return call_next(x)\n```\n\nYou would effectively be creating a clone of `f` that traces every call.\n\n\n## Dependent types\n\nA dependent type is a type that depends on a value. `ovld` supports this, either through `Literal[value]` or `Dependent[bound, check]`. For example, this definition of factorial:\n\n```python\nfrom typing import Literal\nfrom ovld import ovld, recurse, Dependent\n\n@ovld\ndef fact(n: Literal[0]):\n    return 1\n\n@ovld\ndef fact(n: Dependent[int, lambda n: n > 0]):\n    return n * recurse(n - 1)\n\nassert fact(5) == 120\nfact(-1)   # Error!\n```\n\nThe 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]`.\n\n### dependent_check\n\nDefine your own types with the `@dependent_check` decorator:\n\n```python\nimport torch\nfrom ovld import ovld, dependent_check\n\n@dependent_check\ndef Shape(tensor: torch.Tensor, *shape):\n    return (\n        len(tensor.shape) == len(shape)\n        and all(s2 is Any or s1 == s2 for s1, s2 in zip(tensor.shape, shape))\n    )\n\n@dependent_check\ndef Dtype(tensor: torch.Tensor, dtype):\n    return tensor.dtype == dtype\n\n@ovld\ndef f(tensor: Shape[3, Any]):\n    # Matches 3xN tensors\n    ...\n\n@ovld\ndef f(tensor: Shape[2, 2] & Dtype[torch.float32]):\n    # Only matches 2x2 tensors that also have the float32 dtype\n    ...\n```\n\nThe 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`.\n\n## Methods\n\nEither inherit from `OvldBase` or use the `OvldMC` metaclass to use multiple dispatch on methods.\n\n```python\nfrom ovld import OvldBase, OvldMC\n\n# class Cat(OvldBase):  <= Also an option\nclass Cat(metaclass=OvldMC):\n    def interact(self, x: Mouse):\n        return \"catch\"\n\n    def interact(self, x: Food):\n        return \"devour\"\n\n    def interact(self, x: PricelessVase):\n        return \"destroy\"\n```\n\n### Subclasses\n\nSubclasses 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):\n\n\n```python\nfrom ovld import OvldMC, extend_super\n\nclass One(metaclass=OvldMC):\n    def f(self, x: int):\n        return \"an integer\"\n\nclass Two(One):\n    @extend_super\n    def f(self, x: str):\n        return \"a string\"\n\nassert Two().f(1) == \"an integer\"\nassert Two().f(\"s\") == \"a string\"\n```\n\n## Medleys\n\nInheriting from [`ovld.Medley`](https://ovld.readthedocs.io/en/latest/medley/) lets you combine functionality in a new way. Classes created that way are free-form medleys that you can (almost) arbitrarily combine together.\n\nAll medleys are dataclasses and you must define their data fields as you would for a normal dataclass (using `dataclass.field` if needed).\n\n```python\nfrom ovld import Medley\n\nclass Punctuator(Medley):\n    punctuation: str = \".\"\n\n    def __call__(self, x: str):\n        return f\"{x}{self.punctuation}\"\n\nclass Multiplier(Medley):\n    factor: int = 3\n\n    def __call__(self, x: int):\n        return x * self.factor\n\n# You can add the classes together to merge their methods and fields using ovld\nPuMu = Punctuator + Multiplier\nf = PuMu(punctuation=\"!\", factor=3)\n\n# You can also combine existing instances!\nf2 = Punctuator(\"!\") + Multiplier(3)\n\nassert f(\"hello\") == f2(\"hello\") == \"hello!\"\nassert f(10) == f2(10) == 30\n\n# You can also meld medleys inplace, but only if all new fields have defaults\nclass Total(Medley):\n    pass\n\nTotal.extend(Punctuator, Multiplier)\nf3 = Total(punctuation=\"!\", factor=3)\n```\n\n\n# Code generation\n\n(Experimental) For advanced use cases, you can generate custom code for type checkers or overloads. [See here](https://ovld.readthedocs.io/en/latest/codegen/).\n\n\n# Benchmarks\n\n`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.\n\nTime relative to the fastest implementation (1.00) (lower is better).\n\n| Benchmark | custom | [ovld](https://github.com/breuleux/ovld) | [plum](https://github.com/beartype/plum) | [multim](https://github.com/coady/multimethod) | [multid](https://github.com/mrocklin/multipledispatch) | [runtype](https://github.com/erezsh/runtype) | [sd](https://docs.python.org/3/library/functools.html#functools.singledispatch) |\n| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |\n|[trivial](https://github.com/breuleux/ovld/tree/master/benchmarks/test_trivial.py)|1.45|1.00|3.32|4.63|2.04|2.41|1.91|\n|[multer](https://github.com/breuleux/ovld/tree/master/benchmarks/test_multer.py)|1.13|1.00|11.05|4.53|8.31|2.19|7.32|\n|[add](https://github.com/breuleux/ovld/tree/master/benchmarks/test_add.py)|1.08|1.00|3.73|5.21|2.37|2.79|x|\n|[ast](https://github.com/breuleux/ovld/tree/master/benchmarks/test_ast.py)|1.00|1.08|23.14|3.09|1.68|1.91|1.66|\n|[calc](https://github.com/breuleux/ovld/tree/master/benchmarks/test_calc.py)|1.00|1.23|54.61|29.32|x|x|x|\n|[regexp](https://github.com/breuleux/ovld/tree/master/benchmarks/test_regexp.py)|1.00|1.87|19.18|x|x|x|x|\n|[fib](https://github.com/breuleux/ovld/tree/master/benchmarks/test_fib.py)|1.00|3.30|444.31|125.77|x|x|x|\n|[tweaknum](https://github.com/breuleux/ovld/tree/master/benchmarks/test_tweaknum.py)|1.00|2.09|x|x|x|x|x|\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "Overloading Python functions",
    "version": "0.5.0",
    "project_urls": {
        "Documentation": "https://ovld.readthedocs.io/en/latest/",
        "Homepage": "https://ovld.readthedocs.io/en/latest/",
        "Repository": "https://github.com/breuleux/ovld"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "2b190c2c35fd98298c1b984b773a50abdcb52c7a0dc84db58beb6f0dadcf84ea",
                "md5": "4e762712a242e6cf1d4b20f84ef2e164",
                "sha256": "6f136346d529ce76de769a2873f11f0efcf84d97d672660a377cebbaea891978"
            },
            "downloads": -1,
            "filename": "ovld-0.5.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "4e762712a242e6cf1d4b20f84ef2e164",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.9",
            "size": 38173,
            "upload_time": "2025-04-09T03:52:20",
            "upload_time_iso_8601": "2025-04-09T03:52:20.950533Z",
            "url": "https://files.pythonhosted.org/packages/2b/19/0c2c35fd98298c1b984b773a50abdcb52c7a0dc84db58beb6f0dadcf84ea/ovld-0.5.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "d15df6969bd85d70b7558e6ee09d6f13d7b19e501b5748670adb978790b0e47c",
                "md5": "52b097d7c1432037c89f3b22f2c9ed96",
                "sha256": "b237fe34c6ba162c79bf8287951ac987a0b493809987fb3313f4185de0e447a8"
            },
            "downloads": -1,
            "filename": "ovld-0.5.0.tar.gz",
            "has_sig": false,
            "md5_digest": "52b097d7c1432037c89f3b22f2c9ed96",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.9",
            "size": 96350,
            "upload_time": "2025-04-09T03:52:22",
            "upload_time_iso_8601": "2025-04-09T03:52:22.620029Z",
            "url": "https://files.pythonhosted.org/packages/d1/5d/f6969bd85d70b7558e6ee09d6f13d7b19e501b5748670adb978790b0e47c/ovld-0.5.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-04-09 03:52:22",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "breuleux",
    "github_project": "ovld",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "ovld"
}
        
Elapsed time: 0.44380s