oxml中创建CT_Document类

news/2025/1/8 5:21:03/

概述

本文基于python-docx源码,详细记录CT_Document类创建的过程,以此来加深对Python中元类、以及CT_Document元素类的认识。
 

元类简介

元类(MetaClass)是Python中的高级特性。元类是什么呢?Python是面向对象编程语言,在Python中一切事物都是对象。如实例对象的实例化结果,而类则是元类实例化的结果。简而言之,元类是创建“类”的“类”——通过元类的__new__与__init__特殊方法管理类的创建过程。其中type对象是Python中内置的元类对象

那为什么需要元类呢?元类有很强大的功能,本文仅从“为新创建的类自动创建方法”为例进行记录。

元类的定义与使用

通过继承type对象来创建自己的元类:

class MyMetaClass(type):def __new__(cls, name, bases, attrs):print(f"Creating new class: {name}")return super().__new__(cls, name, bases, attrs)

name参数是新创建类的类名称,bases参数是新创建类的父类元祖,attrs是新创建类的属性字典。自定义完元类后,可以在类定义中通过“metaclass”关键字参数明使用自定义的元类,如果不指定,默认值为type对象:

class MyNewClass(metaclass=MyMetaClass):pass

当python解释器创建“基于自定义元类定义的新建类”时,就会调用自定义元类的__new__与__init__特殊方法,从而管理类的创建过程。
 

新建CT_Document元素类

CT_Document源码定义

CT_Document源码定义于“docx.oxml.document”模块,表示一个XML文档元素类(类别lxml.etree.ElementBase)。

class CT_Document(BaseOxmlElement):"""``<w:document>`` element, the root element of a document.xml file."""body: CT_Body = ZeroOrOne("w:body")  # pyright: ignore[reportAssignmentType]@propertydef sectPr_lst(self) -> List[CT_SectPr]:"""All `w:sectPr` elements directly accessible from document element.Note this does not include a `sectPr` child in a paragraphs wrapped inrevision marks or other intervening layer, perhaps `w:sdt` or customXmlelements.`w:sectPr` elements appear in document order. The last one is always`w:body/w:sectPr`, all preceding are `w:p/w:pPr/w:sectPr`."""xpath = "./w:body/w:p/w:pPr/w:sectPr | ./w:body/w:sectPr"return self.xpath(xpath)
  1. CT_Document类定义两个属性,其中body属性值是“CT_Body”类型,其取值为“ZeroOrOne”类型。注意限定性属性名为“w:body”。
  2. 除了sectpr_lstbody属性被显示定义外,其它属性继承于BaseOxmlElement类。

接下来本文将详细记录,python中的元类功能,如何自动为CT_Document添加许多方法。

BaseOxmlElement 基层类

BaseOxmlElement基础元素类是一种类似于lxml.etree.ElementBase的类对象,只是其遵循的是Office Open XML标准。首先看docx.oxml.xmlchemy源码定义:

# -- lxml typing isn't quite right here, just ignore this error on _Element --
class BaseOxmlElement(etree.ElementBase, metaclass=MetaOxmlElement):"""Effective base class for all custom element classes.Adds standardized behavior to all classes in one place."""def __repr__(self):return "<%s '<%s>' at 0x%0x>" % (self.__class__.__name__,self._nsptag,id(self),)

BaseOxmlElement类的定义比较简单,关于XML的元素类功能大部分继承自etree.ElementBase——作为BaseOxmlElement的父类,而其“类型”是MetaOxmlElement元类

MetaOxmlElement元类

MetaOxmlElement元类定义于docx.oxml.xmlchemy模块:

class MetaOxmlElement(type):"""Metaclass for BaseOxmlElement."""def __init__(cls, clsname: str, bases: Tuple[type, ...], namespace: Dict[str, Any]):dispatchable = (OneAndOnlyOne,OneOrMore,OptionalAttribute,RequiredAttribute,ZeroOrMore,ZeroOrOne,ZeroOrOneChoice,)for key, value in namespace.items():if isinstance(value, dispatchable):value.populate_class_members(cls, key)

MetaOxmlElement元类依然继承了type的__new__方法,但覆盖了__init__方法。__init__方法的逻辑也比较简单,如果namespace属性字典中的值是源码中指定的dispatchable类型,则调用对应类的populate_class_members方法。

_BaseChildElement类

_BaseChildElement类定义于docx.oxml.xmlchemy模块,为什么要在这里介绍此类?因为元类MetaOxmlElement的__init__方法中的dispatchable元组中,除了OptionalAttribute与RequiredAttribute外——但功能|角色有很大的相似性,其它类都是继承该基础类;并且后续许多自动为新创建的类添加方法,也与此类有关,因此在此处加以介绍。

class _BaseChildElement:"""Base class for the child-element classes.The child-element sub-classes correspond to varying cardinalities, such as ZeroOrOneand ZeroOrMore."""def __init__(self, nsptagname: str, successors: Tuple[str, ...] = ()):super(_BaseChildElement, self).__init__()self._nsptagname = nsptagnameself._successors = successorsdef populate_class_members(self, element_cls: MetaOxmlElement, prop_name: str) -> None:"""Baseline behavior for adding the appropriate methods to `element_cls`."""self._element_cls = element_clsself._prop_name = prop_name
  1. _BaseChildElement是一个典型的类定义,其父类是Python中的object对象。
  2. 初始化该类需要传入元素“命名空间前缀标签名称”与successors前置元素对象列表——比如一个<w:r>元素,可能需要传入段落格式<w:pPr>等前置元素对象。
  3. _BaseChildElement基础子类的populate_class_members方法的逻辑比较简单,将传入的参数值存储到实例属性中。
  4. 注意:prop_name一般为新创建类的属性名,而element_cls一般为新建类**。**_BaseChildElement或者其子类一般是作为新建类(基于MetaOxmlElement元类)的属性。将会结合后面的实例进行说明。

CT_Document创建细节

step1.Python解释器收集必要信息

本文基于Pycharm & Debug模式,调试下列脚本——仅包含一行代码:

from docx.oxml.document import CT_Document

调试模式下,跳转到源码中的class CT_Document(BaseOxmlElement):行:
CT_Document上下文信息

  1. 由于CT_Document的继承自BaseOxmlElement,而BaseOxmlElement是基于MetaOxmlElement元类创建的,因此CT_Document的默认metaclass为MetaOxmlElement元类。创建CT_Document时,会自动调用MetaOxmlElement元类的__init__方法。

  2. namespace属性字典中body属性值,存储的是一个ZeroOrOne实例对象。
    ZeroOrOne实例对象状态异常

step2. 执行MetaOxmlElement.__init__

在执行MetaOxmlElement.__init__的逻辑中,当key=body & value=ZeroOrOne()时,会执行ZeroOrOne.populate_class_members(cls, key),此时的cls为“CT_Document”类。
cls为CT_Document
此时许多私有方法显示状态异常,是因为“method_name”需要根据“prop_name”动态生成,而“prop_name”还未被ZeroOrOne实例引用。当执行完_BaseChildElement.populate_class_members后,异常状态就会消失。

step3. 执行ZeroOrOne.populate_class_members

ZeroOrOne为CT_Document自动添加方法

_BaseChildElement实例方法被封装的函数自动为新建类添加的方法名模版示例值新建方法功能说明
_add_getterget_child_elementproperty_namebody读取body子节点,如果不存在,则返回Nonebody作为可读特性,会覆盖CT_Document源码定义中的类属性值
_add_creatornew_child_element“_new_%s” % property_name_new_body根据限定性标签名称,创建一个空的子节点私有方法/辅助方法;创建空白子节点的能力继承lxml
_add_inserter_insert_child“_insert_%s” % property_name_insert_body将子节点插入到父节点中的指定为止私有方法/辅助方法;插入子元素节点的能力继承自lxml
_add_adder_add_child“_add_%s” % property_name_add_body新建子节点,并将子节点插入到父节点中的指定为止私有方法/辅助方法;可以看作是_add_creator & _add_inserter 功能的集成
_add_get_or_adderget_or_add_child“get_or_add_%s” % property_nameget_or_add_body获取或者新建目标子节点非私有方法;可以看作是_add_getter & _add_adder 功能的集成
_add_remover_remove_child“_remove_%s” % property_name_remove_body从父节点中删除目标子节点私有方法/辅助方法;删除子节点的能力继承自lxml

"_add_%s"系列的实例方法均定义于 _BaseChildElement,被封装的函数、及新增方法模版名称也均定义于 _BaseChildElement。oxml子库中为新创建的元素类自动添加对应的方法的逻辑,就在"_add_%s"系列的方法中实现
 

执行_BaseChildElement.populate_class_members

创建对新建类、属性名的引用。即第一行将“CT_Document”实例对象与“body”特性名称存储到ZeroOrOne实例对象属性中。第2-7行为CT_Documet类自动添加方法。
执行_BaseChildElement.populate_class_members

self._add_getter

_add_getter方法定义于_BaseChildElement类中,其源码如下:

   def _add_getter(self):"""Add a read-only ``{prop_name}`` property to the element class for this childelement."""property_ = property(self._getter, None, None)# -- assign unconditionally to overwrite element name definition --setattr(self._element_cls, self._prop_name, property_)

其中self.getter实例方法定义于_BaseChildElement类中,其源码如下:

@property
def _getter(self):"""Return a function object suitable for the "get" side of the propertydescriptor.This default getter returns the child element with matching tag name or |None|if not present."""def get_child_element(obj: BaseOxmlElement):return obj.find(qn(self._nsptagname))get_child_element.__doc__ = ("``<%s>`` child element or |None| if not present." % self._nsptagname)return get_child_element

self.getter实例方法即根据限定性标签名称“w:body”在CT_Document元素节点内查找子节点对象。执行setattr(self._element_cls, self._prop_name, property_)之前,CT_Document的body数值存储的是ZeroOrOne实例对象——定义于源码,执行完成之后,CT_Document的body类属性就对应一个body特征了。
setattr执行之前
setattr执行之后

self._add_creator

self._add_creator方法的功能是为新创建的类——根据上下文就是CT_Document,添加一个方法——根据限定性标签名称(w:body),为新创建的类,创建一个空的新子节点元素对象。

    def _add_creator(self):"""Add a ``_new_{prop_name}()`` method to the element class that creates a new,empty element of the correct type, having no attributes."""creator = self._creatorcreator.__doc__ = ('Return a "loose", newly created ``<%s>`` element having no attri'"butes, text, or children." % self._nsptagname)self._add_to_class(self._new_method_name, creator)@propertydef _creator(self) -> Callable[[BaseOxmlElement], BaseOxmlElement]:"""Callable that creates an empty element of the right type, with no attrs."""from docx.oxml.parser import OxmlElementdef new_child_element(obj: BaseOxmlElement):return OxmlElement(self._nsptagname)return new_child_element

注意“self._add_to_class”实例方法定义于_BaseChildElement类中,其功能是为新创建的类添加新方法:

    def _add_to_class(self, name: str, method: Callable[..., Any]):"""Add `method` to the target class as `name`, unless `name` is already definedon the class."""if hasattr(self._element_cls, name):returnsetattr(self._element_cls, name, method)

结合上下文,self._element_cls=CT_Document & name=_new_body & method=self.creator。执行self._add_to_class的后,CT_Document类签名变化如下:
自动添加的_new_body方法

self._add_inserter

_add_inserter方法封装了_insert_child函数——为父节点插入一个子节点。为XML元素节点插入子节点的能力继承自lxml.etree.ElementBase。

    def _add_inserter(self):"""Add an ``_insert_x()`` method to the element class for this child element."""def _insert_child(obj: BaseOxmlElement, child: BaseOxmlElement):obj.insert_element_before(child, *self._successors)return child_insert_child.__doc__ = ("Return the passed ``<%s>`` element after inserting it as a chil""d in the correct sequence." % self._nsptagname)self._add_to_class(self._insert_method_name, _insert_child)

执行完self._add_to_class方法后,其中self._insert_method_name="_insert_body",CT_Document签名中就包含新的方法“_insert_body”:
新创建_insert_body方法

self._add_adder

self._add_adder方法本质是“self._add_creator”与“self._add_inserter”二者的结合。self._add_adder方法封装了**_add_child函数**

    def _add_adder(self):"""Add an ``_add_x()`` method to the element class for this child element."""def _add_child(obj: BaseOxmlElement, **attrs: Any):new_method = getattr(obj, self._new_method_name)child = new_method()for key, value in attrs.items():setattr(child, key, value)insert_method = getattr(obj, self._insert_method_name)insert_method(child)return child_add_child.__doc__ = ("Add a new ``<%s>`` child element unconditionally, inserted in t""he correct sequence." % self._nsptagname)self._add_to_class(self._add_method_name, _add_child)

_add_child函数首先创建一个空的子节点,然后将“attr”属性字典写入到新建的空子节点,并将新建的子节点插入到目标父节点、返回新建的子节点。执行完self._add_to_class(self._add_method_name, _add_child),其中self._add_method_name="_add_body"后,CT_Document多自动添加了一个新方法“_add_body”:
自动添加_add_body方法

self._add_get_or_adder

"self._add_get_or_adder"方法对“get_or_add_child”函数进行了封装——如果父节点包含目标子节点,则直接取出目标子节点;如果不包含则新建一个目标子节点并返回,新建目标子节点依赖之前的“self._add_method_name”方法,即“self._add_body”

    def _add_get_or_adder(self):"""Add a ``get_or_add_x()`` method to the element class for this childelement."""def get_or_add_child(obj: BaseOxmlElement):child = getattr(obj, self._prop_name)if child is None:add_method = getattr(obj, self._add_method_name)child = add_method()return childget_or_add_child.__doc__ = ("Return the ``<%s>`` child element, newly added if not present.") % self._nsptagnameself._add_to_class(self._get_or_add_method_name, get_or_add_child)

self._get_or_add_method_name="get_or_add_body", 执行完“self._add_to_class()”,CT_Document就自动添加了一个新的方法:
新添加的get_or_add_body方法

self.add_remover

"self.add_remover"方法封装了“_remove_child”函数——该函数根据限定性标签名称,从父节点中删除目标子节点。元素节点中删除子节点的能力继承自lxml.etree.ElementBase.

    def _add_remover(self):"""Add a ``_remove_x()`` method to the element class for this child element."""def _remove_child(obj: BaseOxmlElement):obj.remove_all(self._nsptagname)_remove_child.__doc__ = ("Remove all ``<%s>`` child elements.") % self._nsptagnameself._add_to_class(self._remove_method_name, _remove_child)

self._remove_method_name="_remove_body", 执行完“self._add_to_class()”,CT_Document就自动添加了一个新的方法:
新增_remove_body方法

小结

在python-docx子库oxml中,虽然在源码中并未直接定义诸多元素类对子节点元素管理的增删改查方法。但是通过利用Python元类、类继承、以及简洁直观的代码模式设计,为诸多新创建的元素类,如CT_Document,自动添加了对子元素节点的增删改查方法。这种利用Python元类来管理类方法自动创建的模式值得学习。


http://www.ppmy.cn/news/1561470.html

相关文章

关于Mac中的shell

1 MacOS中的shell 介绍&#xff1a; 在 macOS 系统中&#xff0c;Shell 是命令行与系统交互的工具&#xff0c;用于执行命令、运行脚本和管理系统。macOS 提供了多种 Shell&#xff0c;主要包括 bash 和 zsh。在 macOS Catalina&#xff08;10.15&#xff09;之前&#xff0c…

MS7337M集成单通道视频运放与视频同轴线控解码

产品简述 MS7337M 是单通道视频放大器与视频同轴线控解码器为一体的 芯片。视频放大器内部集成 6dB 增益轨到轨输出驱动器以及 6 阶滤波 器&#xff0c; -3dB 带宽达 81MHz 。视频同轴线控解码器内部集成一颗高速处 理器&#xff0c;针对模数混合信号进行有效分离。…

ChromeDriver 版本不匹配问题解决,ChromeDriver最新版本下载安装教程

在 Python 的 Selenium 自动化测试中&#xff0c;ChromeDriver 是一款不可或缺的工具&#xff0c;用于桥接代码与浏览器之间的操作。然而&#xff0c;很多人在运行自动化脚本时都会碰到这样的问题&#xff1a;“session not created: This version of ChromeDriver only suppor…

Alternative to vJoy and FreePIE joystick and input emulators on Linux

这是我之前在个人网站上发布的一篇旧博客文章。我正在将所有内容转移到CSDN。感谢阅读&#xff01; This project is one step forward toward setting up universal mouse steering in racing games on Linux. Some games come with mouse support out of the box, while othe…

明源地产ERP VisitorWeb_XMLHTTP.aspx SQL注入漏洞复现

0x01 产品简介 明源地产ERP是一款专门为房地产行业设计的企业资源规划(ERP)系统,旨在帮助房地产企业实现全面的信息化管理,提高运营效率和管理水平。系统涵盖了项目管理、财务管理、供应链管理、客户关系管理(CRM)、人力资源管理等多个核心功能模块,通过整合企业的各个…

flink的EventTime和Watermark

时间机制 Flink中的时间机制主要用在判断是否触发时间窗口window的计算。 在Flink中有三种时间概念&#xff1a;ProcessTime、IngestionTime、EventTime。 ProcessTime&#xff1a;是在数据抵达算子产生的时间&#xff08;Flink默认使用ProcessTime&#xff09; IngestionT…

哦?将文本转换为专业流程图的终极解决方案?

前言 今天介绍的这款工具号称是将文本转换成专业流程图的终极解决方案。一起来看看是否能满足你的需求吧。 首页 平平无奇的首页&#xff08;通常地下隐藏着不平常的东西&#xff09;&#xff0c;和我们之前介绍过的工具类似&#xff0c;核心就是我们中间的文本输入框。在输入…

【Python】基于blind-watermark库添加图片盲水印

blind-watermark 是一个用于在图像中添加和提取盲水印的 Python 库。盲水印是一种嵌入信息&#xff08;如水印&#xff09;到图像中的方法&#xff0c;使得水印在视觉上不可见&#xff0c;但在需要时可以通过特定的算法进行提取。以下是如何使用 blind-watermark 库来添加和提取…