级别: 初级 David Mertz,博士 (mertz@gnosis.cx), 归类者, Gnosis Software, Inc.
2002 年 10 月 01 日
迄今为止存在的大多数 XML API 已经在编程级别中强制实行了良好的格式,但几乎没有几个 API 能保证有效性。这在整个 XML 处理领域中是个严重的缺陷。本文讨论了作者的
gnosis.xml.validity 库,该库对旨在用于 XML 序列化的 Python 对象强制有效性。
在我为
developerWorks编写的较早的
技巧中,我从概念上查看了面向对象编程(OOP)技术如何与 XML 有效性约束相符。
XML 问题的这篇文章呈现了做到这一点的一个实际 Python 模块的早期版本。您可以用其它编程语言创建类似的功能,但 Python 提供了非常通用的反射机制,而且能够清晰地表达有效性约束。
从表面看,Python ― 需要极其动态(虽然很严格)的类型确定 ― 可能象实现什么在本质上是精致的类型系统的一个奇怪选择。但是您可以感觉到的任何奇怪的东西都是表面的。实际上,虽然象 Java 语言、C++ 和 C# 之类的语言类型系统都是静态的,但是它们太有限而不能向 XML 有效性约束提供非常有意义的帮助。象 Haskell 那样的纯函数型语言可以提供类型层次结构、区别合并、量化、存在类型等等,但 OOP 语言通常缺乏这些功能。在静态类型的 OOP 语言中,您必须将刚好与当前讨论的 Python 库中找到的数量相同的定制有效性构建到库中。
模块
gnosis.xml.validity 能非常有用地与几个与 XML 相关的其它模块相对照。较早的文章(请参阅
参考资料)讨论了已合并到作者的
gnosis.xml 包中的两个其它库。
gnosis.xml.pickle 能够产生无论什么样的任意 Python 对象的专门 XML 序列化,而且使用 Python 的标准
pickle 和
cPickle 模块,它还提供了保存和恢复对象的方法。另外,
gnosis.xml.objectify 是反向操作的:如果存在一个任意的 XML 文档,那么您可以生成一个类似 Python 的对象(会丢失一点有关最初 XML 的信息)。
Python 标准库包含对 XML 文档的 DOM 和 SAX 处理的支持。广泛使用的第三方 Python 包将支持扩展至包括 XSLT 处理:
- DOM(特别是
xml.dom.minidom )为 XML 文档的 OOP-风格操作提供了相当繁重的 API ― 有一些方法共同用于许多编程语言中的 DOM 实现。
- SAX 将 XML 文档视为一系列解析事件,而且它基本上允许过程性编程风格。
- XSLT 声明了一组将 XML 文档转换成别的文档(诸如其它 XML 文档)的规则。
所有这些库都很有用,但没有一个用破坏底层 XML 有效性的方法阻止应用程序修改用 XML 表示的对象。例如,删除、添加或移动一个 DOM 节点都能轻松地创建不能被转储成有效 XML 文档的 DOM 层次结构。
什么构成了有效性?
XML 有效性的基本思想是指定在元素内部可以出现
什么、
每隔多久出现一次以及可以出现的内容有哪些
替代方法。另外,当在一个元素中可以出现多个事物时,还可以指定出现的顺序(或按照需要不指定)。DTD 在所能表达的内容上与 W3C XML Schema 稍有不同,但本质相同。让我们看一下一个高度简化的假想的
dissertation.dtd :
清单 1. 具有所有基本约束的“dissertation”DTD
<!ELEMENT dissertation (dedication?, chapter+, appendix*)>
<!ELEMENT dedication (#PCDATA)>
<!ELEMENT chapter (title, paragraph+)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT paragraph (#PCDATA | figure | table)+>
<!ELEMENT figure EMPTY>
<!ELEMENT table EMPTY>
<!ELEMENT appendix (#PCDATA)> |
在本示例中,dissertation
可以包含
一个dedication,
必须包含(一个或多个)chapter,并且
可以包含(零或多个)appendix。各种子元素以列出的顺序(如果有的话)出现。有些元素只包含字符数据。就
<paragraph> 标记而言,它既可以包含字符数据,
也可以包含一个
<figure> 子元素
或一个
<table> 子元素 ― 或这几个的任意组合。结构可以嵌套,但这一示例包含了每一个基本有效性概念。
gnosis.xml.validity 模块允许您进行创建,例如创建
只能表示有效 dissertation 的
dissertation Python 对象。而且,将该对象转换成 XML ― 使用
print 命令或
str() 函数 ― 时,XML 自动与所期望的 DTD 相匹配。
正在使用的有效性
理解
gnosis.xml.validity 做了什么的最简单方法是看它是如何使用的。从态度上看,
gnosis.xml.validity 将它继承的内容归功于
Spark 解析器。也就是说,
有效性类 是使用 Python 反射定义的,而不是使用传统的顺序编程定义的。这种对称很有趣,因为,从某种意义上说,
Spark 和
gnosis.xml.validity 完成完全相反的事情:前者假定基于规则的结构在外部文本中;后者将它强制在内部对象中。
有效性类非常紧密地基于相应的 DTD 或 XML Schema。一个类只继承自一个相关的有效性类型,然后通过添加类属性进行专门化(如果需要的话)。在一个经常使用的约定中,任何以下划线开始命名的类都表示没有相应标记的结构。例如,在 dissertation(论文)中的
<paragraph> 元素可以包含一个
PCDATA 、
<figure> 和
<table> 元素的集合。装配到
<paragraph> 集合中的分离类型本身没有 XML 标记。因此,这个分离类型在下面的示例中命名为
_mixedpara :
清单 2. dissertation.py
from
gnosis.xml.validity
import
*
class
appendix(PCDATA):
pass
class
table(EMPTY):
pass
class
figure(EMPTY):
pass
class
_mixedpara(Or): _disjoins = (PCDATA, figure, table)
class
paragraph(Some): _type = _mixedpara
class
title(PCDATA):
pass
class
_paras(Some): _type = paragraph
class
chapter(Seq): _order = (title, _paras)
class
dedication(PCDATA):
pass
class
_apps(Any): _type = appendix
class
_chaps(Some): _type = chapter
class
_dedi(Maybe): _type = dedication
class
dissertation(Seq): _order = (_dedi, _chaps, _apps)
|
与 DTD 中的相同,最高级别的特定对象或 XML 文档可以是给定规则的任何标记。
dissertation 在这里碰巧使用的是最高级别,但您也可以创建较低类型的文档。让我们来看一下:
清单 3. 创建有效的 dissertation chapter
>>> from dissertation import chapter, title, _paras, paragraph, PCDATA
>>> chap1 = chapter(( title(PCDATA('About Validity')),
.. _paras([paragraph(PCDATA('It is a good thing'))])
.. ))
>>> print chap1
<chapter><title>About Validity</title>
<paragraph>It is a good thing</paragraph>
</chapter> |
<chapter> 用包含
<title> 和
_paras 列表的元组进行了初始化。依次下来,
<title> 用某个
PCDATA 进行了初始化,
PCDATA 本身是用(Unicode)字符串初始化的。同样,
_paras 列表包含几个 paragraph,paragraph 本身是用
PCDATA 初始化的。如果存在合适的对象,它只要打印它本身就可以作为有效的 XML。
尽管那些嵌套的初始化项遵从指定的 DTD 有效性规则的详细信息,但它们还是有点麻烦。
gnosis.xml.validity 提供了友好得
多的初始化样式。每当需要一个特殊类型时,该类型的初始化器就很明显地被
提升成类型本身。而且,当量化类型通常正是由这个类型的一列内容初始化时,只要指定一个项就可以将它
提升成包含该项的长度为一的列表。提升是递归的。注:使用提升的
Seq 类型必须使用工厂函数
LiftSeq() ,但其它类型可以提升它们自己的初始化参数(详细信息必须利用从不可变的 Python 类型的新样式的继承)。这听起来很复杂,但它实际上非常显而易见:
>>> from dissertation import LiftSeq
>>> chap1 = LiftSeq(chapter,('About Validity','It is a good thing'))
>>> print chap1
<chapter><title>About Validity</title>
<paragraph>It is a good thing</paragraph>
</chapter> |
有效性强制
迄今为止,您已经创建了一些有效的 XML 和对象。那么怎么样呢?您就可以手工编写有效的 XML 文本了。当您想要以有效或无效方法修改对象时,您会意识到
gnosis.xml.validity 的价值。例如,这里是一个有效的修改:
清单 4. 添加一个 paragraph(有效的操作)
>>> paras_ch1 = chap1[1]
>>> paras_ch1 += [paragraph('OOP can enforce it')]
>>> print chap1
<chapter><title>About Validity</title>
<paragraph>It is a good thing</paragraph>
<paragraph>OOP can enforce it</paragraph>
</chapter> |
当您尝试某些不允许的操作时会怎么样呢?例如,一个 dissertation 至多只能有一个 dedication(如
清单 1中指定的):
清单 5. 创建可选的 dedication
>>> from dissertation import _dedi, dedication
>>> Maybe_dedication = _dedi([])
>>> print Maybe_dedication
>>> Maybe_dedication.append(dedication("To Mom."))
>>> print Maybe_dedication
<dedication>To Mom.</dedication>
>>> Maybe_dedication.append(dedication("Also to Dad."))
Traceback (most recent call last):
File "<pyshell#71>", line 1, in ?
Maybe_dedication.append(dedication("Also to Dad."))
File "validity.py", line 140, in append
raise LengthError, self.length_message % self._tag
LengthError: List <_dedi> must have length zero or one |
同样,您不能包括错误类型的信息,即使量化长度是可接受的:
清单 6. 试图添加错误类型的项
>>> from gnosis.xml.validity import ValidityError
>>> try:
.. paras_ch1.append(dedication("To my advisor"))
.. except ValidityError, x:
... print x
Items in _paras must be of type <class 'dissertation.paragraph'>
(not <class 'dissertation.dedication'>) |
与约束不符而可能产生的所有异常都来自
ValidityError 。用
gnosis.xml.validity 库进行编程可能涉及在
try/except 块中包含许多操作;通过尝试一个禁用的操作来创建无效对象应该是不可能的。
有关实现的一些说明
需要记住的是
gnosis.xml.validity 严格地用于 Python 2.2+。尽管在较早的 Python 版本中可能实现它,但是我感觉这个项目可以成为一些较新的 Python 特性的非常好的试验基地。特别是该库利用了类型/类的统一和新样式类。对于未来库版本中的元类,我有几个技巧方面的想法,而且我甚至可能使用特性和插槽。
gnosis.xml.validity 的设计很大程度上依赖 Python 的内省/反射能力。几个抽象类构成了主要功能。这些类中的每个类都必须包含具体子类,这样在实际中才能
执行任何操作,尽管每个子类只需要实现一个类的最大极限属性。当 XML 标记对应于一个类时,标记名就直接取自类名。正如前面提到的,如果类名以一个下划线开始,那么它就没有相对应的 XML 标记。这里采用的基本规则是任何已标记的有效性类都用包围着的开始和结束标记序列化它本身;未标记的类只序列化它的原始内容(不过,该原始内容可以包含本身拥有标记的项)。这个模式强加了一个限制:
gnosis.xml.validity 不能使用指定以下划线开始的 XML 标记的 DTD;在未来版本中
可以除去这个限制,但可能没有必要,除非用户需要这样做。
基本抽象类由以下内容组成:
-
PCDATA
可以直接使用,所以实际上它不是抽象的。
包含
PCDATA 的 XML 元素应该从这个 PCDATA 继承而来,但不需要提供任何更进一步的专门化。不过,在
Or 类型的替代列表中,您只需要列出
PCDATA 。这非常接近于在 DTD 语法上建模。我建议在这样的列表中首先列出
PCDATA (正如 DTD 所需的那样),但当前不强制这样做。
-
EMPTY
也是在 DTD 语法上建模的。与
PCDATA 相同,这个类应该被继承,但不需要更进一步专门化。
-
Or
的子类型必须添加一个
_disjoins 元组作为类属性。通常情况下,该属性将是整个实现。其它有效性类应该列在该元组中。从概念上说,分离应涉及两个或更多事物,但如果当前几乎没有分离,那么就不会产生错误。
-
Seq
的子类型必须添加一个
_order 元组作为类属性。通常情况下,该属性就是整个实现。列在元组中的应该是两个或更多其它有效性类;与
Or 相同,当前不检查该元组长度。在实例化
Seq 子类型时,利用工厂函数
ListSeq() 通常比较安全。
- 在某种程度上,
Quantification
抽象类是个特殊的例子。本文中的示例没有使用
Quantification ,而是使用了它(仍是抽象的)的子类。例如,这里是类
Some 的实现:
清单 7. Quantification 抽象子类 Some
class Some(Quantification):
length_message = "List <%s> must have length >= 1"
min_length = 1
max_length = maxint |
-
Maybe 和
Any 类具有类似的实现。这些
Quantification 子类包含用于 DTD 的所有量化选项,但 XML Schema 允许实现很简单的其它选项(例如,
Three_to_Seven )。我意识到相当好的
length_message 可以从其它属性上生成,但我感觉如果它由程序员编写,那么消息的复数化以及用短语表达就会更好。
-
Quantification 的具体子类必须添加
_type 类属性,它简单地指向另一个有效性类。原则上,具体子类可以添加它自己的
min_length 、
max_length 和
length_message ― 但使用中介感觉象是更好的设计。
剩下要完成什么
到编写本文时为止,
gnosis.xml.validity 在很大程度上是概念证明:还缺少几点。最大的缺憾是还没有添加 XML 标记属性的任何方法 ― 更不要说强制实行这些属性的有效性。在结构上,属性非常象子元素(未排序的),所以类似的强制机制可以添加到
gnosis.xml.validity 的以后版本中。这样的添加当然可以拥有最高的优先级。
gnosis.xml.validity 从添加一些其它便捷的方法中受益:
- 从 DTD 或 XML Schema 自动生成一组 Python 有效性类将会非常好。但是,与 DTD 中的不同,一组 Python 有效性类需要按特定的次序定义 ― 或至少需要按这样的次序在另一个类的属性中命名每个类之前应先定义它。
- 您可能发现从现有的有效 XML 文档上读取很有用,但是没有必要清楚地表明您是如何最佳地完成这个操作的。由于在成员项所包含的内容出现在较大型的结构之前,成员项必须是有效对象,所以最简单的递归继承方法不起作用。但将 XML 文档的序列拆成与有效性类相对应应该是可能的。
- 最后,某种较高级别的接口可能使使用当前的有效性类更容易。当前在库中使用的策略是对每个禁止操作提出异常;但将这个异常包含在更简便的 API 中是可能的。无声故障或标志返回值可能会有用,或另一种用于错误例子的后退操作会有用。确定合适的接口可能需要用户(包括我自己)进行更多实验。
非常欢迎读者对
gnosis.xml.validity 的以后版本应该确定的发展方向提出反馈意见。我认为最初的功能已协助完成了各种 XML 编程任务,但尽管别处已经对库作了有点类似的开发,而我的直觉还是不清楚什么是最有用的。
参考资料
关于作者
对本文的评价
|