| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228 |
- .. Copyright (C) 2001-2020 NLTK Project
- .. For license information, see LICENSE.TXT
- ==================================
- Feature Structures & Unification
- ==================================
- >>> from nltk.featstruct import FeatStruct
- >>> from nltk.sem.logic import Variable, VariableExpression, Expression
- .. note:: For now, featstruct uses the older lambdalogic semantics
- module. Eventually, it should be updated to use the new first
- order predicate logic module.
- Overview
- ~~~~~~~~
- A feature structure is a mapping from feature identifiers to feature
- values, where feature values can be simple values (like strings or
- ints), nested feature structures, or variables:
- >>> fs1 = FeatStruct(number='singular', person=3)
- >>> print(fs1)
- [ number = 'singular' ]
- [ person = 3 ]
- Feature structure may be nested:
- >>> fs2 = FeatStruct(type='NP', agr=fs1)
- >>> print(fs2)
- [ agr = [ number = 'singular' ] ]
- [ [ person = 3 ] ]
- [ ]
- [ type = 'NP' ]
- Variables are used to indicate that two features should be assigned
- the same value. For example, the following feature structure requires
- that the feature fs3['agr']['number'] be bound to the same value as the
- feature fs3['subj']['number'].
- >>> fs3 = FeatStruct(agr=FeatStruct(number=Variable('?n')),
- ... subj=FeatStruct(number=Variable('?n')))
- >>> print(fs3)
- [ agr = [ number = ?n ] ]
- [ ]
- [ subj = [ number = ?n ] ]
- Feature structures are typically used to represent partial information
- about objects. A feature name 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*.
- >>> print(fs2.unify(fs3))
- [ agr = [ number = 'singular' ] ]
- [ [ person = 3 ] ]
- [ ]
- [ subj = [ number = 'singular' ] ]
- [ ]
- [ type = 'NP' ]
- When two inconsistent feature structures are unified, the unification
- fails and returns ``None``.
- >>> fs4 = FeatStruct(agr=FeatStruct(person=1))
- >>> print(fs4.unify(fs2))
- None
- >>> print(fs2.unify(fs4))
- None
- ..
- >>> del fs1, fs2, fs3, fs4 # clean-up
- Feature Structure Types
- -----------------------
- There are actually 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.
- When you construct a feature structure using the `FeatStruct`
- constructor, it will automatically decide which type is appropriate:
- >>> type(FeatStruct(number='singular'))
- <class 'nltk.featstruct.FeatDict'>
- >>> type(FeatStruct([1,2,3]))
- <class 'nltk.featstruct.FeatList'>
- Usually, we will just use feature dictionaries; but sometimes feature
- lists can be useful too. Two feature lists will unify with each other
- only if they have equal lengths, and all of their feature values
- match. If you wish to write a feature list that contains 'unknown'
- values, you must use variables:
- >>> fs1 = FeatStruct([1,2,Variable('?y')])
- >>> fs2 = FeatStruct([1,Variable('?x'),3])
- >>> fs1.unify(fs2)
- [1, 2, 3]
- ..
- >>> del fs1, fs2 # clean-up
- Parsing Feature Structure Strings
- ---------------------------------
- Feature structures can be constructed directly from strings. Often,
- this is more convenient than constructing them directly. NLTK can
- parse most feature strings to produce the corresponding feature
- structures. (But you must restrict your base feature values to
- strings, ints, logic expressions (`nltk.sem.logic.Expression`), and a
- few other types discussed below).
- Feature dictionaries are written like Python dictionaries, except that
- keys are not put in quotes; and square brackets (``[]``) are used
- instead of braces (``{}``):
- >>> FeatStruct('[tense="past", agr=[number="sing", person=3]]')
- [agr=[number='sing', person=3], tense='past']
- If a feature value is a single alphanumeric word, then it does not
- need to be quoted -- it will be automatically treated as a string:
- >>> FeatStruct('[tense=past, agr=[number=sing, person=3]]')
- [agr=[number='sing', person=3], tense='past']
- Feature lists are written like python lists:
- >>> FeatStruct('[1, 2, 3]')
- [1, 2, 3]
- The expression ``[]`` is treated as an empty feature dictionary, not
- an empty feature list:
- >>> type(FeatStruct('[]'))
- <class 'nltk.featstruct.FeatDict'>
- Feature Paths
- -------------
- Features can be specified using *feature paths*, or tuples of feature
- identifiers that specify path through the nested feature structures to
- a value.
- >>> fs1 = FeatStruct('[x=1, y=[1,2,[z=3]]]')
- >>> fs1['y']
- [1, 2, [z=3]]
- >>> fs1['y', 2]
- [z=3]
- >>> fs1['y', 2, 'z']
- 3
- ..
- >>> del fs1 # clean-up
- Reentrance
- ----------
- Feature structures may contain reentrant feature values. A *reentrant
- feature value* is a single feature structure that can be accessed via
- multiple feature paths.
- >>> fs1 = FeatStruct(x='val')
- >>> fs2 = FeatStruct(a=fs1, b=fs1)
- >>> print(fs2)
- [ a = (1) [ x = 'val' ] ]
- [ ]
- [ b -> (1) ]
- >>> fs2
- [a=(1)[x='val'], b->(1)]
- As you can see, reentrane is displayed by marking a feature structure
- with a unique identifier, in this case ``(1)``, the first time it is
- encountered; and then using the special form ``var -> id`` whenever it
- is encountered again. You can use the same notation to directly
- create reentrant feature structures from strings.
- >>> FeatStruct('[a=(1)[], b->(1), c=[d->(1)]]')
- [a=(1)[], b->(1), c=[d->(1)]]
- Reentrant feature structures may contain cycles:
- >>> fs3 = FeatStruct('(1)[a->(1)]')
- >>> fs3['a', 'a', 'a', 'a']
- (1)[a->(1)]
- >>> fs3['a', 'a', 'a', 'a'] is fs3
- True
- 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.
- >>> fs3.unify(FeatStruct('[a=[b=12], c=33]'))
- (1)[a->(1), b=12, c=33]
- ..
- >>> del fs1, fs2, fs3 # clean-up
- Feature Structure Equality
- --------------------------
- Two feature structures are considered equal if they assign the same
- values to all features, *and* they contain the same reentrances.
- >>> fs1 = FeatStruct('[a=(1)[x=1], b->(1)]')
- >>> fs2 = FeatStruct('[a=(1)[x=1], b->(1)]')
- >>> fs3 = FeatStruct('[a=[x=1], b=[x=1]]')
- >>> fs1 == fs1, fs1 is fs1
- (True, True)
- >>> fs1 == fs2, fs1 is fs2
- (True, False)
- >>> fs1 == fs3, fs1 is fs3
- (False, False)
- Note that this differs from how Python dictionaries and lists define
- equality -- in particular, Python dictionaries and lists ignore
- reentrance relations. To test two feature structures for equality
- while ignoring reentrance relations, use the `equal_values()` method:
- >>> fs1.equal_values(fs1)
- True
- >>> fs1.equal_values(fs2)
- True
- >>> fs1.equal_values(fs3)
- True
- ..
- >>> del fs1, fs2, fs3 # clean-up
- Feature Value Sets & Feature Value Tuples
- -----------------------------------------
- `nltk.featstruct` defines two new data types that are intended to be
- used as feature values: `FeatureValueTuple` and `FeatureValueSet`.
- Both of these types are considered base values -- i.e., unification
- does *not* apply to them. However, variable binding *does* apply to
- any values that they contain.
- Feature value tuples are written with parentheses:
- >>> fs1 = FeatStruct('[x=(?x, ?y)]')
- >>> fs1
- [x=(?x, ?y)]
- >>> fs1.substitute_bindings({Variable('?x'): 1, Variable('?y'): 2})
- [x=(1, 2)]
- Feature sets are written with braces:
- >>> fs1 = FeatStruct('[x={?x, ?y}]')
- >>> fs1
- [x={?x, ?y}]
- >>> fs1.substitute_bindings({Variable('?x'): 1, Variable('?y'): 2})
- [x={1, 2}]
- In addition to the basic feature value tuple & set classes, nltk
- defines feature value unions (for sets) and feature value
- concatenations (for tuples). These are written using '+', and can be
- used to combine sets & tuples:
- >>> fs1 = FeatStruct('[x=((1, 2)+?z), z=?z]')
- >>> fs1
- [x=((1, 2)+?z), z=?z]
- >>> fs1.unify(FeatStruct('[z=(3, 4, 5)]'))
- [x=(1, 2, 3, 4, 5), z=(3, 4, 5)]
- Thus, feature value tuples and sets can be used to build up tuples
- and sets of values over the corse of unification. For example, when
- parsing sentences using a semantic feature grammar, feature sets or
- feature tuples can be used to build a list of semantic predicates as
- the sentence is parsed.
- As was mentioned above, unification does not apply to feature value
- tuples and sets. One reason for this that it's impossible to define a
- single correct answer for unification when concatenation is used.
- Consider the following example:
- >>> fs1 = FeatStruct('[x=(1, 2, 3, 4)]')
- >>> fs2 = FeatStruct('[x=(?a+?b), a=?a, b=?b]')
- If unification applied to feature tuples, then the unification
- algorithm would have to arbitrarily choose how to divide the tuple
- (1,2,3,4) into two parts. Instead, the unification algorithm refuses
- to make this decision, and simply unifies based on value. Because
- (1,2,3,4) is not equal to (?a+?b), fs1 and fs2 will not unify:
- >>> print(fs1.unify(fs2))
- None
- If you need a list-like structure that unification does apply to, use
- `FeatList`.
- ..
- >>> del fs1, fs2 # clean-up
- Light-weight 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.
- >>> # Note: pprint prints dicts sorted
- >>> from pprint import pprint
- >>> from nltk.featstruct import unify
- >>> pprint(unify(dict(x=1, y=dict()), dict(a='a', y=dict(b='b'))))
- {'a': 'a', 'x': 1, 'y': {'b': 'b'}}
- 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 & 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.
- Custom Feature Values
- ---------------------
- The abstract base class `CustomFeatureValue` can be used to define new
- base value types that have custom unification methods. For example,
- the following feature value type encodes a range, and defines
- unification as taking the intersection on the ranges:
- >>> from functools import total_ordering
- >>> from nltk.featstruct import CustomFeatureValue, UnificationFailure
- >>> @total_ordering
- ... class Range(CustomFeatureValue):
- ... def __init__(self, low, high):
- ... assert low <= high
- ... self.low = low
- ... self.high = high
- ... def unify(self, other):
- ... if not isinstance(other, Range):
- ... return UnificationFailure
- ... low = max(self.low, other.low)
- ... high = min(self.high, other.high)
- ... if low <= high: return Range(low, high)
- ... else: return UnificationFailure
- ... def __repr__(self):
- ... return '(%s<x<%s)' % (self.low, self.high)
- ... def __eq__(self, other):
- ... if not isinstance(other, Range):
- ... return False
- ... return (self.low == other.low) and (self.high == other.high)
- ... def __lt__(self, other):
- ... if not isinstance(other, Range):
- ... return True
- ... return (self.low, self.high) < (other.low, other.high)
- >>> fs1 = FeatStruct(x=Range(5,8), y=FeatStruct(z=Range(7,22)))
- >>> print(fs1.unify(FeatStruct(x=Range(6, 22))))
- [ x = (6<x<8) ]
- [ ]
- [ y = [ z = (7<x<22) ] ]
- >>> print(fs1.unify(FeatStruct(x=Range(9, 12))))
- None
- >>> print(fs1.unify(FeatStruct(x=12)))
- None
- >>> print(fs1.unify(FeatStruct('[x=?x, y=[z=?x]]')))
- [ x = (7<x<8) ]
- [ ]
- [ y = [ z = (7<x<8) ] ]
- Regression Tests
- ~~~~~~~~~~~~~~~~
- Dictionary access methods (non-mutating)
- ----------------------------------------
- >>> fs1 = FeatStruct(a=1, b=2, c=3)
- >>> fs2 = FeatStruct(x=fs1, y='x')
- Feature structures support all dictionary methods (excluding the class
- method `dict.fromkeys()`). Non-mutating methods:
- >>> sorted(fs2.keys()) # keys()
- ['x', 'y']
- >>> sorted(fs2.values()) # values()
- [[a=1, b=2, c=3], 'x']
- >>> sorted(fs2.items()) # items()
- [('x', [a=1, b=2, c=3]), ('y', 'x')]
- >>> sorted(fs2) # __iter__()
- ['x', 'y']
- >>> 'a' in fs2, 'x' in fs2 # __contains__()
- (False, True)
- >>> fs2.has_key('a'), fs2.has_key('x') # has_key()
- (False, True)
- >>> fs2['x'], fs2['y'] # __getitem__()
- ([a=1, b=2, c=3], 'x')
- >>> fs2['a'] # __getitem__()
- Traceback (most recent call last):
- . . .
- KeyError: 'a'
- >>> fs2.get('x'), fs2.get('y'), fs2.get('a') # get()
- ([a=1, b=2, c=3], 'x', None)
- >>> fs2.get('x', 'hello'), fs2.get('a', 'hello') # get()
- ([a=1, b=2, c=3], 'hello')
- >>> len(fs1), len(fs2) # __len__
- (3, 2)
- >>> fs2.copy() # copy()
- [x=[a=1, b=2, c=3], y='x']
- >>> fs2.copy() is fs2 # copy()
- False
- Note: by default, `FeatStruct.copy()` does a deep copy. Use
- `FeatStruct.copy(deep=False)` for a shallow copy.
- ..
- >>> del fs1, fs2 # clean-up.
- Dictionary access methods (mutating)
- ------------------------------------
- >>> fs1 = FeatStruct(a=1, b=2, c=3)
- >>> fs2 = FeatStruct(x=fs1, y='x')
- Setting features (`__setitem__()`)
- >>> fs1['c'] = 5
- >>> fs1
- [a=1, b=2, c=5]
- >>> fs1['x'] = 12
- >>> fs1
- [a=1, b=2, c=5, x=12]
- >>> fs2['x', 'a'] = 2
- >>> fs2
- [x=[a=2, b=2, c=5, x=12], y='x']
- >>> fs1
- [a=2, b=2, c=5, x=12]
- Deleting features (`__delitem__()`)
- >>> del fs1['x']
- >>> fs1
- [a=2, b=2, c=5]
- >>> del fs2['x', 'a']
- >>> fs1
- [b=2, c=5]
- `setdefault()`:
- >>> fs1.setdefault('b', 99)
- 2
- >>> fs1
- [b=2, c=5]
- >>> fs1.setdefault('x', 99)
- 99
- >>> fs1
- [b=2, c=5, x=99]
- `update()`:
- >>> fs2.update({'a':'A', 'b':'B'}, c='C')
- >>> fs2
- [a='A', b='B', c='C', x=[b=2, c=5, x=99], y='x']
- `pop()`:
- >>> fs2.pop('a')
- 'A'
- >>> fs2
- [b='B', c='C', x=[b=2, c=5, x=99], y='x']
- >>> fs2.pop('a')
- Traceback (most recent call last):
- . . .
- KeyError: 'a'
- >>> fs2.pop('a', 'foo')
- 'foo'
- >>> fs2
- [b='B', c='C', x=[b=2, c=5, x=99], y='x']
- `clear()`:
- >>> fs1.clear()
- >>> fs1
- []
- >>> fs2
- [b='B', c='C', x=[], y='x']
- `popitem()`:
- >>> sorted([fs2.popitem() for i in range(len(fs2))])
- [('b', 'B'), ('c', 'C'), ('x', []), ('y', 'x')]
- >>> fs2
- []
- Once a feature structure has been frozen, it may not be mutated.
- >>> fs1 = FeatStruct('[x=1, y=2, z=[a=3]]')
- >>> fs1.freeze()
- >>> fs1.frozen()
- True
- >>> fs1['z'].frozen()
- True
- >>> fs1['x'] = 5
- Traceback (most recent call last):
- . . .
- ValueError: Frozen FeatStructs may not be modified.
- >>> del fs1['x']
- Traceback (most recent call last):
- . . .
- ValueError: Frozen FeatStructs may not be modified.
- >>> fs1.clear()
- Traceback (most recent call last):
- . . .
- ValueError: Frozen FeatStructs may not be modified.
- >>> fs1.pop('x')
- Traceback (most recent call last):
- . . .
- ValueError: Frozen FeatStructs may not be modified.
- >>> fs1.popitem()
- Traceback (most recent call last):
- . . .
- ValueError: Frozen FeatStructs may not be modified.
- >>> fs1.setdefault('x')
- Traceback (most recent call last):
- . . .
- ValueError: Frozen FeatStructs may not be modified.
- >>> fs1.update(z=22)
- Traceback (most recent call last):
- . . .
- ValueError: Frozen FeatStructs may not be modified.
- ..
- >>> del fs1, fs2 # clean-up.
- Feature Paths
- -------------
- Make sure that __getitem__ with feature paths works as intended:
- >>> fs1 = FeatStruct(a=1, b=2,
- ... c=FeatStruct(
- ... d=FeatStruct(e=12),
- ... f=FeatStruct(g=55, h='hello')))
- >>> fs1[()]
- [a=1, b=2, c=[d=[e=12], f=[g=55, h='hello']]]
- >>> fs1['a'], fs1[('a',)]
- (1, 1)
- >>> fs1['c','d','e']
- 12
- >>> fs1['c','f','g']
- 55
- Feature paths that select unknown features raise KeyError:
- >>> fs1['c', 'f', 'e']
- Traceback (most recent call last):
- . . .
- KeyError: ('c', 'f', 'e')
- >>> fs1['q', 'p']
- Traceback (most recent call last):
- . . .
- KeyError: ('q', 'p')
- Feature paths that try to go 'through' a feature that's not a feature
- structure raise KeyError:
- >>> fs1['a', 'b']
- Traceback (most recent call last):
- . . .
- KeyError: ('a', 'b')
- Feature paths can go through reentrant structures:
- >>> fs2 = FeatStruct('(1)[a=[b=[c->(1), d=5], e=11]]')
- >>> fs2['a', 'b', 'c', 'a', 'e']
- 11
- >>> fs2['a', 'b', 'c', 'a', 'b', 'd']
- 5
- >>> fs2[tuple('abcabcabcabcabcabcabcabcabcabca')]
- (1)[b=[c=[a->(1)], d=5], e=11]
- Indexing requires strings, `Feature`\s, or tuples; other types raise a
- TypeError:
- >>> fs2[12]
- Traceback (most recent call last):
- . . .
- TypeError: Expected feature name or path. Got 12.
- >>> fs2[list('abc')]
- Traceback (most recent call last):
- . . .
- TypeError: Expected feature name or path. Got ['a', 'b', 'c'].
- Feature paths can also be used with `get()`, `has_key()`, and
- `__contains__()`.
- >>> fpath1 = tuple('abcabc')
- >>> fpath2 = tuple('abcabz')
- >>> fs2.get(fpath1), fs2.get(fpath2)
- ((1)[a=[b=[c->(1), d=5], e=11]], None)
- >>> fpath1 in fs2, fpath2 in fs2
- (True, False)
- >>> fs2.has_key(fpath1), fs2.has_key(fpath2)
- (True, False)
- ..
- >>> del fs1, fs2 # clean-up
- Reading Feature Structures
- --------------------------
- Empty feature struct:
- >>> FeatStruct('[]')
- []
- Test features with integer values:
- >>> FeatStruct('[a=12, b=-33, c=0]')
- [a=12, b=-33, c=0]
- Test features with string values. Either single or double quotes may
- be used. Strings are evaluated just like python strings -- in
- particular, you can use escape sequences and 'u' and 'r' prefixes, and
- triple-quoted strings.
- >>> FeatStruct('[a="", b="hello", c="\'", d=\'\', e=\'"\']')
- [a='', b='hello', c="'", d='', e='"']
- >>> FeatStruct(r'[a="\\", b="\"", c="\x6f\\y", d="12"]')
- [a='\\', b='"', c='o\\y', d='12']
- >>> FeatStruct(r'[b=r"a\b\c"]')
- [b='a\\b\\c']
- >>> FeatStruct('[x="""a"""]')
- [x='a']
- Test parsing of reentrant feature structures.
- >>> FeatStruct('[a=(1)[], b->(1)]')
- [a=(1)[], b->(1)]
- >>> FeatStruct('[a=(1)[x=1, y=2], b->(1)]')
- [a=(1)[x=1, y=2], b->(1)]
- Test parsing of cyclic feature structures.
- >>> FeatStruct('[a=(1)[b->(1)]]')
- [a=(1)[b->(1)]]
- >>> FeatStruct('(1)[a=[b=[c->(1)]]]')
- (1)[a=[b=[c->(1)]]]
- Strings of the form "+name" and "-name" may be used to specify boolean
- values.
- >>> FeatStruct('[-bar, +baz, +foo]')
- [-bar, +baz, +foo]
- None, True, and False are recognized as values:
- >>> FeatStruct('[bar=True, baz=False, foo=None]')
- [+bar, -baz, foo=None]
- Special features:
- >>> FeatStruct('NP/VP')
- NP[]/VP[]
- >>> FeatStruct('?x/?x')
- ?x[]/?x[]
- >>> print(FeatStruct('VP[+fin, agr=?x, tense=past]/NP[+pl, agr=?x]'))
- [ *type* = 'VP' ]
- [ ]
- [ [ *type* = 'NP' ] ]
- [ *slash* = [ agr = ?x ] ]
- [ [ pl = True ] ]
- [ ]
- [ agr = ?x ]
- [ fin = True ]
- [ tense = 'past' ]
- Here the slash feature gets coerced:
- >>> FeatStruct('[*slash*=a, x=b, *type*="NP"]')
- NP[x='b']/a[]
- >>> FeatStruct('NP[sem=<bob>]/NP')
- NP[sem=<bob>]/NP[]
- >>> FeatStruct('S[sem=<walk(bob)>]')
- S[sem=<walk(bob)>]
- >>> print(FeatStruct('NP[sem=<bob>]/NP'))
- [ *type* = 'NP' ]
- [ ]
- [ *slash* = [ *type* = 'NP' ] ]
- [ ]
- [ sem = <bob> ]
- Playing with ranges:
- >>> from nltk.featstruct import RangeFeature, FeatStructReader
- >>> width = RangeFeature('width')
- >>> reader = FeatStructReader([width])
- >>> fs1 = reader.fromstring('[*width*=-5:12]')
- >>> fs2 = reader.fromstring('[*width*=2:123]')
- >>> fs3 = reader.fromstring('[*width*=-7:-2]')
- >>> fs1.unify(fs2)
- [*width*=(2, 12)]
- >>> fs1.unify(fs3)
- [*width*=(-5, -2)]
- >>> print(fs2.unify(fs3)) # no overlap in width.
- None
- The slash feature has a default value of 'False':
- >>> print(FeatStruct('NP[]/VP').unify(FeatStruct('NP[]'), trace=1))
- <BLANKLINE>
- Unification trace:
- / NP[]/VP[]
- |\ NP[]
- |
- | Unify feature: *type*
- | / 'NP'
- | |\ 'NP'
- | |
- | +-->'NP'
- |
- | Unify feature: *slash*
- | / VP[]
- | |\ False
- | |
- X X <-- FAIL
- None
- The demo structures from category.py. They all parse, but they don't
- do quite the right thing, -- ?x vs x.
- >>> FeatStruct(pos='n', agr=FeatStruct(number='pl', gender='f'))
- [agr=[gender='f', number='pl'], pos='n']
- >>> FeatStruct(r'NP[sem=<bob>]/NP')
- NP[sem=<bob>]/NP[]
- >>> FeatStruct(r'S[sem=<app(?x, ?y)>]')
- S[sem=<?x(?y)>]
- >>> FeatStruct('?x/?x')
- ?x[]/?x[]
- >>> FeatStruct('VP[+fin, agr=?x, tense=past]/NP[+pl, agr=?x]')
- VP[agr=?x, +fin, tense='past']/NP[agr=?x, +pl]
- >>> FeatStruct('S[sem = <app(?subj, ?vp)>]')
- S[sem=<?subj(?vp)>]
- >>> FeatStruct('S')
- S[]
- The parser also includes support for reading sets and tuples.
- >>> FeatStruct('[x={1,2,2,2}, y={/}]')
- [x={1, 2}, y={/}]
- >>> FeatStruct('[x=(1,2,2,2), y=()]')
- [x=(1, 2, 2, 2), y=()]
- >>> print(FeatStruct('[x=(1,[z=(1,2,?x)],?z,{/})]'))
- [ x = (1, [ z = (1, 2, ?x) ], ?z, {/}) ]
- Note that we can't put a featstruct inside a tuple, because doing so
- would hash it, and it's not frozen yet:
- >>> print(FeatStruct('[x={[]}]'))
- Traceback (most recent call last):
- . . .
- TypeError: FeatStructs must be frozen before they can be hashed.
- There's a special syntax for taking the union of sets: "{...+...}".
- The elements should only be variables or sets.
- >>> FeatStruct('[x={?a+?b+{1,2,3}}]')
- [x={?a+?b+{1, 2, 3}}]
- There's a special syntax for taking the concatenation of tuples:
- "(...+...)". The elements should only be variables or tuples.
- >>> FeatStruct('[x=(?a+?b+(1,2,3))]')
- [x=(?a+?b+(1, 2, 3))]
- Parsing gives helpful messages if your string contains an error.
- >>> FeatStruct('[a=, b=5]]')
- Traceback (most recent call last):
- . . .
- ValueError: Error parsing feature structure
- [a=, b=5]]
- ^ Expected value
- >>> FeatStruct('[a=12 22, b=33]')
- Traceback (most recent call last):
- . . .
- ValueError: Error parsing feature structure
- [a=12 22, b=33]
- ^ Expected comma
- >>> FeatStruct('[a=5] [b=6]')
- Traceback (most recent call last):
- . . .
- ValueError: Error parsing feature structure
- [a=5] [b=6]
- ^ Expected end of string
- >>> FeatStruct(' *++*')
- Traceback (most recent call last):
- . . .
- ValueError: Error parsing feature structure
- *++*
- ^ Expected open bracket or identifier
- >>> FeatStruct('[x->(1)]')
- Traceback (most recent call last):
- . . .
- ValueError: Error parsing feature structure
- [x->(1)]
- ^ Expected bound identifier
- >>> FeatStruct('[x->y]')
- Traceback (most recent call last):
- . . .
- ValueError: Error parsing feature structure
- [x->y]
- ^ Expected identifier
- >>> FeatStruct('')
- Traceback (most recent call last):
- . . .
- ValueError: Error parsing feature structure
- <BLANKLINE>
- ^ Expected open bracket or identifier
- Unification
- -----------
- Very simple unifications give the expected results:
- >>> FeatStruct().unify(FeatStruct())
- []
- >>> FeatStruct(number='singular').unify(FeatStruct())
- [number='singular']
- >>> FeatStruct().unify(FeatStruct(number='singular'))
- [number='singular']
- >>> FeatStruct(number='singular').unify(FeatStruct(person=3))
- [number='singular', person=3]
- Merging nested structures:
- >>> fs1 = FeatStruct('[A=[B=b]]')
- >>> fs2 = FeatStruct('[A=[C=c]]')
- >>> fs1.unify(fs2)
- [A=[B='b', C='c']]
- >>> fs2.unify(fs1)
- [A=[B='b', C='c']]
- A basic case of reentrant unification
- >>> fs4 = FeatStruct('[A=(1)[B=b], E=[F->(1)]]')
- >>> fs5 = FeatStruct("[A=[C='c'], E=[F=[D='d']]]")
- >>> fs4.unify(fs5)
- [A=(1)[B='b', C='c', D='d'], E=[F->(1)]]
- >>> fs5.unify(fs4)
- [A=(1)[B='b', C='c', D='d'], E=[F->(1)]]
- More than 2 paths to a value
- >>> fs1 = FeatStruct("[a=[],b=[],c=[],d=[]]")
- >>> fs2 = FeatStruct('[a=(1)[], b->(1), c->(1), d->(1)]')
- >>> fs1.unify(fs2)
- [a=(1)[], b->(1), c->(1), d->(1)]
- fs1[a] gets unified with itself
- >>> fs1 = FeatStruct('[x=(1)[], y->(1)]')
- >>> fs2 = FeatStruct('[x=(1)[], y->(1)]')
- >>> fs1.unify(fs2)
- [x=(1)[], y->(1)]
- Bound variables should get forwarded appropriately
- >>> fs1 = FeatStruct('[A=(1)[X=x], B->(1), C=?cvar, D=?dvar]')
- >>> fs2 = FeatStruct('[A=(1)[Y=y], B=(2)[Z=z], C->(1), D->(2)]')
- >>> fs1.unify(fs2)
- [A=(1)[X='x', Y='y', Z='z'], B->(1), C->(1), D->(1)]
- >>> fs2.unify(fs1)
- [A=(1)[X='x', Y='y', Z='z'], B->(1), C->(1), D->(1)]
- Cyclic structure created by unification.
- >>> fs1 = FeatStruct('[F=(1)[], G->(1)]')
- >>> fs2 = FeatStruct('[F=[H=(2)[]], G->(2)]')
- >>> fs3 = fs1.unify(fs2)
- >>> fs3
- [F=(1)[H->(1)], G->(1)]
- >>> fs3['F'] is fs3['G']
- True
- >>> fs3['F'] is fs3['G']['H']
- True
- >>> fs3['F'] is fs3['G']['H']['H']
- True
- >>> fs3['F'] is fs3['F']['H']['H']['H']['H']['H']['H']['H']['H']
- True
- Cyclic structure created w/ variables.
- >>> fs1 = FeatStruct('[F=[H=?x]]')
- >>> fs2 = FeatStruct('[F=?x]')
- >>> fs3 = fs1.unify(fs2, rename_vars=False)
- >>> fs3
- [F=(1)[H->(1)]]
- >>> fs3['F'] is fs3['F']['H']
- True
- >>> fs3['F'] is fs3['F']['H']['H']
- True
- >>> fs3['F'] is fs3['F']['H']['H']['H']['H']['H']['H']['H']['H']
- True
- Unifying w/ a cyclic feature structure.
- >>> fs4 = FeatStruct('[F=[H=[H=[H=(1)[]]]], K->(1)]')
- >>> fs3.unify(fs4)
- [F=(1)[H->(1)], K->(1)]
- >>> fs4.unify(fs3)
- [F=(1)[H->(1)], K->(1)]
- Variable bindings should preserve reentrance.
- >>> bindings = {}
- >>> fs1 = FeatStruct("[a=?x]")
- >>> fs2 = fs1.unify(FeatStruct("[a=[]]"), bindings)
- >>> fs2['a'] is bindings[Variable('?x')]
- True
- >>> fs2.unify(FeatStruct("[b=?x]"), bindings)
- [a=(1)[], b->(1)]
- Aliased variable tests
- >>> fs1 = FeatStruct("[a=?x, b=?x]")
- >>> fs2 = FeatStruct("[b=?y, c=?y]")
- >>> bindings = {}
- >>> fs3 = fs1.unify(fs2, bindings)
- >>> fs3
- [a=?x, b=?x, c=?x]
- >>> bindings
- {Variable('?y'): Variable('?x')}
- >>> fs3.unify(FeatStruct("[a=1]"))
- [a=1, b=1, c=1]
- If we keep track of the bindings, then we can use the same variable
- over multiple calls to unify.
- >>> bindings = {}
- >>> fs1 = FeatStruct('[a=?x]')
- >>> fs2 = fs1.unify(FeatStruct('[a=[]]'), bindings)
- >>> fs2.unify(FeatStruct('[b=?x]'), bindings)
- [a=(1)[], b->(1)]
- >>> bindings
- {Variable('?x'): []}
- ..
- >>> del fs1, fs2, fs3, fs4, fs5 # clean-up
- Unification Bindings
- --------------------
- >>> bindings = {}
- >>> fs1 = FeatStruct('[a=?x]')
- >>> fs2 = FeatStruct('[a=12]')
- >>> fs3 = FeatStruct('[b=?x]')
- >>> fs1.unify(fs2, bindings)
- [a=12]
- >>> bindings
- {Variable('?x'): 12}
- >>> fs3.substitute_bindings(bindings)
- [b=12]
- >>> fs3 # substitute_bindings didn't mutate fs3.
- [b=?x]
- >>> fs2.unify(fs3, bindings)
- [a=12, b=12]
- >>> bindings = {}
- >>> fs1 = FeatStruct('[a=?x, b=1]')
- >>> fs2 = FeatStruct('[a=5, b=?x]')
- >>> fs1.unify(fs2, bindings)
- [a=5, b=1]
- >>> sorted(bindings.items())
- [(Variable('?x'), 5), (Variable('?x2'), 1)]
- ..
- >>> del fs1, fs2, fs3 # clean-up
- Expressions
- -----------
- >>> e = Expression.fromstring('\\P y.P(z,y)')
- >>> fs1 = FeatStruct(x=e, y=Variable('z'))
- >>> fs2 = FeatStruct(y=VariableExpression(Variable('John')))
- >>> fs1.unify(fs2)
- [x=<\P y.P(John,y)>, y=<John>]
- Remove Variables
- ----------------
- >>> FeatStruct('[a=?x, b=12, c=[d=?y]]').remove_variables()
- [b=12, c=[]]
- >>> FeatStruct('(1)[a=[b=?x,c->(1)]]').remove_variables()
- (1)[a=[c->(1)]]
- Equality & Hashing
- ------------------
- The `equal_values` method checks whether two feature structures assign
- the same value to every feature. If the optional argument
- ``check_reentrances`` is supplied, then it also returns false if there
- is any difference in the reentrances.
- >>> a = FeatStruct('(1)[x->(1)]')
- >>> b = FeatStruct('(1)[x->(1)]')
- >>> c = FeatStruct('(1)[x=[x->(1)]]')
- >>> d = FeatStruct('[x=(1)[x->(1)]]')
- >>> e = FeatStruct('(1)[x=[x->(1), y=1], y=1]')
- >>> def compare(x,y):
- ... assert x.equal_values(y, True) == y.equal_values(x, True)
- ... assert x.equal_values(y, False) == y.equal_values(x, False)
- ... if x.equal_values(y, True):
- ... assert x.equal_values(y, False)
- ... print('equal values, same reentrance')
- ... elif x.equal_values(y, False):
- ... print('equal values, different reentrance')
- ... else:
- ... print('different values')
- >>> compare(a, a)
- equal values, same reentrance
- >>> compare(a, b)
- equal values, same reentrance
- >>> compare(a, c)
- equal values, different reentrance
- >>> compare(a, d)
- equal values, different reentrance
- >>> compare(c, d)
- equal values, different reentrance
- >>> compare(a, e)
- different values
- >>> compare(c, e)
- different values
- >>> compare(d, e)
- different values
- >>> compare(e, e)
- equal values, same reentrance
- Feature structures may not be hashed until they are frozen:
- >>> hash(a)
- Traceback (most recent call last):
- . . .
- TypeError: FeatStructs must be frozen before they can be hashed.
- >>> a.freeze()
- >>> v = hash(a)
- Feature structures define hash consistently. The following example
- looks at the hash value for each (fs1,fs2) pair; if their hash values
- are not equal, then they must not be equal. If their hash values are
- equal, then display a message, and indicate whether their values are
- indeed equal. Note that c and d currently have the same hash value,
- even though they are not equal. That is not a bug, strictly speaking,
- but it wouldn't be a bad thing if it changed.
- >>> for fstruct in (a, b, c, d, e):
- ... fstruct.freeze()
- >>> for fs1_name in 'abcde':
- ... for fs2_name in 'abcde':
- ... fs1 = locals()[fs1_name]
- ... fs2 = locals()[fs2_name]
- ... if hash(fs1) != hash(fs2):
- ... assert fs1 != fs2
- ... else:
- ... print('%s and %s have the same hash value,' %
- ... (fs1_name, fs2_name))
- ... if fs1 == fs2: print('and are equal')
- ... else: print('and are not equal')
- a and a have the same hash value, and are equal
- a and b have the same hash value, and are equal
- b and a have the same hash value, and are equal
- b and b have the same hash value, and are equal
- c and c have the same hash value, and are equal
- c and d have the same hash value, and are not equal
- d and c have the same hash value, and are not equal
- d and d have the same hash value, and are equal
- e and e have the same hash value, and are equal
- ..
- >>> del a, b, c, d, e, v # clean-up
- Tracing
- -------
- >>> fs1 = FeatStruct('[a=[b=(1)[], c=?x], d->(1), e=[f=?x]]')
- >>> fs2 = FeatStruct('[a=(1)[c="C"], e=[g->(1)]]')
- >>> fs1.unify(fs2, trace=True)
- <BLANKLINE>
- Unification trace:
- / [a=[b=(1)[], c=?x], d->(1), e=[f=?x]]
- |\ [a=(1)[c='C'], e=[g->(1)]]
- |
- | Unify feature: a
- | / [b=[], c=?x]
- | |\ [c='C']
- | |
- | | Unify feature: a.c
- | | / ?x
- | | |\ 'C'
- | | |
- | | +-->Variable('?x')
- | |
- | +-->[b=[], c=?x]
- | Bindings: {?x: 'C'}
- |
- | Unify feature: e
- | / [f=?x]
- | |\ [g=[c='C']]
- | |
- | +-->[f=?x, g=[b=[], c=?x]]
- | Bindings: {?x: 'C'}
- |
- +-->[a=(1)[b=(2)[], c='C'], d->(2), e=[f='C', g->(1)]]
- Bindings: {?x: 'C'}
- [a=(1)[b=(2)[], c='C'], d->(2), e=[f='C', g->(1)]]
- >>>
- >>> fs1 = FeatStruct('[a=?x, b=?z, c=?z]')
- >>> fs2 = FeatStruct('[a=?y, b=?y, c=?q]')
- >>> #fs1.unify(fs2, trace=True)
- >>>
- ..
- >>> del fs1, fs2 # clean-up
- Unification on Dicts & Lists
- ----------------------------
- It's possible to do unification on dictionaries:
- >>> from nltk.featstruct import unify
- >>> pprint(unify(dict(x=1, y=dict(z=2)), dict(x=1, q=5)), width=1)
- {'q': 5, 'x': 1, 'y': {'z': 2}}
- It's possible to do unification on lists as well:
- >>> unify([1, 2, 3], [1, Variable('x'), 3])
- [1, 2, 3]
- Mixing dicts and lists is fine:
- >>> pprint(unify([dict(x=1, y=dict(z=2)),3], [dict(x=1, q=5),3]),
- ... width=1)
- [{'q': 5, 'x': 1, 'y': {'z': 2}}, 3]
- Mixing dicts and FeatStructs is discouraged:
- >>> unify(dict(x=1), FeatStruct(x=1))
- Traceback (most recent call last):
- . . .
- ValueError: Mixing FeatStruct objects with Python dicts and lists is not supported.
- But you can do it if you really want, by explicitly stating that both
- dictionaries and FeatStructs should be treated as feature structures:
- >>> unify(dict(x=1), FeatStruct(x=1), fs_class=(dict, FeatStruct))
- {'x': 1}
- Finding Conflicts
- -----------------
- >>> from nltk.featstruct import conflicts
- >>> fs1 = FeatStruct('[a=[b=(1)[c=2], d->(1), e=[f->(1)]]]')
- >>> fs2 = FeatStruct('[a=[b=[c=[x=5]], d=[c=2], e=[f=[c=3]]]]')
- >>> for path in conflicts(fs1, fs2):
- ... print('%-8s: %r vs %r' % ('.'.join(path), fs1[path], fs2[path]))
- a.b.c : 2 vs [x=5]
- a.e.f.c : 2 vs 3
- ..
- >>> del fs1, fs2 # clean-up
- Retracting Bindings
- -------------------
- >>> from nltk.featstruct import retract_bindings
- >>> bindings = {}
- >>> fs1 = FeatStruct('[a=?x, b=[c=?y]]')
- >>> fs2 = FeatStruct('[a=(1)[c=[d=1]], b->(1)]')
- >>> fs3 = fs1.unify(fs2, bindings)
- >>> print(fs3)
- [ a = (1) [ c = [ d = 1 ] ] ]
- [ ]
- [ b -> (1) ]
- >>> pprint(bindings)
- {Variable('?x'): [c=[d=1]], Variable('?y'): [d=1]}
- >>> retract_bindings(fs3, bindings)
- [a=?x, b=?x]
- >>> pprint(bindings)
- {Variable('?x'): [c=?y], Variable('?y'): [d=1]}
- Squashed Bugs
- ~~~~~~~~~~~~~
- In svn rev 5167, unifying two feature structures that used the same
- variable would cause those variables to become aliased in the output.
- >>> fs1 = FeatStruct('[a=?x]')
- >>> fs2 = FeatStruct('[b=?x]')
- >>> fs1.unify(fs2)
- [a=?x, b=?x2]
- There was a bug in svn revision 5172 that caused `rename_variables` to
- rename variables to names that are already used.
- >>> FeatStruct('[a=?x, b=?x2]').rename_variables(
- ... vars=[Variable('?x')])
- [a=?x3, b=?x2]
- >>> fs1 = FeatStruct('[a=?x]')
- >>> fs2 = FeatStruct('[a=?x, b=?x2]')
- >>> fs1.unify(fs2)
- [a=?x, b=?x2]
- There was a bug in svn rev 5167 that caused us to get the following
- example wrong. Basically the problem was that we only followed
- 'forward' pointers for other, not self, when unifying two feature
- structures. (nb: this test assumes that features are unified in
- alphabetical order -- if they are not, it might pass even if the bug
- is present.)
- >>> fs1 = FeatStruct('[a=[x=1], b=?x, c=?x]')
- >>> fs2 = FeatStruct('[a=(1)[], b->(1), c=[x=2]]')
- >>> print(fs1.unify(fs2))
- None
- ..
- >>> del fs1, fs2 # clean-up
|