662 lines
22 KiB
Python
662 lines
22 KiB
Python
# Natural Language Toolkit: Collections
|
|
#
|
|
# Copyright (C) 2001-2023 NLTK Project
|
|
# Author: Steven Bird <stevenbird1@gmail.com>
|
|
# URL: <https://www.nltk.org/>
|
|
# For license information, see LICENSE.TXT
|
|
|
|
import bisect
|
|
|
|
# this unused import is for python 2.7
|
|
from collections import Counter, defaultdict, deque
|
|
from functools import total_ordering
|
|
from itertools import chain, islice
|
|
|
|
from nltk.internals import raise_unorderable_types, slice_bounds
|
|
|
|
##########################################################################
|
|
# Ordered Dictionary
|
|
##########################################################################
|
|
|
|
|
|
class OrderedDict(dict):
|
|
def __init__(self, data=None, **kwargs):
|
|
self._keys = self.keys(data, kwargs.get("keys"))
|
|
self._default_factory = kwargs.get("default_factory")
|
|
if data is None:
|
|
dict.__init__(self)
|
|
else:
|
|
dict.__init__(self, data)
|
|
|
|
def __delitem__(self, key):
|
|
dict.__delitem__(self, key)
|
|
self._keys.remove(key)
|
|
|
|
def __getitem__(self, key):
|
|
try:
|
|
return dict.__getitem__(self, key)
|
|
except KeyError:
|
|
return self.__missing__(key)
|
|
|
|
def __iter__(self):
|
|
return (key for key in self.keys())
|
|
|
|
def __missing__(self, key):
|
|
if not self._default_factory and key not in self._keys:
|
|
raise KeyError()
|
|
return self._default_factory()
|
|
|
|
def __setitem__(self, key, item):
|
|
dict.__setitem__(self, key, item)
|
|
if key not in self._keys:
|
|
self._keys.append(key)
|
|
|
|
def clear(self):
|
|
dict.clear(self)
|
|
self._keys.clear()
|
|
|
|
def copy(self):
|
|
d = dict.copy(self)
|
|
d._keys = self._keys
|
|
return d
|
|
|
|
def items(self):
|
|
# returns iterator under python 3 and list under python 2
|
|
return zip(self.keys(), self.values())
|
|
|
|
def keys(self, data=None, keys=None):
|
|
if data:
|
|
if keys:
|
|
assert isinstance(keys, list)
|
|
assert len(data) == len(keys)
|
|
return keys
|
|
else:
|
|
assert (
|
|
isinstance(data, dict)
|
|
or isinstance(data, OrderedDict)
|
|
or isinstance(data, list)
|
|
)
|
|
if isinstance(data, dict) or isinstance(data, OrderedDict):
|
|
return data.keys()
|
|
elif isinstance(data, list):
|
|
return [key for (key, value) in data]
|
|
elif "_keys" in self.__dict__:
|
|
return self._keys
|
|
else:
|
|
return []
|
|
|
|
def popitem(self):
|
|
if not self._keys:
|
|
raise KeyError()
|
|
|
|
key = self._keys.pop()
|
|
value = self[key]
|
|
del self[key]
|
|
return (key, value)
|
|
|
|
def setdefault(self, key, failobj=None):
|
|
dict.setdefault(self, key, failobj)
|
|
if key not in self._keys:
|
|
self._keys.append(key)
|
|
|
|
def update(self, data):
|
|
dict.update(self, data)
|
|
for key in self.keys(data):
|
|
if key not in self._keys:
|
|
self._keys.append(key)
|
|
|
|
def values(self):
|
|
# returns iterator under python 3
|
|
return map(self.get, self._keys)
|
|
|
|
|
|
######################################################################
|
|
# Lazy Sequences
|
|
######################################################################
|
|
|
|
|
|
@total_ordering
|
|
class AbstractLazySequence:
|
|
"""
|
|
An abstract base class for read-only sequences whose values are
|
|
computed as needed. Lazy sequences act like tuples -- they can be
|
|
indexed, sliced, and iterated over; but they may not be modified.
|
|
|
|
The most common application of lazy sequences in NLTK is for
|
|
corpus view objects, which provide access to the contents of a
|
|
corpus without loading the entire corpus into memory, by loading
|
|
pieces of the corpus from disk as needed.
|
|
|
|
The result of modifying a mutable element of a lazy sequence is
|
|
undefined. In particular, the modifications made to the element
|
|
may or may not persist, depending on whether and when the lazy
|
|
sequence caches that element's value or reconstructs it from
|
|
scratch.
|
|
|
|
Subclasses are required to define two methods: ``__len__()``
|
|
and ``iterate_from()``.
|
|
"""
|
|
|
|
def __len__(self):
|
|
"""
|
|
Return the number of tokens in the corpus file underlying this
|
|
corpus view.
|
|
"""
|
|
raise NotImplementedError("should be implemented by subclass")
|
|
|
|
def iterate_from(self, start):
|
|
"""
|
|
Return an iterator that generates the tokens in the corpus
|
|
file underlying this corpus view, starting at the token number
|
|
``start``. If ``start>=len(self)``, then this iterator will
|
|
generate no tokens.
|
|
"""
|
|
raise NotImplementedError("should be implemented by subclass")
|
|
|
|
def __getitem__(self, i):
|
|
"""
|
|
Return the *i* th token in the corpus file underlying this
|
|
corpus view. Negative indices and spans are both supported.
|
|
"""
|
|
if isinstance(i, slice):
|
|
start, stop = slice_bounds(self, i)
|
|
return LazySubsequence(self, start, stop)
|
|
else:
|
|
# Handle negative indices
|
|
if i < 0:
|
|
i += len(self)
|
|
if i < 0:
|
|
raise IndexError("index out of range")
|
|
# Use iterate_from to extract it.
|
|
try:
|
|
return next(self.iterate_from(i))
|
|
except StopIteration as e:
|
|
raise IndexError("index out of range") from e
|
|
|
|
def __iter__(self):
|
|
"""Return an iterator that generates the tokens in the corpus
|
|
file underlying this corpus view."""
|
|
return self.iterate_from(0)
|
|
|
|
def count(self, value):
|
|
"""Return the number of times this list contains ``value``."""
|
|
return sum(1 for elt in self if elt == value)
|
|
|
|
def index(self, value, start=None, stop=None):
|
|
"""Return the index of the first occurrence of ``value`` in this
|
|
list that is greater than or equal to ``start`` and less than
|
|
``stop``. Negative start and stop values are treated like negative
|
|
slice bounds -- i.e., they count from the end of the list."""
|
|
start, stop = slice_bounds(self, slice(start, stop))
|
|
for i, elt in enumerate(islice(self, start, stop)):
|
|
if elt == value:
|
|
return i + start
|
|
raise ValueError("index(x): x not in list")
|
|
|
|
def __contains__(self, value):
|
|
"""Return true if this list contains ``value``."""
|
|
return bool(self.count(value))
|
|
|
|
def __add__(self, other):
|
|
"""Return a list concatenating self with other."""
|
|
return LazyConcatenation([self, other])
|
|
|
|
def __radd__(self, other):
|
|
"""Return a list concatenating other with self."""
|
|
return LazyConcatenation([other, self])
|
|
|
|
def __mul__(self, count):
|
|
"""Return a list concatenating self with itself ``count`` times."""
|
|
return LazyConcatenation([self] * count)
|
|
|
|
def __rmul__(self, count):
|
|
"""Return a list concatenating self with itself ``count`` times."""
|
|
return LazyConcatenation([self] * count)
|
|
|
|
_MAX_REPR_SIZE = 60
|
|
|
|
def __repr__(self):
|
|
"""
|
|
Return a string representation for this corpus view that is
|
|
similar to a list's representation; but if it would be more
|
|
than 60 characters long, it is truncated.
|
|
"""
|
|
pieces = []
|
|
length = 5
|
|
for elt in self:
|
|
pieces.append(repr(elt))
|
|
length += len(pieces[-1]) + 2
|
|
if length > self._MAX_REPR_SIZE and len(pieces) > 2:
|
|
return "[%s, ...]" % ", ".join(pieces[:-1])
|
|
return "[%s]" % ", ".join(pieces)
|
|
|
|
def __eq__(self, other):
|
|
return type(self) == type(other) and list(self) == list(other)
|
|
|
|
def __ne__(self, other):
|
|
return not self == other
|
|
|
|
def __lt__(self, other):
|
|
if type(other) != type(self):
|
|
raise_unorderable_types("<", self, other)
|
|
return list(self) < list(other)
|
|
|
|
def __hash__(self):
|
|
"""
|
|
:raise ValueError: Corpus view objects are unhashable.
|
|
"""
|
|
raise ValueError("%s objects are unhashable" % self.__class__.__name__)
|
|
|
|
|
|
class LazySubsequence(AbstractLazySequence):
|
|
"""
|
|
A subsequence produced by slicing a lazy sequence. This slice
|
|
keeps a reference to its source sequence, and generates its values
|
|
by looking them up in the source sequence.
|
|
"""
|
|
|
|
MIN_SIZE = 100
|
|
"""
|
|
The minimum size for which lazy slices should be created. If
|
|
``LazySubsequence()`` is called with a subsequence that is
|
|
shorter than ``MIN_SIZE``, then a tuple will be returned instead.
|
|
"""
|
|
|
|
def __new__(cls, source, start, stop):
|
|
"""
|
|
Construct a new slice from a given underlying sequence. The
|
|
``start`` and ``stop`` indices should be absolute indices --
|
|
i.e., they should not be negative (for indexing from the back
|
|
of a list) or greater than the length of ``source``.
|
|
"""
|
|
# If the slice is small enough, just use a tuple.
|
|
if stop - start < cls.MIN_SIZE:
|
|
return list(islice(source.iterate_from(start), stop - start))
|
|
else:
|
|
return object.__new__(cls)
|
|
|
|
def __init__(self, source, start, stop):
|
|
self._source = source
|
|
self._start = start
|
|
self._stop = stop
|
|
|
|
def __len__(self):
|
|
return self._stop - self._start
|
|
|
|
def iterate_from(self, start):
|
|
return islice(
|
|
self._source.iterate_from(start + self._start), max(0, len(self) - start)
|
|
)
|
|
|
|
|
|
class LazyConcatenation(AbstractLazySequence):
|
|
"""
|
|
A lazy sequence formed by concatenating a list of lists. This
|
|
underlying list of lists may itself be lazy. ``LazyConcatenation``
|
|
maintains an index that it uses to keep track of the relationship
|
|
between offsets in the concatenated lists and offsets in the
|
|
sublists.
|
|
"""
|
|
|
|
def __init__(self, list_of_lists):
|
|
self._list = list_of_lists
|
|
self._offsets = [0]
|
|
|
|
def __len__(self):
|
|
if len(self._offsets) <= len(self._list):
|
|
for _ in self.iterate_from(self._offsets[-1]):
|
|
pass
|
|
return self._offsets[-1]
|
|
|
|
def iterate_from(self, start_index):
|
|
if start_index < self._offsets[-1]:
|
|
sublist_index = bisect.bisect_right(self._offsets, start_index) - 1
|
|
else:
|
|
sublist_index = len(self._offsets) - 1
|
|
|
|
index = self._offsets[sublist_index]
|
|
|
|
# Construct an iterator over the sublists.
|
|
if isinstance(self._list, AbstractLazySequence):
|
|
sublist_iter = self._list.iterate_from(sublist_index)
|
|
else:
|
|
sublist_iter = islice(self._list, sublist_index, None)
|
|
|
|
for sublist in sublist_iter:
|
|
if sublist_index == (len(self._offsets) - 1):
|
|
assert (
|
|
index + len(sublist) >= self._offsets[-1]
|
|
), "offsets not monotonic increasing!"
|
|
self._offsets.append(index + len(sublist))
|
|
else:
|
|
assert self._offsets[sublist_index + 1] == index + len(
|
|
sublist
|
|
), "inconsistent list value (num elts)"
|
|
|
|
yield from sublist[max(0, start_index - index) :]
|
|
|
|
index += len(sublist)
|
|
sublist_index += 1
|
|
|
|
|
|
class LazyMap(AbstractLazySequence):
|
|
"""
|
|
A lazy sequence whose elements are formed by applying a given
|
|
function to each element in one or more underlying lists. The
|
|
function is applied lazily -- i.e., when you read a value from the
|
|
list, ``LazyMap`` will calculate that value by applying its
|
|
function to the underlying lists' value(s). ``LazyMap`` is
|
|
essentially a lazy version of the Python primitive function
|
|
``map``. In particular, the following two expressions are
|
|
equivalent:
|
|
|
|
>>> from nltk.collections import LazyMap
|
|
>>> function = str
|
|
>>> sequence = [1,2,3]
|
|
>>> map(function, sequence) # doctest: +SKIP
|
|
['1', '2', '3']
|
|
>>> list(LazyMap(function, sequence))
|
|
['1', '2', '3']
|
|
|
|
Like the Python ``map`` primitive, if the source lists do not have
|
|
equal size, then the value None will be supplied for the
|
|
'missing' elements.
|
|
|
|
Lazy maps can be useful for conserving memory, in cases where
|
|
individual values take up a lot of space. This is especially true
|
|
if the underlying list's values are constructed lazily, as is the
|
|
case with many corpus readers.
|
|
|
|
A typical example of a use case for this class is performing
|
|
feature detection on the tokens in a corpus. Since featuresets
|
|
are encoded as dictionaries, which can take up a lot of memory,
|
|
using a ``LazyMap`` can significantly reduce memory usage when
|
|
training and running classifiers.
|
|
"""
|
|
|
|
def __init__(self, function, *lists, **config):
|
|
"""
|
|
:param function: The function that should be applied to
|
|
elements of ``lists``. It should take as many arguments
|
|
as there are ``lists``.
|
|
:param lists: The underlying lists.
|
|
:param cache_size: Determines the size of the cache used
|
|
by this lazy map. (default=5)
|
|
"""
|
|
if not lists:
|
|
raise TypeError("LazyMap requires at least two args")
|
|
|
|
self._lists = lists
|
|
self._func = function
|
|
self._cache_size = config.get("cache_size", 5)
|
|
self._cache = {} if self._cache_size > 0 else None
|
|
|
|
# If you just take bool() of sum() here _all_lazy will be true just
|
|
# in case n >= 1 list is an AbstractLazySequence. Presumably this
|
|
# isn't what's intended.
|
|
self._all_lazy = sum(
|
|
isinstance(lst, AbstractLazySequence) for lst in lists
|
|
) == len(lists)
|
|
|
|
def iterate_from(self, index):
|
|
# Special case: one lazy sublist
|
|
if len(self._lists) == 1 and self._all_lazy:
|
|
for value in self._lists[0].iterate_from(index):
|
|
yield self._func(value)
|
|
return
|
|
|
|
# Special case: one non-lazy sublist
|
|
elif len(self._lists) == 1:
|
|
while True:
|
|
try:
|
|
yield self._func(self._lists[0][index])
|
|
except IndexError:
|
|
return
|
|
index += 1
|
|
|
|
# Special case: n lazy sublists
|
|
elif self._all_lazy:
|
|
iterators = [lst.iterate_from(index) for lst in self._lists]
|
|
while True:
|
|
elements = []
|
|
for iterator in iterators:
|
|
try:
|
|
elements.append(next(iterator))
|
|
except: # FIXME: What is this except really catching? StopIteration?
|
|
elements.append(None)
|
|
if elements == [None] * len(self._lists):
|
|
return
|
|
yield self._func(*elements)
|
|
index += 1
|
|
|
|
# general case
|
|
else:
|
|
while True:
|
|
try:
|
|
elements = [lst[index] for lst in self._lists]
|
|
except IndexError:
|
|
elements = [None] * len(self._lists)
|
|
for i, lst in enumerate(self._lists):
|
|
try:
|
|
elements[i] = lst[index]
|
|
except IndexError:
|
|
pass
|
|
if elements == [None] * len(self._lists):
|
|
return
|
|
yield self._func(*elements)
|
|
index += 1
|
|
|
|
def __getitem__(self, index):
|
|
if isinstance(index, slice):
|
|
sliced_lists = [lst[index] for lst in self._lists]
|
|
return LazyMap(self._func, *sliced_lists)
|
|
else:
|
|
# Handle negative indices
|
|
if index < 0:
|
|
index += len(self)
|
|
if index < 0:
|
|
raise IndexError("index out of range")
|
|
# Check the cache
|
|
if self._cache is not None and index in self._cache:
|
|
return self._cache[index]
|
|
# Calculate the value
|
|
try:
|
|
val = next(self.iterate_from(index))
|
|
except StopIteration as e:
|
|
raise IndexError("index out of range") from e
|
|
# Update the cache
|
|
if self._cache is not None:
|
|
if len(self._cache) > self._cache_size:
|
|
self._cache.popitem() # discard random entry
|
|
self._cache[index] = val
|
|
# Return the value
|
|
return val
|
|
|
|
def __len__(self):
|
|
return max(len(lst) for lst in self._lists)
|
|
|
|
|
|
class LazyZip(LazyMap):
|
|
"""
|
|
A lazy sequence whose elements are tuples, each containing the i-th
|
|
element from each of the argument sequences. The returned list is
|
|
truncated in length to the length of the shortest argument sequence. The
|
|
tuples are constructed lazily -- i.e., when you read a value from the
|
|
list, ``LazyZip`` will calculate that value by forming a tuple from
|
|
the i-th element of each of the argument sequences.
|
|
|
|
``LazyZip`` is essentially a lazy version of the Python primitive function
|
|
``zip``. In particular, an evaluated LazyZip is equivalent to a zip:
|
|
|
|
>>> from nltk.collections import LazyZip
|
|
>>> sequence1, sequence2 = [1, 2, 3], ['a', 'b', 'c']
|
|
>>> zip(sequence1, sequence2) # doctest: +SKIP
|
|
[(1, 'a'), (2, 'b'), (3, 'c')]
|
|
>>> list(LazyZip(sequence1, sequence2))
|
|
[(1, 'a'), (2, 'b'), (3, 'c')]
|
|
>>> sequences = [sequence1, sequence2, [6,7,8,9]]
|
|
>>> list(zip(*sequences)) == list(LazyZip(*sequences))
|
|
True
|
|
|
|
Lazy zips can be useful for conserving memory in cases where the argument
|
|
sequences are particularly long.
|
|
|
|
A typical example of a use case for this class is combining long sequences
|
|
of gold standard and predicted values in a classification or tagging task
|
|
in order to calculate accuracy. By constructing tuples lazily and
|
|
avoiding the creation of an additional long sequence, memory usage can be
|
|
significantly reduced.
|
|
"""
|
|
|
|
def __init__(self, *lists):
|
|
"""
|
|
:param lists: the underlying lists
|
|
:type lists: list(list)
|
|
"""
|
|
LazyMap.__init__(self, lambda *elts: elts, *lists)
|
|
|
|
def iterate_from(self, index):
|
|
iterator = LazyMap.iterate_from(self, index)
|
|
while index < len(self):
|
|
yield next(iterator)
|
|
index += 1
|
|
return
|
|
|
|
def __len__(self):
|
|
return min(len(lst) for lst in self._lists)
|
|
|
|
|
|
class LazyEnumerate(LazyZip):
|
|
"""
|
|
A lazy sequence whose elements are tuples, each containing a count (from
|
|
zero) and a value yielded by underlying sequence. ``LazyEnumerate`` is
|
|
useful for obtaining an indexed list. The tuples are constructed lazily
|
|
-- i.e., when you read a value from the list, ``LazyEnumerate`` will
|
|
calculate that value by forming a tuple from the count of the i-th
|
|
element and the i-th element of the underlying sequence.
|
|
|
|
``LazyEnumerate`` is essentially a lazy version of the Python primitive
|
|
function ``enumerate``. In particular, the following two expressions are
|
|
equivalent:
|
|
|
|
>>> from nltk.collections import LazyEnumerate
|
|
>>> sequence = ['first', 'second', 'third']
|
|
>>> list(enumerate(sequence))
|
|
[(0, 'first'), (1, 'second'), (2, 'third')]
|
|
>>> list(LazyEnumerate(sequence))
|
|
[(0, 'first'), (1, 'second'), (2, 'third')]
|
|
|
|
Lazy enumerations can be useful for conserving memory in cases where the
|
|
argument sequences are particularly long.
|
|
|
|
A typical example of a use case for this class is obtaining an indexed
|
|
list for a long sequence of values. By constructing tuples lazily and
|
|
avoiding the creation of an additional long sequence, memory usage can be
|
|
significantly reduced.
|
|
"""
|
|
|
|
def __init__(self, lst):
|
|
"""
|
|
:param lst: the underlying list
|
|
:type lst: list
|
|
"""
|
|
LazyZip.__init__(self, range(len(lst)), lst)
|
|
|
|
|
|
class LazyIteratorList(AbstractLazySequence):
|
|
"""
|
|
Wraps an iterator, loading its elements on demand
|
|
and making them subscriptable.
|
|
__repr__ displays only the first few elements.
|
|
"""
|
|
|
|
def __init__(self, it, known_len=None):
|
|
self._it = it
|
|
self._len = known_len
|
|
self._cache = []
|
|
|
|
def __len__(self):
|
|
if self._len:
|
|
return self._len
|
|
for _ in self.iterate_from(len(self._cache)):
|
|
pass
|
|
self._len = len(self._cache)
|
|
return self._len
|
|
|
|
def iterate_from(self, start):
|
|
"""Create a new iterator over this list starting at the given offset."""
|
|
while len(self._cache) < start:
|
|
v = next(self._it)
|
|
self._cache.append(v)
|
|
i = start
|
|
while i < len(self._cache):
|
|
yield self._cache[i]
|
|
i += 1
|
|
try:
|
|
while True:
|
|
v = next(self._it)
|
|
self._cache.append(v)
|
|
yield v
|
|
except StopIteration:
|
|
pass
|
|
|
|
def __add__(self, other):
|
|
"""Return a list concatenating self with other."""
|
|
return type(self)(chain(self, other))
|
|
|
|
def __radd__(self, other):
|
|
"""Return a list concatenating other with self."""
|
|
return type(self)(chain(other, self))
|
|
|
|
|
|
######################################################################
|
|
# Trie Implementation
|
|
######################################################################
|
|
class Trie(dict):
|
|
"""A Trie implementation for strings"""
|
|
|
|
LEAF = True
|
|
|
|
def __init__(self, strings=None):
|
|
"""Builds a Trie object, which is built around a ``dict``
|
|
|
|
If ``strings`` is provided, it will add the ``strings``, which
|
|
consist of a ``list`` of ``strings``, to the Trie.
|
|
Otherwise, it'll construct an empty Trie.
|
|
|
|
:param strings: List of strings to insert into the trie
|
|
(Default is ``None``)
|
|
:type strings: list(str)
|
|
|
|
"""
|
|
super().__init__()
|
|
if strings:
|
|
for string in strings:
|
|
self.insert(string)
|
|
|
|
def insert(self, string):
|
|
"""Inserts ``string`` into the Trie
|
|
|
|
:param string: String to insert into the trie
|
|
:type string: str
|
|
|
|
:Example:
|
|
|
|
>>> from nltk.collections import Trie
|
|
>>> trie = Trie(["abc", "def"])
|
|
>>> expected = {'a': {'b': {'c': {True: None}}}, \
|
|
'd': {'e': {'f': {True: None}}}}
|
|
>>> trie == expected
|
|
True
|
|
|
|
"""
|
|
if len(string):
|
|
self[string[0]].insert(string[1:])
|
|
else:
|
|
# mark the string is complete
|
|
self[Trie.LEAF] = None
|
|
|
|
def __missing__(self, key):
|
|
self[key] = Trie()
|
|
return self[key]
|