sidekick.proxy

Sidekick’s sidekick.functions.thunk() is a nice way to represent a lazy computation through a function. It, however, breaks existing interfaces since we need to call the result of thunk to obtain its inner value. sidekick.proxy implements a few types that help exposing lazy objects as proxies, sharing the same interfaces as the wrapped objects. This is great for code that relies in duck-typing.

Lazy objects are useful both to declare objects that use yet uninitialized resources and as an optimization that we like to call “opportunistic procrastination”: we delay computation to the last possible time in the hope that we can get away without doing it at all. This is a powerful strategy since our programs tend to compute a lot of things in advance that are not always used during program execution.

Proxy types and functions

Proxy(obj) Base class for proxy types.
deferred(func, *args, **kwargs) Wraps uninitialized object into a proxy shell.
zombie(*args, **kwargs) Provides zombie[class] syntax.
touch(obj) Return a non-proxy, non-zombie version of object.
import_later(path[, package]) Lazily import module or object.

Deferred proxies vs zombies

Sidekick provides two similar kinds of deferred objects: deferred and zombie(). They are both initialized from a callable with arbitrary arguments and delay the execution of that callable until the result is needed. Consider the custom User class bellow.

>>> class User:
...     def __init__(self, **kwargs):
...         for k, v in kwargs.items():
...             setattr(self, k, v)
...     def __repr__(self):
...         data = ('%s: %r' % item for item in self.__dict__.items())
...         return 'User(%s)' % ', '.join(data)
>>> a = sk.deferred(User, name='Me', age=42)
>>> b = sk.zombie(User, name='Me', age=42)

The main difference between deferred and zombie, is that Zombie instances assume the type of the result after they awake, while deferred objects are proxies that simply mimic the interface of the result.

>>> a
Proxy(User(name: 'Me', age: 42))
>>> b
User(name: 'Me', age: 42)

We can see that deferred instances do not change class, while zombie instances do:

>>> type(a), type(b)                                        # doctest: +ELLIPSIS
(<class '...deferred'>, <class 'User'>)

This limitation makes zombie objects somewhat restricted. Delayed execution cannot return any type that has a different C layout as regular Python objects. This excludes all builtin types, C extension classes and even Python classes that define __slots__. On the plus side, zombie objects fully assume the properties of the delayed execution, including its type and can replace them in almost any context.

A slightly safer version of zombie() allows specifying the return type of the resulting object. This opens zombies up to a few additional types (e.g., types that use __slots__) and produces checks if conversion is viable or not.

We specify the return type as an index before declaring the constructor function:

>>> rec = sk.zombie[sk.record](sk.record, x=1, y=2)
>>> type(rec)                                               # doctest: +ELLIPSIS
<class '...SpecializedZombie'>

Touch it, and the zombie awakes

>>> sk.touch(rec)
record(x=1, y=2)
>>> type(rec)                                               # doctest: +ELLIPSIS
<class '...record'>

API reference

class sidekick.proxy.Proxy(obj)[source]

Base class for proxy types.

class sidekick.proxy.deferred(func, *args, **kwargs)[source]

Wraps uninitialized object into a proxy shell.

Object is declared as a thunk and is initialized the first time some attribute or method is requested.

The proxy delegates all methods to the lazy object. Proxies work nicely with duck typing, but are a poor fit to code that relies in explicit instance checks since the deferred object is a Proxy instance.

Usage:
>>> from operator import add
>>> x = sk.deferred(add, 40, 2)  # add function not called yet
>>> print(x)                     # any interaction triggers object construction!
42
class sidekick.proxy.zombie(*args, **kwargs)[source]

Provides zombie[class] syntax.

Implementation is in the metaclass.

sidekick.proxy.touch(obj)[source]

Return a non-proxy, non-zombie version of object. Regular objects are returned as-is.

sidekick.proxy.import_later(path, package=None)[source]

Lazily import module or object.

Lazy imports can dramatically decrease the initialization time of your python modules, specially when heavy weights such as numpy, and pandas are used. Beware that import errors that normally are triggered during import time now can be triggered at first use, which may introduce confusing and hard to spot bugs.

Parameters:
  • path – Python path to module or object. Modules are specified by their Python names (e.g., ‘os.path’) and objects are identified by their module path + “:” + object name (e.g., “os.path.splitext”).
  • package – Package name if path is a relative module path.

Examples

>>> np = sk.import_later('numpy')  # Numpy is not yet imported
>>> np.array([1, 2, 3])  # It imports as soon as required
array([1, 2, 3])

It also accepts relative imports if the package keyword is given. This is great to break circular imports.

>>> mod = sk.import_later('.sub_module', package=__package__)