Zope 3关系索引。zc.relation的前身。
项目描述
zc.relationship 包目前包含两种主要组件:一个关系索引和一些关系容器。它们都设计用于 ZODB 中,尽管索引足够灵活,也可以用于其他上下文。它们共享这样的模型:关系是完整的对象,并针对优化搜索进行索引。它们还共享执行优化非传递和传递关系搜索的能力,以及支持在关系令牌上执行任意过滤搜索的能力。
索引是一个非常通用的组件,可用于优化 N-元关系的搜索,可以单独使用或用于目录中,可以与可插拔的令牌生成方案一起使用,通常试图提供一个相对无政策的工具。它主要被期望用作更专业化和受限制的工具和 API 的引擎。
关系容器使用索引通过派生映射接口管理双向关系。它是索引独立使用的合理示例。
另一个示例,使用容器模型但支持五向关系(“来源”、“目标”、“关系”、“getContext”、“状态”),可以在 plone.relations 中找到。它的 README 是很好的阅读材料。
http://dev.plone.org/plone/browser/plone.relations/trunk/plone/relations
本文件描述了关系索引。有关关系容器的文档,请参阅 container.rst。
请注意:以下描述的 zc.relationship 中的索引现在存在是为了向后兼容。zc.relation.catalog 包含索引代码的最新版本,与旧版本不兼容。
目录
概述
索引对世界的看法非常精确:实例化需要多个指定配置的参数;并且使用索引需要你承认关系及其相关的索引值通常在索引中被令牌化。这种精确性以一些易用性为代价,换取了灵活性、强大和效率的可能性。话虽如此,索引的 API 设计要保持一致,并且基本上遵循“只有一种方法可以这样做”的原则。
最简单的示例
在深入 N-向灵活性和其他更复杂的部分之前,让我们先快速演示一个基本示例:从一个值到另一个值的双向关系。这将让你对关系索引有一个了解,并让你能够合理地使用它进行轻到中等的使用。如果你打算使用更多的功能或在高容量下使用它,请考虑尝试理解整个文档。
假设我们正在模拟人与其主管之间的关系:员工可能只有一个主管。
假设进一步地说,员工姓名是唯一的,可以用作代表员工。我们可以使用姓名作为我们的“标记”。标记类似于关系数据库中的主键,或者在Zope 3中的intid或keyreference,以某种方式唯一地标识一个对象,该对象可以可靠地排序,并且可以在适当的上下文中解析为对象。
>>> from __future__ import print_function >>> from functools import total_ordering >>> employees = {} # we'll use this to resolve the "name" tokens >>> @total_ordering ... class Employee(object): ... def __init__(self, name, supervisor=None): ... if name in employees: ... raise ValueError('employee with same name already exists') ... self.name = name # expect this to be readonly ... self.supervisor = supervisor ... employees[name] = self ... def __repr__(self): # to make the tests prettier... ... return '<' + self.name + '>' ... def __eq__(self, other): ... return self is other ... def __lt__(self, other): # to make the tests prettier... ... # pukes if other doesn't have name ... return self.name < other.name ...
因此,我们需要定义如何将员工转换为他们的标记。这很简单。(我们将在下面详细解释这个函数的参数,但现在我们只是想给出一个“快速概述”。)
>>> def dumpEmployees(emp, index, cache): ... return emp.name ...
我们还需要一种方法将标记转换为员工。我们使用我们的字典来完成这个任务。
>>> def loadEmployees(token, index, cache): ... return employees[token] ...
我们还需要一种方法告诉索引找到索引的管理员。
>>> def supervisor(emp, index): ... return emp.supervisor # None or another employee ...
现在我们已经有了开始索引所需的一切。Index的第一个参数是要索引的属性:我们传递了supervisor函数(在此情况下也用于定义索引的名称,因为我们没有明确传递一个),dump和load函数,以及一个指定可以容纳我们的标记的集合的BTree模块(OO或OL也应该可以工作)。作为关键字参数,我们告诉索引如何转储和加载我们的关系标记——在本例中是相同的函数——以及对于集合来说什么是合理的BTree模块(再次,我们选择OI,但OO或OL也应该可以工作)。
>>> from zc.relationship import index >>> import BTrees >>> ix = index.Index( ... ({'callable': supervisor, 'dump': dumpEmployees, ... 'load': loadEmployees, 'btree': BTrees.family32.OI},), ... dumpRel=dumpEmployees, loadRel=loadEmployees, ... relFamily=BTrees.family32.OI)
现在让我们创建一些员工。
>>> a = Employee('Alice') >>> b = Employee('Betty', a) >>> c = Employee('Chuck', a) >>> d = Employee('Duane', b) >>> e = Employee('Edgar', b) >>> f = Employee('Frank', c) >>> g = Employee('Grant', c) >>> h = Employee('Howie', d)
以一种您将在文档末尾熟悉的方式,让我们展示层次结构。
Alice __/ \__ Betty Chuck / \ / \ Duane Edgar Frank Grant | Howie
那么谁为Alice工作?要询问索引,我们需要告诉它关于他们的信息。
>>> for emp in (a,b,c,d,e,f,g,h): ... ix.index(emp) ...
现在我们可以提问了。我们总是需要用标记来提问。索引提供了一个方法来尝试使其更方便:tokenizeQuery [1]。
查询的拼写将在后面更详细地描述,但简单来说,字典中的键指定属性名称,值指定约束。
>>> t = ix.tokenizeQuery >>> sorted(ix.findRelationshipTokens(t({'supervisor': a}))) ['Betty', 'Chuck'] >>> sorted(ix.findRelationships(t({'supervisor': a}))) [<Betty>, <Chuck>]
我们如何找到员工的上级?好吧,在这种情况下,看看属性!如果您可以使用通常会在ZODB中获胜的属性,那就太好了。如果您想查看索引中的数据,虽然这很简单。Howie的上级是谁?查询中的None键表示我们正在匹配关系标记本身 [2]。
您可以搜索尚未索引的关系。
>>> list(ix.findRelationshipTokens({None: 'Ygritte'})) []
您还可以将搜索与None结合,以示完整。
>>> list(ix.findRelationshipTokens({None: 'Alice', 'supervisor': None})) ['Alice'] >>> list(ix.findRelationshipTokens({None: 'Alice', 'supervisor': 'Betty'})) [] >>> list(ix.findRelationshipTokens({None: 'Betty', 'supervisor': 'Alice'})) ['Betty']
>>> h.supervisor <Duane> >>> list(ix.findValueTokens('supervisor', t({None: h}))) ['Duane'] >>> list(ix.findValues('supervisor', t({None: h}))) [<Duane>]
关于传递性搜索呢?好吧,您需要告诉索引如何遍历树。在像这样的简单情况下,索引的TransposingTransitiveQueriesFactory将起到作用。我们只需告诉工厂将两个键None和“supervisor”进行转换。然后我们可以在传递性搜索的查询中使用它。
>>> factory = index.TransposingTransitiveQueriesFactory(None, 'supervisor')
Howie所有传递性上级是谁(这将在图中查找)?
>>> list(ix.findValueTokens('supervisor', t({None: h}), ... transitiveQueriesFactory=factory)) ['Duane', 'Betty', 'Alice'] >>> list(ix.findValues('supervisor', t({None: h}), ... transitiveQueriesFactory=factory)) [<Duane>, <Betty>, <Alice>]
Betty所有传递性下属是谁,广度优先(这将在图中向下查找)?
>>> people = list(ix.findRelationshipTokens( ... t({'supervisor': b}), transitiveQueriesFactory=factory)) >>> sorted(people[:2]) ['Duane', 'Edgar'] >>> people[2] 'Howie' >>> people = list(ix.findRelationships( ... t({'supervisor': b}), transitiveQueriesFactory=factory)) >>> sorted(people[:2]) [<Duane>, <Edgar>] >>> people[2] <Howie>
这个传递性搜索实际上是您在这里想要的唯一传递性工厂,因此将其作为默认值可能是安全的。虽然索引上的大多数属性必须在实例化时设置,但这个属性我们可以在之后设置。
>>> ix.defaultTransitiveQueriesFactory = factory
现在所有搜索都是传递性的。
>>> list(ix.findValueTokens('supervisor', t({None: h}))) ['Duane', 'Betty', 'Alice'] >>> list(ix.findValues('supervisor', t({None: h}))) [<Duane>, <Betty>, <Alice>] >>> people = list(ix.findRelationshipTokens(t({'supervisor': b}))) >>> sorted(people[:2]) ['Duane', 'Edgar'] >>> people[2] 'Howie' >>> people = list(ix.findRelationships(t({'supervisor': b}))) >>> sorted(people[:2]) [<Duane>, <Edgar>] >>> people[2] <Howie>
我们可以使用maxDepth强制进行非传递性搜索或特定搜索深度 [3]。
具有maxDepth > 1但没有transitiveQueriesFactory的搜索将引发错误。
>>> ix.defaultTransitiveQueriesFactory = None >>> ix.findRelationshipTokens({'supervisor': 'Duane'}, maxDepth=3) Traceback (most recent call last): ... ValueError: if maxDepth not in (None, 1), queryFactory must be available
>>> ix.defaultTransitiveQueriesFactory = factory
>>> list(ix.findValueTokens('supervisor', t({None: h}), maxDepth=1)) ['Duane'] >>> list(ix.findValues('supervisor', t({None: h}), maxDepth=1)) [<Duane>] >>> sorted(ix.findRelationshipTokens(t({'supervisor': b}), maxDepth=1)) ['Duane', 'Edgar'] >>> sorted(ix.findRelationships(t({'supervisor': b}), maxDepth=1)) [<Duane>, <Edgar>]
传递性搜索可以处理递归循环并具有其他功能,这些将在更大的示例和接口中讨论。
我们最后的两个入门示例展示了三种其他方法:isLinked、findRelationshipTokenChains和findRelationshipChains。
isLinked 允许您判断两个查询是否相关联。Alice 是否是 Howie 的主管?Chuck 呢?(请注意,如果您的关联描述了一个层次结构,向上搜索层次结构通常更有效率,因此在这种情况下,第二个问题组通常比第一个问题组更可取。)
>>> ix.isLinked(t({'supervisor': a}), targetQuery=t({None: h})) True >>> ix.isLinked(t({'supervisor': c}), targetQuery=t({None: h})) False >>> ix.isLinked(t({None: h}), targetQuery=t({'supervisor': a})) True >>> ix.isLinked(t({None: h}), targetQuery=t({'supervisor': c})) False
findRelationshipTokenChains 和 findRelationshipChains 帮助您发现事物如何通过传递相关联。一个“链”是一系列的关联。例如,Alice 和 Howie 之间的指挥链是什么?
>>> list(ix.findRelationshipTokenChains( ... t({'supervisor': a}), targetQuery=t({None: h}))) [('Betty', 'Duane', 'Howie')] >>> list(ix.findRelationshipChains( ... t({'supervisor': a}), targetQuery=t({None: h}))) [(<Betty>, <Duane>, <Howie>)]
这为您提供了基本索引功能的一个快速概述。这应该足够您开始使用了。现在,如果您想了解详细信息,我们将更深入地探讨。
开始 N-向示例
为了进一步锻炼索引,我们将构建一个相对复杂的关系以进行索引。让我们假设我们正在模拟一个通用的设置,例如在上下文中 SUBJECT RELATIONSHIPTYPE OBJECT。这可以让用户定义关系类型,并在运行时对其进行索引。上下文可以是某个项目,因此我们可以说
“Fred” 在 “zope.org 重设计项目” 中“担任” “项目经理”。
映射到关系对象的部分,那将是
["Fred" (主题)] ["担任" (关系类型)] ["项目经理" (对象)] 在 ["zope.org 重设计项目" (上下文)]。
没有上下文,您仍然可以进行一些有趣的事情,例如
["Ygritte" (主题)] ["管理" (关系类型)] ["Uther" (对象)]
在我们的新示例中,我们将利用索引可以接受接口属性进行索引的事实。因此,让我们定义一个不带上下文的基本接口,然后是一个带上下文的扩展接口。
>>> from zope import interface >>> class IRelationship(interface.Interface): ... subjects = interface.Attribute( ... 'The sources of the relationship; the subject of the sentence') ... relationshiptype = interface.Attribute( ... '''unicode: the single relationship type of this relationship; ... usually contains the verb of the sentence.''') ... objects = interface.Attribute( ... '''the targets of the relationship; usually a direct or ... indirect object in the sentence''') ... >>> class IContextAwareRelationship(IRelationship): ... def getContext(): ... '''return a context for the relationship''' ...
现在我们将创建一个索引。为此,我们必须至少传递一个可迭代的对象,描述要索引的值。可迭代的每个项目必须是接口元素(与接口关联的 zope.interface.Attribute 或 zope.interface.Method,通常通过类似于 IRelationship['subjects'] 的拼写获得)或字典。每个字典必须具有 'element' 键,这是要索引的接口元素;或者 'callable' 键,这是在上面的简单介绍性示例中显示的可调用函数 [4]。
使用同时包含 'element' 和 'callable' 键的字典实例化索引是一个错误
>>> def subjects(obj, index, cache): ... return obj.subjects ... >>> ix = index.Index( ... ({'element': IRelationship['subjects'], ... 'callable': subjects, 'multiple': True}, ... IRelationship['relationshiptype'], ... {'element': IRelationship['objects'], 'multiple': True}, ... IContextAwareRelationship['getContext']), ... index.TransposingTransitiveQueriesFactory('subjects', 'objects')) Traceback (most recent call last): ... ValueError: cannot provide both callable and element
正如您所期望的,您必须提供其中之一。
>>> ix = index.Index( ... ({'multiple': True}, ... IRelationship['relationshiptype'], ... {'element': IRelationship['objects'], 'multiple': True}, ... IContextAwareRelationship['getContext']), ... index.TransposingTransitiveQueriesFactory('subjects', 'objects')) Traceback (most recent call last): ... ValueError: must provide element or callable
它还可以包含其他键来覆盖元素的默认索引行为。
元素或可调用的 __name__ 将用于在查询中引用此元素,除非字典具有 'name' 键,它必须是一个非空字符串 [5]。
在没有名称的情况下传递可调用函数是可能的,在这种情况下,您必须明确指定一个名称。
>>> @total_ordering ... class AttrGetter(object): ... def __init__(self, attr): ... self.attr = attr ... def __eq__(self, other): ... return self is other ... def __lt__(self, other): ... return self.attr < getattr(other, 'attr', other) ... def __call__(self, obj, index, cache): ... return getattr(obj, self.attr, None) ... >>> subjects = AttrGetter('subjects') >>> ix = index.Index( ... ({'callable': subjects, 'multiple': True}, ... IRelationship['relationshiptype'], ... {'element': IRelationship['objects'], 'multiple': True}, ... IContextAwareRelationship['getContext']), ... index.TransposingTransitiveQueriesFactory('subjects', 'objects')) Traceback (most recent call last): ... ValueError: no name specified >>> ix = index.Index( ... ({'callable': subjects, 'multiple': True, 'name': subjects}, ... IRelationship['relationshiptype'], ... {'element': IRelationship['objects'], 'multiple': True}, ... IContextAwareRelationship['getContext']), ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'))
无论如何,指定相同的名称或元素两次都是错误的。
>>> ix = index.Index( ... ({'callable': subjects, 'multiple': True, 'name': 'objects'}, ... IRelationship['relationshiptype'], ... {'element': IRelationship['objects'], 'multiple': True}, ... IContextAwareRelationship['getContext']), ... index.TransposingTransitiveQueriesFactory('subjects', 'objects')) ... # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: ('name already used', 'objects')
>>> ix = index.Index( ... ({'callable': subjects, 'multiple': True, 'name': 'subjects'}, ... IRelationship['relationshiptype'], ... {'callable': subjects, 'multiple': True, 'name': 'objects'}, ... IContextAwareRelationship['getContext']), ... index.TransposingTransitiveQueriesFactory('subjects', 'objects')) ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE Traceback (most recent call last): ... ValueError: ('element already indexed', <zc.relationship.README.AttrGetter object at ...>)
>>> ix = index.Index( ... ({'element': IRelationship['objects'], 'multiple': True, ... 'name': 'subjects'}, ... IRelationship['relationshiptype'], ... {'element': IRelationship['objects'], 'multiple': True}, ... IContextAwareRelationship['getContext']), ... index.TransposingTransitiveQueriesFactory('subjects', 'objects')) ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE Traceback (most recent call last): ... ValueError: ('element already indexed', <zope.interface.interface.Attribute object at ...>)
除非字典具有 'multiple' 键并且其值相当于 True,否则假定元素是单个值。在我们的示例中,“subjects” 和 “objects” 可能是多个值,而 “relationshiptype” 和 “getContext” 是单个值。
默认情况下,元素的值将被标记并使用 intid 工具解析,并存储在 BTrees.IFBTree 中。如果您想要使对象标记容易与典型的 Zope 3 目录结果合并,这是一个不错的选择。如果您需要对任何元素有不同的行为,您可以为每个字典指定三个键
‘dump’,标记器,一个接收 (obj, index, cache) 并返回标记的可调用函数;
‘load’,标记解析器,一个接收 (token, index, cache) 并返回标记表示的对象的可调用函数;以及
‘btree’,用于存储和处理标记的 btree 模块,例如 BTrees.OOBTree。
如果您提供自定义的“dump”,几乎肯定需要提供自定义的“load”;如果您的标记不是整数,则您需要指定不同的“btree”(截至本文撰写时,为BTrees.OOBTree或BTrees.OIBTree)。
标记化函数(“dump”)必须返回同质、不可变的标记:也就是说,任何给定的标记化器只能返回可以无歧义地排序的标记,这在Python版本之间通常意味着它们都是同一类型。例如,标记化器只能返回整数,或者只能返回字符串,或者只能返回字符串元组等。用于同一索引中不同元素的标记化器可能返回不同的类型。它们也可能返回与其他标记化器相同的值,以表示不同的对象:存储是分开的。
请注意,在字典中,dump和load也可以显式地设置为None:这意味着值已经适当地用作标记。这启用了《优化关系索引使用》部分中描述的优化Optimizing relationship index use [6]。
不允许只提供“load”和“dump”中的一个。
>>> ix = index.Index( ... ({'element': IRelationship['subjects'], 'multiple': True, ... 'name': 'subjects','dump': None}, ... IRelationship['relationshiptype'], ... {'element': IRelationship['objects'], 'multiple': True}, ... IContextAwareRelationship['getContext']), ... index.TransposingTransitiveQueriesFactory('subjects', 'objects')) ... # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: either both of 'dump' and 'load' must be None, or neither
>>> ix = index.Index( ... ({'element': IRelationship['objects'], 'multiple': True, ... 'name': 'subjects','load': None}, ... IRelationship['relationshiptype'], ... {'element': IRelationship['objects'], 'multiple': True}, ... IContextAwareRelationship['getContext']), ... index.TransposingTransitiveQueriesFactory('subjects', 'objects')) ... # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: either both of 'dump' and 'load' must be None, or neither
除了类所需的单个参数外,签名还包含四个可选参数。下一个是“defaultTransitiveQueriesFactory”,允许您指定接口ITransitiveQueriesFactory中描述的可调用对象。如果没有它,传递搜索将需要每次都明确地提供一个工厂,这可能会很麻烦。索引包提供了一个简单的实现,支持在两个索引元素之后执行传递搜索(TransposingTransitiveQueriesFactory),并且本文描述了更复杂的可能传递行为,这些行为可以建模。在我们的示例中,“subjects”和“objects”是默认传递字段,因此如果Ygritte(SUBJECT)管理Uther(OBJECT),并且Uther(SUBJECT)管理Emily(OBJECT),则搜索所有那些由Ygritte传递管理的将Uther从OBJECT转换为SUBJECT并找到Uther管理Emily。类似地,为了找到Emily的所有传递管理者,Uther将在搜索中从SUBJECT转换为OBJECT[7]。
该工厂允许您指定两个名称,这两个名称在传递遍历中会进行转换。通常您希望对于层次结构和类似的变体也是如此:如文本所述,在更复杂的关系中可能需要更复杂的遍历,例如在系谱中。
它支持转换值和关系标记,如文本中所示。
在本页脚注中,我们将使用索引存根来探索这个小的工厂。
>>> factory = index.TransposingTransitiveQueriesFactory( ... 'subjects', 'objects') >>> class StubIndex(object): ... def findValueTokenSet(self, rel, name): ... return { ... ('foo', 'objects'): ('bar',), ... ('bar', 'subjects'): ('foo',)}[(rel, name)] ... >>> ix = StubIndex() >>> list(factory(['foo'], {'subjects': 'foo'}, ix, {})) [{'subjects': 'bar'}] >>> list(factory(['bar'], {'objects': 'bar'}, ix, {})) [{'objects': 'foo'}]
如果您指定了两个字段,则不会进行转换。
>>> list(factory(['foo'], {'objects': 'bar', 'subjects': 'foo'}, ix, {})) []
如果您指定了更多字段,则将保持它们静态。
>>> list(factory(['foo'], {'subjects': 'foo', 'getContext': 'shazam'}, ... ix, {})) == [{'subjects': 'bar', 'getContext': 'shazam'}] True
接下来三个参数“dumpRel”、“loadRel”和“relFamily”与关系标记有关。默认值假设您将使用intid标记关系,因此“dumpRel”和“loadRel”分别使用intid实用程序标记和解析;而“relFamily”默认为BTrees.IFBTree。
如果关系标记(从“findRelationshipChains”或“apply”或“findRelationshipTokenSet”或作为大多数搜索方法的过滤器中的过滤器)要与其他目录结果合并,则关系标记应基于intid,如默认值所示。例如,如果某些关系仅基于安全机制对某些用户可用,并且您保留了一个索引,那么您将希望使用基于当前用户可查看的关系标记的目录索引保留的过滤器的过滤器。
如果您无法或不愿意使用intid关系标记,标记必须仍然是上面描述的索引值标记的同质和不可变的。
最后一个参数是“family”,它实际上默认为BTrees.family32。如果您没有明确指定您的值和关系集的BTree模块,这个值将决定您是使用32位还是64位的IFBTrees [8]。
以下是一个指定family64的例子。这是一个“白盒”演示,它查看了一些内部结构。
>>> ix = index.Index( # 32 bit default ... ({'element': IRelationship['subjects'], 'multiple': True}, ... IRelationship['relationshiptype'], ... {'element': IRelationship['objects'], 'multiple': True}, ... IContextAwareRelationship['getContext']), ... index.TransposingTransitiveQueriesFactory('subjects', 'objects')) >>> ix._relTools['BTree'] is BTrees.family32.IF.BTree True >>> ix._attrs['subjects']['BTree'] is BTrees.family32.IF.BTree True >>> ix._attrs['objects']['BTree'] is BTrees.family32.IF.BTree True >>> ix._attrs['getContext']['BTree'] is BTrees.family32.IF.BTree True
>>> ix = index.Index( # explicit 32 bit ... ({'element': IRelationship['subjects'], 'multiple': True}, ... IRelationship['relationshiptype'], ... {'element': IRelationship['objects'], 'multiple': True}, ... IContextAwareRelationship['getContext']), ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'), ... family=BTrees.family32) >>> ix._relTools['BTree'] is BTrees.family32.IF.BTree True >>> ix._attrs['subjects']['BTree'] is BTrees.family32.IF.BTree True >>> ix._attrs['objects']['BTree'] is BTrees.family32.IF.BTree True >>> ix._attrs['getContext']['BTree'] is BTrees.family32.IF.BTree True
>>> ix = index.Index( # explicit 64 bit ... ({'element': IRelationship['subjects'], 'multiple': True}, ... IRelationship['relationshiptype'], ... {'element': IRelationship['objects'], 'multiple': True}, ... IContextAwareRelationship['getContext']), ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'), ... family=BTrees.family64) >>> ix._relTools['BTree'] is BTrees.family64.IF.BTree True >>> ix._attrs['subjects']['BTree'] is BTrees.family64.IF.BTree True >>> ix._attrs['objects']['BTree'] is BTrees.family64.IF.BTree True >>> ix._attrs['getContext']['BTree'] is BTrees.family64.IF.BTree True
如果我们注册了IIntId实用工具并想使用默认设置,那么为我们的关系实例化索引将类似于以下:
>>> ix = index.Index( ... ({'element': IRelationship['subjects'], 'multiple': True}, ... IRelationship['relationshiptype'], ... {'element': IRelationship['objects'], 'multiple': True}, ... IContextAwareRelationship['getContext']), ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'))
这是一个简单的情况。几乎没有麻烦,我们就得到了一个IIndex和一个实现ITransitiveQueriesFactory的默认TransitiveQueriesFactory,它按照上述方式切换主语和对象。
>>> from zc.relationship import interfaces >>> from zope.interface.verify import verifyObject >>> verifyObject(interfaces.IIndex, ix) True >>> verifyObject( ... interfaces.ITransitiveQueriesFactory, ... ix.defaultTransitiveQueriesFactory) True
然而,为了更复杂的例子,我们将更充分地利用索引的选项——我们将使用至少一个‘name’,‘dump’,‘load’和‘btree’。
‘subjects’和‘objects’将使用一个基于自定义整数的标记生成器。它们将共享标记,这将使我们能够使用默认的TransposingTransitiveQueriesFactory。我们可以继续使用IFBTree集,因为标记仍然是整数。
‘relationshiptype’将使用名称‘reltype’,并且将仅使用Unicode值作为标记,不进行转换,但进行注册检查。
‘getContext’将使用名称‘context’,但将继续使用intid实用工具并使用其接口的名称。我们将在稍后看到,在处理不同标记源之间的传递遍历必须谨慎处理。
我们还将使用intid实用工具来解决关系标记。请参见关系容器(和container.rst)中更改关系类型的示例,尤其是在keyref.py中。
以下是我们将用于‘subjects’和‘objects’标记的方法,接着是我们将用于‘relationshiptypes’标记的方法。
>>> lookup = {} >>> counter = [0] >>> prefix = '_z_token__' >>> def dump(obj, index, cache): ... assert (interfaces.IIndex.providedBy(index) and ... isinstance(cache, dict)), ( ... 'did not receive correct arguments') ... token = getattr(obj, prefix, None) ... if token is None: ... token = counter[0] ... counter[0] += 1 ... if counter[0] >= 2147483647: ... raise RuntimeError("Whoa! That's a lot of ids!") ... assert token not in lookup ... setattr(obj, prefix, token) ... lookup[token] = obj ... return token ... >>> def load(token, index, cache): ... assert (interfaces.IIndex.providedBy(index) and ... isinstance(cache, dict)), ( ... 'did not receive correct arguments') ... return lookup[token] ... >>> relTypes = [] >>> def relTypeDump(obj, index, cache): ... assert obj in relTypes, 'unknown relationshiptype' ... return obj ... >>> def relTypeLoad(token, index, cache): ... assert token in relTypes, 'unknown relationshiptype' ... return token ...
请注意,如果我们在乎基于ZODB的持久性,这些实现是完全荒谬的:为了使其半可接受,我们应该在某个合理持久数据结构中持久化存储计数器、查找和relTypes。这只是一个演示示例。
现在我们可以创建一个索引。
正如我们的初始示例一样,我们将使用索引模块中定义的简单传递查询工厂来为我们的默认传递行为:当你想要进行传递搜索时,将‘subjects’与‘objects’进行转置,并保留所有其他内容;如果同时提供了主语和对象,则不进行任何传递搜索。
>>> from BTrees import OIBTree # could also be OOBTree >>> ix = index.Index( ... ({'element': IRelationship['subjects'], 'multiple': True, ... 'dump': dump, 'load': load}, ... {'element': IRelationship['relationshiptype'], ... 'dump': relTypeDump, 'load': relTypeLoad, 'btree': OIBTree, ... 'name': 'reltype'}, ... {'element': IRelationship['objects'], 'multiple': True, ... 'dump': dump, 'load': load}, ... {'element': IContextAwareRelationship['getContext'], ... 'name': 'context'}), ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'))
我们希望在系统中某个地方放置索引,以便它可以找到intid实用工具。我们将像示例的一部分一样将其添加为实用工具。只要索引有一个有效的__parent__,它自己通过传递连接到具有所需intid实用工具的站点管理器,一切应该都正常,因此不需要将其作为实用工具安装。这只是一个示例。
>>> from zope import interface >>> sm = app.getSiteManager() >>> sm['rel_index'] = ix >>> import zope.interface.interfaces >>> registry = zope.interface.interfaces.IComponentRegistry(sm) >>> registry.registerUtility(ix, interfaces.IIndex) >>> import transaction >>> transaction.commit()
现在我们将创建一些代表性的对象,我们可以将它们关联起来,并创建和索引我们的第一个示例关系。
在示例中,请注意,上下文仅作为ISpecialRelationship对象的适配器可用:索引尝试将对象适配到适当的接口,如果无法适配,则认为值是空的。
>>> import persistent >>> from zope.app.container.contained import Contained >>> class Base(persistent.Persistent, Contained): ... def __init__(self, name): ... self.name = name ... def __repr__(self): ... return '<%s %r>' % (self.__class__.__name__, self.name) ... >>> class Person(Base): pass ... >>> class Role(Base): pass ... >>> class Project(Base): pass ... >>> class Company(Base): pass ... >>> @interface.implementer(IRelationship) ... class Relationship(persistent.Persistent, Contained): ... def __init__(self, subjects, relationshiptype, objects): ... self.subjects = subjects ... assert relationshiptype in relTypes ... self.relationshiptype = relationshiptype ... self.objects = objects ... def __repr__(self): ... return '<%r %s %r>' % ( ... self.subjects, self.relationshiptype, self.objects) ... >>> class ISpecialRelationship(interface.Interface): ... pass ... >>> from zope import component >>> @component.adapter(ISpecialRelationship) ... @interface.implementer(IContextAwareRelationship) ... class ContextRelationshipAdapter(object): ... def __init__(self, adapted): ... self.adapted = adapted ... def getContext(self): ... return getattr(self.adapted, '_z_context__', None) ... def setContext(self, value): ... self.adapted._z_context__ = value ... def __getattr__(self, name): ... return getattr(self.adapted, name) ... >>> component.provideAdapter(ContextRelationshipAdapter) >>> @interface.implementer(ISpecialRelationship) ... class SpecialRelationship(Relationship): ... pass ... >>> people = {} >>> for p in ['Abe', 'Bran', 'Cathy', 'David', 'Emily', 'Fred', 'Gary', ... 'Heather', 'Ingrid', 'Jim', 'Karyn', 'Lee', 'Mary', ... 'Nancy', 'Olaf', 'Perry', 'Quince', 'Rob', 'Sam', 'Terry', ... 'Uther', 'Van', 'Warren', 'Xen', 'Ygritte', 'Zane']: ... app[p] = people[p] = Person(p) ... >>> relTypes.extend( ... ['has the role of', 'manages', 'taught', 'commissioned']) >>> roles = {} >>> for r in ['Project Manager', 'Software Engineer', 'Designer', ... 'Systems Administrator', 'Team Leader', 'Mascot']: ... app[r] = roles[r] = Role(r) ... >>> projects = {} >>> for p in ['zope.org redesign', 'Zope 3 manual', ... 'improved test coverage', 'Vault design and implementation']: ... app[p] = projects[p] = Project(p) ... >>> companies = {} >>> for c in ['Ynod Corporation', 'HAL, Inc.', 'Zookd']: ... app[c] = companies[c] = Company(c) ...>>> app['fredisprojectmanager'] = rel = SpecialRelationship( ... (people['Fred'],), 'has the role of', (roles['Project Manager'],)) >>> IContextAwareRelationship(rel).setContext( ... projects['zope.org redesign']) >>> ix.index(rel) >>> transaction.commit()
令牌转换
在我们检查搜索功能之前,我们应该简要讨论索引上的标记化API。所有搜索查询都必须使用值标记,搜索结果有时可以是值或关系标记。因此,在标记和真实值之间进行转换可能很重要。索引为此目的提供了许多转换方法。
其中最重要的可能是tokenizeQuery:它接受一个查询,其中每个键和值都是索引值的名称和实际值;然后返回一个查询,其中实际值已转换为标记。例如,考虑以下示例。要可靠地显示转换有些困难(例如,我们无法知道intid标记将是什么),所以我们只显示结果值是输入的标记化版本。
>>> res = ix.tokenizeQuery( ... {'objects': roles['Project Manager'], ... 'context': projects['zope.org redesign']}) >>> res['objects'] == dump(roles['Project Manager'], ix, {}) True >>> from zope.app.intid.interfaces import IIntIds >>> intids = component.getUtility(IIntIds, context=ix) >>> res['context'] == intids.getId(projects['zope.org redesign']) True
标记化查询可以使用resolveQuery再次解析为值。
>>> sorted(ix.resolveQuery(res).items()) # doctest: +NORMALIZE_WHITESPACE [('context', <Project 'zope.org redesign'>), ('objects', <Role 'Project Manager'>)]
其他有用的转换包括 tokenizeValues,它返回给定索引名称值的可迭代标记;
>>> examples = (people['Abe'], people['Bran'], people['Cathy']) >>> res = list(ix.tokenizeValues(examples, 'subjects')) >>> res == [dump(o, ix, {}) for o in examples] True
resolveValueTokens,它返回给定索引名称标记的可迭代值;
>>> list(ix.resolveValueTokens(res, 'subjects')) [<Person 'Abe'>, <Person 'Bran'>, <Person 'Cathy'>]
tokenizeRelationship,它返回给定关系的标记;
>>> res = ix.tokenizeRelationship(rel) >>> res == intids.getId(rel) True
resolveRelationshipToken,它返回给定标记的关系;
>>> ix.resolveRelationshipToken(res) is rel True
tokenizeRelationships,它返回给定关系的可迭代标记;以及
>>> app['another_rel'] = another_rel = Relationship( ... (companies['Ynod Corporation'],), 'commissioned', ... (projects['Vault design and implementation'],)) >>> res = list(ix.tokenizeRelationships((another_rel, rel))) >>> res == [intids.getId(r) for r in (another_rel, rel)] True
resolveRelationshipTokens,它返回给定标记的可迭代关系。
>>> list(ix.resolveRelationshipTokens(res)) == [another_rel, rel] True
基本搜索
现在我们转向接口的核心:搜索。索引接口定义了几个搜索方法
findValues 和 findValueTokens 询问“这与什么相关?”;
findRelationshipChains 和 findRelationshipTokenChains 询问“如何相关?”,特别是对于传递搜索;
isLinked 询问“是否存在这样的关系?”;
findRelationshipTokenSet 询问“与我的查询匹配的哪些是转述关系?”并且特别适用于索引数据结构的底层使用;
findRelationships 提出同样的问题,但返回关系迭代器而不是标记集;
findValueTokenSet 询问“对于这个特定的索引名称和关系标记,哪些是值标记?”并且适用于索引数据结构的底层使用,如传递查询工厂;并且
标准的 zope.index 方法 apply 实质上通过查询对象拼写暴露了 findRelationshipTokenSet 和 findValueTokens 方法。
findRelationshipChains 和 findRelationshipTokenChains 是成对的方法,做相同的工作,但有和没有解析结果标记;同样,findValues 和 findValueTokens 也是以同样的方式成对。
非常重要的一点是,所有查询都必须使用标记,而不是实际对象。如上所述,索引提供了一种方法来简化这个要求,形式为 tokenizeQuery 方法,该方法将包含对象的字典转换为包含标记的字典。你将在下面看到,我们通过将 tokenizeQuery 存储在 'q' 名称中,来缩短我们的调用。
>>> q = ix.tokenizeQuery
我们已经索引了第一个示例关系——“Fred 在 zope.org 重设计中担任项目经理角色”——因此我们可以搜索它。我们将首先查看 findValues 和 findValueTokens。在这里,我们询问‘谁在 zope.org 重设计中担任项目经理角色?’我们首先使用 findValues,然后使用 findValueTokens [9]。
findValueTokens 和 findValues 如果尝试获取未索引的值将引发错误。
>>> list(ix.findValues( ... 'folks', ... q({'reltype': 'has the role of', ... 'objects': roles['Project Manager'], ... 'context': projects['zope.org redesign']}))) Traceback (most recent call last): ... ValueError: ('name not indexed', 'folks')
>>> list(ix.findValueTokens( ... 'folks', ... q({'reltype': 'has the role of', ... 'objects': roles['Project Manager'], ... 'context': projects['zope.org redesign']}))) Traceback (most recent call last): ... ValueError: ('name not indexed', 'folks')
>>> list(ix.findValues( ... 'subjects', ... q({'reltype': 'has the role of', ... 'objects': roles['Project Manager'], ... 'context': projects['zope.org redesign']}))) [<Person 'Fred'>]
>>> [load(t, ix, {}) for t in ix.findValueTokens( ... 'subjects', ... q({'reltype': 'has the role of', ... 'objects': roles['Project Manager'], ... 'context': projects['zope.org redesign']}))] [<Person 'Fred'>]
如果不传递查询给这些方法,则获取给定名称在 BTree 中的所有索引值(不要修改此!这是一个内部数据结构,我们直接传递它,因为您可以使用 BTree 集合操作有效地使用它)。在这种情况下,我们只索引了一个关系,因此其主题是此结果中的主题。
>>> res = ix.findValueTokens('subjects', maxDepth=1) >>> res # doctest: +ELLIPSIS <BTrees.IOBTree.IOBTree object at ...> >>> [load(t, ix, {}) for t in res] [<Person 'Fred'>]
如果我们想找到 Fred 是其主题的所有关系,我们可以使用 findRelationshipTokenSet。它与 findValueTokenSet 结合使用,有助于在您想要以其他搜索方法不支持的方式使用数据时,在相当低的级别查询索引数据结构。
findRelationshipTokenSet,给定一个包含 {indexName: token} 的单个字典,返回一个基于索引中关系 btree 家族的标记集,这些标记集不传递性地匹配它;
>>> res = ix.findRelationshipTokenSet(q({'subjects': people['Fred']})) >>> res # doctest: +ELLIPSIS <BTrees.IFBTree.IFTreeSet object at ...> >>> [intids.getObject(t) for t in res] [<(<Person 'Fred'>,) has the role of (<Role 'Project Manager'>,)>]
实际上,它等同于不带传递性和不带任何过滤器的 findRelationshipTokens 调用。
>>> res2 = ix.findRelationshipTokens( ... q({'subjects': people['Fred']}), maxDepth=1) >>> res2 is res True
findRelationshipTokenSet 方法始终返回一个集合,即使查询没有结果。
>>> res = ix.findRelationshipTokenSet(q({'subjects': people['Ygritte']})) >>> res # doctest: +ELLIPSIS <BTrees.IFBTree.IFTreeSet object at ...> >>> list(res) []
空查询返回索引中的所有关系(其他搜索方法也是如此)。
>>> res = ix.findRelationshipTokenSet({}) >>> res # doctest: +ELLIPSIS <BTrees.IFBTree.IFTreeSet object at ...> >>> len(res) == ix.documentCount() True >>> for r in ix.resolveRelationshipTokens(res): ... if r not in ix: ... print('oops') ... break ... else: ... print('correct') ... correct
findRelationships 可以做同样的事情,但是解析关系。
>>> list(ix.findRelationships(q({'subjects': people['Fred']}))) [<(<Person 'Fred'>,) has the role of (<Role 'Project Manager'>,)>]
然而,与 findRelationshipTokens 类似,与 findRelationshipTokenSet 不同,findRelationships 可以递归地使用,如本文档的引言部分所示。
findValueTokenSet 在给定一个关系令牌和值名称的情况下,返回该关系的基于值(基于值的 btree 家族)的值令牌集。
>>> src = ix.findRelationshipTokenSet(q({'subjects': people['Fred']}))>>> res = ix.findValueTokenSet(list(src)[0], 'subjects') >>> res # doctest: +ELLIPSIS <BTrees.IFBTree.IFTreeSet object at ...> >>> [load(t, ix, {}) for t in res] [<Person 'Fred'>]
与 findRelationshipTokenSet 和 findRelationshipTokens 类似,findValueTokenSet 等同于 findValueTokens,但不进行递归搜索或过滤。
>>> res2 = ix.findValueTokenSet(list(src)[0], 'subjects') >>> res2 is res True
apply 方法是 zope.index.interfaces.IIndexSearch 接口的一部分,基本上只能复制 findValueTokens 和 findRelationshipTokenSet 搜索调用。唯一额外的功能是结果始终是 IFBTree 集合:如果请求的令牌不在 IFBTree 集合中(例如,基于实例化的“btree”键),则索引将引发 ValueError。一个包装字典指定搜索的类型,值应该是搜索的参数。
在这里,我们询问 zope.org 重设计的当前已知角色。
>>> res = ix.apply({'values': ... {'resultName': 'objects', 'query': ... q({'reltype': 'has the role of', ... 'context': projects['zope.org redesign']})}}) >>> res # doctest: +ELLIPSIS IFSet([...]) >>> [load(t, ix, {}) for t in res] [<Role 'Project Manager'>]
理想情况下,这将失败,因为虽然令牌是整数,但实际上不能与基于 intid 的目录结果合并。然而,如果索引可以确定返回的集合不是 IFTreeSet 或 IFSet,则只会抱怨。
在这里,我们询问具有“具有角色”类型的关系。
>>> res = ix.apply({'relationships': ... q({'reltype': 'has the role of'})}) >>> res # doctest: +ELLIPSIS <BTrees.IFBTree.IFTreeSet object at ...> >>> [intids.getObject(t) for t in res] [<(<Person 'Fred'>,) has the role of (<Role 'Project Manager'>,)>]
在这里,我们询问 zope.org 重设计的已知关系类型。它将失败,因为结果不能表示为 IFBTree.IFTreeSet。
>>> res = ix.apply({'values': ... {'resultName': 'reltype', 'query': ... q({'context': projects['zope.org redesign']})}}) ... # doctest: +NORMALIZE_WHITESPACE Traceback (most recent call last): ... ValueError: cannot fulfill `apply` interface because cannot return an (I|L)FBTree-based result
如果您请求关系,而这些关系未存储在 IFBTree 或 LFBTree 结构中,将引发相同类型的错误。[10]
字典中只能有一个键。
>>> res = ix.apply({'values': ... {'resultName': 'objects', 'query': ... q({'reltype': 'has the role of', ... 'context': projects['zope.org redesign']})}, ... 'relationships': q({'reltype': 'has the role of'})}) Traceback (most recent call last): ... ValueError: one key in the primary query dictionary
键必须是“values”或“relationships”之一。
>>> res = ix.apply({'kumquats': ... {'resultName': 'objects', 'query': ... q({'reltype': 'has the role of', ... 'context': projects['zope.org redesign']})}}) Traceback (most recent call last): ... ValueError: ('unknown query type', 'kumquats')
如果关系使用 LFBTrees,则搜索是正常的。
>>> ix2 = index.Index( # explicit 64 bit ... ({'element': IRelationship['subjects'], 'multiple': True}, ... IRelationship['relationshiptype'], ... {'element': IRelationship['objects'], 'multiple': True}, ... IContextAwareRelationship['getContext']), ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'), ... family=BTrees.family64)
>>> list(ix2.apply({'values': ... {'resultName': 'objects', 'query': ... q({'subjects': people['Gary']})}})) []
>>> list(ix2.apply({'relationships': ... q({'subjects': people['Gary']})})) []
但是,正如主文本中所示,如果使用其他 BTree 模块进行关系,则会出错。
>>> ix2 = index.Index( # explicit 64 bit ... ({'element': IRelationship['subjects'], 'multiple': True}, ... IRelationship['relationshiptype'], ... {'element': IRelationship['objects'], 'multiple': True}, ... IContextAwareRelationship['getContext']), ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'), ... relFamily=BTrees.OIBTree)
>>> list(ix2.apply({'relationships': ... q({'subjects': people['Gary']})})) Traceback (most recent call last): ... ValueError: cannot fulfill `apply` interface because cannot return an (I|L)FBTree-based result
最后的基本搜索方法 isLinked、findRelationshipTokenChains 和 findRelationshipChains 对于递归搜索最有用。我们尚未创建任何可以递归使用的关联。它们仍然可以与递归搜索一起工作,因此我们在这里作为介绍进行演示,然后在我们介绍递归关系时进一步讨论。
findRelationshipChains 和 findRelationshipTokenChains 允许您找到递归关系路径。目前一个单一的关系——一个单一的点——无法形成很大的线。所以,首先,这里是一个有点无用的例子
>>> [[intids.getObject(t) for t in path] for path in ... ix.findRelationshipTokenChains( ... q({'reltype': 'has the role of'}))] ... # doctest: +NORMALIZE_WHITESPACE [[<(<Person 'Fred'>,) has the role of (<Role 'Project Manager'>,)>]]
这是无用的,因为没有机会进行递归搜索,所以最好使用 findRelationshipTokenSet。这将在稍后变得更有趣。
这是使用 findRelationshipChains 的相同示例,它自己解析关系令牌。
>>> list(ix.findRelationshipChains(q({'reltype': 'has the role of'}))) ... # doctest: +NORMALIZE_WHITESPACE [(<(<Person 'Fred'>,) has the role of (<Role 'Project Manager'>,)>,)]
isLinked 如果至少有一条路径匹配搜索,则返回布尔值——实际上,实现基本上是
try: iter(ix.findRelationshipTokenChains(...args...)).next() except StopIteration: return False else: return True
所以,我们可以说
>>> ix.isLinked(q({'subjects': people['Fred']})) True >>> ix.isLinked(q({'subjects': people['Gary']})) False >>> ix.isLinked(q({'subjects': people['Fred'], ... 'reltype': 'manages'})) False
这本身就有一定的实用性,可以用来测试基本的断言。它也可以用于递归搜索,如下文所示。
一个更简单的示例
(这是为了测试即使在递归查询工厂未设置的情况下,搜索简单关系也能正常工作。)
让我们创建一个非常简单的关系类型,使用字符串作为源和目标类型
>>> class IStringRelation(interface.Interface): ... name = interface.Attribute("The name of the value.") ... value = interface.Attribute("The value associated with the name.")>>> @interface.implementer(IStringRelation) ... class StringRelation(persistent.Persistent, Contained): ... ... def __init__(self, name, value): ... self.name = name ... self.value = value>>> app[u"string-relation-1"] = StringRelation("name1", "value1") >>> app[u"string-relation-2"] = StringRelation("name2", "value2")>>> transaction.commit()
现在我们可以创建一个使用这些的索引
>>> from BTrees import OOBTree>>> sx = index.Index( ... ({"element": IStringRelation["name"], ... "load": None, "dump": None, "btree": OOBTree}, ... {"element": IStringRelation["value"], ... "load": None, "dump": None, "btree": OOBTree}, ... ))>>> app["sx"] = sx >>> transaction.commit()
然后我们将关系添加到索引中
>>> app["sx"].index(app["string-relation-1"]) >>> app["sx"].index(app["string-relation-2"])
获取关系应该非常简单。让我们查找所有与“name1”关联的值
>>> query = sx.tokenizeQuery({"name": "name1"}) >>> list(sx.findValues("value", query)) ['value1']
搜索空集
我们已经检查了最基本搜索功能。索引和搜索的另一个特性是可以搜索到空集的关系,或者,在我们的示例中,对于单值关系如“reltype”和“context”,None。
让我们添加一个“管理”关系类型,没有上下文;以及一个“委托”关系类型,和一个公司上下文。
顺便说一下,有两种方法可以添加索引。我们已经看到索引有一个接受关系的“index”方法。这里我们使用的是“index_doc”,这是在zope.index.interfaces.IInjection中定义的一个方法,它要求令牌已经生成。由于我们使用intids对关系进行令牌化,我们必须将它们添加到ZODB应用程序对象中,以便它们具有连接的可能性。
>>> app['abeAndBran'] = rel = Relationship( ... (people['Abe'],), 'manages', (people['Bran'],)) >>> ix.index_doc(intids.register(rel), rel) >>> app['abeAndVault'] = rel = SpecialRelationship( ... (people['Abe'],), 'commissioned', ... (projects['Vault design and implementation'],)) >>> IContextAwareRelationship(rel).setContext(companies['Zookd']) >>> ix.index_doc(intids.register(rel), rel)
现在我们可以搜索没有上下文的Abe的关系。None值始终用于匹配空集和单个None值。目前索引不支持任何其他“空”值。
>>> sorted( ... repr(load(t, ix, {})) for t in ix.findValueTokens( ... 'objects', ... q({'subjects': people['Abe']}))) ["<Person 'Bran'>", "<Project 'Vault design and implementation'>"] >>> [load(t, ix, {}) for t in ix.findValueTokens( ... 'objects', q({'subjects': people['Abe'], 'context': None}))] [<Person 'Bran'>] >>> sorted( ... repr(v) for v in ix.findValues( ... 'objects', ... q({'subjects': people['Abe']}))) ["<Person 'Bran'>", "<Project 'Vault design and implementation'>"] >>> list(ix.findValues( ... 'objects', q({'subjects': people['Abe'], 'context': None}))) [<Person 'Bran'>]
请注意,索引目前不支持搜索具有任何值或一组值的关系。这可能在以后添加;这种查询的拼写是更麻烦的部分之一。
与传递搜索一起工作
也可以进行传递搜索。这可以让你找到所有传递上司或下属,在我们的“管理”关系类型中。让我们设置一些示例关系。用字母代表我们的人,我们将创建三个这样的层次结构
A JK R / \ / \ B C LM NOP S T U / \ | | /| | \ D E F Q V W X | | | \--Y H G | | Z I
这意味着,例如,人“A”(“Abe”)管理“B”(“Bran”)和“C”(“Cathy”)。
我们已经有了Abe到Bran的关系,所以我们将只添加剩余的。
>>> relmap = ( ... ('A', 'C'), ('B', 'D'), ('B', 'E'), ('C', 'F'), ... ('F', 'G'), ('D', 'H'), ('H', 'I'), ('JK', 'LM'), ('JK', 'NOP'), ... ('LM', 'Q'), ('R', 'STU'), ('S', 'VW'), ('T', 'X'), ('UX', 'Y'), ... ('Y', 'Z')) >>> letters = dict((name[0], ob) for name, ob in people.items()) >>> for subs, obs in relmap: ... subs = tuple(letters[l] for l in subs) ... obs = tuple(letters[l] for l in obs) ... app['%sManages%s' % (''.join(o.name for o in subs), ... ''.join(o.name for o in obs))] = rel = ( ... Relationship(subs, 'manages', obs)) ... ix.index(rel) ...
现在我们可以进行传递性和非传递性搜索。这里有一些例子。
>>> [load(t, ix, {}) for t in ix.findValueTokens( ... 'subjects', ... q({'objects': people['Ingrid'], ... 'reltype': 'manages'})) ... ] [<Person 'Heather'>, <Person 'David'>, <Person 'Bran'>, <Person 'Abe'>]
这里是使用findValues做的同样的事情。
>>> list(ix.findValues( ... 'subjects', ... q({'objects': people['Ingrid'], ... 'reltype': 'manages'}))) [<Person 'Heather'>, <Person 'David'>, <Person 'Bran'>, <Person 'Abe'>]
请注意,它们是有序的,从搜索起点离开。它也是广度优先的—例如,看看Zane的上级列表:Xen和Uther排在Rob和Terry之前。
>>> res = list(ix.findValues( ... 'subjects', ... q({'objects': people['Zane'], 'reltype': 'manages'}))) >>> res[0] <Person 'Ygritte'> >>> sorted(repr(p) for p in res[1:3]) ["<Person 'Uther'>", "<Person 'Xen'>"] >>> sorted(repr(p) for p in res[3:]) ["<Person 'Rob'>", "<Person 'Terry'>"]
请注意,在遍历过程中保持搜索的所有元素—只有转置的值会改变,其余的保持静态。例如,请注意这两个结果之间的区别。
>>> [load(t, ix, {}) for t in ix.findValueTokens( ... 'objects', ... q({'subjects': people['Cathy'], 'reltype': 'manages'}))] [<Person 'Fred'>, <Person 'Gary'>] >>> res = [load(t, ix, {}) for t in ix.findValueTokens( ... 'objects', ... q({'subjects': people['Cathy']}))] >>> res[0] <Person 'Fred'> >>> sorted(repr(i) for i in res[1:]) ["<Person 'Gary'>", "<Role 'Project Manager'>"]
第一次搜索得到了我们预期的管理关系类型的结果—从Cathy出发,关系类型保持不变,我们只得到了Gary的下属。第二次搜索没有指定关系类型,因此传递搜索包括了第一个添加的角色(Fred是zope.org重设计的项目经理)。
maxDepth参数允许控制搜索多远。例如,如果我们只想搜索Bran的下属至多两步深,我们可以这样做
>>> res = [load(t, ix, {}) for t in ix.findValueTokens( ... 'objects', ... q({'subjects': people['Bran']}), ... maxDepth=2)] >>> sorted(repr(i) for i in res) ["<Person 'David'>", "<Person 'Emily'>", "<Person 'Heather'>"]
对findValues来说也是一样。
>>> res = list(ix.findValues( ... 'objects', ... q({'subjects': people['Bran']}), maxDepth=2)) >>> sorted(repr(i) for i in res) ["<Person 'David'>", "<Person 'Emily'>", "<Person 'Heather'>"]
也可以通过使用下面将要描述的targetFilter参数来实现最小深度—即必须遍历的关系数量,以便得到结果。现在,我们将按照参数列表的顺序继续,所以下一个是filter。
filter参数接受一个提供interfaces.IFilter的对象(如函数)。如接口列表所示,它接收当前的关系令牌链(“relchain”)、启动搜索的原始查询(“query”)、索引对象(“index”)以及在整个搜索过程中使用并在之后丢弃的字典,可以用于优化(“cache”)。它应该返回一个布尔值,该值确定是否应该使用给定的relchain—遍历或返回。例如,如果安全规定当前用户只能看到某些关系,则可以使用过滤器来仅使可用的关系可遍历。其他用途包括仅获取在给定时间之后创建的关系或具有某些注释的关系(在解析令牌后可用)。
让我们看一个只允许给定集合中关系的过滤器示例,就像基于安全的过滤器可能工作的方式一样。然后我们将使用它来模拟当前用户除了Xen之外,看不到Ygritte被Uther管理的情况。
>>> s = set(intids.getId(r) for r in app.values() ... if IRelationship.providedBy(r)) >>> relset = list( ... ix.findRelationshipTokenSet(q({'subjects': people['Xen']}))) >>> len(relset) 1 >>> s.remove(relset[0]) >>> dump(people['Uther'], ix, {}) in list( ... ix.findValueTokens('subjects', q({'objects': people['Ygritte']}))) True >>> dump(people['Uther'], ix, {}) in list(ix.findValueTokens( ... 'subjects', q({'objects': people['Ygritte']}), ... filter=lambda relchain, query, index, cache: relchain[-1] in s)) False >>> people['Uther'] in list( ... ix.findValues('subjects', q({'objects': people['Ygritte']}))) True >>> people['Uther'] in list(ix.findValues( ... 'subjects', q({'objects': people['Ygritte']}), ... filter=lambda relchain, query, index, cache: relchain[-1] in s)) False
接下来两个搜索参数是targetQuery和targetFilter。它们都是对搜索方法输出的过滤器,而不会影响遍历/搜索过程。targetQuery接受与主查询相同的查询,而targetFilter接受与filter参数相同的IFilter。targetFilter可以完成targetQuery的所有工作,但targetQuery使得一个常见的情况——想要找到两个对象之间的路径,或者两个对象是否相互链接,例如——变得方便。
我们暂时跳过targetQuery(当我们重新访问findRelationshipChains和isLinked时我们将回来),看看targetFilter。targetFilter可用于许多任务,例如仅返回具有特殊注释关系的值,或者仅返回在两阶段搜索中穿越了一定枢纽关系的值,或其他任务。然而,一个非常简单的任务就是有效地指定最小遍历深度。在这里,我们找到正好在Bran下方两步的人,不多也不少。我们做了两次,一次使用findValueTokens,一次使用findValues。
>>> [load(t, ix, {}) for t in ix.findValueTokens( ... 'objects', q({'subjects': people['Bran']}), maxDepth=2, ... targetFilter=lambda relchain, q, i, c: len(relchain)>=2)] [<Person 'Heather'>] >>> list(ix.findValues( ... 'objects', q({'subjects': people['Bran']}), maxDepth=2, ... targetFilter=lambda relchain, q, i, c: len(relchain)>=2)) [<Person 'Heather'>]
Heather是唯一一个正好在Bran下方两步的人。
注意,我们指定了maxDepth和targetFilter。我们可以通过指定targetFilter为len(relchain)==2而没有maxDepth来获得相同的结果,但是它们在效率上有一个重要的区别。maxDepth和filter可以减少索引的工作量,因为它们可以在达到最大深度或失败过滤器后停止搜索;targetFilter和targetQuery参数只是隐藏了获得的结果,这可以在getValues的情况下减少一点工作量,但通常不会减少任何遍历工作量。
搜索方法的最后一个参数是transitiveQueriesFactory。这是一个在搜索期间替换索引默认遍历工厂的强工具。这允许为单个搜索进行自定义遍历,并可以支持许多高级用例。例如,我们的索引假设您想要遍历对象和来源,并且上下文应该是恒定的;这可能不是始终希望的遍历行为。如果我们有一个关系PERSON1教授PERSON2(PERSON3的课程),那么要找到任何给定人的老师,您可能想遍历PERSON1,但有时您可能还想遍历PERSON3。您可以通过提供不同的工厂来更改此行为。
为了展示这个例子,我们需要添加一些更多关系。我们将说Mary教授RobAbe的课程;Olaf教授ZaneBran的课程;Cathy教授BranLee的课程;David教授AbeZane的课程;Emily教授MaryYgritte的课程。
在图中,左侧线条表示“教授”,右侧线条表示“课程”,所以
E Y \ / M
应读作“Emily教授MaryYgritte的课程”。这是完整的图表
C L \ / O B \ / E Y D Z \ / \ / M A \ / \ / R
您可以看到,Rob的老师间接路径是Mary和Emily,但Rob的课程间接路径是Abe、Zane、Bran和Lee。
当间接遍历跨越标记类型时,间接查询工厂必须做额外的工作。我们之前已经使用TransposingTransitiveQueriesFactory构建了我们的转置器,但现在我们需要编写一个自定义的,它翻译标记(哦!一个TokenTranslatingTransitiveQueriesFactory!……也许我们不会走到这一步……)。
我们将添加关系,构建自定义间接工厂,然后再次进行两次搜索工作,一次使用findValueTokens,一次使用findValues。
>>> for triple in ('EMY', 'MRA', 'DAZ', 'OZB', 'CBL'): ... teacher, student, source = (letters[l] for l in triple) ... rel = SpecialRelationship((teacher,), 'taught', (student,)) ... app['%sTaught%sTo%s' % ( ... teacher.name, source.name, student.name)] = rel ... IContextAwareRelationship(rel).setContext(source) ... ix.index_doc(intids.register(rel), rel) ...>>> def transitiveFactory(relchain, query, index, cache): ... dynamic = cache.get('dynamic') ... if dynamic is None: ... intids = cache['intids'] = component.getUtility( ... IIntIds, context=index) ... static = cache['static'] = {} ... dynamic = cache['dynamic'] = [] ... names = ['objects', 'context'] ... for nm, val in query.items(): ... try: ... ix = names.index(nm) ... except ValueError: ... static[nm] = val ... else: ... if dynamic: ... # both were specified: no transitive search known. ... del dynamic[:] ... cache['intids'] = False ... break ... else: ... dynamic.append(nm) ... dynamic.append(names[not ix]) ... else: ... intids = component.getUtility(IIntIds, context=index) ... if dynamic[0] == 'objects': ... def translate(t): ... return dump(intids.getObject(t), index, cache) ... else: ... def translate(t): ... return intids.register(load(t, index, cache)) ... cache['translate'] = translate ... else: ... static = cache['static'] ... translate = cache['translate'] ... if dynamic: ... for r in index.findValueTokenSet(relchain[-1], dynamic[1]): ... res = {dynamic[0]: translate(r)} ... res.update(static) ... yield res>>> [load(t, ix, {}) for t in ix.findValueTokens( ... 'subjects', ... q({'objects': people['Rob'], 'reltype': 'taught'}))] [<Person 'Mary'>, <Person 'Emily'>] >>> [intids.getObject(t) for t in ix.findValueTokens( ... 'context', ... q({'objects': people['Rob'], 'reltype': 'taught'}), ... transitiveQueriesFactory=transitiveFactory)] [<Person 'Abe'>, <Person 'Zane'>, <Person 'Bran'>, <Person 'Lee'>]>>> list(ix.findValues( ... 'subjects', ... q({'objects': people['Rob'], 'reltype': 'taught'}))) [<Person 'Mary'>, <Person 'Emily'>] >>> list(ix.findValues( ... 'context', ... q({'objects': people['Rob'], 'reltype': 'taught'}), ... transitiveQueriesFactory=transitiveFactory)) [<Person 'Abe'>, <Person 'Zane'>, <Person 'Bran'>, <Person 'Lee'>]
间接查询工厂非常强大,我们在这个文档中还没有结束谈论它们:请参阅下文的“间接映射多个元素”。
我们现在已经讨论过,或者至少提到了所有可用的搜索参数。《apply方法中的'value'搜索具有与findValues相同的参数和功能,因此也可以执行这些间接技巧。让我们找到所有Karyn的下级。
>>> res = ix.apply({'values': ... {'resultName': 'objects', 'query': ... q({'reltype': 'manages', ... 'subjects': people['Karyn']})}}) >>> res # doctest: +ELLIPSIS IFSet([...]) >>> sorted(repr(load(t, ix, {})) for t in res) ... # doctest: +NORMALIZE_WHITESPACE ["<Person 'Lee'>", "<Person 'Mary'>", "<Person 'Nancy'>", "<Person 'Olaf'>", "<Person 'Perry'>", "<Person 'Quince'>"]
当我们回到 findRelationshipChains 和 findRelationshipTokenChains 时,我们也会回到上面推迟的搜索参数:targetQuery。
findRelationshipChains 和 findRelationshipTokenChains 可以简单地找到所有路径
>>> res = [repr([intids.getObject(t) for t in path]) for path in ... ix.findRelationshipTokenChains( ... q({'reltype': 'manages', 'subjects': people['Jim']} ... ))] >>> len(res) 3 >>> sorted(res[:2]) # doctest: +NORMALIZE_WHITESPACE ["[<(<Person 'Jim'>, <Person 'Karyn'>) manages (<Person 'Lee'>, <Person 'Mary'>)>]", "[<(<Person 'Jim'>, <Person 'Karyn'>) manages (<Person 'Nancy'>, <Person 'Olaf'>, <Person 'Perry'>)>]"] >>> res[2] # doctest: +NORMALIZE_WHITESPACE "[<(<Person 'Jim'>, <Person 'Karyn'>) manages (<Person 'Lee'>, <Person 'Mary'>)>, <(<Person 'Lee'>, <Person 'Mary'>) manages (<Person 'Quince'>,)>]" >>> res == [repr(list(p)) for p in ... ix.findRelationshipChains( ... q({'reltype': 'manages', 'subjects': people['Jim']} ... ))] True
就像 findValues 一样,这是一个广度优先搜索。
如果我们使用 findRelationshipChains 中的 targetQuery,可以找到两个搜索之间的所有路径。例如,考虑 Rob 和 Ygritte 之间的路径。如果要求搜索主管,findValues 搜索只会包含 Rob 一次,但这里有两个路径。这些路径可以通过 targetQuery 找到。
>>> res = [repr([intids.getObject(t) for t in path]) for path in ... ix.findRelationshipTokenChains( ... q({'reltype': 'manages', 'subjects': people['Rob']}), ... targetQuery=q({'objects': people['Ygritte']}))] >>> len(res) 2 >>> sorted(res[:2]) # doctest: +NORMALIZE_WHITESPACE ["[<(<Person 'Rob'>,) manages (<Person 'Sam'>, <Person 'Terry'>, <Person 'Uther'>)>, <(<Person 'Terry'>,) manages (<Person 'Xen'>,)>, <(<Person 'Uther'>, <Person 'Xen'>) manages (<Person 'Ygritte'>,)>]", "[<(<Person 'Rob'>,) manages (<Person 'Sam'>, <Person 'Terry'>, <Person 'Uther'>)>, <(<Person 'Uther'>, <Person 'Xen'>) manages (<Person 'Ygritte'>,)>]"]
这是一个没有结果的查询
>>> len(list(ix.findRelationshipTokenChains( ... q({'reltype': 'manages', 'subjects': people['Rob']}), ... targetQuery=q({'objects': companies['Zookd']})))) 0
您可以将 targetQuery 与 targetFilter 结合使用。这里我们随意说我们正在寻找一条在 Rob 和 Ygritte 之间至少有 3 个链接的路径。
>>> res = [repr([intids.getObject(t) for t in path]) for path in ... ix.findRelationshipTokenChains( ... q({'reltype': 'manages', 'subjects': people['Rob']}), ... targetQuery=q({'objects': people['Ygritte']}), ... targetFilter=lambda relchain, q, i, c: len(relchain)>=3)] >>> len(res) 1 >>> res # doctest: +NORMALIZE_WHITESPACE ["[<(<Person 'Rob'>,) manages (<Person 'Sam'>, <Person 'Terry'>, <Person 'Uther'>)>, <(<Person 'Terry'>,) manages (<Person 'Xen'>,)>, <(<Person 'Uther'>, <Person 'Xen'>) manages (<Person 'Ygritte'>,)>]"]
isLinked 与所有其他传递性方法具有相同的参数。例如,Rob 和 Ygritte 是传递性链接,但 Abe 和 Zane 不是。
>>> ix.isLinked( ... q({'reltype': 'manages', 'subjects': people['Rob']}), ... targetQuery=q({'objects': people['Ygritte']})) True >>> ix.isLinked( ... q({'reltype': 'manages', 'subjects': people['Abe']}), ... targetQuery=q({'objects': people['Ygritte']})) False
检测循环
假设我们正在模拟一个“隐藏的国王”:一个高层管理人员也作为仆人工作,以了解他员工的生活。我们可以用几种可能比我们现在做的方法更有意义的方式来模拟这种情况,但为了展示工作中的循环,我们只是添加一个额外的联系,让 Abe 为 Gary 工作。这意味着从 Ingrid 到最长路径的长度大大增加 - 理论上,由于循环,它是无限长的。
索引跟踪这个循环,并在循环发生时停止,并在循环重复任何关系之前停止。它将具有循环的链标记为特殊类型的元组,该元组实现了 ICircularRelationshipPath。该元组具有一个包含一个或多个搜索的 'cycled' 属性,这些搜索相当于遵循循环(给定相同的 transitiveMap)。
让我们实际看看我们描述的例子。
>>> res = list(ix.findRelationshipTokenChains( ... q({'objects': people['Ingrid'], 'reltype': 'manages'}))) >>> len(res) 4 >>> len(res[3]) 4 >>> interfaces.ICircularRelationshipPath.providedBy(res[3]) False >>> rel = Relationship( ... (people['Gary'],), 'manages', (people['Abe'],)) >>> app['GaryManagesAbe'] = rel >>> ix.index(rel) >>> res = list(ix.findRelationshipTokenChains( ... q({'objects': people['Ingrid'], 'reltype': 'manages'}))) >>> len(res) 8 >>> len(res[7]) 8 >>> interfaces.ICircularRelationshipPath.providedBy(res[7]) True >>> [sorted(ix.resolveQuery(search).items()) for search in res[7].cycled] [[('objects', <Person 'Abe'>), ('reltype', 'manages')]] >>> tuple(ix.resolveRelationshipTokens(res[7])) ... # doctest: +NORMALIZE_WHITESPACE (<(<Person 'Heather'>,) manages (<Person 'Ingrid'>,)>, <(<Person 'David'>,) manages (<Person 'Heather'>,)>, <(<Person 'Bran'>,) manages (<Person 'David'>,)>, <(<Person 'Abe'>,) manages (<Person 'Bran'>,)>, <(<Person 'Gary'>,) manages (<Person 'Abe'>,)>, <(<Person 'Fred'>,) manages (<Person 'Gary'>,)>, <(<Person 'Cathy'>,) manages (<Person 'Fred'>,)>, <(<Person 'Abe'>,) manages (<Person 'Cathy'>,)>)
对于 findRelationshipChains 也是同样的道理。请注意,.cycled 属性中的查询没有解决:它仍然是继续循环所需的查询。
>>> res = list(ix.findRelationshipChains( ... q({'objects': people['Ingrid'], 'reltype': 'manages'}))) >>> len(res) 8 >>> len(res[7]) 8 >>> interfaces.ICircularRelationshipPath.providedBy(res[7]) True >>> [sorted(ix.resolveQuery(search).items()) for search in res[7].cycled] [[('objects', <Person 'Abe'>), ('reltype', 'manages')]] >>> res[7] # doctest: +NORMALIZE_WHITESPACE cycle(<(<Person 'Heather'>,) manages (<Person 'Ingrid'>,)>, <(<Person 'David'>,) manages (<Person 'Heather'>,)>, <(<Person 'Bran'>,) manages (<Person 'David'>,)>, <(<Person 'Abe'>,) manages (<Person 'Bran'>,)>, <(<Person 'Gary'>,) manages (<Person 'Abe'>,)>, <(<Person 'Fred'>,) manages (<Person 'Gary'>,)>, <(<Person 'Cathy'>,) manages (<Person 'Fred'>,)>, <(<Person 'Abe'>,) manages (<Person 'Cathy'>,)>)
顺便说一下,新关系没有什么特别的。如果我们开始寻找 Fred 的主管,循环标记将给出指向 Fred 作为自己主管的关系。没有进一步的帮助和政策,计算机无法知道哪个是“原因”。
处理循环可能会有点棘手。现在想象一下,我们有一个只涉及一个引起循环的关系的循环。另一个对象应该继续被跟踪。
例如,让 Q 管理 L 和 Y。到 L 的链接将是一个循环,但到 Y 的链接不是,应该被跟踪。这意味着只有中间的关系链会被标记为循环。
>>> rel = Relationship((people['Quince'],), 'manages', ... (people['Lee'], people['Ygritte'])) >>> app['QuinceManagesLeeYgritte'] = rel >>> ix.index_doc(intids.register(rel), rel) >>> res = [p for p in ix.findRelationshipTokenChains( ... q({'reltype': 'manages', 'subjects': people['Mary']}))] >>> [interfaces.ICircularRelationshipPath.providedBy(p) for p in res] [False, True, False] >>> [[intids.getObject(t) for t in p] for p in res] ... # doctest: +NORMALIZE_WHITESPACE [[<(<Person 'Lee'>, <Person 'Mary'>) manages (<Person 'Quince'>,)>], [<(<Person 'Lee'>, <Person 'Mary'>) manages (<Person 'Quince'>,)>, <(<Person 'Quince'>,) manages (<Person 'Lee'>, <Person 'Ygritte'>)>], [<(<Person 'Lee'>, <Person 'Mary'>) manages (<Person 'Quince'>,)>, <(<Person 'Quince'>,) manages (<Person 'Lee'>, <Person 'Ygritte'>)>, <(<Person 'Ygritte'>,) manages (<Person 'Zane'>,)>]] >>> [sorted( ... (nm, nm == 'reltype' and t or load(t, ix, {})) ... for nm, t in search.items()) for search in res[1].cycled] [[('reltype', 'manages'), ('subjects', <Person 'Lee'>)]]
传递映射多个元素
传递性搜索可以执行 transitiveQueriesFactory 返回的任何搜索,这意味着可以模拟复杂的传递性行为。例如,想象一下家谱关系。假设基本关系是“男性和女性有子女”。通过传递性走到祖先或后代需要区分男性和女性子女,以便正确生成传递性搜索。这可以通过解决每个子女标记并检查对象来实现,或者更有效的方法可能是获取索引的男性和女性集合(并将它缓存到缓存字典中用于进一步的传递性步骤),并通过索引集合的成员身份检查性别。这两种方法都可以通过传递性查询工厂执行。一个完整的例子留给读者作为练习。
谎言、该死的谎言和统计数据
zope.index.interfaces.IStatistics方法实现提供了最小的可观察性。wordCount总是返回0,因为单词与这种索引无关。documentCount返回索引的关系数。
>>> ix.wordCount() 0 >>> ix.documentCount() 25
重新索引和删除关系
通常,在应用程序的生命周期中使用索引需要更改索引对象。根据zope.index接口,index_doc可以重新索引关系,unindex_doc可以移除它们,而clear可以清除整个索引。
在这里,我们将zope.org项目管理者从Fred更改为Emily。
>>> [load(t, ix, {}) for t in ix.findValueTokens( ... 'subjects', ... q({'reltype': 'has the role of', ... 'objects': roles['Project Manager'], ... 'context': projects['zope.org redesign']}))] [<Person 'Fred'>] >>> rel = intids.getObject(list(ix.findRelationshipTokenSet( ... q({'reltype': 'has the role of', ... 'objects': roles['Project Manager'], ... 'context': projects['zope.org redesign']})))[0]) >>> rel.subjects = (people['Emily'],) >>> ix.index_doc(intids.register(rel), rel) >>> q = ix.tokenizeQuery >>> [load(t, ix, {}) for t in ix.findValueTokens( ... 'subjects', ... q({'reltype': 'has the role of', ... 'objects': roles['Project Manager'], ... 'context': projects['zope.org redesign']}))] [<Person 'Emily'>]
在这里,我们移除了导致“国王伪装”场景中Abe循环的关系。
>>> res = list(ix.findRelationshipTokenChains( ... q({'objects': people['Ingrid'], ... 'reltype': 'manages'}))) >>> len(res) 8 >>> len(res[7]) 8 >>> interfaces.ICircularRelationshipPath.providedBy(res[7]) True >>> rel = intids.getObject(list(ix.findRelationshipTokenSet( ... q({'subjects': people['Gary'], 'reltype': 'manages', ... 'objects': people['Abe']})))[0]) >>> ix.unindex(rel) # == ix.unindex_doc(intids.getId(rel)) >>> ix.documentCount() 24 >>> res = list(ix.findRelationshipTokenChains( ... q({'objects': people['Ingrid'], 'reltype': 'manages'}))) >>> len(res) 4 >>> len(res[3]) 4 >>> interfaces.ICircularRelationshipPath.providedBy(res[3]) False
最后,我们清除了整个索引。
>>> ix.clear() >>> ix.documentCount() 0 >>> list(ix.findRelationshipTokenChains( ... q({'objects': people['Ingrid'], 'reltype': 'manages'}))) [] >>> [load(t, ix, {}) for t in ix.findValueTokens( ... 'subjects', ... q({'reltype': 'has the role of', ... 'objects': roles['Project Manager'], ... 'context': projects['zope.org redesign']}))] []
优化关系索引的使用
索引中集成了三个优化机会。
使用缓存来加载和保存标记;
不加载或保存标记(值本身可能用作标记);并且
返回值与结果家族属于同一btree家族。
对于某些操作,尤其是当单个关系值有数百或数千个成员时,这些优化可以使一些常见的重新索引工作速度提高约100倍。
最简单(可能也是最不实用)的优化是,所有由单个操作生成的保存调用和加载调用共享一个调用类型(保存/加载)的缓存字典。例如,我们可以存储intids实用工具,这样我们只需要进行一次实用工具查找,之后它只是一个字典查找。这正是index.py中默认的generateToken和resolveToken函数所做的事情:看看它们作为例子。
另一种优化是不加载或保存标记,而使用可能是标记的值。如果标记在C中有__cmp__(或等效),例如内置类型如int,这将特别有用。为了指定这种行为,你可以创建一个索引,将索引属性描述的“加载”和“保存”值显式设置为None。
>>> ix = index.Index( ... ({'element': IRelationship['subjects'], 'multiple': True, ... 'dump': None, 'load': None}, ... {'element': IRelationship['relationshiptype'], ... 'dump': relTypeDump, 'load': relTypeLoad, 'btree': OIBTree, ... 'name': 'reltype'}, ... {'element': IRelationship['objects'], 'multiple': True, ... 'dump': None, 'load': None}, ... {'element': IContextAwareRelationship['getContext'], ... 'name': 'context'}), ... index.TransposingTransitiveQueriesFactory('subjects', 'objects')) ... >>> sm['rel_index_2'] = ix >>> app['ex_rel_1'] = rel = Relationship((1,), 'has the role of', (2,)) >>> ix.index(rel) >>> list(ix.findValueTokens('objects', {'subjects': 1})) [2]
最后,如果您有与特定属性存储的BTree相同类型的单个关系,并且该关系涉及数百或数千个对象,那么如果值是该BTree的“倍数”,这将是一个巨大的胜利。默认的属性BTree家族是IFBTree;IOBTree也是一个不错的选择,并且可能对某些应用程序更可取。
>>> ix = index.Index( ... ({'element': IRelationship['subjects'], 'multiple': True, ... 'dump': None, 'load': None}, ... {'element': IRelationship['relationshiptype'], ... 'dump': relTypeDump, 'load': relTypeLoad, 'btree': OIBTree, ... 'name': 'reltype'}, ... {'element': IRelationship['objects'], 'multiple': True, ... 'dump': None, 'load': None}, ... {'element': IContextAwareRelationship['getContext'], ... 'name': 'context'}), ... index.TransposingTransitiveQueriesFactory('subjects', 'objects')) ... >>> sm['rel_index_3'] = ix >>> from BTrees import IFBTree >>> app['ex_rel_2'] = rel = Relationship( ... IFBTree.IFTreeSet((1,)), 'has the role of', IFBTree.IFTreeSet()) >>> ix.index(rel) >>> list(ix.findValueTokens('objects', {'subjects': 1})) [] >>> list(ix.findValueTokens('subjects', {'objects': None})) [1]
重新索引是可能出现一些重大改进的地方。以下操作练习了优化代码。
>>> rel.objects.insert(2) 1 >>> ix.index(rel) >>> list(ix.findValueTokens('objects', {'subjects': 1})) [2] >>> rel.subjects = IFBTree.IFTreeSet((3,4,5)) >>> ix.index(rel) >>> list(ix.findValueTokens('objects', {'subjects': 3})) [2]>>> rel.subjects.insert(6) 1 >>> ix.index(rel) >>> list(ix.findValueTokens('objects', {'subjects': 6})) [2]>>> rel.subjects.update(range(100, 200)) 100 >>> ix.index(rel) >>> list(ix.findValueTokens('objects', {'subjects': 100})) [2]>>> rel.subjects = IFBTree.IFTreeSet((3,4,5,6)) >>> ix.index(rel) >>> list(ix.findValueTokens('objects', {'subjects': 3})) [2]>>> rel.subjects = IFBTree.IFTreeSet(()) >>> ix.index(rel) >>> list(ix.findValueTokens('objects', {'subjects': 3})) []>>> rel.subjects = IFBTree.IFTreeSet((3,4,5)) >>> ix.index(rel) >>> list(ix.findValueTokens('objects', {'subjects': 3})) [2]
tokenizeValues和resolveValueTokens在没有任何加载器或保存器的情况下可以正确工作——也就是说,它们什么也不做。
>>> ix.tokenizeValues((3,4,5), 'subjects') (3, 4, 5) >>> ix.resolveValueTokens((3,4,5), 'subjects') (3, 4, 5)
__contains__ 和取消索引
您可以使用__contains__测试关系是否在索引中。请注意,这使用的是实际的关系,而不是关系标记。
>>> ix = index.Index( ... ({'element': IRelationship['subjects'], 'multiple': True, ... 'dump': dump, 'load': load}, ... {'element': IRelationship['relationshiptype'], ... 'dump': relTypeDump, 'load': relTypeLoad, 'btree': OIBTree, ... 'name': 'reltype'}, ... {'element': IRelationship['objects'], 'multiple': True, ... 'dump': dump, 'load': load}, ... {'element': IContextAwareRelationship['getContext'], ... 'name': 'context'}), ... index.TransposingTransitiveQueriesFactory('subjects', 'objects')) >>> ix.documentCount() 0 >>> app['fredisprojectmanager'].subjects = (people['Fred'],) >>> ix.index(app['fredisprojectmanager']) >>> ix.index(app['another_rel']) >>> ix.documentCount() 2 >>> app['fredisprojectmanager'] in ix True >>> list(ix.findValues( ... 'subjects', ... q({'reltype': 'has the role of', ... 'objects': roles['Project Manager'], ... 'context': projects['zope.org redesign']}))) [<Person 'Fred'>]>>> app['another_rel'] in ix True>>> app['abeAndBran'] in ix False
如前所述,您可以使用unindex(relationship)或unindex_doc(relationship token)来取消索引。
>>> ix.unindex_doc(ix.tokenizeRelationship(app['fredisprojectmanager'])) >>> app['fredisprojectmanager'] in ix False >>> list(ix.findValues( ... 'subjects', ... q({'reltype': 'has the role of', ... 'objects': roles['Project Manager'], ... 'context': projects['zope.org redesign']}))) []>>> ix.unindex(app['another_rel']) >>> app['another_rel'] in ix False
根据zope.index.interfaces.IInjection定义,如果关系不在索引中,则调用unindex_doc是不执行任何操作的;unindex也同理。但apply和其他与zope.index相关的方 法是明显的例外。
>>> ix.unindex(app['abeAndBran']) >>> ix.unindex_doc(ix.tokenizeRelationship(app['abeAndBran']))
apply和其他与zope.index相关的方 法是明显的例外。
关系容器
关系容器持有IRelationship对象。它包括一个API来搜索关系及其关联的对象,包括直接和间接关联的对象。关系本身就是对象,并且它们可以作为其他关系的源或目标进行关联。
在此包中目前有两种接口实现。一种使用intids,另一种使用键引用。它们各有优缺点。
intids使得直接获取intid值成为可能。这可以使将结果与目录搜索和其他intid-based索引合并变得更容易。可能更重要的是,它不会在搜索时为关系创建鬼影对象(除非绝对必要,例如,使用关系过滤器),而是仅使用intids进行搜索。这在您正在搜索大型关系数据库时非常重要:在另一种实现中,关系对象和相关的键引用链接可能会清空整个ZODB对象缓存,这可能导致您整个应用程序的不愉快性能特性。
另一方面,可用的intid数量有限:sys.maxint,或在32位机器上为2147483647。随着intid使用的增加,查找唯一intid的效率会降低。这可以通过将IOBTrees的最大整数增加到64位(9223372036854775807)或使用keyref实现来解决。keyref实现还可以消除依赖——即intid工具本身——如果需要的话。这很重要,如果你无法依赖intid工具,或者相关对象跨越intid工具时。最后,可能直接访问keyref实现的底层属性比intid解引用更快,但这尚未证实,可能是错误的。
在我们的例子中,我们将假设我们已经从一个可用的来源导入了一个容器和一个关系。你可以使用特定于你使用的关联,或者共享中的通用关联,只要它符合接口要求。
还需要注意的是,虽然关系对象是设计的重要部分,但它们不应被滥用。如果你想在其他关系上存储其他数据,应该将其存储在另一个持久对象中,例如属性注解的btree。通常,关系对象将在接口、注解以及可能在对象本身上的小型轻量值的基础上有所不同。
我们将假设有一个名为< cite>app cite>的应用程序,其中包含30个对象(命名为‘ob0’至‘ob29’),我们将对其进行关联。
创建关系容器很容易。我们将使用一个抽象容器,但它可以是来自keyref或intid模块的。
>>> from zc.relationship import interfaces >>> container = Container() >>> from zope.interface.verify import verifyObject >>> verifyObject(interfaces.IRelationshipContainer, container) True
容器可以用作其他对象的组成部分,或作为独立本地实用程序。以下是将它作为本地实用程序添加的一个示例。
>>> sm = app.getSiteManager() >>> sm['lineage_relationship'] = container >>> import zope.interface.interfaces >>> registry = zope.interface.interfaces.IComponentRegistry(sm) >>> registry.registerUtility( ... container, interfaces.IRelationshipContainer, 'lineage') >>> import transaction >>> transaction.commit()
添加关系也很容易:实例化和添加。< cite>add cite>方法添加对象并分配它们随机的字母数字键。
>>> rel = Relationship((app['ob0'],), (app['ob1'],)) >>> verifyObject(interfaces.IRelationship, rel) True >>> container.add(rel)
尽管容器没有< cite>__setitem__ cite>和< cite>__delitem__ cite>(定义< cite>add cite>和< cite>remove cite>),但它确实定义了Python基本映射接口的只读元素。
>>> container[rel.__name__] is rel True >>> len(container) 1 >>> list(container.keys()) == [rel.__name__] True >>> list(container) == [rel.__name__] True >>> list(container.values()) == [rel] True >>> container.get(rel.__name__) is rel True >>> container.get('17') is None True >>> rel.__name__ in container True >>> '17' in container False >>> list(container.items()) == [(rel.__name__, rel)] True
它还支持四种搜索方法:< cite>findTargets cite>、< cite>findSources cite>、< cite>findRelationships cite>和< cite>isLinked cite>。让我们添加更多关系并检查一些相对简单的情况。
>>> container.add(Relationship((app['ob1'],), (app['ob2'],))) >>> container.add(Relationship((app['ob1'],), (app['ob3'],))) >>> container.add(Relationship((app['ob0'],), (app['ob3'],))) >>> container.add(Relationship((app['ob0'],), (app['ob4'],))) >>> container.add(Relationship((app['ob2'],), (app['ob5'],))) >>> transaction.commit() # this is indicative of a bug in ZODB; if you ... # do not do this then new objects will deactivate themselves into ... # nothingness when _p_deactivate is called
现在有六个直接关系(所有关系都指向图中的下方)
ob0 | |\ ob1 | | | | | | ob2 ob3 ob4 | ob5
映射方法仍然与新添加的内容保持一致。
>>> len(container) 6 >>> len(container.keys()) 6 >>> sorted(container.keys()) == sorted( ... v.__name__ for v in container.values()) True >>> sorted(container.items()) == sorted( ... zip(container.keys(), container.values())) True >>> len([v for v in container.values() if container[v.__name__] is v]) 6 >>> sorted(container.keys()) == sorted(container) True
更有趣的是,让我们检查一些搜索方法。ob0的直接目标是什么?
>>> container.findTargets(app['ob0']) # doctest: +ELLIPSIS <generator object ...>
啊哈!这是一个生成器!让我们再试一次。
>>> sorted(o.id for o in container.findTargets(app['ob0'])) ['ob1', 'ob3', 'ob4']
好的,关于那些不超过两个关系距离的,我们使用< cite>maxDepth cite>参数,它是第二个有意义的参数。
>>> sorted(o.id for o in container.findTargets(app['ob0'], 2)) ['ob1', 'ob2', 'ob3', 'ob4']
请注意,尽管ob3可以通过一个和两个关系获得,但它只返回一次。
传递None将获取所有相关对象——这里与传递3或任何更大的整数相同。
>>> sorted(o.id for o in container.findTargets(app['ob0'], None)) ['ob1', 'ob2', 'ob3', 'ob4', 'ob5'] >>> sorted(o.id for o in container.findTargets(app['ob0'], 3)) ['ob1', 'ob2', 'ob3', 'ob4', 'ob5'] >>> sorted(o.id for o in container.findTargets(app['ob0'], 25)) ['ob1', 'ob2', 'ob3', 'ob4', 'ob5']
即使我们放入一个循环也是如此。我们将在ob5和ob1之间放入一个循环并查看结果。
算法的一个重要方面是它首先返回更近的关系,我们在这里可以看到这一点。
>>> container.add(Relationship((app['ob5'],), (app['ob1'],))) >>> transaction.commit() >>> sorted(o.id for o in container.findTargets(app['ob0'], None)) ['ob1', 'ob2', 'ob3', 'ob4', 'ob5'] >>> res = list(o.id for o in container.findTargets(app['ob0'], None)) >>> sorted(res[:3]) # these are all one step away ['ob1', 'ob3', 'ob4'] >>> res[3:] # ob 2 is two steps, and ob5 is three steps. ['ob2', 'ob5']
当你看到源在目标中时,你就知道你在一个循环内部。
>>> sorted(o.id for o in container.findTargets(app['ob1'], None)) ['ob1', 'ob2', 'ob3', 'ob5'] >>> sorted(o.id for o in container.findTargets(app['ob2'], None)) ['ob1', 'ob2', 'ob3', 'ob5'] >>> sorted(o.id for o in container.findTargets(app['ob5'], None)) ['ob1', 'ob2', 'ob3', 'ob5']
如果你请求的不是正整数的距离,你会得到一个ValueError。
>>> container.findTargets(app['ob0'], 0) Traceback (most recent call last): ... ValueError: maxDepth must be None or a positive integer >>> container.findTargets(app['ob0'], -1) Traceback (most recent call last): ... ValueError: maxDepth must be None or a positive integer >>> container.findTargets(app['ob0'], 'kumquat') # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: ...
< cite>findSources cite>方法与< cite>findTargets cite>是镜像:给定一个目标,它找到所有源。使用上面构建的相同关系树,我们将搜索一些源。
>>> container.findSources(app['ob0']) # doctest: +ELLIPSIS <generator object ...> >>> list(container.findSources(app['ob0'])) [] >>> list(o.id for o in container.findSources(app['ob4'])) ['ob0'] >>> list(o.id for o in container.findSources(app['ob4'], None)) ['ob0'] >>> sorted(o.id for o in container.findSources(app['ob1'])) ['ob0', 'ob5'] >>> sorted(o.id for o in container.findSources(app['ob1'], 2)) ['ob0', 'ob2', 'ob5'] >>> sorted(o.id for o in container.findSources(app['ob1'], 3)) ['ob0', 'ob1', 'ob2', 'ob5'] >>> sorted(o.id for o in container.findSources(app['ob1'], None)) ['ob0', 'ob1', 'ob2', 'ob5'] >>> sorted(o.id for o in container.findSources(app['ob3'])) ['ob0', 'ob1'] >>> sorted(o.id for o in container.findSources(app['ob3'], None)) ['ob0', 'ob1', 'ob2', 'ob5'] >>> list(o.id for o in container.findSources(app['ob5'])) ['ob2'] >>> list(o.id for o in container.findSources(app['ob5'], maxDepth=2)) ['ob2', 'ob1'] >>> sorted(o.id for o in container.findSources(app['ob5'], maxDepth=3)) ['ob0', 'ob1', 'ob2', 'ob5'] >>> container.findSources(app['ob0'], 0) Traceback (most recent call last): ... ValueError: maxDepth must be None or a positive integer >>> container.findSources(app['ob0'], -1) Traceback (most recent call last): ... ValueError: maxDepth must be None or a positive integer >>> container.findSources(app['ob0'], 'kumquat') # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: ...
< cite>findRelationships cite>方法查找两个对象之间或从/到两个对象的所有关系。因为它支持传递关系,所以结果迭代器的每个成员都是一个关系元组。
所有参数对findRelationships来说是可选的,但至少需要传入source或target中的一个。搜索深度默认为一层关系,就像其他方法一样。
>>> container.findRelationships(source=app['ob0']) # doctest: +ELLIPSIS <generator object ...> >>> sorted( ... [repr(rel) for rel in path] ... for path in container.findRelationships(source=app['ob0'])) ... # doctest: +NORMALIZE_WHITESPACE [['<Relationship from (<Demo ob0>,) to (<Demo ob1>,)>'], ['<Relationship from (<Demo ob0>,) to (<Demo ob3>,)>'], ['<Relationship from (<Demo ob0>,) to (<Demo ob4>,)>']] >>> list(container.findRelationships(target=app['ob0'])) [] >>> sorted( ... [repr(rel) for rel in path] ... for path in container.findRelationships(target=app['ob3'])) ... # doctest: +NORMALIZE_WHITESPACE [['<Relationship from (<Demo ob0>,) to (<Demo ob3>,)>'], ['<Relationship from (<Demo ob1>,) to (<Demo ob3>,)>']] >>> list( ... [repr(rel) for rel in path] ... for path in container.findRelationships( ... source=app['ob1'], target=app['ob3'])) ... # doctest: +NORMALIZE_WHITESPACE [['<Relationship from (<Demo ob1>,) to (<Demo ob3>,)>']] >>> container.findRelationships() Traceback (most recent call last): ... ValueError: at least one of `source` and `target` must be provided
它们也可以作为位置参数使用,顺序为source和target。
>>> sorted( ... [repr(rel) for rel in path] ... for path in container.findRelationships(app['ob1'])) ... # doctest: +NORMALIZE_WHITESPACE [['<Relationship from (<Demo ob1>,) to (<Demo ob2>,)>'], ['<Relationship from (<Demo ob1>,) to (<Demo ob3>,)>']] >>> sorted( ... [repr(rel) for rel in path] ... for path in container.findRelationships(app['ob5'], app['ob1'])) ... # doctest: +NORMALIZE_WHITESPACE [['<Relationship from (<Demo ob5>,) to (<Demo ob1>,)>']]
maxDepth同样可用,但现在它是第三个位置参数,所以与其他方法相比,关键字使用会更频繁。请注意,第二条路径有两个成员:从ob1到ob2,然后从ob2到ob5。
>>> sorted( ... [repr(rel) for rel in path] ... for path in container.findRelationships(app['ob1'], maxDepth=2)) ... # doctest: +NORMALIZE_WHITESPACE [['<Relationship from (<Demo ob1>,) to (<Demo ob2>,)>'], ['<Relationship from (<Demo ob1>,) to (<Demo ob2>,)>', '<Relationship from (<Demo ob2>,) to (<Demo ob5>,)>'], ['<Relationship from (<Demo ob1>,) to (<Demo ob3>,)>']]
返回的是独特的关系,而不是独特对象。因此,尽管ob3只有两个传递来源ob1和ob0,但它有三个传递路径。
>>> sorted( ... [repr(rel) for rel in path] ... for path in container.findRelationships( ... target=app['ob3'], maxDepth=2)) ... # doctest: +NORMALIZE_WHITESPACE [['<Relationship from (<Demo ob0>,) to (<Demo ob1>,)>', '<Relationship from (<Demo ob1>,) to (<Demo ob3>,)>'], ['<Relationship from (<Demo ob0>,) to (<Demo ob3>,)>'], ['<Relationship from (<Demo ob1>,) to (<Demo ob3>,)>'], ['<Relationship from (<Demo ob5>,) to (<Demo ob1>,)>', '<Relationship from (<Demo ob1>,) to (<Demo ob3>,)>']]
对于ob0的目标也是如此。
>>> sorted( ... [repr(rel) for rel in path] ... for path in container.findRelationships( ... source=app['ob0'], maxDepth=2)) ... # doctest: +NORMALIZE_WHITESPACE [['<Relationship from (<Demo ob0>,) to (<Demo ob1>,)>'], ['<Relationship from (<Demo ob0>,) to (<Demo ob1>,)>', '<Relationship from (<Demo ob1>,) to (<Demo ob2>,)>'], ['<Relationship from (<Demo ob0>,) to (<Demo ob1>,)>', '<Relationship from (<Demo ob1>,) to (<Demo ob3>,)>'], ['<Relationship from (<Demo ob0>,) to (<Demo ob3>,)>'], ['<Relationship from (<Demo ob0>,) to (<Demo ob4>,)>']]
循环关系以实现ICircularRelationshipPath的特殊元组返回。例如,考虑所有从ob0出发的路径。首先请注意,所有路径都是按从短到长的顺序排列的。
>>> res = list( ... [repr(rel) for rel in path] ... for path in container.findRelationships( ... app['ob0'], maxDepth=None)) ... # doctest: +NORMALIZE_WHITESPACE >>> sorted(res[:3]) # one step away # doctest: +NORMALIZE_WHITESPACE [['<Relationship from (<Demo ob0>,) to (<Demo ob1>,)>'], ['<Relationship from (<Demo ob0>,) to (<Demo ob3>,)>'], ['<Relationship from (<Demo ob0>,) to (<Demo ob4>,)>']] >>> sorted(res[3:5]) # two steps away # doctest: +NORMALIZE_WHITESPACE [['<Relationship from (<Demo ob0>,) to (<Demo ob1>,)>', '<Relationship from (<Demo ob1>,) to (<Demo ob2>,)>'], ['<Relationship from (<Demo ob0>,) to (<Demo ob1>,)>', '<Relationship from (<Demo ob1>,) to (<Demo ob3>,)>']] >>> res[5:] # three and four steps away # doctest: +NORMALIZE_WHITESPACE [['<Relationship from (<Demo ob0>,) to (<Demo ob1>,)>', '<Relationship from (<Demo ob1>,) to (<Demo ob2>,)>', '<Relationship from (<Demo ob2>,) to (<Demo ob5>,)>'], ['<Relationship from (<Demo ob0>,) to (<Demo ob1>,)>', '<Relationship from (<Demo ob1>,) to (<Demo ob2>,)>', '<Relationship from (<Demo ob2>,) to (<Demo ob5>,)>', '<Relationship from (<Demo ob5>,) to (<Demo ob1>,)>']]
最后一个路径是循环的。
现在我们将表达式更改只为包含实现ICircularRelationshipPath的路径。
>>> list( ... [repr(rel) for rel in path] ... for path in container.findRelationships( ... app['ob0'], maxDepth=None) ... if interfaces.ICircularRelationshipPath.providedBy(path)) ... # doctest: +NORMALIZE_WHITESPACE [['<Relationship from (<Demo ob0>,) to (<Demo ob1>,)>', '<Relationship from (<Demo ob1>,) to (<Demo ob2>,)>', '<Relationship from (<Demo ob2>,) to (<Demo ob5>,)>', '<Relationship from (<Demo ob5>,) to (<Demo ob1>,)>']]
请注意,由于关系可能有多个目标,即使对不生成循环的目标进行遍历,具有循环的关系也可能被遍历。更远的路径不会标记为循环。
循环路径不仅有一个标记接口来识别它们,还包括一个
>>> path = [path for path in container.findRelationships( ... app['ob0'], maxDepth=None) ... if interfaces.ICircularRelationshipPath.providedBy(path)][0] >>> path.cycled [{'source': <Demo ob1>}] >>> app['ob1'] in path[-1].targets True
如果只提供了目标,循环搜索将从路径中的第一个关系开始继续。
>>> path = [path for path in container.findRelationships( ... target=app['ob5'], maxDepth=None) ... if interfaces.ICircularRelationshipPath.providedBy(path)][0] >>> path # doctest: +NORMALIZE_WHITESPACE cycle(<Relationship from (<Demo ob5>,) to (<Demo ob1>,)>, <Relationship from (<Demo ob1>,) to (<Demo ob2>,)>, <Relationship from (<Demo ob2>,) to (<Demo ob5>,)>) >>> path.cycled [{'target': <Demo ob5>}]
maxDepth也可以与源和目标的组合一起使用。
>>> list(container.findRelationships( ... app['ob0'], app['ob5'], maxDepth=None)) ... # doctest: +NORMALIZE_WHITESPACE [(<Relationship from (<Demo ob0>,) to (<Demo ob1>,)>, <Relationship from (<Demo ob1>,) to (<Demo ob2>,)>, <Relationship from (<Demo ob2>,) to (<Demo ob5>,)>)]
像往常一样,maxDepth必须是一个正整数或None。
>>> container.findRelationships(app['ob0'], maxDepth=0) Traceback (most recent call last): ... ValueError: maxDepth must be None or a positive integer >>> container.findRelationships(app['ob0'], maxDepth=-1) Traceback (most recent call last): ... ValueError: maxDepth must be None or a positive integer >>> container.findRelationships(app['ob0'], maxDepth='kumquat') ... # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: ...
isLinked方法是一种方便的方式来测试两个对象是否链接,或者一个对象是否是图中的源或目标。它默认为maxDepth为1。
>>> container.isLinked(app['ob0'], app['ob1']) True >>> container.isLinked(app['ob0'], app['ob2']) False
请注意,当只提供源或目标之一时,maxDepth是没有意义的。
>>> container.isLinked(source=app['ob29']) False >>> container.isLinked(target=app['ob29']) False >>> container.isLinked(source=app['ob0']) True >>> container.isLinked(target=app['ob4']) True >>> container.isLinked(source=app['ob4']) False >>> container.isLinked(target=app['ob0']) False
但搜索两个对象之间的链接时,设置maxDepth的作用与平时一样。
>>> container.isLinked(app['ob0'], app['ob2'], maxDepth=2) True >>> container.isLinked(app['ob0'], app['ob5'], maxDepth=2) False >>> container.isLinked(app['ob0'], app['ob5'], maxDepth=3) True >>> container.isLinked(app['ob0'], app['ob5'], maxDepth=None) True
像往常一样,maxDepth必须是一个正整数或None。
>>> container.isLinked(app['ob0'], app['ob1'], maxDepth=0) Traceback (most recent call last): ... ValueError: maxDepth must be None or a positive integer >>> container.isLinked(app['ob0'], app['ob1'], maxDepth=-1) Traceback (most recent call last): ... ValueError: maxDepth must be None or a positive integer >>> container.isLinked(app['ob0'], app['ob1'], maxDepth='kumquat') ... # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: ...
remove方法是核心接口的倒数第二个方法:它允许您从容器中删除关系。它需要一个关系对象。
例如,让我们删除我们为了创建循环而创建的从ob5到ob1的关系。
>>> res = list(container.findTargets(app['ob2'], None)) # before removal >>> len(res) 4 >>> res[:2] [<Demo ob5>, <Demo ob1>] >>> sorted(repr(o) for o in res[2:]) ['<Demo ob2>', '<Demo ob3>'] >>> res = list(container.findSources(app['ob2'], None)) # before removal >>> res[0] <Demo ob1> >>> res[3] <Demo ob2> >>> sorted(repr(o) for o in res[1:3]) ['<Demo ob0>', '<Demo ob5>'] >>> rel = list(container.findRelationships(app['ob5'], app['ob1']))[0][0] >>> rel.sources (<Demo ob5>,) >>> rel.targets (<Demo ob1>,) >>> container.remove(rel) >>> list(container.findRelationships(app['ob5'], app['ob1'])) [] >>> list(container.findTargets(app['ob2'], None)) # after removal [<Demo ob5>] >>> list(container.findSources(app['ob2'], None)) # after removal [<Demo ob1>, <Demo ob0>]
最后,reindex方法允许对容器中已经存在的对象进行重新索引。关系对象默认实现当源和目标发生变化时自动调用此方法。
为了重申,关系看起来是这样的。
ob0 | |\ ob1 | | | | | | ob2 ob3 ob4 | ob5
我们将用ob3和ob4替换,这样图表看起来就像这样。
ob0 | |\ ob1 | | | | | | ob2 ob4 ob3 | ob5 >>> sorted(ob.id for ob in container.findTargets(app['ob1'])) ['ob2', 'ob3'] >>> sorted(ob.id for ob in container.findSources(app['ob3'])) ['ob0', 'ob1'] >>> sorted(ob.id for ob in container.findSources(app['ob4'])) ['ob0'] >>> rel = next( ... iter(container.findRelationships(app['ob1'], app['ob3']) ... ))[0] >>> rel.targets (<Demo ob3>,) >>> rel.targets = [app['ob4']] # this calls reindex >>> rel.targets (<Demo ob4>,) >>> sorted(ob.id for ob in container.findTargets(app['ob1'])) ['ob2', 'ob4'] >>> sorted(ob.id for ob in container.findSources(app['ob3'])) ['ob0'] >>> sorted(ob.id for ob in container.findSources(app['ob4'])) ['ob0', 'ob1']
如果改变源也会发生类似的情况。我们将图表改为这样。
ob0 | |\ ob1 | | | | | ob2 | ob3 | \ | ob5 ob4 >>> rel.sources (<Demo ob1>,) >>> rel.sources = (app['ob2'],) # this calls reindex >>> rel.sources (<Demo ob2>,) >>> sorted(ob.id for ob in container.findTargets(app['ob1'])) ['ob2'] >>> sorted(ob.id for ob in container.findTargets(app['ob2'])) ['ob4', 'ob5'] >>> sorted(ob.id for ob in container.findTargets(app['ob0'])) ['ob1', 'ob3', 'ob4'] >>> sorted(ob.id for ob in container.findSources(app['ob4'])) ['ob0', 'ob2']
高级用法
关系容器还可以执行其他四个高级技巧:启用搜索过滤器;允许单个关系有多个源和目标;允许关联关系;以及公开未解析的令牌结果。
搜索过滤器
由于关系本身就是对象,因此可能存在许多有趣的用法。它们可以实现额外的接口,有注释,并具有其他属性。其中一个用途是仅查找具有提供给定接口的关系的路径上的对象。允许在findSources、findTargets、findRelationships和isLinked中使用filter参数,支持此类用例。
例如,想象我们改变关系,如下面的图表所示。xxx线表示实现ISpecialRelationship的关系。
ob0 x |x ob1 | x x | x ob2 | ob3 | x | ob5 ob4
也就是说,从ob0到ob1、ob0到ob3、ob1到ob2和ob2到ob4的关系实现了特殊接口。让我们首先实现这一点。
>>> from zope import interface >>> class ISpecialInterface(interface.Interface): ... """I'm special! So special!""" ... >>> for src, tgt in ( ... (app['ob0'], app['ob1']), ... (app['ob0'], app['ob3']), ... (app['ob1'], app['ob2']), ... (app['ob2'], app['ob4'])): ... rel = list(container.findRelationships(src, tgt))[0][0] ... interface.directlyProvides(rel, ISpecialInterface) ...
现在我们可以使用ISpecialInterface.providedBy作为上述所有方法的过滤器。
findTargets
>>> sorted(ob.id for ob in container.findTargets(app['ob0'])) ['ob1', 'ob3', 'ob4'] >>> sorted(ob.id for ob in container.findTargets( ... app['ob0'], filter=ISpecialInterface.providedBy)) ['ob1', 'ob3'] >>> sorted(ob.id for ob in container.findTargets( ... app['ob0'], maxDepth=None)) ['ob1', 'ob2', 'ob3', 'ob4', 'ob5'] >>> sorted(ob.id for ob in container.findTargets( ... app['ob0'], maxDepth=None, filter=ISpecialInterface.providedBy)) ['ob1', 'ob2', 'ob3', 'ob4']
findSources
>>> sorted(ob.id for ob in container.findSources(app['ob4'])) ['ob0', 'ob2'] >>> sorted(ob.id for ob in container.findSources( ... app['ob4'], filter=ISpecialInterface.providedBy)) ['ob2'] >>> sorted(ob.id for ob in container.findSources( ... app['ob4'], maxDepth=None)) ['ob0', 'ob1', 'ob2'] >>> sorted(ob.id for ob in container.findSources( ... app['ob4'], maxDepth=None, filter=ISpecialInterface.providedBy)) ['ob0', 'ob1', 'ob2'] >>> sorted(ob.id for ob in container.findSources( ... app['ob5'], maxDepth=None)) ['ob0', 'ob1', 'ob2'] >>> list(ob.id for ob in container.findSources( ... app['ob5'], filter=ISpecialInterface.providedBy)) []
findRelationships
>>> len(list(container.findRelationships( ... app['ob0'], app['ob4'], maxDepth=None))) 2 >>> len(list(container.findRelationships( ... app['ob0'], app['ob4'], maxDepth=None, ... filter=ISpecialInterface.providedBy))) 1 >>> len(list(container.findRelationships(app['ob0']))) 3 >>> len(list(container.findRelationships( ... app['ob0'], filter=ISpecialInterface.providedBy))) 2
isLinked
>>> container.isLinked(app['ob0'], app['ob5'], maxDepth=None) True >>> container.isLinked( ... app['ob0'], app['ob5'], maxDepth=None, ... filter=ISpecialInterface.providedBy) False >>> container.isLinked( ... app['ob0'], app['ob2'], maxDepth=None, ... filter=ISpecialInterface.providedBy) True >>> container.isLinked( ... app['ob0'], app['ob4']) True >>> container.isLinked( ... app['ob0'], app['ob4'], ... filter=ISpecialInterface.providedBy) False
多个来源和/或目标;重复关系
关系不一定只存在于单一源和单一目标之间。对此有许多可能的处理方法,但一种简单的方法是允许关系有多个源和多个目标。这是关系容器支持的一种方法。
>>> container.add(Relationship( ... (app['ob2'], app['ob4'], app['ob5'], app['ob6'], app['ob7']), ... (app['ob1'], app['ob4'], app['ob8'], app['ob9'], app['ob10']))) >>> container.add(Relationship( ... (app['ob10'], app['ob0']), ... (app['ob7'], app['ob3'])))
在我们检查结果之前,先看看这些。
其中有趣的一点是,我们在第一个例子中重复了ob2->ob4关系,在第二个例子中重复了ob0->ob3关系。关系容器不限制重复关系:它只是添加和索引它们,并在findRelationships中包含额外的路径。
>>> sorted(o.id for o in container.findTargets(app['ob4'])) ['ob1', 'ob10', 'ob4', 'ob8', 'ob9'] >>> sorted(o.id for o in container.findTargets(app['ob10'])) ['ob3', 'ob7'] >>> sorted(o.id for o in container.findTargets(app['ob4'], maxDepth=2)) ['ob1', 'ob10', 'ob2', 'ob3', 'ob4', 'ob7', 'ob8', 'ob9'] >>> sorted( ... [repr(rel) for rel in path] ... for path in container.findRelationships( ... app['ob2'], app['ob4'])) ... # doctest: +NORMALIZE_WHITESPACE [['<Relationship from (<Demo ob2>, <Demo ob4>, <Demo ob5>, <Demo ob6>, <Demo ob7>) to (<Demo ob1>, <Demo ob4>, <Demo ob8>, <Demo ob9>, <Demo ob10>)>'], ['<Relationship from (<Demo ob2>,) to (<Demo ob4>,)>']]
还有一个自反关系,其中ob4指向ob4。它被标记为一个循环。
>>> list(container.findRelationships(app['ob4'], app['ob4'])) ... # doctest: +NORMALIZE_WHITESPACE [cycle(<Relationship from (<Demo ob2>, <Demo ob4>, <Demo ob5>, <Demo ob6>, <Demo ob7>) to (<Demo ob1>, <Demo ob4>, <Demo ob8>, <Demo ob9>, <Demo ob10>)>,)] >>> list(container.findRelationships(app['ob4'], app['ob4']))[0].cycled [{'source': <Demo ob4>}]
关联关系和关系容器
关系是对象。我们已展示了如何通过实现不同的接口和注释来实现这一点。这也意味着关系是第一类对象,可以相互关联。这允许关系跟踪创建其他关系的用户,以及其他用例。
甚至关系容器本身也可以是关系容器中的节点。
>>> container1 = app['container1'] = Container() >>> container2 = app['container2'] = Container() >>> rel = Relationship((container1,), (container2,)) >>> container.add(rel) >>> container.isLinked(container1, container2) True
公开未解决令牌
对于特定的用例,通常是优化,有时访问给定实现的原生结果是有用的。例如,如果一个关系有许多成员,那么基于intid的关系容器返回实际intid可能是有意义的。
这些容器包括三种用于此类用例的方法:findTargetTokens、findSourceTokens和findRelationshipTokens。它们接受与同名的类似方法相同的参数。
便利类
存在三个方便的类,用于具有单个源和/或单个目标的关联。
一对一关系
OneToOneRelationship将单个源与单个目标相关联。
>>> from zc.relationship.shared import OneToOneRelationship >>> rel = OneToOneRelationship(app['ob20'], app['ob21'])>>> verifyObject(interfaces.IOneToOneRelationship, rel) True
所有容器方法都像通用多对多关系一样工作。我们重复了上面主部分中定义的一些测试(那里定义的所有关系实际上都是一对一关系)。
>>> container.add(rel) >>> container.add(OneToOneRelationship(app['ob21'], app['ob22'])) >>> container.add(OneToOneRelationship(app['ob21'], app['ob23'])) >>> container.add(OneToOneRelationship(app['ob20'], app['ob23'])) >>> container.add(OneToOneRelationship(app['ob20'], app['ob24'])) >>> container.add(OneToOneRelationship(app['ob22'], app['ob25'])) >>> rel = OneToOneRelationship(app['ob25'], app['ob21']) >>> container.add(rel)
findTargets
>>> sorted(o.id for o in container.findTargets(app['ob20'], 2)) ['ob21', 'ob22', 'ob23', 'ob24']
findSources
>>> sorted(o.id for o in container.findSources(app['ob21'], 2)) ['ob20', 'ob22', 'ob25']
findRelationships
>>> sorted( ... [repr(rel) for rel in path] ... for path in container.findRelationships(app['ob21'], maxDepth=2)) ... # doctest: +NORMALIZE_WHITESPACE [['<Relationship from (<Demo ob21>,) to (<Demo ob22>,)>'], ['<Relationship from (<Demo ob21>,) to (<Demo ob22>,)>', '<Relationship from (<Demo ob22>,) to (<Demo ob25>,)>'], ['<Relationship from (<Demo ob21>,) to (<Demo ob23>,)>']]>>> sorted( ... [repr(rel) for rel in path] ... for path in container.findRelationships( ... target=app['ob23'], maxDepth=2)) ... # doctest: +NORMALIZE_WHITESPACE [['<Relationship from (<Demo ob20>,) to (<Demo ob21>,)>', '<Relationship from (<Demo ob21>,) to (<Demo ob23>,)>'], ['<Relationship from (<Demo ob20>,) to (<Demo ob23>,)>'], ['<Relationship from (<Demo ob21>,) to (<Demo ob23>,)>'], ['<Relationship from (<Demo ob25>,) to (<Demo ob21>,)>', '<Relationship from (<Demo ob21>,) to (<Demo ob23>,)>']]>>> list(container.findRelationships( ... app['ob20'], app['ob25'], maxDepth=None)) ... # doctest: +NORMALIZE_WHITESPACE [(<Relationship from (<Demo ob20>,) to (<Demo ob21>,)>, <Relationship from (<Demo ob21>,) to (<Demo ob22>,)>, <Relationship from (<Demo ob22>,) to (<Demo ob25>,)>)]>>> list( ... [repr(rel) for rel in path] ... for path in container.findRelationships( ... app['ob20'], maxDepth=None) ... if interfaces.ICircularRelationshipPath.providedBy(path)) ... # doctest: +NORMALIZE_WHITESPACE [['<Relationship from (<Demo ob20>,) to (<Demo ob21>,)>', '<Relationship from (<Demo ob21>,) to (<Demo ob22>,)>', '<Relationship from (<Demo ob22>,) to (<Demo ob25>,)>', '<Relationship from (<Demo ob25>,) to (<Demo ob21>,)>']]
isLinked
>>> container.isLinked(source=app['ob20']) True >>> container.isLinked(target=app['ob24']) True >>> container.isLinked(source=app['ob24']) False >>> container.isLinked(target=app['ob20']) False >>> container.isLinked(app['ob20'], app['ob22'], maxDepth=2) True >>> container.isLinked(app['ob20'], app['ob25'], maxDepth=2) False
删除
>>> res = list(container.findTargets(app['ob22'], None)) # before removal >>> res[:2] [<Demo ob25>, <Demo ob21>] >>> container.remove(rel) >>> list(container.findTargets(app['ob22'], None)) # after removal [<Demo ob25>]
重新索引
>>> rel = next( ... iter(container.findRelationships(app['ob21'], app['ob23'])) ... )[0]>>> rel.target <Demo ob23> >>> rel.target = app['ob24'] # this calls reindex >>> rel.target <Demo ob24>>>> rel.source <Demo ob21> >>> rel.source = app['ob22'] # this calls reindex >>> rel.source <Demo ob22>
多对一关系
ManyToOneRelationship将多个源与单个目标相关联。
>>> from zc.relationship.shared import ManyToOneRelationship >>> rel = ManyToOneRelationship((app['ob22'], app['ob26']), app['ob24'])>>> verifyObject(interfaces.IManyToOneRelationship, rel) True>>> container.add(rel) >>> container.add(ManyToOneRelationship( ... (app['ob26'], app['ob23']), ... app['ob20']))
关系图现在看起来像这样
ob20 (ob22, obj26) (ob26, obj23) | |\ | | ob21 | | obj24 obj20 | | | ob22 | ob23 | \ | ob25 ob24
我们通过obj23创建了obj20的循环。
>>> sorted(o.id for o in container.findSources(app['ob24'], None)) ['ob20', 'ob21', 'ob22', 'ob23', 'ob26']>>> sorted(o.id for o in container.findSources(app['ob20'], None)) ['ob20', 'ob23', 'ob26']>>> list(container.findRelationships(app['ob20'], app['ob20'], None)) ... # doctest: +NORMALIZE_WHITESPACE [cycle(<Relationship from (<Demo ob20>,) to (<Demo ob23>,)>, <Relationship from (<Demo ob26>, <Demo ob23>) to (<Demo ob20>,)>)] >>> list(container.findRelationships( ... app['ob20'], app['ob20'], 2))[0].cycled [{'source': <Demo ob20>}]
ManyToOneRelationship的sources属性是可变的,而它的targets属性是不可变的。
>>> rel.sources (<Demo ob22>, <Demo ob26>) >>> rel.sources = [app['ob26'], app['ob24']]>>> rel.targets (<Demo ob24>,) >>> rel.targets = (app['ob22'],) Traceback (most recent call last): ... AttributeError: can't set attribute
但关系还有一个额外的可变target属性。
>>> rel.target <Demo ob24> >>> rel.target = app['ob22']
一对多关系
OneToManyRelationship将单个源与多个目标相关联。
>>> from zc.relationship.shared import OneToManyRelationship >>> rel = OneToManyRelationship(app['ob22'], (app['ob20'], app['ob27']))>>> verifyObject(interfaces.IOneToManyRelationship, rel) True>>> container.add(rel) >>> container.add(OneToManyRelationship( ... app['ob20'], ... (app['ob23'], app['ob28'])))
更新后的图看起来像这样
ob20 (ob26, obj24) (ob26, obj23) | |\ | | ob21 | | obj22 obj20 | | | | | ob22 | ob23 (ob20, obj27) (ob23, obj28) | \ | ob25 ob24
现在对于ob22共有三个循环。
>>> sorted(o.id for o in container.findTargets(app['ob22'])) ['ob20', 'ob24', 'ob25', 'ob27'] >>> sorted(o.id for o in container.findTargets(app['ob22'], None)) ['ob20', 'ob21', 'ob22', 'ob23', 'ob24', 'ob25', 'ob27', 'ob28']>>> sorted(o.id for o in container.findTargets(app['ob20'])) ['ob21', 'ob23', 'ob24', 'ob28'] >>> sorted(o.id for o in container.findTargets(app['ob20'], None)) ['ob20', 'ob21', 'ob22', 'ob23', 'ob24', 'ob25', 'ob27', 'ob28']>>> sorted(repr(c) for c in ... container.findRelationships(app['ob22'], app['ob22'], None)) ... # doctest: +NORMALIZE_WHITESPACE ['cycle(<Relationship from (<Demo ob22>,) to (<Demo ob20>, <Demo ob27>)>, <Relationship from (<Demo ob20>,) to (<Demo ob21>,)>, <Relationship from (<Demo ob21>,) to (<Demo ob22>,)>)', 'cycle(<Relationship from (<Demo ob22>,) to (<Demo ob20>, <Demo ob27>)>, <Relationship from (<Demo ob20>,) to (<Demo ob24>,)>, <Relationship from (<Demo ob26>, <Demo ob24>) to (<Demo ob22>,)>)', 'cycle(<Relationship from (<Demo ob22>,) to (<Demo ob24>,)>, <Relationship from (<Demo ob26>, <Demo ob24>) to (<Demo ob22>,)>)']
OneToManyRelationship的targets属性是可变的,而它的sources属性是不可变的。
>>> rel.targets (<Demo ob20>, <Demo ob27>) >>> rel.targets = [app['ob28'], app['ob21']]>>> rel.sources (<Demo ob22>,) >>> rel.sources = (app['ob23'],) Traceback (most recent call last): ... AttributeError: can't set attribute
但关系还有一个额外的可变source属性。
>>> rel.source <Demo ob22> >>> rel.target = app['ob23']
变更
2.1 (2021-03-22)
支持Python 3.7至3.9。
更新到zope.component >= 5。
2.0.post1 (2018-06-19)
通过使用正确的ReST语法修复PyPI页面。
2.0 (2018-06-19)
2.x系列与1.x系列几乎完全兼容。一个值得注意的不兼容性不会影响关系容器的使用,并且足够小,希望不会影响任何人。
新要求
zc.relation
与 1.0 不兼容
findRelationships现在将使用默认的transitiveQueriesFactory,如果已设置。如果您不希望这种行为,请将maxDepth设置为1。
一些实例化异常有不同的错误消息。
2.0 中的变更
关系索引代码已被移至zc.relation,并在那里进行了重大重构。一个完全向后兼容的子类保留在zc.relationship.index中。
支持64位和32位BTree家族
通过传递可调用对象而不是接口元素(仍然支持)来指定索引值。
在findValues和findValueTokens中,query参数现在是可选的。如果query在布尔上下文中评估为False,则返回所有值或值标记。值标记将显式使用底层BTree存储返回。然后可以直接用于其他BTree操作。
在这些和其他情况下,您永远不应该修改返回的结果!它们可能是内部数据结构(且有意如此,以便可以用于其他用途的效率较高的集合操作)。接口有望阐明哪些调用将返回内部数据结构。
README有一个新的开始,它不仅演示了一些新功能,而且试图比后面的部分简单一些。
findRelationships和新的方法findRelationshipTokens可以递归地找到关系和非递归地找到关系。当非递归地使用findRelationshipTokens时,重复了findRelationshipTokenSet的行为。(findRelationshipTokenSet在API中保持不变,未弃用,是findValueTokenSet的配套工具。)
索引模块的100%测试覆盖率(按照通常具有误导性的行分析::-)。
与Python 2.7和Python >= 3.5进行了测试。
添加了测试额外功能,以声明对zope.app.folder的测试依赖。
1.1 分支
支持Zope 3.4/Zope 2.11/ZODB 3.8
1.1.0
调整以适应ZODB 3.8中的BTrees更改(感谢Juergen Kartnaller)
将buildout转换为仅依赖于eggs
1.0 分支
支持Zope 3.3/Zope 2.10/ZODB 3.7
1.0.2
从Markus Kemmerling合并了关系容器中的测试和错误修复
ManyToOneRelationship实例化失败
当源和目标都不为空,但bool(target)评估为False时,findRelationships方法行为异常。
ISourceRelationship和ITargetRelationship存在错误。
1.0.1
从Gabriel Shaar合并了测试和错误修复
if the target parameter is a container with no objects, then `shared.AbstractContainer.isLinked` resolves to False in a bool context and tokenization fails. `target and tokenize({'target': target})` returns the target instead of the result of the tokenize function.
使README.rst测试在更广泛的机器集上通过(这是一个测试改进;关系索引没有脆弱性)。由Gabriel Shaar报告。
1.0.0
首次发布
项目详情
下载文件
下载适用于您平台文件的文件。如果您不确定选择哪个,请了解有关安装包的更多信息。
源分布
构建分布
zc.relationship-2.1.tar.gz的散列
算法 | 散列摘要 | |
---|---|---|
SHA256 | 3e58eb3ce8df49580a4b044bcf622620e430b3f44bd5c009d06deeb10115f6f4 |
|
MD5 | e9446a907c1913c708939f714207db26 |
|
BLAKE2b-256 | 892a88a5e8febff114c2ab41168c2e6fbcb81f73b8e547203c76afb674621dec |
zc.relationship-2.1-py2.py3-none-any.whl的哈希值
算法 | 散列摘要 | |
---|---|---|
SHA256 | 1aa2d38a283fd3a9d06f10b395f9343506bb2b2715449dd1f19cfdedf1ada872 |
|
MD5 | 8d68070cc3c22d67fda37e82d2569786 |
|
BLAKE2b-256 | 7a2556997bb34fb96e225895ebc2dfe5f1be4c26978f96219414b11bcb26af09 |