2618 lines
101 KiB
Python
2618 lines
101 KiB
Python
|
# Natural Language Toolkit: Feature Structures
|
||
|
#
|
||
|
# Copyright (C) 2001-2018 NLTK Project
|
||
|
# Author: Edward Loper <edloper@gmail.com>,
|
||
|
# Rob Speer,
|
||
|
# Steven Bird <stevenbird1@gmail.com>
|
||
|
# URL: <http://nltk.sourceforge.net>
|
||
|
# For license information, see LICENSE.TXT
|
||
|
|
||
|
"""
|
||
|
Basic data classes for representing feature structures, and for
|
||
|
performing basic operations on those feature structures. A feature
|
||
|
structure is a mapping from feature identifiers to feature values,
|
||
|
where each feature value is either a basic value (such as a string or
|
||
|
an integer), or a nested feature structure. There are two types of
|
||
|
feature structure, implemented by two subclasses of ``FeatStruct``:
|
||
|
|
||
|
- feature dictionaries, implemented by ``FeatDict``, act like
|
||
|
Python dictionaries. Feature identifiers may be strings or
|
||
|
instances of the ``Feature`` class.
|
||
|
- feature lists, implemented by ``FeatList``, act like Python
|
||
|
lists. Feature identifiers are integers.
|
||
|
|
||
|
Feature structures are typically used to represent partial information
|
||
|
about objects. A feature identifier that is not mapped to a value
|
||
|
stands for a feature whose value is unknown (*not* a feature without
|
||
|
a value). Two feature structures that represent (potentially
|
||
|
overlapping) information about the same object can be combined by
|
||
|
unification. When two inconsistent feature structures are unified,
|
||
|
the unification fails and returns None.
|
||
|
|
||
|
Features can be specified using "feature paths", or tuples of feature
|
||
|
identifiers that specify path through the nested feature structures to
|
||
|
a value. Feature structures may contain reentrant feature values. A
|
||
|
"reentrant feature value" is a single feature value that can be
|
||
|
accessed via multiple feature paths. Unification preserves the
|
||
|
reentrance relations imposed by both of the unified feature
|
||
|
structures. In the feature structure resulting from unification, any
|
||
|
modifications to a reentrant feature value will be visible using any
|
||
|
of its feature paths.
|
||
|
|
||
|
Feature structure variables are encoded using the ``nltk.sem.Variable``
|
||
|
class. The variables' values are tracked using a bindings
|
||
|
dictionary, which maps variables to their values. When two feature
|
||
|
structures are unified, a fresh bindings dictionary is created to
|
||
|
track their values; and before unification completes, all bound
|
||
|
variables are replaced by their values. Thus, the bindings
|
||
|
dictionaries are usually strictly internal to the unification process.
|
||
|
However, it is possible to track the bindings of variables if you
|
||
|
choose to, by supplying your own initial bindings dictionary to the
|
||
|
``unify()`` function.
|
||
|
|
||
|
When unbound variables are unified with one another, they become
|
||
|
aliased. This is encoded by binding one variable to the other.
|
||
|
|
||
|
Lightweight Feature Structures
|
||
|
==============================
|
||
|
Many of the functions defined by ``nltk.featstruct`` can be applied
|
||
|
directly to simple Python dictionaries and lists, rather than to
|
||
|
full-fledged ``FeatDict`` and ``FeatList`` objects. In other words,
|
||
|
Python ``dicts`` and ``lists`` can be used as "light-weight" feature
|
||
|
structures.
|
||
|
|
||
|
>>> from nltk.featstruct import unify
|
||
|
>>> unify(dict(x=1, y=dict()), dict(a='a', y=dict(b='b'))) # doctest: +SKIP
|
||
|
{'y': {'b': 'b'}, 'x': 1, 'a': 'a'}
|
||
|
|
||
|
However, you should keep in mind the following caveats:
|
||
|
|
||
|
- Python dictionaries & lists ignore reentrance when checking for
|
||
|
equality between values. But two FeatStructs with different
|
||
|
reentrances are considered nonequal, even if all their base
|
||
|
values are equal.
|
||
|
|
||
|
- FeatStructs can be easily frozen, allowing them to be used as
|
||
|
keys in hash tables. Python dictionaries and lists can not.
|
||
|
|
||
|
- FeatStructs display reentrance in their string representations;
|
||
|
Python dictionaries and lists do not.
|
||
|
|
||
|
- FeatStructs may *not* be mixed with Python dictionaries and lists
|
||
|
(e.g., when performing unification).
|
||
|
|
||
|
- FeatStructs provide a number of useful methods, such as ``walk()``
|
||
|
and ``cyclic()``, which are not available for Python dicts and lists.
|
||
|
|
||
|
In general, if your feature structures will contain any reentrances,
|
||
|
or if you plan to use them as dictionary keys, it is strongly
|
||
|
recommended that you use full-fledged ``FeatStruct`` objects.
|
||
|
"""
|
||
|
from __future__ import print_function, unicode_literals, division
|
||
|
|
||
|
import re
|
||
|
import copy
|
||
|
from functools import total_ordering
|
||
|
|
||
|
from six import integer_types, string_types
|
||
|
|
||
|
from nltk.internals import read_str, raise_unorderable_types
|
||
|
from nltk.sem.logic import (Variable, Expression, SubstituteBindingsI,
|
||
|
LogicParser, LogicalExpressionException)
|
||
|
from nltk.compat import python_2_unicode_compatible, unicode_repr
|
||
|
|
||
|
|
||
|
######################################################################
|
||
|
# Feature Structure
|
||
|
######################################################################
|
||
|
|
||
|
@total_ordering
|
||
|
class FeatStruct(SubstituteBindingsI):
|
||
|
"""
|
||
|
A mapping from feature identifiers to feature values, where each
|
||
|
feature value is either a basic value (such as a string or an
|
||
|
integer), or a nested feature structure. There are two types of
|
||
|
feature structure:
|
||
|
|
||
|
- feature dictionaries, implemented by ``FeatDict``, act like
|
||
|
Python dictionaries. Feature identifiers may be strings or
|
||
|
instances of the ``Feature`` class.
|
||
|
- feature lists, implemented by ``FeatList``, act like Python
|
||
|
lists. Feature identifiers are integers.
|
||
|
|
||
|
Feature structures may be indexed using either simple feature
|
||
|
identifiers or 'feature paths.' A feature path is a sequence
|
||
|
of feature identifiers that stand for a corresponding sequence of
|
||
|
indexing operations. In particular, ``fstruct[(f1,f2,...,fn)]`` is
|
||
|
equivalent to ``fstruct[f1][f2]...[fn]``.
|
||
|
|
||
|
Feature structures may contain reentrant feature structures. A
|
||
|
"reentrant feature structure" is a single feature structure
|
||
|
object that can be accessed via multiple feature paths. Feature
|
||
|
structures may also be cyclic. A feature structure is "cyclic"
|
||
|
if there is any feature path from the feature structure to itself.
|
||
|
|
||
|
Two feature structures are considered equal if they assign the
|
||
|
same values to all features, and have the same reentrancies.
|
||
|
|
||
|
By default, feature structures are mutable. They may be made
|
||
|
immutable with the ``freeze()`` method. Once they have been
|
||
|
frozen, they may be hashed, and thus used as dictionary keys.
|
||
|
"""
|
||
|
|
||
|
_frozen = False
|
||
|
""":ivar: A flag indicating whether this feature structure is
|
||
|
frozen or not. Once this flag is set, it should never be
|
||
|
un-set; and no further modification should be made to this
|
||
|
feature structue."""
|
||
|
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
# { Constructor
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
|
||
|
def __new__(cls, features=None, **morefeatures):
|
||
|
"""
|
||
|
Construct and return a new feature structure. If this
|
||
|
constructor is called directly, then the returned feature
|
||
|
structure will be an instance of either the ``FeatDict`` class
|
||
|
or the ``FeatList`` class.
|
||
|
|
||
|
:param features: The initial feature values for this feature
|
||
|
structure:
|
||
|
- FeatStruct(string) -> FeatStructReader().read(string)
|
||
|
- FeatStruct(mapping) -> FeatDict(mapping)
|
||
|
- FeatStruct(sequence) -> FeatList(sequence)
|
||
|
- FeatStruct() -> FeatDict()
|
||
|
:param morefeatures: If ``features`` is a mapping or None,
|
||
|
then ``morefeatures`` provides additional features for the
|
||
|
``FeatDict`` constructor.
|
||
|
"""
|
||
|
# If the FeatStruct constructor is called directly, then decide
|
||
|
# whether to create a FeatDict or a FeatList, based on the
|
||
|
# contents of the `features` argument.
|
||
|
if cls is FeatStruct:
|
||
|
if features is None:
|
||
|
return FeatDict.__new__(FeatDict, **morefeatures)
|
||
|
elif _is_mapping(features):
|
||
|
return FeatDict.__new__(FeatDict, features, **morefeatures)
|
||
|
elif morefeatures:
|
||
|
raise TypeError('Keyword arguments may only be specified '
|
||
|
'if features is None or is a mapping.')
|
||
|
if isinstance(features, string_types):
|
||
|
if FeatStructReader._START_FDICT_RE.match(features):
|
||
|
return FeatDict.__new__(FeatDict, features, **morefeatures)
|
||
|
else:
|
||
|
return FeatList.__new__(FeatList, features, **morefeatures)
|
||
|
elif _is_sequence(features):
|
||
|
return FeatList.__new__(FeatList, features)
|
||
|
else:
|
||
|
raise TypeError('Expected string or mapping or sequence')
|
||
|
|
||
|
# Otherwise, construct the object as normal.
|
||
|
else:
|
||
|
return super(FeatStruct, cls).__new__(cls, features,
|
||
|
**morefeatures)
|
||
|
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
# { Uniform Accessor Methods
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
# These helper functions allow the methods defined by FeatStruct
|
||
|
# to treat all feature structures as mappings, even if they're
|
||
|
# really lists. (Lists are treated as mappings from ints to vals)
|
||
|
|
||
|
def _keys(self):
|
||
|
"""Return an iterable of the feature identifiers used by this
|
||
|
FeatStruct."""
|
||
|
raise NotImplementedError() # Implemented by subclasses.
|
||
|
|
||
|
def _values(self):
|
||
|
"""Return an iterable of the feature values directly defined
|
||
|
by this FeatStruct."""
|
||
|
raise NotImplementedError() # Implemented by subclasses.
|
||
|
|
||
|
def _items(self):
|
||
|
"""Return an iterable of (fid,fval) pairs, where fid is a
|
||
|
feature identifier and fval is the corresponding feature
|
||
|
value, for all features defined by this FeatStruct."""
|
||
|
raise NotImplementedError() # Implemented by subclasses.
|
||
|
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
# { Equality & Hashing
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
|
||
|
def equal_values(self, other, check_reentrance=False):
|
||
|
"""
|
||
|
Return True if ``self`` and ``other`` assign the same value to
|
||
|
to every feature. In particular, return true if
|
||
|
``self[p]==other[p]`` for every feature path *p* such
|
||
|
that ``self[p]`` or ``other[p]`` is a base value (i.e.,
|
||
|
not a nested feature structure).
|
||
|
|
||
|
:param check_reentrance: If True, then also return False if
|
||
|
there is any difference between the reentrances of ``self``
|
||
|
and ``other``.
|
||
|
:note: the ``==`` is equivalent to ``equal_values()`` with
|
||
|
``check_reentrance=True``.
|
||
|
"""
|
||
|
return self._equal(other, check_reentrance, set(), set(), set())
|
||
|
|
||
|
def __eq__(self, other):
|
||
|
"""
|
||
|
Return true if ``self`` and ``other`` are both feature structures,
|
||
|
assign the same values to all features, and contain the same
|
||
|
reentrances. I.e., return
|
||
|
``self.equal_values(other, check_reentrance=True)``.
|
||
|
|
||
|
:see: ``equal_values()``
|
||
|
"""
|
||
|
return self._equal(other, True, set(), set(), set())
|
||
|
|
||
|
def __ne__(self, other):
|
||
|
return not self == other
|
||
|
|
||
|
def __lt__(self, other):
|
||
|
if not isinstance(other, FeatStruct):
|
||
|
# raise_unorderable_types("<", self, other)
|
||
|
# Sometimes feature values can be pure strings,
|
||
|
# so we need to be able to compare with non-featstructs:
|
||
|
return self.__class__.__name__ < other.__class__.__name__
|
||
|
else:
|
||
|
return len(self) < len(other)
|
||
|
|
||
|
def __hash__(self):
|
||
|
"""
|
||
|
If this feature structure is frozen, return its hash value;
|
||
|
otherwise, raise ``TypeError``.
|
||
|
"""
|
||
|
if not self._frozen:
|
||
|
raise TypeError('FeatStructs must be frozen before they '
|
||
|
'can be hashed.')
|
||
|
try:
|
||
|
return self._hash
|
||
|
except AttributeError:
|
||
|
self._hash = self._calculate_hashvalue(set())
|
||
|
return self._hash
|
||
|
|
||
|
def _equal(self, other, check_reentrance, visited_self,
|
||
|
visited_other, visited_pairs):
|
||
|
"""
|
||
|
Return True iff self and other have equal values.
|
||
|
|
||
|
:param visited_self: A set containing the ids of all ``self``
|
||
|
feature structures we've already visited.
|
||
|
:param visited_other: A set containing the ids of all ``other``
|
||
|
feature structures we've already visited.
|
||
|
:param visited_pairs: A set containing ``(selfid, otherid)`` pairs
|
||
|
for all pairs of feature structures we've already visited.
|
||
|
"""
|
||
|
# If we're the same object, then we're equal.
|
||
|
if self is other: return True
|
||
|
|
||
|
# If we have different classes, we're definitely not equal.
|
||
|
if self.__class__ != other.__class__: return False
|
||
|
|
||
|
# If we define different features, we're definitely not equal.
|
||
|
# (Perform len test first because it's faster -- we should
|
||
|
# do profiling to see if this actually helps)
|
||
|
if len(self) != len(other): return False
|
||
|
if set(self._keys()) != set(other._keys()): return False
|
||
|
|
||
|
# If we're checking reentrance, then any time we revisit a
|
||
|
# structure, make sure that it was paired with the same
|
||
|
# feature structure that it is now. Note: if check_reentrance,
|
||
|
# then visited_pairs will never contain two pairs whose first
|
||
|
# values are equal, or two pairs whose second values are equal.
|
||
|
if check_reentrance:
|
||
|
if id(self) in visited_self or id(other) in visited_other:
|
||
|
return (id(self), id(other)) in visited_pairs
|
||
|
|
||
|
# If we're not checking reentrance, then we still need to deal
|
||
|
# with cycles. If we encounter the same (self, other) pair a
|
||
|
# second time, then we won't learn anything more by examining
|
||
|
# their children a second time, so just return true.
|
||
|
else:
|
||
|
if (id(self), id(other)) in visited_pairs:
|
||
|
return True
|
||
|
|
||
|
# Keep track of which nodes we've visited.
|
||
|
visited_self.add(id(self))
|
||
|
visited_other.add(id(other))
|
||
|
visited_pairs.add((id(self), id(other)))
|
||
|
|
||
|
# Now we have to check all values. If any of them don't match,
|
||
|
# then return false.
|
||
|
for (fname, self_fval) in self._items():
|
||
|
other_fval = other[fname]
|
||
|
if isinstance(self_fval, FeatStruct):
|
||
|
if not self_fval._equal(other_fval, check_reentrance,
|
||
|
visited_self, visited_other,
|
||
|
visited_pairs):
|
||
|
return False
|
||
|
else:
|
||
|
if self_fval != other_fval: return False
|
||
|
|
||
|
# Everything matched up; return true.
|
||
|
return True
|
||
|
|
||
|
def _calculate_hashvalue(self, visited):
|
||
|
"""
|
||
|
Return a hash value for this feature structure.
|
||
|
|
||
|
:require: ``self`` must be frozen.
|
||
|
:param visited: A set containing the ids of all feature
|
||
|
structures we've already visited while hashing.
|
||
|
"""
|
||
|
if id(self) in visited: return 1
|
||
|
visited.add(id(self))
|
||
|
|
||
|
hashval = 5831
|
||
|
for (fname, fval) in sorted(self._items()):
|
||
|
hashval *= 37
|
||
|
hashval += hash(fname)
|
||
|
hashval *= 37
|
||
|
if isinstance(fval, FeatStruct):
|
||
|
hashval += fval._calculate_hashvalue(visited)
|
||
|
else:
|
||
|
hashval += hash(fval)
|
||
|
# Convert to a 32 bit int.
|
||
|
hashval = int(hashval & 0x7fffffff)
|
||
|
return hashval
|
||
|
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
# { Freezing
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
|
||
|
#: Error message used by mutating methods when called on a frozen
|
||
|
#: feature structure.
|
||
|
_FROZEN_ERROR = "Frozen FeatStructs may not be modified."
|
||
|
|
||
|
def freeze(self):
|
||
|
"""
|
||
|
Make this feature structure, and any feature structures it
|
||
|
contains, immutable. Note: this method does not attempt to
|
||
|
'freeze' any feature value that is not a ``FeatStruct``; it
|
||
|
is recommended that you use only immutable feature values.
|
||
|
"""
|
||
|
if self._frozen: return
|
||
|
self._freeze(set())
|
||
|
|
||
|
def frozen(self):
|
||
|
"""
|
||
|
Return True if this feature structure is immutable. Feature
|
||
|
structures can be made immutable with the ``freeze()`` method.
|
||
|
Immutable feature structures may not be made mutable again,
|
||
|
but new mutable copies can be produced with the ``copy()`` method.
|
||
|
"""
|
||
|
return self._frozen
|
||
|
|
||
|
def _freeze(self, visited):
|
||
|
"""
|
||
|
Make this feature structure, and any feature structure it
|
||
|
contains, immutable.
|
||
|
|
||
|
:param visited: A set containing the ids of all feature
|
||
|
structures we've already visited while freezing.
|
||
|
"""
|
||
|
if id(self) in visited: return
|
||
|
visited.add(id(self))
|
||
|
self._frozen = True
|
||
|
for (fname, fval) in sorted(self._items()):
|
||
|
if isinstance(fval, FeatStruct):
|
||
|
fval._freeze(visited)
|
||
|
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
# { Copying
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
|
||
|
def copy(self, deep=True):
|
||
|
"""
|
||
|
Return a new copy of ``self``. The new copy will not be frozen.
|
||
|
|
||
|
:param deep: If true, create a deep copy; if false, create
|
||
|
a shallow copy.
|
||
|
"""
|
||
|
if deep:
|
||
|
return copy.deepcopy(self)
|
||
|
else:
|
||
|
return self.__class__(self)
|
||
|
|
||
|
# Subclasses should define __deepcopy__ to ensure that the new
|
||
|
# copy will not be frozen.
|
||
|
def __deepcopy__(self, memo):
|
||
|
raise NotImplementedError() # Implemented by subclasses.
|
||
|
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
# { Structural Information
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
|
||
|
def cyclic(self):
|
||
|
"""
|
||
|
Return True if this feature structure contains itself.
|
||
|
"""
|
||
|
return self._find_reentrances({})[id(self)]
|
||
|
|
||
|
def walk(self):
|
||
|
"""
|
||
|
Return an iterator that generates this feature structure, and
|
||
|
each feature structure it contains. Each feature structure will
|
||
|
be generated exactly once.
|
||
|
"""
|
||
|
return self._walk(set())
|
||
|
|
||
|
def _walk(self, visited):
|
||
|
"""
|
||
|
Return an iterator that generates this feature structure, and
|
||
|
each feature structure it contains.
|
||
|
|
||
|
:param visited: A set containing the ids of all feature
|
||
|
structures we've already visited while freezing.
|
||
|
"""
|
||
|
raise NotImplementedError() # Implemented by subclasses.
|
||
|
|
||
|
def _walk(self, visited):
|
||
|
if id(self) in visited: return
|
||
|
visited.add(id(self))
|
||
|
yield self
|
||
|
for fval in self._values():
|
||
|
if isinstance(fval, FeatStruct):
|
||
|
for elt in fval._walk(visited):
|
||
|
yield elt
|
||
|
|
||
|
# Walk through the feature tree. The first time we see a feature
|
||
|
# value, map it to False (not reentrant). If we see a feature
|
||
|
# value more than once, then map it to True (reentrant).
|
||
|
def _find_reentrances(self, reentrances):
|
||
|
"""
|
||
|
Return a dictionary that maps from the ``id`` of each feature
|
||
|
structure contained in ``self`` (including ``self``) to a
|
||
|
boolean value, indicating whether it is reentrant or not.
|
||
|
"""
|
||
|
if id(self) in reentrances:
|
||
|
# We've seen it more than once.
|
||
|
reentrances[id(self)] = True
|
||
|
else:
|
||
|
# This is the first time we've seen it.
|
||
|
reentrances[id(self)] = False
|
||
|
|
||
|
# Recurse to contained feature structures.
|
||
|
for fval in self._values():
|
||
|
if isinstance(fval, FeatStruct):
|
||
|
fval._find_reentrances(reentrances)
|
||
|
|
||
|
return reentrances
|
||
|
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
# { Variables & Bindings
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
|
||
|
def substitute_bindings(self, bindings):
|
||
|
""":see: ``nltk.featstruct.substitute_bindings()``"""
|
||
|
return substitute_bindings(self, bindings)
|
||
|
|
||
|
def retract_bindings(self, bindings):
|
||
|
""":see: ``nltk.featstruct.retract_bindings()``"""
|
||
|
return retract_bindings(self, bindings)
|
||
|
|
||
|
def variables(self):
|
||
|
""":see: ``nltk.featstruct.find_variables()``"""
|
||
|
return find_variables(self)
|
||
|
|
||
|
def rename_variables(self, vars=None, used_vars=(), new_vars=None):
|
||
|
""":see: ``nltk.featstruct.rename_variables()``"""
|
||
|
return rename_variables(self, vars, used_vars, new_vars)
|
||
|
|
||
|
def remove_variables(self):
|
||
|
"""
|
||
|
Return the feature structure that is obtained by deleting
|
||
|
any feature whose value is a ``Variable``.
|
||
|
|
||
|
:rtype: FeatStruct
|
||
|
"""
|
||
|
return remove_variables(self)
|
||
|
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
# { Unification
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
|
||
|
def unify(self, other, bindings=None, trace=False,
|
||
|
fail=None, rename_vars=True):
|
||
|
return unify(self, other, bindings, trace, fail, rename_vars)
|
||
|
|
||
|
def subsumes(self, other):
|
||
|
"""
|
||
|
Return True if ``self`` subsumes ``other``. I.e., return true
|
||
|
If unifying ``self`` with ``other`` would result in a feature
|
||
|
structure equal to ``other``.
|
||
|
"""
|
||
|
return subsumes(self, other)
|
||
|
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
# { String Representations
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
|
||
|
def __repr__(self):
|
||
|
"""
|
||
|
Display a single-line representation of this feature structure,
|
||
|
suitable for embedding in other representations.
|
||
|
"""
|
||
|
return self._repr(self._find_reentrances({}), {})
|
||
|
|
||
|
def _repr(self, reentrances, reentrance_ids):
|
||
|
"""
|
||
|
Return a string representation of this feature structure.
|
||
|
|
||
|
:param reentrances: A dictionary that maps from the ``id`` of
|
||
|
each feature value in self, indicating whether that value
|
||
|
is reentrant or not.
|
||
|
:param reentrance_ids: A dictionary mapping from each ``id``
|
||
|
of a feature value to a unique identifier. This is modified
|
||
|
by ``repr``: the first time a reentrant feature value is
|
||
|
displayed, an identifier is added to ``reentrance_ids`` for it.
|
||
|
"""
|
||
|
raise NotImplementedError()
|
||
|
|
||
|
|
||
|
# Mutation: disable if frozen.
|
||
|
_FROZEN_ERROR = "Frozen FeatStructs may not be modified."
|
||
|
_FROZEN_NOTICE = "\n%sIf self is frozen, raise ValueError."
|
||
|
|
||
|
|
||
|
def _check_frozen(method, indent=''):
|
||
|
"""
|
||
|
Given a method function, return a new method function that first
|
||
|
checks if ``self._frozen`` is true; and if so, raises ``ValueError``
|
||
|
with an appropriate message. Otherwise, call the method and return
|
||
|
its result.
|
||
|
"""
|
||
|
|
||
|
def wrapped(self, *args, **kwargs):
|
||
|
if self._frozen:
|
||
|
raise ValueError(_FROZEN_ERROR)
|
||
|
else:
|
||
|
return method(self, *args, **kwargs)
|
||
|
|
||
|
wrapped.__name__ = method.__name__
|
||
|
wrapped.__doc__ = (method.__doc__ or '') + (_FROZEN_NOTICE % indent)
|
||
|
return wrapped
|
||
|
|
||
|
|
||
|
######################################################################
|
||
|
# Feature Dictionary
|
||
|
######################################################################
|
||
|
|
||
|
@python_2_unicode_compatible
|
||
|
class FeatDict(FeatStruct, dict):
|
||
|
"""
|
||
|
A feature structure that acts like a Python dictionary. I.e., a
|
||
|
mapping from feature identifiers to feature values, where a feature
|
||
|
identifier can be a string or a ``Feature``; and where a feature value
|
||
|
can be either a basic value (such as a string or an integer), or a nested
|
||
|
feature structure. A feature identifiers for a ``FeatDict`` is
|
||
|
sometimes called a "feature name".
|
||
|
|
||
|
Two feature dicts are considered equal if they assign the same
|
||
|
values to all features, and have the same reentrances.
|
||
|
|
||
|
:see: ``FeatStruct`` for information about feature paths, reentrance,
|
||
|
cyclic feature structures, mutability, freezing, and hashing.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, features=None, **morefeatures):
|
||
|
"""
|
||
|
Create a new feature dictionary, with the specified features.
|
||
|
|
||
|
:param features: The initial value for this feature
|
||
|
dictionary. If ``features`` is a ``FeatStruct``, then its
|
||
|
features are copied (shallow copy). If ``features`` is a
|
||
|
dict, then a feature is created for each item, mapping its
|
||
|
key to its value. If ``features`` is a string, then it is
|
||
|
processed using ``FeatStructReader``. If ``features`` is a list of
|
||
|
tuples ``(name, val)``, then a feature is created for each tuple.
|
||
|
:param morefeatures: Additional features for the new feature
|
||
|
dictionary. If a feature is listed under both ``features`` and
|
||
|
``morefeatures``, then the value from ``morefeatures`` will be
|
||
|
used.
|
||
|
"""
|
||
|
if isinstance(features, string_types):
|
||
|
FeatStructReader().fromstring(features, self)
|
||
|
self.update(**morefeatures)
|
||
|
else:
|
||
|
# update() checks the types of features.
|
||
|
self.update(features, **morefeatures)
|
||
|
|
||
|
# ////////////////////////////////////////////////////////////
|
||
|
# { Dict methods
|
||
|
# ////////////////////////////////////////////////////////////
|
||
|
_INDEX_ERROR = str("Expected feature name or path. Got %r.")
|
||
|
|
||
|
def __getitem__(self, name_or_path):
|
||
|
"""If the feature with the given name or path exists, return
|
||
|
its value; otherwise, raise ``KeyError``."""
|
||
|
if isinstance(name_or_path, (string_types, Feature)):
|
||
|
return dict.__getitem__(self, name_or_path)
|
||
|
elif isinstance(name_or_path, tuple):
|
||
|
try:
|
||
|
val = self
|
||
|
for fid in name_or_path:
|
||
|
if not isinstance(val, FeatStruct):
|
||
|
raise KeyError # path contains base value
|
||
|
val = val[fid]
|
||
|
return val
|
||
|
except (KeyError, IndexError):
|
||
|
raise KeyError(name_or_path)
|
||
|
else:
|
||
|
raise TypeError(self._INDEX_ERROR % name_or_path)
|
||
|
|
||
|
def get(self, name_or_path, default=None):
|
||
|
"""If the feature with the given name or path exists, return its
|
||
|
value; otherwise, return ``default``."""
|
||
|
try:
|
||
|
return self[name_or_path]
|
||
|
except KeyError:
|
||
|
return default
|
||
|
|
||
|
def __contains__(self, name_or_path):
|
||
|
"""Return true if a feature with the given name or path exists."""
|
||
|
try:
|
||
|
self[name_or_path]; return True
|
||
|
except KeyError:
|
||
|
return False
|
||
|
|
||
|
def has_key(self, name_or_path):
|
||
|
"""Return true if a feature with the given name or path exists."""
|
||
|
return name_or_path in self
|
||
|
|
||
|
def __delitem__(self, name_or_path):
|
||
|
"""If the feature with the given name or path exists, delete
|
||
|
its value; otherwise, raise ``KeyError``."""
|
||
|
if self._frozen: raise ValueError(_FROZEN_ERROR)
|
||
|
if isinstance(name_or_path, (string_types, Feature)):
|
||
|
return dict.__delitem__(self, name_or_path)
|
||
|
elif isinstance(name_or_path, tuple):
|
||
|
if len(name_or_path) == 0:
|
||
|
raise ValueError("The path () can not be set")
|
||
|
else:
|
||
|
parent = self[name_or_path[:-1]]
|
||
|
if not isinstance(parent, FeatStruct):
|
||
|
raise KeyError(name_or_path) # path contains base value
|
||
|
del parent[name_or_path[-1]]
|
||
|
else:
|
||
|
raise TypeError(self._INDEX_ERROR % name_or_path)
|
||
|
|
||
|
def __setitem__(self, name_or_path, value):
|
||
|
"""Set the value for the feature with the given name or path
|
||
|
to ``value``. If ``name_or_path`` is an invalid path, raise
|
||
|
``KeyError``."""
|
||
|
if self._frozen: raise ValueError(_FROZEN_ERROR)
|
||
|
if isinstance(name_or_path, (string_types, Feature)):
|
||
|
return dict.__setitem__(self, name_or_path, value)
|
||
|
elif isinstance(name_or_path, tuple):
|
||
|
if len(name_or_path) == 0:
|
||
|
raise ValueError("The path () can not be set")
|
||
|
else:
|
||
|
parent = self[name_or_path[:-1]]
|
||
|
if not isinstance(parent, FeatStruct):
|
||
|
raise KeyError(name_or_path) # path contains base value
|
||
|
parent[name_or_path[-1]] = value
|
||
|
else:
|
||
|
raise TypeError(self._INDEX_ERROR % name_or_path)
|
||
|
|
||
|
clear = _check_frozen(dict.clear)
|
||
|
pop = _check_frozen(dict.pop)
|
||
|
popitem = _check_frozen(dict.popitem)
|
||
|
setdefault = _check_frozen(dict.setdefault)
|
||
|
|
||
|
def update(self, features=None, **morefeatures):
|
||
|
if self._frozen: raise ValueError(_FROZEN_ERROR)
|
||
|
if features is None:
|
||
|
items = ()
|
||
|
elif hasattr(features, 'items') and callable(features.items):
|
||
|
items = features.items()
|
||
|
elif hasattr(features, '__iter__'):
|
||
|
items = features
|
||
|
else:
|
||
|
raise ValueError('Expected mapping or list of tuples')
|
||
|
|
||
|
for key, val in items:
|
||
|
if not isinstance(key, (string_types, Feature)):
|
||
|
raise TypeError('Feature names must be strings')
|
||
|
self[key] = val
|
||
|
for key, val in morefeatures.items():
|
||
|
if not isinstance(key, (string_types, Feature)):
|
||
|
raise TypeError('Feature names must be strings')
|
||
|
self[key] = val
|
||
|
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
# { Copying
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
|
||
|
def __deepcopy__(self, memo):
|
||
|
memo[id(self)] = selfcopy = self.__class__()
|
||
|
for (key, val) in self._items():
|
||
|
selfcopy[copy.deepcopy(key, memo)] = copy.deepcopy(val, memo)
|
||
|
return selfcopy
|
||
|
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
# { Uniform Accessor Methods
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
|
||
|
def _keys(self):
|
||
|
return self.keys()
|
||
|
|
||
|
def _values(self):
|
||
|
return self.values()
|
||
|
|
||
|
def _items(self):
|
||
|
return self.items()
|
||
|
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
# { String Representations
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
|
||
|
def __str__(self):
|
||
|
"""
|
||
|
Display a multi-line representation of this feature dictionary
|
||
|
as an FVM (feature value matrix).
|
||
|
"""
|
||
|
return '\n'.join(self._str(self._find_reentrances({}), {}))
|
||
|
|
||
|
def _repr(self, reentrances, reentrance_ids):
|
||
|
segments = []
|
||
|
prefix = ''
|
||
|
suffix = ''
|
||
|
|
||
|
# If this is the first time we've seen a reentrant structure,
|
||
|
# then assign it a unique identifier.
|
||
|
if reentrances[id(self)]:
|
||
|
assert id(self) not in reentrance_ids
|
||
|
reentrance_ids[id(self)] = repr(len(reentrance_ids) + 1)
|
||
|
|
||
|
# sorting note: keys are unique strings, so we'll never fall
|
||
|
# through to comparing values.
|
||
|
for (fname, fval) in sorted(self.items()):
|
||
|
display = getattr(fname, 'display', None)
|
||
|
if id(fval) in reentrance_ids:
|
||
|
segments.append('%s->(%s)' %
|
||
|
(fname, reentrance_ids[id(fval)]))
|
||
|
elif (display == 'prefix' and not prefix and
|
||
|
isinstance(fval, (Variable, string_types))):
|
||
|
prefix = '%s' % fval
|
||
|
elif display == 'slash' and not suffix:
|
||
|
if isinstance(fval, Variable):
|
||
|
suffix = '/%s' % fval.name
|
||
|
else:
|
||
|
suffix = '/%s' % unicode_repr(fval)
|
||
|
elif isinstance(fval, Variable):
|
||
|
segments.append('%s=%s' % (fname, fval.name))
|
||
|
elif fval is True:
|
||
|
segments.append('+%s' % fname)
|
||
|
elif fval is False:
|
||
|
segments.append('-%s' % fname)
|
||
|
elif isinstance(fval, Expression):
|
||
|
segments.append('%s=<%s>' % (fname, fval))
|
||
|
elif not isinstance(fval, FeatStruct):
|
||
|
segments.append('%s=%s' % (fname, unicode_repr(fval)))
|
||
|
else:
|
||
|
fval_repr = fval._repr(reentrances, reentrance_ids)
|
||
|
segments.append('%s=%s' % (fname, fval_repr))
|
||
|
# If it's reentrant, then add on an identifier tag.
|
||
|
if reentrances[id(self)]:
|
||
|
prefix = '(%s)%s' % (reentrance_ids[id(self)], prefix)
|
||
|
return '%s[%s]%s' % (prefix, ', '.join(segments), suffix)
|
||
|
|
||
|
def _str(self, reentrances, reentrance_ids):
|
||
|
"""
|
||
|
:return: A list of lines composing a string representation of
|
||
|
this feature dictionary.
|
||
|
:param reentrances: A dictionary that maps from the ``id`` of
|
||
|
each feature value in self, indicating whether that value
|
||
|
is reentrant or not.
|
||
|
:param reentrance_ids: A dictionary mapping from each ``id``
|
||
|
of a feature value to a unique identifier. This is modified
|
||
|
by ``repr``: the first time a reentrant feature value is
|
||
|
displayed, an identifier is added to ``reentrance_ids`` for
|
||
|
it.
|
||
|
"""
|
||
|
# If this is the first time we've seen a reentrant structure,
|
||
|
# then tack on an id string.
|
||
|
if reentrances[id(self)]:
|
||
|
assert id(self) not in reentrance_ids
|
||
|
reentrance_ids[id(self)] = repr(len(reentrance_ids) + 1)
|
||
|
|
||
|
# Special case: empty feature dict.
|
||
|
if len(self) == 0:
|
||
|
if reentrances[id(self)]:
|
||
|
return ['(%s) []' % reentrance_ids[id(self)]]
|
||
|
else:
|
||
|
return ['[]']
|
||
|
|
||
|
# What's the longest feature name? Use this to align names.
|
||
|
maxfnamelen = max(len("%s" % k) for k in self.keys())
|
||
|
|
||
|
lines = []
|
||
|
# sorting note: keys are unique strings, so we'll never fall
|
||
|
# through to comparing values.
|
||
|
for (fname, fval) in sorted(self.items()):
|
||
|
fname = ("%s" % fname).ljust(maxfnamelen)
|
||
|
if isinstance(fval, Variable):
|
||
|
lines.append('%s = %s' % (fname, fval.name))
|
||
|
|
||
|
elif isinstance(fval, Expression):
|
||
|
lines.append('%s = <%s>' % (fname, fval))
|
||
|
|
||
|
elif isinstance(fval, FeatList):
|
||
|
fval_repr = fval._repr(reentrances, reentrance_ids)
|
||
|
lines.append('%s = %s' % (fname, unicode_repr(fval_repr)))
|
||
|
|
||
|
elif not isinstance(fval, FeatDict):
|
||
|
# It's not a nested feature structure -- just print it.
|
||
|
lines.append('%s = %s' % (fname, unicode_repr(fval)))
|
||
|
|
||
|
elif id(fval) in reentrance_ids:
|
||
|
# It's a feature structure we've seen before -- print
|
||
|
# the reentrance id.
|
||
|
lines.append('%s -> (%s)' % (fname, reentrance_ids[id(fval)]))
|
||
|
|
||
|
else:
|
||
|
# It's a new feature structure. Separate it from
|
||
|
# other values by a blank line.
|
||
|
if lines and lines[-1] != '': lines.append('')
|
||
|
|
||
|
# Recursively print the feature's value (fval).
|
||
|
fval_lines = fval._str(reentrances, reentrance_ids)
|
||
|
|
||
|
# Indent each line to make room for fname.
|
||
|
fval_lines = [(' ' * (maxfnamelen + 3)) + l for l in fval_lines]
|
||
|
|
||
|
# Pick which line we'll display fname on, & splice it in.
|
||
|
nameline = (len(fval_lines) - 1) // 2
|
||
|
fval_lines[nameline] = (
|
||
|
fname + ' =' + fval_lines[nameline][maxfnamelen + 2:])
|
||
|
|
||
|
# Add the feature structure to the output.
|
||
|
lines += fval_lines
|
||
|
|
||
|
# Separate FeatStructs by a blank line.
|
||
|
lines.append('')
|
||
|
|
||
|
# Get rid of any excess blank lines.
|
||
|
if lines[-1] == '': lines.pop()
|
||
|
|
||
|
# Add brackets around everything.
|
||
|
maxlen = max(len(line) for line in lines)
|
||
|
lines = ['[ %s%s ]' % (line, ' ' * (maxlen - len(line))) for line in lines]
|
||
|
|
||
|
# If it's reentrant, then add on an identifier tag.
|
||
|
if reentrances[id(self)]:
|
||
|
idstr = '(%s) ' % reentrance_ids[id(self)]
|
||
|
lines = [(' ' * len(idstr)) + l for l in lines]
|
||
|
idline = (len(lines) - 1) // 2
|
||
|
lines[idline] = idstr + lines[idline][len(idstr):]
|
||
|
|
||
|
return lines
|
||
|
|
||
|
|
||
|
######################################################################
|
||
|
# Feature List
|
||
|
######################################################################
|
||
|
|
||
|
class FeatList(FeatStruct, list):
|
||
|
"""
|
||
|
A list of feature values, where each feature value is either a
|
||
|
basic value (such as a string or an integer), or a nested feature
|
||
|
structure.
|
||
|
|
||
|
Feature lists may contain reentrant feature values. A "reentrant
|
||
|
feature value" is a single feature value that can be accessed via
|
||
|
multiple feature paths. Feature lists may also be cyclic.
|
||
|
|
||
|
Two feature lists are considered equal if they assign the same
|
||
|
values to all features, and have the same reentrances.
|
||
|
|
||
|
:see: ``FeatStruct`` for information about feature paths, reentrance,
|
||
|
cyclic feature structures, mutability, freezing, and hashing.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, features=()):
|
||
|
"""
|
||
|
Create a new feature list, with the specified features.
|
||
|
|
||
|
:param features: The initial list of features for this feature
|
||
|
list. If ``features`` is a string, then it is paresd using
|
||
|
``FeatStructReader``. Otherwise, it should be a sequence
|
||
|
of basic values and nested feature structures.
|
||
|
"""
|
||
|
if isinstance(features, string_types):
|
||
|
FeatStructReader().fromstring(features, self)
|
||
|
else:
|
||
|
list.__init__(self, features)
|
||
|
|
||
|
# ////////////////////////////////////////////////////////////
|
||
|
# { List methods
|
||
|
# ////////////////////////////////////////////////////////////
|
||
|
_INDEX_ERROR = "Expected int or feature path. Got %r."
|
||
|
|
||
|
def __getitem__(self, name_or_path):
|
||
|
if isinstance(name_or_path, integer_types):
|
||
|
return list.__getitem__(self, name_or_path)
|
||
|
elif isinstance(name_or_path, tuple):
|
||
|
try:
|
||
|
val = self
|
||
|
for fid in name_or_path:
|
||
|
if not isinstance(val, FeatStruct):
|
||
|
raise KeyError # path contains base value
|
||
|
val = val[fid]
|
||
|
return val
|
||
|
except (KeyError, IndexError):
|
||
|
raise KeyError(name_or_path)
|
||
|
else:
|
||
|
raise TypeError(self._INDEX_ERROR % name_or_path)
|
||
|
|
||
|
def __delitem__(self, name_or_path):
|
||
|
"""If the feature with the given name or path exists, delete
|
||
|
its value; otherwise, raise ``KeyError``."""
|
||
|
if self._frozen: raise ValueError(_FROZEN_ERROR)
|
||
|
if isinstance(name_or_path, (integer_types, slice)):
|
||
|
return list.__delitem__(self, name_or_path)
|
||
|
elif isinstance(name_or_path, tuple):
|
||
|
if len(name_or_path) == 0:
|
||
|
raise ValueError("The path () can not be set")
|
||
|
else:
|
||
|
parent = self[name_or_path[:-1]]
|
||
|
if not isinstance(parent, FeatStruct):
|
||
|
raise KeyError(name_or_path) # path contains base value
|
||
|
del parent[name_or_path[-1]]
|
||
|
else:
|
||
|
raise TypeError(self._INDEX_ERROR % name_or_path)
|
||
|
|
||
|
def __setitem__(self, name_or_path, value):
|
||
|
"""Set the value for the feature with the given name or path
|
||
|
to ``value``. If ``name_or_path`` is an invalid path, raise
|
||
|
``KeyError``."""
|
||
|
if self._frozen: raise ValueError(_FROZEN_ERROR)
|
||
|
if isinstance(name_or_path, (integer_types, slice)):
|
||
|
return list.__setitem__(self, name_or_path, value)
|
||
|
elif isinstance(name_or_path, tuple):
|
||
|
if len(name_or_path) == 0:
|
||
|
raise ValueError("The path () can not be set")
|
||
|
else:
|
||
|
parent = self[name_or_path[:-1]]
|
||
|
if not isinstance(parent, FeatStruct):
|
||
|
raise KeyError(name_or_path) # path contains base value
|
||
|
parent[name_or_path[-1]] = value
|
||
|
else:
|
||
|
raise TypeError(self._INDEX_ERROR % name_or_path)
|
||
|
|
||
|
# __delslice__ = _check_frozen(list.__delslice__, ' ')
|
||
|
# __setslice__ = _check_frozen(list.__setslice__, ' ')
|
||
|
__iadd__ = _check_frozen(list.__iadd__)
|
||
|
__imul__ = _check_frozen(list.__imul__)
|
||
|
append = _check_frozen(list.append)
|
||
|
extend = _check_frozen(list.extend)
|
||
|
insert = _check_frozen(list.insert)
|
||
|
pop = _check_frozen(list.pop)
|
||
|
remove = _check_frozen(list.remove)
|
||
|
reverse = _check_frozen(list.reverse)
|
||
|
sort = _check_frozen(list.sort)
|
||
|
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
# { Copying
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
|
||
|
def __deepcopy__(self, memo):
|
||
|
memo[id(self)] = selfcopy = self.__class__()
|
||
|
selfcopy.extend(copy.deepcopy(fval, memo) for fval in self)
|
||
|
return selfcopy
|
||
|
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
# { Uniform Accessor Methods
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
|
||
|
def _keys(self):
|
||
|
return list(range(len(self)))
|
||
|
|
||
|
def _values(self):
|
||
|
return self
|
||
|
|
||
|
def _items(self):
|
||
|
return enumerate(self)
|
||
|
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
# { String Representations
|
||
|
##////////////////////////////////////////////////////////////
|
||
|
|
||
|
# Special handling for: reentrances, variables, expressions.
|
||
|
def _repr(self, reentrances, reentrance_ids):
|
||
|
# If this is the first time we've seen a reentrant structure,
|
||
|
# then assign it a unique identifier.
|
||
|
if reentrances[id(self)]:
|
||
|
assert id(self) not in reentrance_ids
|
||
|
reentrance_ids[id(self)] = repr(len(reentrance_ids) + 1)
|
||
|
prefix = '(%s)' % reentrance_ids[id(self)]
|
||
|
else:
|
||
|
prefix = ''
|
||
|
|
||
|
segments = []
|
||
|
for fval in self:
|
||
|
if id(fval) in reentrance_ids:
|
||
|
segments.append('->(%s)' % reentrance_ids[id(fval)])
|
||
|
elif isinstance(fval, Variable):
|
||
|
segments.append(fval.name)
|
||
|
elif isinstance(fval, Expression):
|
||
|
segments.append('%s' % fval)
|
||
|
elif isinstance(fval, FeatStruct):
|
||
|
segments.append(fval._repr(reentrances, reentrance_ids))
|
||
|
else:
|
||
|
segments.append('%s' % unicode_repr(fval))
|
||
|
|
||
|
return '%s[%s]' % (prefix, ', '.join(segments))
|
||
|
|
||
|
|
||
|
######################################################################
|
||
|
# Variables & Bindings
|
||
|
######################################################################
|
||
|
|
||
|
def substitute_bindings(fstruct, bindings, fs_class='default'):
|
||
|
"""
|
||
|
Return the feature structure that is obtained by replacing each
|
||
|
variable bound by ``bindings`` with its binding. If a variable is
|
||
|
aliased to a bound variable, then it will be replaced by that
|
||
|
variable's value. If a variable is aliased to an unbound
|
||
|
variable, then it will be replaced by that variable.
|
||
|
|
||
|
:type bindings: dict(Variable -> any)
|
||
|
:param bindings: A dictionary mapping from variables to values.
|
||
|
"""
|
||
|
if fs_class == 'default': fs_class = _default_fs_class(fstruct)
|
||
|
fstruct = copy.deepcopy(fstruct)
|
||
|
_substitute_bindings(fstruct, bindings, fs_class, set())
|
||
|
return fstruct
|
||
|
|
||
|
|
||
|
def _substitute_bindings(fstruct, bindings, fs_class, visited):
|
||
|
# Visit each node only once:
|
||
|
if id(fstruct) in visited: return
|
||
|
visited.add(id(fstruct))
|
||
|
|
||
|
if _is_mapping(fstruct):
|
||
|
items = fstruct.items()
|
||
|
elif _is_sequence(fstruct):
|
||
|
items = enumerate(fstruct)
|
||
|
else:
|
||
|
raise ValueError('Expected mapping or sequence')
|
||
|
for (fname, fval) in items:
|
||
|
while (isinstance(fval, Variable) and fval in bindings):
|
||
|
fval = fstruct[fname] = bindings[fval]
|
||
|
if isinstance(fval, fs_class):
|
||
|
_substitute_bindings(fval, bindings, fs_class, visited)
|
||
|
elif isinstance(fval, SubstituteBindingsI):
|
||
|
fstruct[fname] = fval.substitute_bindings(bindings)
|
||
|
|
||
|
|
||
|
def retract_bindings(fstruct, bindings, fs_class='default'):
|
||
|
"""
|
||
|
Return the feature structure that is obtained by replacing each
|
||
|
feature structure value that is bound by ``bindings`` with the
|
||
|
variable that binds it. A feature structure value must be
|
||
|
identical to a bound value (i.e., have equal id) to be replaced.
|
||
|
|
||
|
``bindings`` is modified to point to this new feature structure,
|
||
|
rather than the original feature structure. Feature structure
|
||
|
values in ``bindings`` may be modified if they are contained in
|
||
|
``fstruct``.
|
||
|
"""
|
||
|
if fs_class == 'default': fs_class = _default_fs_class(fstruct)
|
||
|
(fstruct, new_bindings) = copy.deepcopy((fstruct, bindings))
|
||
|
bindings.update(new_bindings)
|
||
|
inv_bindings = dict((id(val), var) for (var, val) in bindings.items())
|
||
|
_retract_bindings(fstruct, inv_bindings, fs_class, set())
|
||
|
return fstruct
|
||
|
|
||
|
|
||
|
def _retract_bindings(fstruct, inv_bindings, fs_class, visited):
|
||
|
# Visit each node only once:
|
||
|
if id(fstruct) in visited: return
|
||
|
visited.add(id(fstruct))
|
||
|
|
||
|
if _is_mapping(fstruct):
|
||
|
items = fstruct.items()
|
||
|
elif _is_sequence(fstruct):
|
||
|
items = enumerate(fstruct)
|
||
|
else:
|
||
|
raise ValueError('Expected mapping or sequence')
|
||
|
for (fname, fval) in items:
|
||
|
if isinstance(fval, fs_class):
|
||
|
if id(fval) in inv_bindings:
|
||
|
fstruct[fname] = inv_bindings[id(fval)]
|
||
|
_retract_bindings(fval, inv_bindings, fs_class, visited)
|
||
|
|
||
|
|
||
|
def find_variables(fstruct, fs_class='default'):
|
||
|
"""
|
||
|
:return: The set of variables used by this feature structure.
|
||
|
:rtype: set(Variable)
|
||
|
"""
|
||
|
if fs_class == 'default': fs_class = _default_fs_class(fstruct)
|
||
|
return _variables(fstruct, set(), fs_class, set())
|
||
|
|
||
|
|
||
|
def _variables(fstruct, vars, fs_class, visited):
|
||
|
# Visit each node only once:
|
||
|
if id(fstruct) in visited: return
|
||
|
visited.add(id(fstruct))
|
||
|
if _is_mapping(fstruct):
|
||
|
items = fstruct.items()
|
||
|
elif _is_sequence(fstruct):
|
||
|
items = enumerate(fstruct)
|
||
|
else:
|
||
|
raise ValueError('Expected mapping or sequence')
|
||
|
for (fname, fval) in items:
|
||
|
if isinstance(fval, Variable):
|
||
|
vars.add(fval)
|
||
|
elif isinstance(fval, fs_class):
|
||
|
_variables(fval, vars, fs_class, visited)
|
||
|
elif isinstance(fval, SubstituteBindingsI):
|
||
|
vars.update(fval.variables())
|
||
|
return vars
|
||
|
|
||
|
|
||
|
def rename_variables(fstruct, vars=None, used_vars=(), new_vars=None,
|
||
|
fs_class='default'):
|
||
|
"""
|
||
|
Return the feature structure that is obtained by replacing
|
||
|
any of this feature structure's variables that are in ``vars``
|
||
|
with new variables. The names for these new variables will be
|
||
|
names that are not used by any variable in ``vars``, or in
|
||
|
``used_vars``, or in this feature structure.
|
||
|
|
||
|
:type vars: set
|
||
|
:param vars: The set of variables that should be renamed.
|
||
|
If not specified, ``find_variables(fstruct)`` is used; i.e., all
|
||
|
variables will be given new names.
|
||
|
:type used_vars: set
|
||
|
:param used_vars: A set of variables whose names should not be
|
||
|
used by the new variables.
|
||
|
:type new_vars: dict(Variable -> Variable)
|
||
|
:param new_vars: A dictionary that is used to hold the mapping
|
||
|
from old variables to new variables. For each variable *v*
|
||
|
in this feature structure:
|
||
|
|
||
|
- If ``new_vars`` maps *v* to *v'*, then *v* will be
|
||
|
replaced by *v'*.
|
||
|
- If ``new_vars`` does not contain *v*, but ``vars``
|
||
|
does contain *v*, then a new entry will be added to
|
||
|
``new_vars``, mapping *v* to the new variable that is used
|
||
|
to replace it.
|
||
|
|
||
|
To consistently rename the variables in a set of feature
|
||
|
structures, simply apply rename_variables to each one, using
|
||
|
the same dictionary:
|
||
|
|
||
|
>>> from nltk.featstruct import FeatStruct
|
||
|
>>> fstruct1 = FeatStruct('[subj=[agr=[gender=?y]], obj=[agr=[gender=?y]]]')
|
||
|
>>> fstruct2 = FeatStruct('[subj=[agr=[number=?z,gender=?y]], obj=[agr=[number=?z,gender=?y]]]')
|
||
|
>>> new_vars = {} # Maps old vars to alpha-renamed vars
|
||
|
>>> fstruct1.rename_variables(new_vars=new_vars)
|
||
|
[obj=[agr=[gender=?y2]], subj=[agr=[gender=?y2]]]
|
||
|
>>> fstruct2.rename_variables(new_vars=new_vars)
|
||
|
[obj=[agr=[gender=?y2, number=?z2]], subj=[agr=[gender=?y2, number=?z2]]]
|
||
|
|
||
|
If new_vars is not specified, then an empty dictionary is used.
|
||
|
"""
|
||
|
if fs_class == 'default': fs_class = _default_fs_class(fstruct)
|
||
|
|
||
|
# Default values:
|
||
|
if new_vars is None: new_vars = {}
|
||
|
if vars is None:
|
||
|
vars = find_variables(fstruct, fs_class)
|
||
|
else:
|
||
|
vars = set(vars)
|
||
|
|
||
|
# Add our own variables to used_vars.
|
||
|
used_vars = find_variables(fstruct, fs_class).union(used_vars)
|
||
|
|
||
|
# Copy ourselves, and rename variables in the copy.
|
||
|
return _rename_variables(copy.deepcopy(fstruct), vars, used_vars,
|
||
|
new_vars, fs_class, set())
|
||
|
|
||
|
|
||
|
def _rename_variables(fstruct, vars, used_vars, new_vars, fs_class, visited):
|
||
|
if id(fstruct) in visited: return
|
||
|
visited.add(id(fstruct))
|
||
|
if _is_mapping(fstruct):
|
||
|
items = fstruct.items()
|
||
|
elif _is_sequence(fstruct):
|
||
|
items = enumerate(fstruct)
|
||
|
else:
|
||
|
raise ValueError('Expected mapping or sequence')
|
||
|
for (fname, fval) in items:
|
||
|
if isinstance(fval, Variable):
|
||
|
# If it's in new_vars, then rebind it.
|
||
|
if fval in new_vars:
|
||
|
fstruct[fname] = new_vars[fval]
|
||
|
# If it's in vars, pick a new name for it.
|
||
|
elif fval in vars:
|
||
|
new_vars[fval] = _rename_variable(fval, used_vars)
|
||
|
fstruct[fname] = new_vars[fval]
|
||
|
used_vars.add(new_vars[fval])
|
||
|
elif isinstance(fval, fs_class):
|
||
|
_rename_variables(fval, vars, used_vars, new_vars,
|
||
|
fs_class, visited)
|
||
|
elif isinstance(fval, SubstituteBindingsI):
|
||
|
# Pick new names for any variables in `vars`
|
||
|
for var in fval.variables():
|
||
|
if var in vars and var not in new_vars:
|
||
|
new_vars[var] = _rename_variable(var, used_vars)
|
||
|
used_vars.add(new_vars[var])
|
||
|
# Replace all variables in `new_vars`.
|
||
|
fstruct[fname] = fval.substitute_bindings(new_vars)
|
||
|
return fstruct
|
||
|
|
||
|
|
||
|
def _rename_variable(var, used_vars):
|
||
|
name, n = re.sub('\d+$', '', var.name), 2
|
||
|
if not name: name = '?'
|
||
|
while Variable('%s%s' % (name, n)) in used_vars: n += 1
|
||
|
return Variable('%s%s' % (name, n))
|
||
|
|
||
|
|
||
|
def remove_variables(fstruct, fs_class='default'):
|
||
|
"""
|
||
|
:rtype: FeatStruct
|
||
|
:return: The feature structure that is obtained by deleting
|
||
|
all features whose values are ``Variables``.
|
||
|
"""
|
||
|
if fs_class == 'default': fs_class = _default_fs_class(fstruct)
|
||
|
return _remove_variables(copy.deepcopy(fstruct), fs_class, set())
|
||
|
|
||
|
|
||
|
def _remove_variables(fstruct, fs_class, visited):
|
||
|
if id(fstruct) in visited:
|
||
|
return
|
||
|
visited.add(id(fstruct))
|
||
|
|
||
|
if _is_mapping(fstruct):
|
||
|
items = list(fstruct.items())
|
||
|
elif _is_sequence(fstruct):
|
||
|
items = list(enumerate(fstruct))
|
||
|
else:
|
||
|
raise ValueError('Expected mapping or sequence')
|
||
|
|
||
|
for (fname, fval) in items:
|
||
|
if isinstance(fval, Variable):
|
||
|
del fstruct[fname]
|
||
|
elif isinstance(fval, fs_class):
|
||
|
_remove_variables(fval, fs_class, visited)
|
||
|
return fstruct
|
||
|
|
||
|
|
||
|
######################################################################
|
||
|
# Unification
|
||
|
######################################################################
|
||
|
|
||
|
@python_2_unicode_compatible
|
||
|
class _UnificationFailure(object):
|
||
|
def __repr__(self):
|
||
|
return 'nltk.featstruct.UnificationFailure'
|
||
|
|
||
|
|
||
|
UnificationFailure = _UnificationFailure()
|
||
|
"""A unique value used to indicate unification failure. It can be
|
||
|
returned by ``Feature.unify_base_values()`` or by custom ``fail()``
|
||
|
functions to indicate that unificaiton should fail."""
|
||
|
|
||
|
|
||
|
# The basic unification algorithm:
|
||
|
# 1. Make copies of self and other (preserving reentrance)
|
||
|
# 2. Destructively unify self and other
|
||
|
# 3. Apply forward pointers, to preserve reentrance.
|
||
|
# 4. Replace bound variables with their values.
|
||
|
def unify(fstruct1, fstruct2, bindings=None, trace=False,
|
||
|
fail=None, rename_vars=True, fs_class='default'):
|
||
|
"""
|
||
|
Unify ``fstruct1`` with ``fstruct2``, and return the resulting feature
|
||
|
structure. This unified feature structure is the minimal
|
||
|
feature structure that contains all feature value assignments from both
|
||
|
``fstruct1`` and ``fstruct2``, and that preserves all reentrancies.
|
||
|
|
||
|
If no such feature structure exists (because ``fstruct1`` and
|
||
|
``fstruct2`` specify incompatible values for some feature), then
|
||
|
unification fails, and ``unify`` returns None.
|
||
|
|
||
|
Bound variables are replaced by their values. Aliased
|
||
|
variables are replaced by their representative variable
|
||
|
(if unbound) or the value of their representative variable
|
||
|
(if bound). I.e., if variable *v* is in ``bindings``,
|
||
|
then *v* is replaced by ``bindings[v]``. This will
|
||
|
be repeated until the variable is replaced by an unbound
|
||
|
variable or a non-variable value.
|
||
|
|
||
|
Unbound variables are bound when they are unified with
|
||
|
values; and aliased when they are unified with variables.
|
||
|
I.e., if variable *v* is not in ``bindings``, and is
|
||
|
unified with a variable or value *x*, then
|
||
|
``bindings[v]`` is set to *x*.
|
||
|
|
||
|
If ``bindings`` is unspecified, then all variables are
|
||
|
assumed to be unbound. I.e., ``bindings`` defaults to an
|
||
|
empty dict.
|
||
|
|
||
|
>>> from nltk.featstruct import FeatStruct
|
||
|
>>> FeatStruct('[a=?x]').unify(FeatStruct('[b=?x]'))
|
||
|
[a=?x, b=?x2]
|
||
|
|
||
|
:type bindings: dict(Variable -> any)
|
||
|
:param bindings: A set of variable bindings to be used and
|
||
|
updated during unification.
|
||
|
:type trace: bool
|
||
|
:param trace: If true, generate trace output.
|
||
|
:type rename_vars: bool
|
||
|
:param rename_vars: If True, then rename any variables in
|
||
|
``fstruct2`` that are also used in ``fstruct1``, in order to
|
||
|
avoid collisions on variable names.
|
||
|
"""
|
||
|
# Decide which class(es) will be treated as feature structures,
|
||
|
# for the purposes of unification.
|
||
|
if fs_class == 'default':
|
||
|
fs_class = _default_fs_class(fstruct1)
|
||
|
if _default_fs_class(fstruct2) != fs_class:
|
||
|
raise ValueError("Mixing FeatStruct objects with Python "
|
||
|
"dicts and lists is not supported.")
|
||
|
assert isinstance(fstruct1, fs_class)
|
||
|
assert isinstance(fstruct2, fs_class)
|
||
|
|
||
|
# If bindings are unspecified, use an empty set of bindings.
|
||
|
user_bindings = (bindings is not None)
|
||
|
if bindings is None: bindings = {}
|
||
|
|
||
|
# Make copies of fstruct1 and fstruct2 (since the unification
|
||
|
# algorithm is destructive). Do it all at once, to preserve
|
||
|
# reentrance links between fstruct1 and fstruct2. Copy bindings
|
||
|
# as well, in case there are any bound vars that contain parts
|
||
|
# of fstruct1 or fstruct2.
|
||
|
(fstruct1copy, fstruct2copy, bindings_copy) = (
|
||
|
copy.deepcopy((fstruct1, fstruct2, bindings)))
|
||
|
|
||
|
# Copy the bindings back to the original bindings dict.
|
||
|
bindings.update(bindings_copy)
|
||
|
|
||
|
if rename_vars:
|
||
|
vars1 = find_variables(fstruct1copy, fs_class)
|
||
|
vars2 = find_variables(fstruct2copy, fs_class)
|
||
|
_rename_variables(fstruct2copy, vars1, vars2, {}, fs_class, set())
|
||
|
|
||
|
# Do the actual unification. If it fails, return None.
|
||
|
forward = {}
|
||
|
if trace: _trace_unify_start((), fstruct1copy, fstruct2copy)
|
||
|
try:
|
||
|
result = _destructively_unify(fstruct1copy, fstruct2copy, bindings,
|
||
|
forward, trace, fail, fs_class, ())
|
||
|
except _UnificationFailureError:
|
||
|
return None
|
||
|
|
||
|
# _destructively_unify might return UnificationFailure, e.g. if we
|
||
|
# tried to unify a mapping with a sequence.
|
||
|
if result is UnificationFailure:
|
||
|
if fail is None:
|
||
|
return None
|
||
|
else:
|
||
|
return fail(fstruct1copy, fstruct2copy, ())
|
||
|
|
||
|
# Replace any feature structure that has a forward pointer
|
||
|
# with the target of its forward pointer.
|
||
|
result = _apply_forwards(result, forward, fs_class, set())
|
||
|
if user_bindings: _apply_forwards_to_bindings(forward, bindings)
|
||
|
|
||
|
# Replace bound vars with values.
|
||
|
_resolve_aliases(bindings)
|
||
|
_substitute_bindings(result, bindings, fs_class, set())
|
||
|
|
||
|
# Return the result.
|
||
|
if trace: _trace_unify_succeed((), result)
|
||
|
if trace: _trace_bindings((), bindings)
|
||
|
return result
|
||
|
|
||
|
|
||
|
class _UnificationFailureError(Exception):
|
||
|
"""An exception that is used by ``_destructively_unify`` to abort
|
||
|
unification when a failure is encountered."""
|
||
|
|
||
|
|
||
|
def _destructively_unify(fstruct1, fstruct2, bindings, forward,
|
||
|
trace, fail, fs_class, path):
|
||
|
"""
|
||
|
Attempt to unify ``fstruct1`` and ``fstruct2`` by modifying them
|
||
|
in-place. If the unification succeeds, then ``fstruct1`` will
|
||
|
contain the unified value, the value of ``fstruct2`` is undefined,
|
||
|
and forward[id(fstruct2)] is set to fstruct1. If the unification
|
||
|
fails, then a _UnificationFailureError is raised, and the
|
||
|
values of ``fstruct1`` and ``fstruct2`` are undefined.
|
||
|
|
||
|
:param bindings: A dictionary mapping variables to values.
|
||
|
:param forward: A dictionary mapping feature structures ids
|
||
|
to replacement structures. When two feature structures
|
||
|
are merged, a mapping from one to the other will be added
|
||
|
to the forward dictionary; and changes will be made only
|
||
|
to the target of the forward dictionary.
|
||
|
``_destructively_unify`` will always 'follow' any links
|
||
|
in the forward dictionary for fstruct1 and fstruct2 before
|
||
|
actually unifying them.
|
||
|
:param trace: If true, generate trace output
|
||
|
:param path: The feature path that led us to this unification
|
||
|
step. Used for trace output.
|
||
|
"""
|
||
|
# If fstruct1 is already identical to fstruct2, we're done.
|
||
|
# Note: this, together with the forward pointers, ensures
|
||
|
# that unification will terminate even for cyclic structures.
|
||
|
if fstruct1 is fstruct2:
|
||
|
if trace: _trace_unify_identity(path, fstruct1)
|
||
|
return fstruct1
|
||
|
|
||
|
# Set fstruct2's forward pointer to point to fstruct1; this makes
|
||
|
# fstruct1 the canonical copy for fstruct2. Note that we need to
|
||
|
# do this before we recurse into any child structures, in case
|
||
|
# they're cyclic.
|
||
|
forward[id(fstruct2)] = fstruct1
|
||
|
|
||
|
# Unifying two mappings:
|
||
|
if _is_mapping(fstruct1) and _is_mapping(fstruct2):
|
||
|
for fname in fstruct1:
|
||
|
if getattr(fname, 'default', None) is not None:
|
||
|
fstruct2.setdefault(fname, fname.default)
|
||
|
for fname in fstruct2:
|
||
|
if getattr(fname, 'default', None) is not None:
|
||
|
fstruct1.setdefault(fname, fname.default)
|
||
|
|
||
|
# Unify any values that are defined in both fstruct1 and
|
||
|
# fstruct2. Copy any values that are defined in fstruct2 but
|
||
|
# not in fstruct1 to fstruct1. Note: sorting fstruct2's
|
||
|
# features isn't actually necessary; but we do it to give
|
||
|
# deterministic behavior, e.g. for tracing.
|
||
|
for fname, fval2 in sorted(fstruct2.items()):
|
||
|
if fname in fstruct1:
|
||
|
fstruct1[fname] = _unify_feature_values(
|
||
|
fname, fstruct1[fname], fval2, bindings,
|
||
|
forward, trace, fail, fs_class, path + (fname,))
|
||
|
else:
|
||
|
fstruct1[fname] = fval2
|
||
|
|
||
|
return fstruct1 # Contains the unified value.
|
||
|
|
||
|
# Unifying two sequences:
|
||
|
elif _is_sequence(fstruct1) and _is_sequence(fstruct2):
|
||
|
# If the lengths don't match, fail.
|
||
|
if len(fstruct1) != len(fstruct2):
|
||
|
return UnificationFailure
|
||
|
|
||
|
# Unify corresponding values in fstruct1 and fstruct2.
|
||
|
for findex in range(len(fstruct1)):
|
||
|
fstruct1[findex] = _unify_feature_values(
|
||
|
findex, fstruct1[findex], fstruct2[findex], bindings,
|
||
|
forward, trace, fail, fs_class, path + (findex,))
|
||
|
|
||
|
return fstruct1 # Contains the unified value.
|
||
|
|
||
|
# Unifying sequence & mapping: fail. The failure function
|
||
|
# doesn't get a chance to recover in this case.
|
||
|
elif ((_is_sequence(fstruct1) or _is_mapping(fstruct1)) and
|
||
|
(_is_sequence(fstruct2) or _is_mapping(fstruct2))):
|
||
|
return UnificationFailure
|
||
|
|
||
|
# Unifying anything else: not allowed!
|
||
|
raise TypeError('Expected mappings or sequences')
|
||
|
|
||
|
|
||
|
def _unify_feature_values(fname, fval1, fval2, bindings, forward,
|
||
|
trace, fail, fs_class, fpath):
|
||
|
"""
|
||
|
Attempt to unify ``fval1`` and and ``fval2``, and return the
|
||
|
resulting unified value. The method of unification will depend on
|
||
|
the types of ``fval1`` and ``fval2``:
|
||
|
|
||
|
1. If they're both feature structures, then destructively
|
||
|
unify them (see ``_destructively_unify()``.
|
||
|
2. If they're both unbound variables, then alias one variable
|
||
|
to the other (by setting bindings[v2]=v1).
|
||
|
3. If one is an unbound variable, and the other is a value,
|
||
|
then bind the unbound variable to the value.
|
||
|
4. If one is a feature structure, and the other is a base value,
|
||
|
then fail.
|
||
|
5. If they're both base values, then unify them. By default,
|
||
|
this will succeed if they are equal, and fail otherwise.
|
||
|
"""
|
||
|
if trace: _trace_unify_start(fpath, fval1, fval2)
|
||
|
|
||
|
# Look up the "canonical" copy of fval1 and fval2
|
||
|
while id(fval1) in forward: fval1 = forward[id(fval1)]
|
||
|
while id(fval2) in forward: fval2 = forward[id(fval2)]
|
||
|
|
||
|
# If fval1 or fval2 is a bound variable, then
|
||
|
# replace it by the variable's bound value. This
|
||
|
# includes aliased variables, which are encoded as
|
||
|
# variables bound to other variables.
|
||
|
fvar1 = fvar2 = None
|
||
|
while isinstance(fval1, Variable) and fval1 in bindings:
|
||
|
fvar1 = fval1
|
||
|
fval1 = bindings[fval1]
|
||
|
while isinstance(fval2, Variable) and fval2 in bindings:
|
||
|
fvar2 = fval2
|
||
|
fval2 = bindings[fval2]
|
||
|
|
||
|
# Case 1: Two feature structures (recursive case)
|
||
|
if isinstance(fval1, fs_class) and isinstance(fval2, fs_class):
|
||
|
result = _destructively_unify(fval1, fval2, bindings, forward,
|
||
|
trace, fail, fs_class, fpath)
|
||
|
|
||
|
# Case 2: Two unbound variables (create alias)
|
||
|
elif (isinstance(fval1, Variable) and
|
||
|
isinstance(fval2, Variable)):
|
||
|
if fval1 != fval2: bindings[fval2] = fval1
|
||
|
result = fval1
|
||
|
|
||
|
# Case 3: An unbound variable and a value (bind)
|
||
|
elif isinstance(fval1, Variable):
|
||
|
bindings[fval1] = fval2
|
||
|
result = fval1
|
||
|
elif isinstance(fval2, Variable):
|
||
|
bindings[fval2] = fval1
|
||
|
result = fval2
|
||
|
|
||
|
# Case 4: A feature structure & a base value (fail)
|
||
|
elif isinstance(fval1, fs_class) or isinstance(fval2, fs_class):
|
||
|
result = UnificationFailure
|
||
|
|
||
|
# Case 5: Two base values
|
||
|
else:
|
||
|
# Case 5a: Feature defines a custom unification method for base values
|
||
|
if isinstance(fname, Feature):
|
||
|
result = fname.unify_base_values(fval1, fval2, bindings)
|
||
|
# Case 5b: Feature value defines custom unification method
|
||
|
elif isinstance(fval1, CustomFeatureValue):
|
||
|
result = fval1.unify(fval2)
|
||
|
# Sanity check: unify value should be symmetric
|
||
|
if (isinstance(fval2, CustomFeatureValue) and
|
||
|
result != fval2.unify(fval1)):
|
||
|
raise AssertionError(
|
||
|
'CustomFeatureValue objects %r and %r disagree '
|
||
|
'about unification value: %r vs. %r' %
|
||
|
(fval1, fval2, result, fval2.unify(fval1)))
|
||
|
elif isinstance(fval2, CustomFeatureValue):
|
||
|
result = fval2.unify(fval1)
|
||
|
# Case 5c: Simple values -- check if they're equal.
|
||
|
else:
|
||
|
if fval1 == fval2:
|
||
|
result = fval1
|
||
|
else:
|
||
|
result = UnificationFailure
|
||
|
|
||
|
# If either value was a bound variable, then update the
|
||
|
# bindings. (This is really only necessary if fname is a
|
||
|
# Feature or if either value is a CustomFeatureValue.)
|
||
|
if result is not UnificationFailure:
|
||
|
if fvar1 is not None:
|
||
|
bindings[fvar1] = result
|
||
|
result = fvar1
|
||
|
if fvar2 is not None and fvar2 != fvar1:
|
||
|
bindings[fvar2] = result
|
||
|
result = fvar2
|
||
|
|
||
|
# If we unification failed, call the failure function; it
|
||
|
# might decide to continue anyway.
|
||
|
if result is UnificationFailure:
|
||
|
if fail is not None: result = fail(fval1, fval2, fpath)
|
||
|
if trace: _trace_unify_fail(fpath[:-1], result)
|
||
|
if result is UnificationFailure:
|
||
|
raise _UnificationFailureError
|
||
|
|
||
|
# Normalize the result.
|
||
|
if isinstance(result, fs_class):
|
||
|
result = _apply_forwards(result, forward, fs_class, set())
|
||
|
|
||
|
if trace: _trace_unify_succeed(fpath, result)
|
||
|
if trace and isinstance(result, fs_class):
|
||
|
_trace_bindings(fpath, bindings)
|
||
|
|
||
|
return result
|
||
|
|
||
|
|
||
|
def _apply_forwards_to_bindings(forward, bindings):
|
||
|
"""
|
||
|
Replace any feature structure that has a forward pointer with
|
||
|
the target of its forward pointer (to preserve reentrancy).
|
||
|
"""
|
||
|
for (var, value) in bindings.items():
|
||
|
while id(value) in forward:
|
||
|
value = forward[id(value)]
|
||
|
bindings[var] = value
|
||
|
|
||
|
|
||
|
def _apply_forwards(fstruct, forward, fs_class, visited):
|
||
|
"""
|
||
|
Replace any feature structure that has a forward pointer with
|
||
|
the target of its forward pointer (to preserve reentrancy).
|
||
|
"""
|
||
|
# Follow our own forwards pointers (if any)
|
||
|
while id(fstruct) in forward: fstruct = forward[id(fstruct)]
|
||
|
|
||
|
# Visit each node only once:
|
||
|
if id(fstruct) in visited: return
|
||
|
visited.add(id(fstruct))
|
||
|
|
||
|
if _is_mapping(fstruct):
|
||
|
items = fstruct.items()
|
||
|
elif _is_sequence(fstruct):
|
||
|
items = enumerate(fstruct)
|
||
|
else:
|
||
|
raise ValueError('Expected mapping or sequence')
|
||
|
for fname, fval in items:
|
||
|
if isinstance(fval, fs_class):
|
||
|
# Replace w/ forwarded value.
|
||
|
while id(fval) in forward:
|
||
|
fval = forward[id(fval)]
|
||
|
fstruct[fname] = fval
|
||
|
# Recurse to child.
|
||
|
_apply_forwards(fval, forward, fs_class, visited)
|
||
|
|
||
|
return fstruct
|
||
|
|
||
|
|
||
|
def _resolve_aliases(bindings):
|
||
|
"""
|
||
|
Replace any bound aliased vars with their binding; and replace
|
||
|
any unbound aliased vars with their representative var.
|
||
|
"""
|
||
|
for (var, value) in bindings.items():
|
||
|
while isinstance(value, Variable) and value in bindings:
|
||
|
value = bindings[var] = bindings[value]
|
||
|
|
||
|
|
||
|
def _trace_unify_start(path, fval1, fval2):
|
||
|
if path == ():
|
||
|
print('\nUnification trace:')
|
||
|
else:
|
||
|
fullname = '.'.join("%s" % n for n in path)
|
||
|
print(' ' + '| ' * (len(path) - 1) + '|')
|
||
|
print(' ' + '| ' * (len(path) - 1) + '| Unify feature: %s' % fullname)
|
||
|
print(' ' + '| ' * len(path) + ' / ' + _trace_valrepr(fval1))
|
||
|
print(' ' + '| ' * len(path) + '|\\ ' + _trace_valrepr(fval2))
|
||
|
|
||
|
|
||
|
def _trace_unify_identity(path, fval1):
|
||
|
print(' ' + '| ' * len(path) + '|')
|
||
|
print(' ' + '| ' * len(path) + '| (identical objects)')
|
||
|
print(' ' + '| ' * len(path) + '|')
|
||
|
print(' ' + '| ' * len(path) + '+-->' + unicode_repr(fval1))
|
||
|
|
||
|
|
||
|
def _trace_unify_fail(path, result):
|
||
|
if result is UnificationFailure:
|
||
|
resume = ''
|
||
|
else:
|
||
|
resume = ' (nonfatal)'
|
||
|
print(' ' + '| ' * len(path) + '| |')
|
||
|
print(' ' + 'X ' * len(path) + 'X X <-- FAIL' + resume)
|
||
|
|
||
|
|
||
|
def _trace_unify_succeed(path, fval1):
|
||
|
# Print the result.
|
||
|
print(' ' + '| ' * len(path) + '|')
|
||
|
print(' ' + '| ' * len(path) + '+-->' + unicode_repr(fval1))
|
||
|
|
||
|
|
||
|
def _trace_bindings(path, bindings):
|
||
|
# Print the bindings (if any).
|
||
|
if len(bindings) > 0:
|
||
|
binditems = sorted(bindings.items(), key=lambda v: v[0].name)
|
||
|
bindstr = '{%s}' % ', '.join(
|
||
|
'%s: %s' % (var, _trace_valrepr(val))
|
||
|
for (var, val) in binditems)
|
||
|
print(' ' + '| ' * len(path) + ' Bindings: ' + bindstr)
|
||
|
|
||
|
|
||
|
def _trace_valrepr(val):
|
||
|
if isinstance(val, Variable):
|
||
|
return '%s' % val
|
||
|
else:
|
||
|
return '%s' % unicode_repr(val)
|
||
|
|
||
|
|
||
|
def subsumes(fstruct1, fstruct2):
|
||
|
"""
|
||
|
Return True if ``fstruct1`` subsumes ``fstruct2``. I.e., return
|
||
|
true if unifying ``fstruct1`` with ``fstruct2`` would result in a
|
||
|
feature structure equal to ``fstruct2.``
|
||
|
|
||
|
:rtype: bool
|
||
|
"""
|
||
|
return fstruct2 == unify(fstruct1, fstruct2)
|
||
|
|
||
|
|
||
|
def conflicts(fstruct1, fstruct2, trace=0):
|
||
|
"""
|
||
|
Return a list of the feature paths of all features which are
|
||
|
assigned incompatible values by ``fstruct1`` and ``fstruct2``.
|
||
|
|
||
|
:rtype: list(tuple)
|
||
|
"""
|
||
|
conflict_list = []
|
||
|
|
||
|
def add_conflict(fval1, fval2, path):
|
||
|
conflict_list.append(path)
|
||
|
return fval1
|
||
|
|
||
|
unify(fstruct1, fstruct2, fail=add_conflict, trace=trace)
|
||
|
return conflict_list
|
||
|
|
||
|
|
||
|
######################################################################
|
||
|
# Helper Functions
|
||
|
######################################################################
|
||
|
|
||
|
def _is_mapping(v):
|
||
|
return hasattr(v, '__contains__') and hasattr(v, 'keys')
|
||
|
|
||
|
|
||
|
def _is_sequence(v):
|
||
|
return (hasattr(v, '__iter__') and hasattr(v, '__len__') and
|
||
|
not isinstance(v, string_types))
|
||
|
|
||
|
|
||
|
def _default_fs_class(obj):
|
||
|
if isinstance(obj, FeatStruct): return FeatStruct
|
||
|
if isinstance(obj, (dict, list)):
|
||
|
return (dict, list)
|
||
|
else:
|
||
|
raise ValueError('To unify objects of type %s, you must specify '
|
||
|
'fs_class explicitly.' % obj.__class__.__name__)
|
||
|
|
||
|
|
||
|
######################################################################
|
||
|
# FeatureValueSet & FeatureValueTuple
|
||
|
######################################################################
|
||
|
|
||
|
class SubstituteBindingsSequence(SubstituteBindingsI):
|
||
|
"""
|
||
|
A mixin class for sequence clases that distributes variables() and
|
||
|
substitute_bindings() over the object's elements.
|
||
|
"""
|
||
|
|
||
|
def variables(self):
|
||
|
return ([elt for elt in self if isinstance(elt, Variable)] +
|
||
|
sum([list(elt.variables()) for elt in self
|
||
|
if isinstance(elt, SubstituteBindingsI)], []))
|
||
|
|
||
|
def substitute_bindings(self, bindings):
|
||
|
return self.__class__([self.subst(v, bindings) for v in self])
|
||
|
|
||
|
def subst(self, v, bindings):
|
||
|
if isinstance(v, SubstituteBindingsI):
|
||
|
return v.substitute_bindings(bindings)
|
||
|
else:
|
||
|
return bindings.get(v, v)
|
||
|
|
||
|
|
||
|
@python_2_unicode_compatible
|
||
|
class FeatureValueTuple(SubstituteBindingsSequence, tuple):
|
||
|
"""
|
||
|
A base feature value that is a tuple of other base feature values.
|
||
|
FeatureValueTuple implements ``SubstituteBindingsI``, so it any
|
||
|
variable substitutions will be propagated to the elements
|
||
|
contained by the set. A ``FeatureValueTuple`` is immutable.
|
||
|
"""
|
||
|
|
||
|
def __repr__(self): # [xx] really use %s here?
|
||
|
if len(self) == 0: return '()'
|
||
|
return '(%s)' % ', '.join('%s' % (b,) for b in self)
|
||
|
|
||
|
|
||
|
@python_2_unicode_compatible
|
||
|
class FeatureValueSet(SubstituteBindingsSequence, frozenset):
|
||
|
"""
|
||
|
A base feature value that is a set of other base feature values.
|
||
|
FeatureValueSet implements ``SubstituteBindingsI``, so it any
|
||
|
variable substitutions will be propagated to the elements
|
||
|
contained by the set. A ``FeatureValueSet`` is immutable.
|
||
|
"""
|
||
|
|
||
|
def __repr__(self): # [xx] really use %s here?
|
||
|
if len(self) == 0: return '{/}' # distinguish from dict.
|
||
|
# n.b., we sort the string reprs of our elements, to ensure
|
||
|
# that our own repr is deterministic.
|
||
|
return '{%s}' % ', '.join(sorted('%s' % (b,) for b in self))
|
||
|
|
||
|
__str__ = __repr__
|
||
|
|
||
|
|
||
|
@python_2_unicode_compatible
|
||
|
class FeatureValueUnion(SubstituteBindingsSequence, frozenset):
|
||
|
"""
|
||
|
A base feature value that represents the union of two or more
|
||
|
``FeatureValueSet`` or ``Variable``.
|
||
|
"""
|
||
|
|
||
|
def __new__(cls, values):
|
||
|
# If values contains FeatureValueUnions, then collapse them.
|
||
|
values = _flatten(values, FeatureValueUnion)
|
||
|
|
||
|
# If the resulting list contains no variables, then
|
||
|
# use a simple FeatureValueSet instead.
|
||
|
if sum(isinstance(v, Variable) for v in values) == 0:
|
||
|
values = _flatten(values, FeatureValueSet)
|
||
|
return FeatureValueSet(values)
|
||
|
|
||
|
# If we contain a single variable, return that variable.
|
||
|
if len(values) == 1:
|
||
|
return list(values)[0]
|
||
|
|
||
|
# Otherwise, build the FeatureValueUnion.
|
||
|
return frozenset.__new__(cls, values)
|
||
|
|
||
|
def __repr__(self):
|
||
|
# n.b., we sort the string reprs of our elements, to ensure
|
||
|
# that our own repr is deterministic. also, note that len(self)
|
||
|
# is guaranteed to be 2 or more.
|
||
|
return '{%s}' % '+'.join(sorted('%s' % (b,) for b in self))
|
||
|
|
||
|
|
||
|
@python_2_unicode_compatible
|
||
|
class FeatureValueConcat(SubstituteBindingsSequence, tuple):
|
||
|
"""
|
||
|
A base feature value that represents the concatenation of two or
|
||
|
more ``FeatureValueTuple`` or ``Variable``.
|
||
|
"""
|
||
|
|
||
|
def __new__(cls, values):
|
||
|
# If values contains FeatureValueConcats, then collapse them.
|
||
|
values = _flatten(values, FeatureValueConcat)
|
||
|
|
||
|
# If the resulting list contains no variables, then
|
||
|
# use a simple FeatureValueTuple instead.
|
||
|
if sum(isinstance(v, Variable) for v in values) == 0:
|
||
|
values = _flatten(values, FeatureValueTuple)
|
||
|
return FeatureValueTuple(values)
|
||
|
|
||
|
# If we contain a single variable, return that variable.
|
||
|
if len(values) == 1:
|
||
|
return list(values)[0]
|
||
|
|
||
|
# Otherwise, build the FeatureValueConcat.
|
||
|
return tuple.__new__(cls, values)
|
||
|
|
||
|
def __repr__(self):
|
||
|
# n.b.: len(self) is guaranteed to be 2 or more.
|
||
|
return '(%s)' % '+'.join('%s' % (b,) for b in self)
|
||
|
|
||
|
|
||
|
def _flatten(lst, cls):
|
||
|
"""
|
||
|
Helper function -- return a copy of list, with all elements of
|
||
|
type ``cls`` spliced in rather than appended in.
|
||
|
"""
|
||
|
result = []
|
||
|
for elt in lst:
|
||
|
if isinstance(elt, cls):
|
||
|
result.extend(elt)
|
||
|
else:
|
||
|
result.append(elt)
|
||
|
return result
|
||
|
|
||
|
|
||
|
######################################################################
|
||
|
# Specialized Features
|
||
|
######################################################################
|
||
|
|
||
|
@total_ordering
|
||
|
@python_2_unicode_compatible
|
||
|
class Feature(object):
|
||
|
"""
|
||
|
A feature identifier that's specialized to put additional
|
||
|
constraints, default values, etc.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, name, default=None, display=None):
|
||
|
assert display in (None, 'prefix', 'slash')
|
||
|
|
||
|
self._name = name # [xx] rename to .identifier?
|
||
|
self._default = default # [xx] not implemented yet.
|
||
|
self._display = display
|
||
|
|
||
|
if self._display == 'prefix':
|
||
|
self._sortkey = (-1, self._name)
|
||
|
elif self._display == 'slash':
|
||
|
self._sortkey = (1, self._name)
|
||
|
else:
|
||
|
self._sortkey = (0, self._name)
|
||
|
|
||
|
@property
|
||
|
def name(self):
|
||
|
"""The name of this feature."""
|
||
|
return self._name
|
||
|
|
||
|
@property
|
||
|
def default(self):
|
||
|
"""Default value for this feature."""
|
||
|
return self._default
|
||
|
|
||
|
@property
|
||
|
def display(self):
|
||
|
"""Custom display location: can be prefix, or slash."""
|
||
|
return self._display
|
||
|
|
||
|
def __repr__(self):
|
||
|
return '*%s*' % self.name
|
||
|
|
||
|
def __lt__(self, other):
|
||
|
if isinstance(other, string_types):
|
||
|
return True
|
||
|
if not isinstance(other, Feature):
|
||
|
raise_unorderable_types("<", self, other)
|
||
|
return self._sortkey < other._sortkey
|
||
|
|
||
|
def __eq__(self, other):
|
||
|
return type(self) == type(other) and self._name == other._name
|
||
|
|
||
|
def __ne__(self, other):
|
||
|
return not self == other
|
||
|
|
||
|
def __hash__(self):
|
||
|
return hash(self._name)
|
||
|
|
||
|
# ////////////////////////////////////////////////////////////
|
||
|
# These can be overridden by subclasses:
|
||
|
# ////////////////////////////////////////////////////////////
|
||
|
|
||
|
def read_value(self, s, position, reentrances, parser):
|
||
|
return parser.read_value(s, position, reentrances)
|
||
|
|
||
|
def unify_base_values(self, fval1, fval2, bindings):
|
||
|
"""
|
||
|
If possible, return a single value.. If not, return
|
||
|
the value ``UnificationFailure``.
|
||
|
"""
|
||
|
if fval1 == fval2:
|
||
|
return fval1
|
||
|
else:
|
||
|
return UnificationFailure
|
||
|
|
||
|
|
||
|
class SlashFeature(Feature):
|
||
|
def read_value(self, s, position, reentrances, parser):
|
||
|
return parser.read_partial(s, position, reentrances)
|
||
|
|
||
|
|
||
|
class RangeFeature(Feature):
|
||
|
RANGE_RE = re.compile('(-?\d+):(-?\d+)')
|
||
|
|
||
|
def read_value(self, s, position, reentrances, parser):
|
||
|
m = self.RANGE_RE.match(s, position)
|
||
|
if not m: raise ValueError('range', position)
|
||
|
return (int(m.group(1)), int(m.group(2))), m.end()
|
||
|
|
||
|
def unify_base_values(self, fval1, fval2, bindings):
|
||
|
if fval1 is None: return fval2
|
||
|
if fval2 is None: return fval1
|
||
|
rng = max(fval1[0], fval2[0]), min(fval1[1], fval2[1])
|
||
|
if rng[1] < rng[0]: return UnificationFailure
|
||
|
return rng
|
||
|
|
||
|
|
||
|
SLASH = SlashFeature('slash', default=False, display='slash')
|
||
|
TYPE = Feature('type', display='prefix')
|
||
|
|
||
|
|
||
|
######################################################################
|
||
|
# Specialized Feature Values
|
||
|
######################################################################
|
||
|
|
||
|
@total_ordering
|
||
|
class CustomFeatureValue(object):
|
||
|
"""
|
||
|
An abstract base class for base values that define a custom
|
||
|
unification method. The custom unification method of
|
||
|
``CustomFeatureValue`` will be used during unification if:
|
||
|
|
||
|
- The ``CustomFeatureValue`` is unified with another base value.
|
||
|
- The ``CustomFeatureValue`` is not the value of a customized
|
||
|
``Feature`` (which defines its own unification method).
|
||
|
|
||
|
If two ``CustomFeatureValue`` objects are unified with one another
|
||
|
during feature structure unification, then the unified base values
|
||
|
they return *must* be equal; otherwise, an ``AssertionError`` will
|
||
|
be raised.
|
||
|
|
||
|
Subclasses must define ``unify()``, ``__eq__()`` and ``__lt__()``.
|
||
|
Subclasses may also wish to define ``__hash__()``.
|
||
|
"""
|
||
|
|
||
|
def unify(self, other):
|
||
|
"""
|
||
|
If this base value unifies with ``other``, then return the
|
||
|
unified value. Otherwise, return ``UnificationFailure``.
|
||
|
"""
|
||
|
raise NotImplementedError('abstract base class')
|
||
|
|
||
|
def __eq__(self, other):
|
||
|
raise NotImplementedError('abstract base class')
|
||
|
|
||
|
def __ne__(self, other):
|
||
|
return not self == other
|
||
|
|
||
|
def __lt__(self, other):
|
||
|
raise NotImplementedError('abstract base class')
|
||
|
|
||
|
def __hash__(self):
|
||
|
raise TypeError('%s objects or unhashable' % self.__class__.__name__)
|
||
|
|
||
|
|
||
|
######################################################################
|
||
|
# Feature Structure Reader
|
||
|
######################################################################
|
||
|
|
||
|
class FeatStructReader(object):
|
||
|
def __init__(self, features=(SLASH, TYPE), fdict_class=FeatStruct,
|
||
|
flist_class=FeatList, logic_parser=None):
|
||
|
self._features = dict((f.name, f) for f in features)
|
||
|
self._fdict_class = fdict_class
|
||
|
self._flist_class = flist_class
|
||
|
self._prefix_feature = None
|
||
|
self._slash_feature = None
|
||
|
for feature in features:
|
||
|
if feature.display == 'slash':
|
||
|
if self._slash_feature:
|
||
|
raise ValueError('Multiple features w/ display=slash')
|
||
|
self._slash_feature = feature
|
||
|
if feature.display == 'prefix':
|
||
|
if self._prefix_feature:
|
||
|
raise ValueError('Multiple features w/ display=prefix')
|
||
|
self._prefix_feature = feature
|
||
|
self._features_with_defaults = [feature for feature in features
|
||
|
if feature.default is not None]
|
||
|
if logic_parser is None:
|
||
|
logic_parser = LogicParser()
|
||
|
self._logic_parser = logic_parser
|
||
|
|
||
|
def fromstring(self, s, fstruct=None):
|
||
|
"""
|
||
|
Convert a string representation of a feature structure (as
|
||
|
displayed by repr) into a ``FeatStruct``. This process
|
||
|
imposes the following restrictions on the string
|
||
|
representation:
|
||
|
|
||
|
- Feature names cannot contain any of the following:
|
||
|
whitespace, parentheses, quote marks, equals signs,
|
||
|
dashes, commas, and square brackets. Feature names may
|
||
|
not begin with plus signs or minus signs.
|
||
|
- Only the following basic feature value are supported:
|
||
|
strings, integers, variables, None, and unquoted
|
||
|
alphanumeric strings.
|
||
|
- For reentrant values, the first mention must specify
|
||
|
a reentrance identifier and a value; and any subsequent
|
||
|
mentions must use arrows (``'->'``) to reference the
|
||
|
reentrance identifier.
|
||
|
"""
|
||
|
s = s.strip()
|
||
|
value, position = self.read_partial(s, 0, {}, fstruct)
|
||
|
if position != len(s):
|
||
|
self._error(s, 'end of string', position)
|
||
|
return value
|
||
|
|
||
|
_START_FSTRUCT_RE = re.compile(r'\s*(?:\((\d+)\)\s*)?(\??[\w-]+)?(\[)')
|
||
|
_END_FSTRUCT_RE = re.compile(r'\s*]\s*')
|
||
|
_SLASH_RE = re.compile(r'/')
|
||
|
_FEATURE_NAME_RE = re.compile(r'\s*([+-]?)([^\s\(\)<>"\'\-=\[\],]+)\s*')
|
||
|
_REENTRANCE_RE = re.compile(r'\s*->\s*')
|
||
|
_TARGET_RE = re.compile(r'\s*\((\d+)\)\s*')
|
||
|
_ASSIGN_RE = re.compile(r'\s*=\s*')
|
||
|
_COMMA_RE = re.compile(r'\s*,\s*')
|
||
|
_BARE_PREFIX_RE = re.compile(r'\s*(?:\((\d+)\)\s*)?(\??[\w-]+\s*)()')
|
||
|
# This one is used to distinguish fdicts from flists:
|
||
|
_START_FDICT_RE = re.compile(r'(%s)|(%s\s*(%s\s*(=|->)|[+-]%s|\]))' % (
|
||
|
_BARE_PREFIX_RE.pattern, _START_FSTRUCT_RE.pattern,
|
||
|
_FEATURE_NAME_RE.pattern, _FEATURE_NAME_RE.pattern))
|
||
|
|
||
|
def read_partial(self, s, position=0, reentrances=None, fstruct=None):
|
||
|
"""
|
||
|
Helper function that reads in a feature structure.
|
||
|
|
||
|
:param s: The string to read.
|
||
|
:param position: The position in the string to start parsing.
|
||
|
:param reentrances: A dictionary from reentrance ids to values.
|
||
|
Defaults to an empty dictionary.
|
||
|
:return: A tuple (val, pos) of the feature structure created by
|
||
|
parsing and the position where the parsed feature structure ends.
|
||
|
:rtype: bool
|
||
|
"""
|
||
|
if reentrances is None: reentrances = {}
|
||
|
try:
|
||
|
return self._read_partial(s, position, reentrances, fstruct)
|
||
|
except ValueError as e:
|
||
|
if len(e.args) != 2: raise
|
||
|
self._error(s, *e.args)
|
||
|
|
||
|
def _read_partial(self, s, position, reentrances, fstruct=None):
|
||
|
# Create the new feature structure
|
||
|
if fstruct is None:
|
||
|
if self._START_FDICT_RE.match(s, position):
|
||
|
fstruct = self._fdict_class()
|
||
|
else:
|
||
|
fstruct = self._flist_class()
|
||
|
|
||
|
# Read up to the open bracket.
|
||
|
match = self._START_FSTRUCT_RE.match(s, position)
|
||
|
if not match:
|
||
|
match = self._BARE_PREFIX_RE.match(s, position)
|
||
|
if not match:
|
||
|
raise ValueError('open bracket or identifier', position)
|
||
|
position = match.end()
|
||
|
|
||
|
# If there as an identifier, record it.
|
||
|
if match.group(1):
|
||
|
identifier = match.group(1)
|
||
|
if identifier in reentrances:
|
||
|
raise ValueError('new identifier', match.start(1))
|
||
|
reentrances[identifier] = fstruct
|
||
|
|
||
|
if isinstance(fstruct, FeatDict):
|
||
|
fstruct.clear()
|
||
|
return self._read_partial_featdict(s, position, match,
|
||
|
reentrances, fstruct)
|
||
|
else:
|
||
|
del fstruct[:]
|
||
|
return self._read_partial_featlist(s, position, match,
|
||
|
reentrances, fstruct)
|
||
|
|
||
|
def _read_partial_featlist(self, s, position, match,
|
||
|
reentrances, fstruct):
|
||
|
# Prefix features are not allowed:
|
||
|
if match.group(2): raise ValueError('open bracket')
|
||
|
# Bare prefixes are not allowed:
|
||
|
if not match.group(3): raise ValueError('open bracket')
|
||
|
|
||
|
# Build a list of the features defined by the structure.
|
||
|
while position < len(s):
|
||
|
# Check for the close bracket.
|
||
|
match = self._END_FSTRUCT_RE.match(s, position)
|
||
|
if match is not None:
|
||
|
return fstruct, match.end()
|
||
|
|
||
|
# Reentances have the form "-> (target)"
|
||
|
match = self._REENTRANCE_RE.match(s, position)
|
||
|
if match:
|
||
|
position = match.end()
|
||
|
match = self._TARGET_RE.match(s, position)
|
||
|
if not match: raise ValueError('identifier', position)
|
||
|
target = match.group(1)
|
||
|
if target not in reentrances:
|
||
|
raise ValueError('bound identifier', position)
|
||
|
position = match.end()
|
||
|
fstruct.append(reentrances[target])
|
||
|
|
||
|
# Anything else is a value.
|
||
|
else:
|
||
|
value, position = (
|
||
|
self._read_value(0, s, position, reentrances))
|
||
|
fstruct.append(value)
|
||
|
|
||
|
# If there's a close bracket, handle it at the top of the loop.
|
||
|
if self._END_FSTRUCT_RE.match(s, position):
|
||
|
continue
|
||
|
|
||
|
# Otherwise, there should be a comma
|
||
|
match = self._COMMA_RE.match(s, position)
|
||
|
if match is None: raise ValueError('comma', position)
|
||
|
position = match.end()
|
||
|
|
||
|
# We never saw a close bracket.
|
||
|
raise ValueError('close bracket', position)
|
||
|
|
||
|
def _read_partial_featdict(self, s, position, match,
|
||
|
reentrances, fstruct):
|
||
|
# If there was a prefix feature, record it.
|
||
|
if match.group(2):
|
||
|
if self._prefix_feature is None:
|
||
|
raise ValueError('open bracket or identifier', match.start(2))
|
||
|
prefixval = match.group(2).strip()
|
||
|
if prefixval.startswith('?'):
|
||
|
prefixval = Variable(prefixval)
|
||
|
fstruct[self._prefix_feature] = prefixval
|
||
|
|
||
|
# If group 3 is empty, then we just have a bare prefix, so
|
||
|
# we're done.
|
||
|
if not match.group(3):
|
||
|
return self._finalize(s, match.end(), reentrances, fstruct)
|
||
|
|
||
|
# Build a list of the features defined by the structure.
|
||
|
# Each feature has one of the three following forms:
|
||
|
# name = value
|
||
|
# name -> (target)
|
||
|
# +name
|
||
|
# -name
|
||
|
while position < len(s):
|
||
|
# Use these variables to hold info about each feature:
|
||
|
name = value = None
|
||
|
|
||
|
# Check for the close bracket.
|
||
|
match = self._END_FSTRUCT_RE.match(s, position)
|
||
|
if match is not None:
|
||
|
return self._finalize(s, match.end(), reentrances, fstruct)
|
||
|
|
||
|
# Get the feature name's name
|
||
|
match = self._FEATURE_NAME_RE.match(s, position)
|
||
|
if match is None: raise ValueError('feature name', position)
|
||
|
name = match.group(2)
|
||
|
position = match.end()
|
||
|
|
||
|
# Check if it's a special feature.
|
||
|
if name[0] == '*' and name[-1] == '*':
|
||
|
name = self._features.get(name[1:-1])
|
||
|
if name is None:
|
||
|
raise ValueError('known special feature', match.start(2))
|
||
|
|
||
|
# Check if this feature has a value already.
|
||
|
if name in fstruct:
|
||
|
raise ValueError('new name', match.start(2))
|
||
|
|
||
|
# Boolean value ("+name" or "-name")
|
||
|
if match.group(1) == '+': value = True
|
||
|
if match.group(1) == '-': value = False
|
||
|
|
||
|
# Reentrance link ("-> (target)")
|
||
|
if value is None:
|
||
|
match = self._REENTRANCE_RE.match(s, position)
|
||
|
if match is not None:
|
||
|
position = match.end()
|
||
|
match = self._TARGET_RE.match(s, position)
|
||
|
if not match:
|
||
|
raise ValueError('identifier', position)
|
||
|
target = match.group(1)
|
||
|
if target not in reentrances:
|
||
|
raise ValueError('bound identifier', position)
|
||
|
position = match.end()
|
||
|
value = reentrances[target]
|
||
|
|
||
|
# Assignment ("= value").
|
||
|
if value is None:
|
||
|
match = self._ASSIGN_RE.match(s, position)
|
||
|
if match:
|
||
|
position = match.end()
|
||
|
value, position = (
|
||
|
self._read_value(name, s, position, reentrances))
|
||
|
# None of the above: error.
|
||
|
else:
|
||
|
raise ValueError('equals sign', position)
|
||
|
|
||
|
# Store the value.
|
||
|
fstruct[name] = value
|
||
|
|
||
|
# If there's a close bracket, handle it at the top of the loop.
|
||
|
if self._END_FSTRUCT_RE.match(s, position):
|
||
|
continue
|
||
|
|
||
|
# Otherwise, there should be a comma
|
||
|
match = self._COMMA_RE.match(s, position)
|
||
|
if match is None: raise ValueError('comma', position)
|
||
|
position = match.end()
|
||
|
|
||
|
# We never saw a close bracket.
|
||
|
raise ValueError('close bracket', position)
|
||
|
|
||
|
def _finalize(self, s, pos, reentrances, fstruct):
|
||
|
"""
|
||
|
Called when we see the close brace -- checks for a slash feature,
|
||
|
and adds in default values.
|
||
|
"""
|
||
|
# Add the slash feature (if any)
|
||
|
match = self._SLASH_RE.match(s, pos)
|
||
|
if match:
|
||
|
name = self._slash_feature
|
||
|
v, pos = self._read_value(name, s, match.end(), reentrances)
|
||
|
fstruct[name] = v
|
||
|
## Add any default features. -- handle in unficiation instead?
|
||
|
# for feature in self._features_with_defaults:
|
||
|
# fstruct.setdefault(feature, feature.default)
|
||
|
# Return the value.
|
||
|
return fstruct, pos
|
||
|
|
||
|
def _read_value(self, name, s, position, reentrances):
|
||
|
if isinstance(name, Feature):
|
||
|
return name.read_value(s, position, reentrances, self)
|
||
|
else:
|
||
|
return self.read_value(s, position, reentrances)
|
||
|
|
||
|
def read_value(self, s, position, reentrances):
|
||
|
for (handler, regexp) in self.VALUE_HANDLERS:
|
||
|
match = regexp.match(s, position)
|
||
|
if match:
|
||
|
handler_func = getattr(self, handler)
|
||
|
return handler_func(s, position, reentrances, match)
|
||
|
raise ValueError('value', position)
|
||
|
|
||
|
def _error(self, s, expected, position):
|
||
|
lines = s.split('\n')
|
||
|
while position > len(lines[0]):
|
||
|
position -= len(lines.pop(0)) + 1 # +1 for the newline.
|
||
|
estr = ('Error parsing feature structure\n ' +
|
||
|
lines[0] + '\n ' + ' ' * position + '^ ' +
|
||
|
'Expected %s' % expected)
|
||
|
raise ValueError(estr)
|
||
|
|
||
|
# ////////////////////////////////////////////////////////////
|
||
|
# { Value Readers
|
||
|
# ////////////////////////////////////////////////////////////
|
||
|
|
||
|
#: A table indicating how feature values should be processed. Each
|
||
|
#: entry in the table is a pair (handler, regexp). The first entry
|
||
|
#: with a matching regexp will have its handler called. Handlers
|
||
|
#: should have the following signature::
|
||
|
#:
|
||
|
#: def handler(s, position, reentrances, match): ...
|
||
|
#:
|
||
|
#: and should return a tuple (value, position), where position is
|
||
|
#: the string position where the value ended. (n.b.: order is
|
||
|
#: important here!)
|
||
|
VALUE_HANDLERS = [
|
||
|
('read_fstruct_value', _START_FSTRUCT_RE),
|
||
|
('read_var_value', re.compile(r'\?[a-zA-Z_][a-zA-Z0-9_]*')),
|
||
|
('read_str_value', re.compile("[uU]?[rR]?(['\"])")),
|
||
|
('read_int_value', re.compile(r'-?\d+')),
|
||
|
('read_sym_value', re.compile(r'[a-zA-Z_][a-zA-Z0-9_]*')),
|
||
|
('read_app_value', re.compile(r'<(app)\((\?[a-z][a-z]*)\s*,'
|
||
|
r'\s*(\?[a-z][a-z]*)\)>')),
|
||
|
# ('read_logic_value', re.compile(r'<([^>]*)>')),
|
||
|
# lazily match any character after '<' until we hit a '>' not preceded by '-'
|
||
|
('read_logic_value', re.compile(r'<(.*?)(?<!-)>')),
|
||
|
('read_set_value', re.compile(r'{')),
|
||
|
('read_tuple_value', re.compile(r'\(')),
|
||
|
]
|
||
|
|
||
|
def read_fstruct_value(self, s, position, reentrances, match):
|
||
|
return self.read_partial(s, position, reentrances)
|
||
|
|
||
|
def read_str_value(self, s, position, reentrances, match):
|
||
|
return read_str(s, position)
|
||
|
|
||
|
def read_int_value(self, s, position, reentrances, match):
|
||
|
return int(match.group()), match.end()
|
||
|
|
||
|
# Note: the '?' is included in the variable name.
|
||
|
def read_var_value(self, s, position, reentrances, match):
|
||
|
return Variable(match.group()), match.end()
|
||
|
|
||
|
_SYM_CONSTS = {'None': None, 'True': True, 'False': False}
|
||
|
|
||
|
def read_sym_value(self, s, position, reentrances, match):
|
||
|
val, end = match.group(), match.end()
|
||
|
return self._SYM_CONSTS.get(val, val), end
|
||
|
|
||
|
def read_app_value(self, s, position, reentrances, match):
|
||
|
"""Mainly included for backwards compat."""
|
||
|
return self._logic_parser.parse('%s(%s)' % match.group(2, 3)), match.end()
|
||
|
|
||
|
def read_logic_value(self, s, position, reentrances, match):
|
||
|
try:
|
||
|
try:
|
||
|
expr = self._logic_parser.parse(match.group(1))
|
||
|
except LogicalExpressionException:
|
||
|
raise ValueError()
|
||
|
return expr, match.end()
|
||
|
except ValueError:
|
||
|
raise ValueError('logic expression', match.start(1))
|
||
|
|
||
|
def read_tuple_value(self, s, position, reentrances, match):
|
||
|
return self._read_seq_value(s, position, reentrances, match, ')',
|
||
|
FeatureValueTuple, FeatureValueConcat)
|
||
|
|
||
|
def read_set_value(self, s, position, reentrances, match):
|
||
|
return self._read_seq_value(s, position, reentrances, match, '}',
|
||
|
FeatureValueSet, FeatureValueUnion)
|
||
|
|
||
|
def _read_seq_value(self, s, position, reentrances, match,
|
||
|
close_paren, seq_class, plus_class):
|
||
|
"""
|
||
|
Helper function used by read_tuple_value and read_set_value.
|
||
|
"""
|
||
|
cp = re.escape(close_paren)
|
||
|
position = match.end()
|
||
|
# Special syntax fo empty tuples:
|
||
|
m = re.compile(r'\s*/?\s*%s' % cp).match(s, position)
|
||
|
if m: return seq_class(), m.end()
|
||
|
# Read values:
|
||
|
values = []
|
||
|
seen_plus = False
|
||
|
while True:
|
||
|
# Close paren: return value.
|
||
|
m = re.compile(r'\s*%s' % cp).match(s, position)
|
||
|
if m:
|
||
|
if seen_plus:
|
||
|
return plus_class(values), m.end()
|
||
|
else:
|
||
|
return seq_class(values), m.end()
|
||
|
|
||
|
# Read the next value.
|
||
|
val, position = self.read_value(s, position, reentrances)
|
||
|
values.append(val)
|
||
|
|
||
|
# Comma or looking at close paren
|
||
|
m = re.compile(r'\s*(,|\+|(?=%s))\s*' % cp).match(s, position)
|
||
|
if not m: raise ValueError("',' or '+' or '%s'" % cp, position)
|
||
|
if m.group(1) == '+': seen_plus = True
|
||
|
position = m.end()
|
||
|
|
||
|
|
||
|
######################################################################
|
||
|
# { Demo
|
||
|
######################################################################
|
||
|
|
||
|
def display_unification(fs1, fs2, indent=' '):
|
||
|
# Print the two input feature structures, side by side.
|
||
|
fs1_lines = ("%s" % fs1).split('\n')
|
||
|
fs2_lines = ("%s" % fs2).split('\n')
|
||
|
if len(fs1_lines) > len(fs2_lines):
|
||
|
blankline = '[' + ' ' * (len(fs2_lines[0]) - 2) + ']'
|
||
|
fs2_lines += [blankline] * len(fs1_lines)
|
||
|
else:
|
||
|
blankline = '[' + ' ' * (len(fs1_lines[0]) - 2) + ']'
|
||
|
fs1_lines += [blankline] * len(fs2_lines)
|
||
|
for (fs1_line, fs2_line) in zip(fs1_lines, fs2_lines):
|
||
|
print(indent + fs1_line + ' ' + fs2_line)
|
||
|
print(indent + '-' * len(fs1_lines[0]) + ' ' + '-' * len(fs2_lines[0]))
|
||
|
|
||
|
linelen = len(fs1_lines[0]) * 2 + 3
|
||
|
print(indent + '| |'.center(linelen))
|
||
|
print(indent + '+-----UNIFY-----+'.center(linelen))
|
||
|
print(indent + '|'.center(linelen))
|
||
|
print(indent + 'V'.center(linelen))
|
||
|
|
||
|
bindings = {}
|
||
|
|
||
|
result = fs1.unify(fs2, bindings)
|
||
|
if result is None:
|
||
|
print(indent + '(FAILED)'.center(linelen))
|
||
|
else:
|
||
|
print('\n'.join(indent + l.center(linelen)
|
||
|
for l in ("%s" % result).split('\n')))
|
||
|
if bindings and len(bindings.bound_variables()) > 0:
|
||
|
print(repr(bindings).center(linelen))
|
||
|
return result
|
||
|
|
||
|
|
||
|
def interactive_demo(trace=False):
|
||
|
import random, sys
|
||
|
|
||
|
HELP = '''
|
||
|
1-%d: Select the corresponding feature structure
|
||
|
q: Quit
|
||
|
t: Turn tracing on or off
|
||
|
l: List all feature structures
|
||
|
?: Help
|
||
|
'''
|
||
|
|
||
|
print('''
|
||
|
This demo will repeatedly present you with a list of feature
|
||
|
structures, and ask you to choose two for unification. Whenever a
|
||
|
new feature structure is generated, it is added to the list of
|
||
|
choices that you can pick from. However, since this can be a
|
||
|
large number of feature structures, the demo will only print out a
|
||
|
random subset for you to choose between at a given time. If you
|
||
|
want to see the complete lists, type "l". For a list of valid
|
||
|
commands, type "?".
|
||
|
''')
|
||
|
print('Press "Enter" to continue...')
|
||
|
sys.stdin.readline()
|
||
|
|
||
|
fstruct_strings = [
|
||
|
'[agr=[number=sing, gender=masc]]',
|
||
|
'[agr=[gender=masc, person=3]]',
|
||
|
'[agr=[gender=fem, person=3]]',
|
||
|
'[subj=[agr=(1)[]], agr->(1)]',
|
||
|
'[obj=?x]', '[subj=?x]',
|
||
|
'[/=None]', '[/=NP]',
|
||
|
'[cat=NP]', '[cat=VP]', '[cat=PP]',
|
||
|
'[subj=[agr=[gender=?y]], obj=[agr=[gender=?y]]]',
|
||
|
'[gender=masc, agr=?C]',
|
||
|
'[gender=?S, agr=[gender=?S,person=3]]'
|
||
|
]
|
||
|
|
||
|
all_fstructs = [(i, FeatStruct(fstruct_strings[i]))
|
||
|
for i in range(len(fstruct_strings))]
|
||
|
|
||
|
def list_fstructs(fstructs):
|
||
|
for i, fstruct in fstructs:
|
||
|
print()
|
||
|
lines = ("%s" % fstruct).split('\n')
|
||
|
print('%3d: %s' % (i + 1, lines[0]))
|
||
|
for line in lines[1:]: print(' ' + line)
|
||
|
print()
|
||
|
|
||
|
while True:
|
||
|
# Pick 5 feature structures at random from the master list.
|
||
|
MAX_CHOICES = 5
|
||
|
if len(all_fstructs) > MAX_CHOICES:
|
||
|
fstructs = sorted(random.sample(all_fstructs, MAX_CHOICES))
|
||
|
else:
|
||
|
fstructs = all_fstructs
|
||
|
|
||
|
print('_' * 75)
|
||
|
|
||
|
print('Choose two feature structures to unify:')
|
||
|
list_fstructs(fstructs)
|
||
|
|
||
|
selected = [None, None]
|
||
|
for (nth, i) in (('First', 0), ('Second', 1)):
|
||
|
while selected[i] is None:
|
||
|
print(('%s feature structure (1-%d,q,t,l,?): '
|
||
|
% (nth, len(all_fstructs))), end=' ')
|
||
|
try:
|
||
|
input = sys.stdin.readline().strip()
|
||
|
if input in ('q', 'Q', 'x', 'X'): return
|
||
|
if input in ('t', 'T'):
|
||
|
trace = not trace
|
||
|
print(' Trace = %s' % trace)
|
||
|
continue
|
||
|
if input in ('h', 'H', '?'):
|
||
|
print(HELP % len(fstructs));
|
||
|
continue
|
||
|
if input in ('l', 'L'):
|
||
|
list_fstructs(all_fstructs);
|
||
|
continue
|
||
|
num = int(input) - 1
|
||
|
selected[i] = all_fstructs[num][1]
|
||
|
print()
|
||
|
except:
|
||
|
print('Bad sentence number')
|
||
|
continue
|
||
|
|
||
|
if trace:
|
||
|
result = selected[0].unify(selected[1], trace=1)
|
||
|
else:
|
||
|
result = display_unification(selected[0], selected[1])
|
||
|
if result is not None:
|
||
|
for i, fstruct in all_fstructs:
|
||
|
if repr(result) == repr(fstruct): break
|
||
|
else:
|
||
|
all_fstructs.append((len(all_fstructs), result))
|
||
|
|
||
|
print('\nType "Enter" to continue unifying; or "q" to quit.')
|
||
|
input = sys.stdin.readline().strip()
|
||
|
if input in ('q', 'Q', 'x', 'X'): return
|
||
|
|
||
|
|
||
|
def demo(trace=False):
|
||
|
"""
|
||
|
Just for testing
|
||
|
"""
|
||
|
# import random
|
||
|
|
||
|
# processor breaks with values like '3rd'
|
||
|
fstruct_strings = [
|
||
|
'[agr=[number=sing, gender=masc]]',
|
||
|
'[agr=[gender=masc, person=3]]',
|
||
|
'[agr=[gender=fem, person=3]]',
|
||
|
'[subj=[agr=(1)[]], agr->(1)]',
|
||
|
'[obj=?x]', '[subj=?x]',
|
||
|
'[/=None]', '[/=NP]',
|
||
|
'[cat=NP]', '[cat=VP]', '[cat=PP]',
|
||
|
'[subj=[agr=[gender=?y]], obj=[agr=[gender=?y]]]',
|
||
|
'[gender=masc, agr=?C]',
|
||
|
'[gender=?S, agr=[gender=?S,person=3]]'
|
||
|
]
|
||
|
all_fstructs = [FeatStruct(fss) for fss in fstruct_strings]
|
||
|
# MAX_CHOICES = 5
|
||
|
# if len(all_fstructs) > MAX_CHOICES:
|
||
|
# fstructs = random.sample(all_fstructs, MAX_CHOICES)
|
||
|
# fstructs.sort()
|
||
|
# else:
|
||
|
# fstructs = all_fstructs
|
||
|
|
||
|
for fs1 in all_fstructs:
|
||
|
for fs2 in all_fstructs:
|
||
|
print("\n*******************\nfs1 is:\n%s\n\nfs2 is:\n%s\n\nresult is:\n%s" % (fs1, fs2, unify(fs1, fs2)))
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
demo()
|
||
|
|
||
|
__all__ = ['FeatStruct', 'FeatDict', 'FeatList', 'unify', 'subsumes', 'conflicts',
|
||
|
'Feature', 'SlashFeature', 'RangeFeature', 'SLASH', 'TYPE',
|
||
|
'FeatStructReader']
|