oxml中创建CT_Document类

概述

本文基于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]

    @property
    def 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 in
        revision marks or other intervening layer, perhaps `w:sdt` or customXml
        elements.

        `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 ZeroOrOne
    and ZeroOrMore.
    """

    def __init__(self, nsptagname: str, successors: Tuple[str, ...] = ()):
        super(_BaseChildElement, self).__init__()
        self._nsptagname = nsptagname
        self._successors = successors

    def 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_cls
        self._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 child
       element."""
       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 property
    descriptor.

    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._creator
        creator.__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)

    @property
    def _creator(self) -> Callable[[BaseOxmlElement], BaseOxmlElement]:
        """Callable that creates an empty element of the right type, with no attrs."""
        from docx.oxml.parser import OxmlElement

        def 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 defined
        on the class."""
        if hasattr(self._element_cls, name):
            return
        setattr(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 child
        element."""

        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 child

        get_or_add_child.__doc__ = (
            "Return the ``<%s>`` child element, newly added if not present."
        ) % self._nsptagname
        self._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._nsptagname
        self._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元类来管理类方法自动创建的模式值得学习。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/950421.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Tableau数据可视化与仪表盘搭建-基础图表制作

目录 对比分析&#xff1a;比大小 柱状图 条形图 数据钻取 筛选器 热力图 气泡图 变化分析&#xff1a;看趋势 折线图 预测 面积图 关系分布&#xff1a;看位置 散点图 直方图 地图 构成分析&#xff1a;看占比 饼图 树地图 堆积图 对比分析&#xff1a;比大…

QML学习(八) Quick中的基础组件:Item,Rectangle,MouseArea说明及使用场景和使用方法

上一篇中我们从设计器里可以看到Qt Quick-Base中有几大基础组件&#xff0c;如下图&#xff0c;这篇文章先介绍下Item&#xff0c;Rectangle&#xff0c;MouseArea这三个的说明及使用场景和使用方法 Item Item 是 QML 中所有可视元素的基类&#xff0c;是一个非常基础和通用的…

万界星空科技质量管理QMS系统具体功能介绍

一、什么是QMS系统&#xff0c;有什么价值&#xff1f; 1、QMS 系统即质量管理系统&#xff08;Quality Management System&#xff09;。 它是一套用于管理和控制企业产品或服务质量的集成化体系。 2、QMS 系统的价值主要体现在以下几个方面&#xff1a; 确保产品质量一致性…

字符串哈希stl解决

题目如下 STL的unordered-set STL的map 谢谢观看&#xff01;&#xff01;&#xff01;

JAVA I/O流练习1

往D盘中的JAVA复习文件夹中写数据&#xff1a; 数据改了一下哈&#xff1a; import java.io.*; import java.util.Scanner; public class Test {public static void main(String[] args) throws IOException {String fileName"D:JAVA复习\\grade.txt";FileWriter w…

英伟达Project Digits赋能医疗大模型:创新应用与未来展望

英伟达Project Digits赋能医疗大模型&#xff1a;创新应用与未来展望 一、引言 1.1 研究背景与意义 在当今数字化时代&#xff0c;医疗行业作为关乎国计民生的关键领域&#xff0c;正面临着前所未有的挑战与机遇。一方面&#xff0c;传统医疗模式在应对海量医疗数据的处理、复…

OpenAI 故障复盘 - 阿里云容器服务与可观测产品如何保障大规模 K8s 集群稳定性

本文作者&#xff1a; 容器服务团队&#xff1a;刘佳旭、冯诗淳 可观测团队&#xff1a;竺夏栋、麻嘉豪、隋吉智 一、前言 Kubernetes(K8s)架构已经是当今 IT 架构的主流与事实标准&#xff08;CNCF Survey[1]&#xff09;。随着承接的业务规模越来越大&#xff0c;用户也在使…

移动电商的崛起与革新:以开源AI智能名片2+1链动模式S2B2C商城小程序为例的深度剖析

摘要&#xff1a;本文旨在探讨移动电商的崛起背景、特点及其对传统电商模式的革新影响&#xff0c;并以开源AI智能名片21链动模式S2B2C商城小程序为具体案例&#xff0c;深入分析其在移动电商领域的创新实践。随着移动互联网技术的飞速发展&#xff0c;移动电商已成为电商行业的…

el-table 合并单元格

参考文章&#xff1a;vue3.0 el-table 动态合并单元格 - flyComeOn - 博客园 <el-table :data"tableData" border empty-text"暂无数据" :header-cell-style"{ background: #f5f7fa }" class"parent-table" :span-method"obj…

C/C++进阶-函数

C/C入门-函数起始 函数引用与指针函数参数 指针写法 和 数组写法数组的引用右值引用概念&#xff1a;**反汇编&#xff1a;**总结用结构体的示例再理解一遍 函数的本质栈分析栈溢出攻击 函数重载函数重载 进阶 思考函数重载补充 函数模板&#xff08;1&#xff09;&#xff08;…

通俗易懂之线性回归时序预测PyTorch实践

线性回归&#xff08;Linear Regression&#xff09;是机器学习中最基本且广泛应用的算法之一。它不仅作为入门学习的经典案例&#xff0c;也是许多复杂模型的基础。本文将全面介绍线性回归的原理、应用&#xff0c;并通过一段PyTorch代码进行实践演示&#xff0c;帮助读者深入…

分布式主键ID生成方式-snowflake雪花算法

这里写自定义目录标题 一、业务场景二、技术选型1、UUID方案2、Leaf方案-美团&#xff08;基于数据库自增id&#xff09;3、Snowflake雪花算法方案 总结 一、业务场景 大量的业务数据需要保存到数据库中&#xff0c;原来的单库单表的方式扛不住大数据量、高并发&#xff0c;需…

在 C# 中显示动画 GIF 并在运行时更改它们

您可以通过将按钮、图片框、标签或其他控件的Image属性设置为 GIF 文件 来显示动画 GIF 。&#xff08;如果您在窗体的BackgroundImage属性中显示一个&#xff0c;则不会获得动画。&#xff09; 有几种方法可以在运行时更改 GIF。 首先&#xff0c;您可以将 GIF 添加为资源。…

【技术支持】安卓无线adb调试连接方式

Android 10 及更低版本&#xff0c;需要借助 USB 手机和电脑需连接在同一 WiFi 下&#xff1b;手机开启开发者选项和 USB 调试模式&#xff0c;并通过 USB 连接电脑&#xff08;即adb devices可以查看到手机&#xff09;&#xff1b;设置手机的监听adb tcpip 5555;拔掉 USB 线…

【网络】计算机网络的分类 局域网 (LAN) 广域网 (WAN) 城域网 (MAN)个域网(PAN)

局域网是通过路由器接入广域网的 分布范围 局域网Local Area Network&#xff1a;小范围覆盖&#xff0c;速度高&#xff0c;延迟低(办公室&#xff0c;家庭&#xff0c;校园&#xff0c;网络) 广域网Wide Area Network 大范围覆盖&#xff0c;速度相对低&#xff0c;延迟高…

scanf:数据之舟的摆渡人,静卧输入港湾的诗意守候

大家好啊&#xff0c;我是小象٩(๑ω๑)۶ 我的博客&#xff1a;Xiao Xiangζั͡ޓއއ 很高兴见到大家&#xff0c;希望能够和大家一起交流学习&#xff0c;共同进步。* 这一节我们主要来学习scanf的基本用法&#xff0c;了解scanf返回值&#xff0c;懂得scanf占位符和赋值…

win10 gt520+p106双卡测试

安装391.35驱动失败,虽然gpuz和设备管理器显示正常但没有nvidia控制面板 重启进安全模式,ddu卸载,再次重启到安全模式,安装391.01驱动,显示3dvision安装失败,重启再看已经有nvidia控制面板了 修改p106注册表 AdapterType 1 EnableMsHybrid 1 计算机\HKEY_LOCAL_MACHINE\SYSTE…

C# OpenCV机器视觉:霍夫变换

在一个阳光灿烂得近乎放肆的午后&#xff0c;阿强的实验室就像被施了魔法的科学城堡&#xff0c;到处闪耀着神秘的科技光芒。阿强呢&#xff0c;像个即将踏上惊险征程的探险家&#xff0c;一屁股坐在那堆满奇奇怪怪设备的桌前&#xff0c;眼神中透露出按捺不住的兴奋劲儿&#…

【深度学习基础】线性神经网络 | 线性回归的简洁实现

【作者主页】Francek Chen 【专栏介绍】 ⌈ ⌈ ⌈PyTorch深度学习 ⌋ ⌋ ⌋ 深度学习 (DL, Deep Learning) 特指基于深层神经网络模型和方法的机器学习。它是在统计机器学习、人工神经网络等算法模型基础上&#xff0c;结合当代大数据和大算力的发展而发展出来的。深度学习最重…

工业级手持地面站(支持Android和IOS)技术详解!

一、硬件平台的选择 无人机遥控器为了支持Android和iOS系统&#xff0c;通常会选择高性能的处理器和操作系统作为硬件基础。例如&#xff0c;一些高端遥控器可能采用基于ARM架构的高性能处理器&#xff0c;这些处理器能够高效地运行Android或iOS操作系统&#xff0c;并提供足够…