Python 使用属性进行 XML 序列化

5 阅读3分钟

在 Python 中,需要将复杂的数据结构序列化为显式 XML 字符串。在 C# 中,可以通过在数据结构上添加自定义属性(例如 [XmlElement] 或 [XmlAttribute])并调用“序列化”函数轻松实现此操作。然而,在 Python 中,没有类似的功能。虽然可以找到许多手动解析结构的示例,但这些示例并不适合我们的需求。

那么,有没有办法模拟 C# 中的这种功能呢?以以下代码为例:

public enum eType {

    [XmlEnum("multi")]
    Multiple,

    [XmlEnum("mutex1")]
    Single,

    [XmlEnum("product")]
    Product,

    [XmlEnum("alias")]
    Alias
}

[Serializable]
[XmlRoot("root")]
public class RootClass{

    public RootClass() {
        Metadata = new Metadata ();
        FeatureDictionary = new FeatureDictionary ();
    }

    [XmlElement("metadata")]
    public Metadata Metadata { get; set; }

    [XmlElement("feature-dictionary")]
    public FeatureDictionary FeatureDictionary { get; set; }

}

[Serializable]
public class Metadata {

    public Metadata() {
        Meta = new List<Meta> ();
    }

    [XmlAttribute("status")]
    public string Status { get; set; }

    [XmlAttribute("url")]
    public string URL { get; set; }

    [XmlAttribute("view")]
    public string View { get; set; }

    [XmlElement("meta")]
    public List<Meta> Meta { get; set; }

}

在 Python 中能否实现类似的功能?请注意,上面的代码只包含了定义 XML 的代码的 1/20。

2、解决方案

可以使用 Python 描述符来创建对象上的属性,这些属性知道如何序列化和反序列化它们自己。描述符是 Python 用于创建 @property 装饰器的一种机制:它们包含 getter 和 setter 方法,并且可以具有局部状态,因此它们可以在数据和 XML 之间提供良好的过渡。结合一个类或装饰器来自动化批量序列化/反序列化附加到对象上的描述符的过程,就可以获得 C# XML 序列化系统的大部分功能。

通常情况下,代码应该如下所示(使用著名的 XML ISBN 示例):

@xmlobject("Book")
class Book( object ):

    author = XElement( 'AuthorsText' )
    title = XElement( 'Title' )
    bookId = XAttrib( 'book_id' )
    isbn = IntAttrib( 'isbn' )
    publisher = XInstance( 'PublisherText', Publisher )

这里的赋值语法正在为实例中的所有字段(如 author、title 等)创建类级描述符。每个描述符对其他 Python 代码来说看起来像一个普通的字段,因此可以像下面这样操作:

book.author = 'Joyce, James'

每个描述符在内部存储一个 xml 节点或属性,当被调用进行序列化时,它将返回相应的 XML:

from xml.etree.cElementTree import ElementTree, Element

class XElement( object ):
    '''
    Simple XML serializable field
    '''

    def __init__( self, path):    
        self.path = path
        self._xml = Element(path) # using an ElementTree or lxml element as internal storage

    def get_xml( self, inst ):
        return inst._xml

    def _get_element( self ):
        return self.path

    def _get_attribute( self ):
        return None

    # the getter and setter push values into the underlying xml and return them from there
    def __get__( self, instance, owner=None ):
         myxml = self.get_xml( instance )
         underlying = myxml.find( self.path )
         return underlying.text 

    def __set__( self, instance, value, owner=None ):
        myxml= self._get_xml( instance )
        underlying = myxml.find( self.path )
        underlying.text = value

相应的 XAttrib 类也做同样的事情,只是在一个属性而不是一个元素中。

class XAttrib( XElement):
    '''
     Wraps a property in an attribute on the containing xml tag specified by 'path'
    '''

    def __get__( self, instance, owner=None ):
        return self._get_xml( instance ).attrib[self.path]  
        # again, using ElementTree under the hood

    def __set__( self, instance, value, owner=None ):
        myxml = self._get_xml( instance )
        has_element = myxml.get( self.path, 'NOT_FOUND' )
        if has_element == 'NOT_FOUND':
           raise Exception, "instance has no element path"
        myxml.set( self.path, value )

    def _get_element( self ):
        return None  #so outside code knows we are an attrib

    def _get_attribute( self ):
        return self.path

为了将它们全部联系起来,拥有类需要在初始化时设置描述符,这样每个实例级描述符都指向拥有实例自己的 XML 元素中的一个 XML 节点。这样,对实例属性的更改就会自动反映在拥有者的 XML 中。

def create_defaults( target_cls):
     # where target class is the serializable class, eg 'Book'
     # here _et_xml() would return the class level Element, just
     # as in the XElement and XAttribute.  Good use for a decorator!

     myxml = target_cls.get_xml()

     default_attribs = [item for item in target_cls.__class__.__dict__.values() 
                                 if issubclass( item.__class__, XElement) ]
     #default attribs will be all the descriptors in the target class

     for item in default_attribs:
        element_name = item._get_element()
        #update the xml for the owning class with 
        # all the XElements
        if element_name:
            new_element = Element( element_name )
            new_element.text = str( item.DEFAULT_VAL )
            myxml.append( new_element )

        # then update the owning XML with the attributes 
     for item in default_attribs:
         attribpath = item._get_attribute()
         if attrib:
             myxml.set( attribpath, str( item.DEFAULT_VAL ) )

如果代码没有直接运行,不用担心。这是从一个工作示例中提取出来的,在尝试使示例可读并删除特定于应用程序的细节时,可能引入了一些错误。