Skip to main content

Static variables for Python

Project description

Static variables for Python

NOTE:

This is still very much a work in progress, and will segfault if you give it anything that is mildly complex. It will probably not work on any implementation except CPython.

Usage

static_variables

from static_variables import resolve_static

@resolve_static(static_variables={'counter': 0})
def f():
    counter += 1
    return counter

print(f())  # 1
print(f())  # 2
print(f())  # 3

The signature for static_variables is Mapping[str, Any], where the key is the name of the variable and the value is the initial value.

Also note that static variables will override global, nonlocal and local variables with the same name.

Set the value to static_variables.NO_VALUE to have no value in the beginning:

from static_variables import resolve_static, NO_VALUE

@resolve_static(staic_variables={'value': NO_VALUE})
def get_value():
    try:
        return value
    except NameError:
        value = could_be_anything()
        # `value` could also be `None`, so `None`
        # is not a sensible default.
        return value

get_value()  # Runs `could_be_anything`
get_value()  # Return the static value

static

from static_variables import static, resolve_static

# You don't really need to import `static`, it just stops
# IDEs from complaining.

@resolve_static
def f(to_add=None):
     ls = static([])
     if to_add is not None:
         ls.append(to_add)
     return ls

ls = f()
f(3)
assert ls == [3]  # True
assert ls is f()

Since Python variables are more like name tags, static will only really work well for mutable objects, like lists or sets.

For example, the following does not work:

@resolve_static
def f():
    counter = static(0)
    counter += 1
    return counter

assert f() == 1  # True
assert f() == 2  # False

You would have to use the static_variables argument to achieve this.

The static variable will always have the same id. They will refer to the same object, and is stored at the end of a function’s function.__code__.co_consts

Empty set literals

Since sets came after dictionaries, the {} literal is an empty dictionary. This changes that.

@resolve_static(empty_set_literal=True)
def f():
    return {}

assert f() == set()  # True
assert f() != {}  # True; {} is dict() in the outer scope.

You can also use EMPTY_SET to avoid turning all {} into empty sets.

from static_variables import resolve_static, EMPTY_SET

# Again, you don't need to import EMPTY_SET.
# It just stops IDEs from complaining.

@resolve_static(empty_set_literal=False)
def f():
    my_dict = {}
    my_set = EMPTY_SET  # Equivalent to `set()` but faster.
    return type(my_dict), type(my_set)

assert f() == (dict, set)  # True

Speed?

It would actually be faster to use static, as it delegates some processing to declaration time, instead of run time.

Take these two snippets:

def product_4(it):
    return itertools.product(it, repeat=4)

@resolve_static
def static_product_4(it):
    return static(itertools.product)(it, repeat=4)

And their disassembly:

product_4(it)
              0 LOAD_GLOBAL              0 (itertools)
              2 LOAD_ATTR                1 (product)
              4 LOAD_FAST                0 (it)
              6 LOAD_CONST               1 (4)
              8 LOAD_CONST               2 (('repeat',))
             10 CALL_FUNCTION_KW         2
             12 RETURN_VALUE
static_product_4(it)
              0 LOAD_CONST               3 (<class 'itertools.product'>)
              2 LOAD_FAST                0 (it)
              4 LOAD_CONST               1 (4)
              6 LOAD_CONST               2 (('repeat',))
              8 CALL_FUNCTION_KW         2
             10 RETURN_VALUE

The static version just loads the itertools.product constant, whilst the normal version looks up a global variable and an attribute on one.

Empty set literals and EMPTY_SET are equivalent and both faster than set().

They are not equivalent to static(set()) which would be faster, but it would be the same static set.

Installation

From PyPI

$ pip install static_variables

From source

$ git clone 'https://github.com/MitalAshok/static_variables.git'
$ python ./static_variables/setup.py install

How does it work?

static_variables

This creates a new variable in the closure of a function. The closure remains between function calls.

It replaces (LOAD|STORE|DELETE)_GLOBAL and (LOAD|STORE|DELETE)_FAST (local variables) opcodes in the bytecode with (LOAD|STORE|DELETE)_DEREF (load from the closure) ones.

static

The bytecode in Python is stack-based. resolve_static looks for a LOAD_GLOBAL 'static' opcode and then starts tracking what the size of the stack will be. When the stack size reaches 0 and a CALL_FUNCTION 1 (call the top of the stack with 1 item from below it on the stack) opcode is reached, it extracts the bytecode, creates a new function, and calls it to evaluate the bytecode. The whole static(...) is replaced with LOAD_CONST, to load a constant value which is appended to the code’s co_consts.

empty_set_literal

While iterating over the bytecode, if BUILD_MAP 0 is encountered (Create a new dictionary from the previous 0 items. i.e., an empty dictionary), it is replaced with BUILD_SET 0, which creates an empty set instead. This opcode still exists even though it doesn’t naturally occur so that it’s argument still correlates with the number of items to pop off of the stack to build the set with.

If a LOAD_GLOBAL 'EMPTY_SET' is encountered, it is always replaced with a BUILD_SET 0 (i.e., a new empty set.)

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

static_variables-0.0.5.tar.gz (13.4 kB view hashes)

Uploaded Source

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