Skip to main content
File added late

1 file was added to this release more than 14 days after its initial publication. Inspect the release files before installing.

Hirola

PyPI version https://img.shields.io/badge/coverage-100%25-%23184159

NumPy vectorized hash table written in C for fast (roughly 10x faster) set/dict like operations.

A hirola.HashTable is to dict what numpy.array is to list. By imposing some constraints, vectorising, and translating into C, the speed can be improved dramatically. For hirola, these constraints are:

  • Keys must all be of the same predetermined type and size.

  • The maximum size of a table must be chosen in advance and managed explicitly.

  • To get any performance boost, operations should be done in bulk.

  • Elements can not be removed.

If any of the above are not satisfied for your use case then don’t use hirola.

Installation

Install Hirola with pip:

pip install hirola

Quickstart

HashTable

At the rawest and dirtiest level lives the HashTable class. A HashTable can be though of as a dict but with only an enumeration for values. To construct an empty hash table:

import numpy as np
from hirola import HashTable

table = HashTable(
    20,  # <--- Maximum size for the table - up to 20 keys.
    "U10",  # <--- NumPy dtype - strings of up to 10 characters.
)

Keys may be added individually…

>>> table.add("cat")
0

… But it’s much more efficient to add in bulk. The return value is an enumeration of when each key was first added. Duplicate keys are not re-added.

>>> table.add(["dog", "cat", "moose", "gruffalo"])
array([1, 0, 2, 3])

Multidimensional inputs give multidimensional outputs of matching shapes.

>>> table.add([["rabbit", "cat"],
...            ["gruffalo", "moose"],
...            ["werewolf", "gremlin"]])
array([[4, 0],
       [3, 2],
       [5, 6]])

Inspect all keys added so far via the keys attribute. (Note that, unlike dict.keys(), it’s a property instead of a method.)

>>> table.keys
array(['cat', 'dog', 'moose', 'gruffalo', 'rabbit', 'werewolf', 'gremlin'],
      dtype='<U10')

Key indices can be retrieved with table.get(key) or just table[key]. Again, retrieval is NumPy vectorised and is much faster if given large arrays of inputs rather than one at a time.

>>> table.get("dog")
1
>>> table[["moose", "gruffalo"]]
array([2, 3])

Like the Python dict, using table[key] raises a KeyError if keys are missing but using table.get(key) returns a configurable default. Unlike Python’s dict, the default is -1.

>>> table["tortoise"]
KeyError: "key = 'tortoise' is not in this table."
>>> table.get("tortoise")
-1
>>> table.get("tortoise", default=99)
99
>>> table.get(["cat", "bear", "tortoise"], default=[100, 101, 102])
array([  0, 101, 102])

Choosing a max size

Unlike Python’s set and dict, Hirola does not manage its size automatically by default (although it can be reconfigured to). To prevent wasted resizing (which is what Python does under the hood), you have full control of and responsibility for how much space the table uses. Obviously the table has to be large enough to fit all the keys in it. Additionally, when a hash table gets to close to full it becomes much slower. Depending on how much you favour speed over memory you should add 20-50% extra headroom. If you intend to a lot of looking up of the same small set of values then it can continue to run faster if you increase max to 2-3x its minimal size.

Structured key data types

To indicate that an array axis should be considered as a single key, use NumPy’s structured dtypes. In the following example, the data type (points.dtype, 3) indicates that a 3D point - a triplet of floats - should be considered as one object. See help(HashTable.dtype) for more information of specifying dtypes. Only the last axis or last axes may be thought of as single keys. For other setups, first convert with numpy.transpose().

import numpy as np
from hirola import HashTable

# Create a cloud of 3D points with duplicates. This is 3000 points in total,
# with up to 1000 unique points.
points = np.random.uniform(-30, 30, (1000, 3))[np.random.choice(1000, 3000)]

# Create an empty hash table.
# In practice, you generally don't know how many unique elements there are
# so we'll pretend we don't either an assume the worst case of all 3000 are
# unique. We'll also give 25% padding for speed.
table = HashTable(len(points) * 1.25, (points.dtype, 3))

# Add all points to the table.
ids = table.add(points)

Duplicate-free contents can be accessed from table.keys:

>>> table.keys  # <--- These are `points` but with no duplicates.
array([[  3.47736554, -15.17112511,  -9.51454466],
       [ -6.46948046,  23.64504329, -16.25743105],
       [-27.02527253, -16.1967225 , -10.11544157],
       ...,
       [  3.75972597,   1.24130412,  -8.14337206],
       [-13.62256791,  11.76551455, -13.31312988],
       [  0.19851678,   4.06221179, -22.69006592]])
>>> table.keys.shape
(954, 3)

Each point’s location in table.keys is returned by table.add(), similarly to numpy.unique(..., return_args=True).

>>> ids  # <--- These are the indices in `table.keys` of each point in `points`.
array([  0,   1,   2, ..., 290, 242, 669])
>>> np.array_equal(table.keys[ids], points)
True

Lookup the indices of points without adding them using table.get().

Handling of nearly full hash tables

HashTables become very slow when almost full. As of v0.3.0, an efficiency warning will notify you if a table exceeds 90% full. This warning can be reconfigured into an error, silenced or set to resize the table automatically to make room. These are demonstrated in the example constructors below:

# The default: Issue a warning when the table is 90% full.
HashTable(..., almost_full=(0.9, "warn"))

# Disable all "almost full" behaviours.
HashTable(..., almost_full=None)

# To consider a table exceeding 80% full as an error use:
HashTable(..., almost_full=(0.8, "raise"))

# To automatically triple in size whenever the table exceeds 80% full use:
HashTable(..., almost_full=(0.8, 3.0))

Resizing tables is slow which is why it’s not enabled by default. It should be avoided unless you really have no idea how big your table will need to be.

Recipes

A HashTable can be used to replicate a dict, set or a collections.Counter. These might turn into their own proper classes in the future or they might not.

Using a HashTable as a dict

A dict requires a second array for values. The output of HashTable.add() and HashTable.get() should be used as indices of values:

import numpy as np
from hirola import HashTable

# The `keys` - will be populated with names of African countries.
countries = HashTable(40, (str, 20))
# The `values` - will be populated with the names of each country's capital city.
capitals = np.empty(countries.max, (str, 20))

Add or set items using the pattern values[table.add(key)] = value:

capitals[countries.add("Algeria")] = "Al Jaza'ir"

Or in bulk:

new_keys = ["Angola", "Botswana", "Burkina Faso"]
new_values = ["Luanda", "Gaborone", "Ouagadougou"]
capitals[countries.add(new_keys)] = new_values

Like Python dicts, overwriting values is exactly the same as writing them.

Retrieve values with values[table[key]]:

>>> capitals[countries["Botswana"]]
'Gaborone'
>>> capitals[countries["Botswana", "Algeria"]]
array(['Gaborone', "Al Jaza'ir"], dtype='<U20')

View all keys and values with table.keys and values[:len(table)]. A HashTable remembers the order keys were first added so this dict is automatically a sorted dict.

# keys
>>> countries.keys
array(['Algeria', 'Angola', 'Botswana', 'Burkina Faso'], dtype='<U20')
# values
>>> capitals[:len(countries)]
array(["Al Jaza'ir", 'Luanda', 'Gaborone', 'Ouagadougou'], dtype='<U20')

Depending on the usage scenario, it may or may not make sense to want an equivalent to dict.items(). If you do want an equivalent, use numpy.rec.fromarrays([table.keys, values[:len(table)]]), possibly adding a names= option:

>>> np.rec.fromarrays([countries.keys, capitals[:len(countries)]],
...                   names="countries,capitals")
rec.array([('Algeria', "Al Jaza'ir"), ('Angola', 'Luanda'),
           ('Botswana', 'Gaborone'), ('Burkina Faso', 'Ouagadougou')],
          dtype=[('countries', '<U20'), ('capitals', '<U20')])

If the keys and values have the same dtype then numpy.c_ works too.

>>> np.c_[countries.keys, capitals[:len(countries)]]
array([['Algeria', "Al Jaza'ir"],
       ['Angola', 'Luanda'],
       ['Botswana', 'Gaborone'],
       ['Burkina Faso', 'Ouagadougou']], dtype='<U20')

Using a HashTable as a set

To get set-like capabilities from a HashTable, leverage the contains() method. For these examples we will experiment with integer multiples of 3 and 7.

import numpy as np

of_3s = np.arange(0, 100, 3)
of_7s = np.arange(0, 100, 7)

We’ll only require one array to be converted into a hash table. The other can remain as an array. If both are hash tables, simply use one table’s keys attribute as the array.

from hirola import HashTable

table_of_3s = HashTable(len(of_3s) * 1.25, of_3s.dtype)
table_of_3s.add(of_3s)

Use table.contains() as a vectorised version of in.

>>> table_of_3s.contains(of_7s)
array([ True, False, False,  True, False, False,  True, False, False,
        True, False, False,  True, False, False])

From the above, the common set operations can be derived with following:

  • set.intersection() - Values in the array and in the set:

>>> of_7s[table_of_3s.contains(of_7s)]
array([ 0, 21, 42, 63, 84])
  • Set subtraction - Values in the array which are not in the set:

>>> of_7s[~table_of_3s.contains(of_7s)]
array([ 7, 14, 28, 35, 49, 56, 70, 77, 91, 98])
  • set.union() - Values in either the table or in the tested array (with no duplicates):

>>> np.concatenate([table_of_3s.keys, of_7s[~table_of_3s.contains(of_7s)]], axis=0)
array([ 0,  3,  6,  9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48,
       51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99,
        7, 14, 28, 35, 49, 56, 70, 77, 91, 98])

Using a HashTable as a collections.Counter

For this example, let’s give ourselves something a bit more substantial to work on. Counting word frequencies in Shakespeare’s Hamlet play is the trendy example for collections.Counter and it’s what we’ll use too.

from urllib.request import urlopen
import re
import numpy as np

hamlet = urlopen("https://gist.githubusercontent.com/provpup/2fc41686eab7400b796b/raw/b575bd01a58494dfddc1d6429ef0167e709abf9b/hamlet.txt").read()
words = np.array(re.findall(rb"([\w']+)", hamlet))

A counter is just a dict with integer values and a dict is just a hash table with a separate array for values.

from hirola import HashTable

word_table = HashTable(len(words), words.dtype)
counts = np.zeros(word_table.max, dtype=int)

The only new functionality that is not defined in using a hash table as a dict is the ability to count keys as they are added. To count new elements use the rather odd line np.add(counts, table.add(keys), 1).

np.add.at(counts, word_table.add(words), 1)

This line does what you might expect counts[word_table.add(words)] += 1 to do but, due to the way NumPy works, the latter form fails to increment each count more than once if words contains duplicates.

Use NumPy’s indirect sorting functions to get most or least common keys.

# Get the most common word.
>>> word_table.keys[counts[:len(word_table)].argmax()]
b'the'

# Get the top 10 most common words. Note that these are unsorted.
>>> word_table.keys[counts[:len(word_table)].argpartition(-10)[-10:]]
array([b'it', b'and', b'my', b'of', b'in', b'a', b'to', b'the', b'I',
       b'you'], dtype='|S14')

# Get all words in ascending order of commonness.
>>> word_table.keys[counts[:len(word_table)].argsort()]
array([b'END', b'whereat', b"griev'd", ..., b'to', b'and', b'the'],
      dtype='|S14')

A Security Note

Unlike the builtin hash() used internally by Python’s set and dict, hirola does not randomise a hash seed on startup making an online server running hirola more vulnerable to denial of service attacks. In such an attack, the attacker clogs up your server by sending it requests that he/she knows will cause hash collisions and therefore slow it down. Whereas a Python hash table’s size is always predictably the next power of 8 above len(table) * 3 / 2, a hirola.HashTable() may be any size meaning that you can make an attack considerably more difficult by adding a little randomness to the sizes of your hash tables. But if your writing an online server which performs dictionary lookup based on user input and your user-base doesn’t like you much or you have some very spiteful below-the-belt competitors then I recommend that you don’t use this library.

Credits

This package was initially created with Cookiecutter and a fork of the audreyr/cookiecutter-pypackage project template.

Metadata

Release files for hirola 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

File added late

1 file was uploaded more than 14 days after the first file in this release.

While project maintainers occasionally add legitimate files to an existing release, late additions can also indicate a security compromise.

We recommend inspecting the release files before installing.

Source distribution (sdist)

Source distribution for hirola 0.3.0
File Size Uploaded Provenance Actions
hirola-0.3.0.tar.gz 25.5 kB Without provenance Details for hirola-0.3.0.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for hirola 0.3.0
File Actions
hirola-0.3.0-py3-none-win_arm64.whl Python 3 none Windows ARM64 Details for hirola-0.3.0-py3-none-win_arm64.whl
hirola-0.3.0-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details for hirola-0.3.0-py3-none-win_amd64.whl
hirola-0.3.0-py3-none-win32.whl Python 3 none Windows x86-32 Details for hirola-0.3.0-py3-none-win32.whl
hirola-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl Python 3 none Linux glibc 2.17+ x86-64 Details for hirola-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
hirola-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl Python 3 none Linux glibc 2.17+ ARM64 Details for hirola-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
hirola-0.3.0-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl Python 3 none Linux glibc 2.5+ x86-64 Details for hirola-0.3.0-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl
hirola-0.3.0-py3-none-manylinux_2_5_i686.manylinux1_i686.whl Python 3 none Linux glibc 2.5+ x86-32 Details for hirola-0.3.0-py3-none-manylinux_2_5_i686.manylinux1_i686.whl
hirola-0.3.0-py3-none-macosx_10_6_universal2.whl Python 3 none macOS 10.6+ universal2 (ARM64, x86-64) Details for hirola-0.3.0-py3-none-macosx_10_6_universal2.whl

Total release size: 230.6 kB

Release files / hirola-0.3.0.tar.gz

Download URL hirola-0.3.0.tar.gz
Size 25.5 kB
Tags Source
SHA-256 checksum
How to use checksums
68fc96b00be7b55f9268dba03d25eba24692a2684e0f81778a2021a2a3789194
BLAKE2b-256 checksum
How to use checksums
9ef8808540c0c674d3a098caa0ccd1955d5f9bf020b77903072f176c7eb331df
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.4.2 importlib_metadata/4.6.3 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.0 CPython/3.9.6

Release files / hirola-0.3.0-py3-none-win_arm64.whl

File added late

This file was uploaded more than 14 days after the first file in this release.

While project maintainers occasionally add legitimate files to an existing release, late additions can also indicate a security compromise.

We recommend inspecting the release file before installing.

Download URL hirola-0.3.0-py3-none-win_arm64.whl
Size 41.1 kB
Tags Python 3 Windows ARM64
SHA-256 checksum
How to use checksums
98d67c42304e2885b2f66ecbd4ffdbf70e6b092e5f426f903e0049b4b3c8cf9d
BLAKE2b-256 checksum
How to use checksums
6f8401d88448b423b0971c650468f2d059fe71345294a6984d03913d0426fa94
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/4.0.2 CPython/3.10.9

Release files / hirola-0.3.0-py3-none-win_amd64.whl

Download URL hirola-0.3.0-py3-none-win_amd64.whl
Size 29.7 kB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
907b3a6102da1dc24312df8884f0fda74d3564d499f680b670144aa924f01573
BLAKE2b-256 checksum
How to use checksums
ff6c229526d659d5ac872f684dcc821635e91f2f571cd0e0959e2892e4969df7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.4.2 importlib_metadata/4.6.3 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.0 CPython/3.9.6

Release files / hirola-0.3.0-py3-none-win32.whl

Download URL hirola-0.3.0-py3-none-win32.whl
Size 29.2 kB
Tags Python 3 Windows x86-32
SHA-256 checksum
How to use checksums
2f641758a35f014c680bf2b11d41274ac2ef7e59a9659b33237e0ca8e2bc13b7
BLAKE2b-256 checksum
How to use checksums
edbe5d397fd1c24d9ca9014aa230d082730388c1352efbe8f1bbed4bf0d1cd67
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.4.2 importlib_metadata/4.6.3 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.0 CPython/3.9.6

Release files / hirola-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL hirola-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 21.1 kB
Tags Linux glibc 2.17+ x86-64 Python 3
SHA-256 checksum
How to use checksums
efa212fe8ee42293fdd317f74f3a8565c658e23369548cc102708090225d1871
BLAKE2b-256 checksum
How to use checksums
ac8e0eccb528701273640dd4f13678a0c1352176166aecd1ee7f4fa29355132b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.4.2 importlib_metadata/4.6.3 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.0 CPython/3.9.6

Release files / hirola-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL hirola-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 20.7 kB
Tags Linux glibc 2.17+ ARM64 Python 3
SHA-256 checksum
How to use checksums
b2f823b3ccbed238e6dd979409f4d84f7828041622aae71292f57553a8b66391
BLAKE2b-256 checksum
How to use checksums
980f919059255415debec0c372f35eef794d54d4684435c284e8be1b243f8bd5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.4.2 importlib_metadata/4.6.3 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.0 CPython/3.9.6

Release files / hirola-0.3.0-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl

Download URL hirola-0.3.0-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl
Size 20.4 kB
Tags Linux glibc 2.5+ x86-64 Python 3
SHA-256 checksum
How to use checksums
a3823e98d6ec17ed73c71caacc3d30d64c80c5e359992859537541579e723440
BLAKE2b-256 checksum
How to use checksums
5e9a43fafb3a67f22636324bc1b218063052688beeec01efe5ac3389458a547c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.4.2 importlib_metadata/4.6.3 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.0 CPython/3.9.6

Release files / hirola-0.3.0-py3-none-manylinux_2_5_i686.manylinux1_i686.whl

Download URL hirola-0.3.0-py3-none-manylinux_2_5_i686.manylinux1_i686.whl
Size 20.4 kB
Tags Linux glibc 2.5+ x86-32 Python 3
SHA-256 checksum
How to use checksums
d0656d9cc5dd588d035d43b05cfa79c242d509a6e2da706cbd4c63525ebc777e
BLAKE2b-256 checksum
How to use checksums
32cea99dda38eb13798b922769c138de47abd94d0b98dd36364b094deec7bd25
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.4.2 importlib_metadata/4.6.3 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.0 CPython/3.9.6

Release files / hirola-0.3.0-py3-none-macosx_10_6_universal2.whl

Download URL hirola-0.3.0-py3-none-macosx_10_6_universal2.whl
Size 22.6 kB
Tags Python 3 macOS 10.6+ universal2 (ARM64, x86-64)
SHA-256 checksum
How to use checksums
5ef89e18ee4186b2e0d0595728a2d6d46d0258a3043c9f0f9a348249b8d9b3a5
BLAKE2b-256 checksum
How to use checksums
b2169298cca16e26d2fc3751a2b93469cc14a6f8bb9fb5f4087977926d936383
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.4.2 importlib_metadata/4.6.3 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.0 CPython/3.9.6

Security for hirola 0.3.0

Upload timing

File added late

1 file was uploaded more than 14 days after the first file in this release.

While project maintainers occasionally add legitimate files to an existing release, late additions can also indicate a security compromise.


Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page