SkillAgentSearch skills...

Pyrsistent

Persistent/Immutable/Functional data structures for Python

Install / Use

/learn @tobgu/Pyrsistent
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Pyrsistent

.. image:: https://github.com/tobgu/pyrsistent/actions/workflows/tests.yaml/badge.svg :target: https://github.com/tobgu/pyrsistent/actions/workflows/tests.yaml

.. image:: https://coveralls.io/repos/github/tobgu/pyrsistent/badge.svg?branch=master :target: https://coveralls.io/github/tobgu/pyrsistent?branch=master

.. _Pyrthon: https://www.github.com/tobgu/pyrthon .. _Pyrsistent_extras: https://github.com/mingmingrr/pyrsistent-extras

Pyrsistent is a number of persistent collections (by some referred to as functional data structures). Persistent in the sense that they are immutable.

All methods on a data structure that would normally mutate it instead return a new copy of the structure containing the requested updates. The original structure is left untouched.

This will simplify the reasoning about what a program does since no hidden side effects ever can take place to these data structures. You can rest assured that the object you hold a reference to will remain the same throughout its lifetime and need not worry that somewhere five stack levels below you in the darkest corner of your application someone has decided to remove that element that you expected to be there.

Pyrsistent is influenced by persistent data structures such as those found in the standard library of Clojure. The data structures are designed to share common elements through path copying. It aims at taking these concepts and make them as pythonic as possible so that they can be easily integrated into any python program without hassle.

If you want use literal syntax to define them in your code rather than function calls check out Pyrthon_. Be aware, that one is experimental, unmaintained and alpha software.

If you cannot find the persistent data structure you're looking for here you may want to take a look at Pyrsistent_extras_ which is maintained by @mingmingrr. If you still don't find what you're looking for please open an issue for discussion. If we agree that functionality is missing you may want to go ahead and create a Pull Request implement the missing functionality.

Examples

.. Sequence: collections .. Hashable: collections .. Mapping: collections .. Mappings: collections .. Set: collections .. _collections: https://docs.python.org/3/library/collections.abc.html .. _documentation: http://pyrsistent.readthedocs.org/

The collection types and key features currently implemented are:

  • PVector_, similar to a python list
  • PMap_, similar to dict
  • PSet_, similar to set
  • PRecord_, a PMap on steroids with fixed fields, optional type and invariant checking and much more
  • PClass_, a Python class fixed fields, optional type and invariant checking and much more
  • Checked collections_, PVector, PMap and PSet with optional type and invariance checks and more
  • PBag, similar to collections.Counter
  • PList, a classic singly linked list
  • PDeque, similar to collections.deque
  • Immutable object type (immutable) built on the named tuple
  • freeze_ and thaw_ functions to convert between pythons standard collections and pyrsistent collections.
  • Flexible transformations_ of arbitrarily complex structures built from PMaps and PVectors.

Below are examples of common usage patterns for some of the structures and features. More information and full documentation for all data structures is available in the documentation_.

.. _PVector:

PVector

With full support for the Sequence_ protocol PVector is meant as a drop in replacement to the built in list from a readers
point of view. Write operations of course differ since no in place mutation is done but naming should be in line
with corresponding operations on the built in list.

Support for the Hashable_ protocol also means that it can be used as key in Mappings_.

Appends are amortized O(1). Random access and insert is log32(n) where n is the size of the vector.

.. code:: python

    >>> from pyrsistent import v, pvector

    # No mutation of vectors once created, instead they
    # are "evolved" leaving the original untouched
    >>> v1 = v(1, 2, 3)
    >>> v2 = v1.append(4)
    >>> v3 = v2.set(1, 5)
    >>> v1
    pvector([1, 2, 3])
    >>> v2
    pvector([1, 2, 3, 4])
    >>> v3
    pvector([1, 5, 3, 4])

    # Random access and slicing
    >>> v3[1]
    5
    >>> v3[1:3]
    pvector([5, 3])

    # Iteration
    >>> list(x + 1 for x in v3)
    [2, 6, 4, 5]
    >>> pvector(2 * x for x in range(3))
    pvector([0, 2, 4])

.. _PMap:

PMap
~~~~
With full support for the Mapping_ protocol PMap is meant as a drop in replacement to the built in dict from a readers point
of view. Support for the Hashable_ protocol also means that it can be used as key in other Mappings_.

Random access and insert is log32(n) where n is the size of the map.

.. code:: python

    >>> from pyrsistent import m, pmap, v

    # No mutation of maps once created, instead they are
    # "evolved" leaving the original untouched
    >>> m1 = m(a=1, b=2)
    >>> m2 = m1.set('c', 3)
    >>> m3 = m2.set('a', 5)
    >>> m1
    pmap({'a': 1, 'b': 2})
    >>> m2
    pmap({'a': 1, 'c': 3, 'b': 2})
    >>> m3
    pmap({'a': 5, 'c': 3, 'b': 2})
    >>> m3['a']
    5

    # Evolution of nested persistent structures
    >>> m4 = m(a=5, b=6, c=v(1, 2))
    >>> m4.transform(('c', 1), 17)
    pmap({'a': 5, 'c': pvector([1, 17]), 'b': 6})
    >>> m5 = m(a=1, b=2)

    # Evolve by merging with other mappings
    >>> m5.update(m(a=2, c=3), {'a': 17, 'd': 35})
    pmap({'a': 17, 'c': 3, 'b': 2, 'd': 35})
    >>> pmap({'x': 1, 'y': 2}) + pmap({'y': 3, 'z': 4})
    pmap({'y': 3, 'x': 1, 'z': 4})

    # Dict-like methods to convert to list and iterate
    >>> m3.items()
    pvector([('a', 5), ('c', 3), ('b', 2)])
    >>> list(m3)
    ['a', 'c', 'b']

.. _PSet:

PSet
~~~~
With full support for the Set_ protocol PSet is meant as a drop in replacement to the built in set from a readers point
of view. Support for the Hashable_ protocol also means that it can be used as key in Mappings_.

Random access and insert is log32(n) where n is the size of the set.

.. code:: python

    >>> from pyrsistent import s

    # No mutation of sets once created, you know the story...
    >>> s1 = s(1, 2, 3, 2)
    >>> s2 = s1.add(4)
    >>> s3 = s1.remove(1)
    >>> s1
    pset([1, 2, 3])
    >>> s2
    pset([1, 2, 3, 4])
    >>> s3
    pset([2, 3])

    # Full support for set operations
    >>> s1 | s(3, 4, 5)
    pset([1, 2, 3, 4, 5])
    >>> s1 & s(3, 4, 5)
    pset([3])
    >>> s1 < s2
    True
    >>> s1 < s(3, 4, 5)
    False

.. _PRecord:

PRecord

A PRecord is a PMap with a fixed set of specified fields. Records are declared as python classes inheriting from PRecord. Because it is a PMap it has full support for all Mapping methods such as iteration and element access using subscript notation.

.. code:: python

>>> from pyrsistent import PRecord, field
>>> class ARecord(PRecord):
...     x = field()
...
>>> r = ARecord(x=3)
>>> r
ARecord(x=3)
>>> r.x
3
>>> r.set(x=2)
ARecord(x=2)
>>> r.set(y=2)
Traceback (most recent call last):
AttributeError: 'y' is not among the specified fields for ARecord

Type information


It is possible to add type information to the record to enforce type checks. Multiple allowed types can be specified by providing an iterable of types.

.. code:: python

>>> class BRecord(PRecord):
...     x = field(type=int)
...     y = field(type=(int, type(None)))
...
>>> BRecord(x=3, y=None)
BRecord(y=None, x=3)
>>> BRecord(x=3.0)
Traceback (most recent call last):
PTypeError: Invalid type for field BRecord.x, was float

Custom types (classes) that are iterable should be wrapped in a tuple to prevent their members being added to the set of valid types. Although Enums in particular are now supported without wrapping, see #83 for more information.

Mandatory fields


Fields are not mandatory by default but can be specified as such. If fields are missing an InvariantException will be thrown which contains information about the missing fields.

.. code:: python

>>> from pyrsistent import InvariantException
>>> class CRecord(PRecord):
...     x = field(mandatory=True)
...
>>> r = CRecord(x=3)
>>> try:
...    r.discard('x')
... except InvariantException as e:
...    print(e.missing_fields)
...
('CRecord.x',)

Invariants


It is possible to add invariants that must hold when evolving the record. Invariants can be specified on both field and record level. If invariants fail an InvariantException will be thrown which contains information about the failing invariants. An invariant function should return a tuple consisting of a boolean that tells if the invariant holds or not and an object describing the invariant. This object can later be used to identify which invariant that failed.

The global invariant function is only executed if all field invariants hold.

Global invariants are inherited to subclasses.

.. code:: python

>>> class RestrictedVector(PRecord):
...     __invariant__ = lambda r: (r.y >= r.x, 'x larger than y')
...     x = field(invariant=lambda x: (x > 0, 'x negative'))
...     y = field(invariant=lambda y: (y > 0, 'y negative'))
...
>>> r = RestrictedVector(y=3, x=2)
>>> try:
...    r.set(x=-1, y=-2)
... except InvariantException as e:
...    print(e.invariant_errors)
...
('y negative', 'x negative')
>>> try:
...    r.set(x=2, y=1)
... except InvariantException as e:
...    print(e.invariant_errors)
...
('x larger than y',)

Invariants may also contain multiple assertions. For those cases the invariant function should return a tuple of invariant tuples as described above. This structure is reflected in the invariant_errors attribute of the exception which will contain tuples with data from all failed invariants. Eg:

.. code::

View on GitHub
GitHub Stars2.2k
CategoryDevelopment
Updated2d ago
Forks161

Languages

Python

Security Score

100/100

Audited on Mar 30, 2026

No findings