Skip to main content

The classic ``compose``, with all the Pythonic features.

Project description

The classic compose, with all the Pythonic features.

This compose follows the lead of functools.partial and returns callable compose objects which:

  • have a regular and unambiguous repr,

  • retain correct signature introspection,

  • allow introspection of the composed callables,

  • can be type-checked,

  • can still be weakly referenced and have attributes,

  • will merge when nested, and

  • can be pickled (if all composed callables can be pickled).

This compose also fails fast with a TypeError if any argument is not callable, or when called with no arguments.

Versioning

This library’s version numbers follow the SemVer 2.0.0 specification.

The current version number is available in the variable __version__, as is normal for Python modules.

Installation

pip install compose

Usage

Import compose:

from compose import compose

All the usual function composition you know and love:

>>> def double(x):
...     return x * 2
...
>>> def increment(x):
...     return x + 1
...
>>> double_then_increment = compose(increment, double)
>>> double_then_increment(1)
3

Of course any number of functions can be composed:

>>> def double(x):
...     return x * 2
...
>>> times_eight = compose(douple, double, double)
>>> times_16 = compose(douple, double, double, double)

We still get the correct signature introspection:

>>> def f(a, b, c=0, **kwargs):
...     pass
...
>>> def g(x):
...     pass
...
>>> g_of_f = compose(g, f)
>>> import inspect
>>> inspect.signature(g_of_f)
<Signature (a, b, c=0, **kwargs)>

And we can inspect all the composed callables:

>>> g_of_f.functions  # in order of execution:
(<function f at 0x4048e6f0>, <function g at 0x405228e8>)

When programmatically inspecting arbitrary callables, we can check if we are looking at a compose instance:

>>> isinstance(g_of_f, compose)
True

Design Decisions

  • The result of compose should be a drop-in replacement to functions in as many code paths as possible. Therefore:

    • The real signature of the composed function (the signature of the “inner-most” function) is exposed in the standard Python way (by assigning that function in __wrapped__).

    • Arbitrary attribute assignment (__dict__) should work, because Python allows people to do that to functions.

    • Weak references (__weakref__) are supported, because Python allows weakly referencing functions.

  • Failing fast as much as possible because that is important to help debugging by keeping errors local to their causes.

  • __wrapped__ cannot be a @property because several functions in the standard library cannot handle that.

    As a minor point, “portability conservatism”: it is safer to bet on the most conservative feature-set possible.

  • Storing the first function separately from the rest allows __call__ to be written more efficiently, simply, and clearly.

  • Treating compose() without any arguments as an error, instead of as producing a no-op passthrough identity function, because:

    1. It avoids turning mistakes into silent misbehavior by default.

    2. It is the more flexible way: people can do

      compose = partial(compose, identity)

      but going the other way is less trivial.

  • Despite compose() being an error, __init__(self, *functions)__ is used instead of __init__(self, function, *functions)__ to produce a reliably nice and clear error message for that error.

  • Using functools.recursive_repr if available because if recursion somehow ever happens, having a working and recursion-safe __repr__ would likely be extremely helpful for debugging and code robustness.

    Not going beyond that because the code involved would be complex and not portable across Python implementations and the right place for that is a separate polyfil, monkey-patched or added in manually.

  • Manually getting self from *args in __call__ portably makes self a positional-only argument.

    If the user makes a typo, **-splats arguments, or otherwise ends up passing self in kwargs, maybe even intentionally, function composition should still work correctly - in this case, silent seemingly-successful unintended misbehavior would be awful.

    This is the same care we see taken in the implementation of functools.partial.

  • Not using __slots__ because:

    1. __wrapped__ cannot be in __slots__ because that has the same problem as making it a @property (see above).

    2. __wrapped__ can be implemented with __getattr__, but this would cause an inconsistent error string or traceback when trying to get non-existent attributes relative to other typical objects, and did not seem to actually perform better.

    3. Due to the above two reasons, __dict__ will always be created and initialized in __init__, so it would not save space.

    4. For what compose is doing, using __slots__ does not seem to significantly increase execution speed anyway.

Download files

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

Source Distribution

compose-1.0.0.tar.gz (5.3 kB view details)

Uploaded Source

Built Distribution

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

compose-1.0.0-py2.py3-none-any.whl (5.5 kB view details)

Uploaded Python 2Python 3

File details

Details for the file compose-1.0.0.tar.gz.

File metadata

  • Download URL: compose-1.0.0.tar.gz
  • Upload date:
  • Size: 5.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.13.0 pkginfo/1.5.0.1 requests/2.21.0 setuptools/41.0.1 requests-toolbelt/0.9.1 tqdm/4.32.1 CPython/3.6.3

File hashes

Hashes for compose-1.0.0.tar.gz
Algorithm Hash digest
SHA256 048a8f23b5127a83c99fe9f2e4de638dfc0d2cb76c9db3f39babe8d831fdcf7c
MD5 0c210c3348a5d9af43c4ae70005cf748
BLAKE2b-256 0c9d27df8f871de0183b8b829411fcb8db16caa1b093002168c5bf0b94fe96a5

See more details on using hashes here.

File details

Details for the file compose-1.0.0-py2.py3-none-any.whl.

File metadata

  • Download URL: compose-1.0.0-py2.py3-none-any.whl
  • Upload date:
  • Size: 5.5 kB
  • Tags: Python 2, Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.13.0 pkginfo/1.5.0.1 requests/2.21.0 setuptools/41.0.1 requests-toolbelt/0.9.1 tqdm/4.32.1 CPython/3.6.3

File hashes

Hashes for compose-1.0.0-py2.py3-none-any.whl
Algorithm Hash digest
SHA256 6c63ce9800e600e0e810dd04fff53f02846db15a38a5153997241b0b3df697c0
MD5 d68d92ca2f9fca6ef34881ae3c1c4289
BLAKE2b-256 fcee0d2592931f0195ca2ef9b062ce98aff87b8eb229be894baab7982b0c5a89

See more details on using hashes here.

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