为CT_P自动注册与CT_Run相关的方法

概述

在docx.oxml.text.paragraph模块中定义了CT_P段落对象元素类,但是CT_P中并未定义add_r等与CT_Run相关的方法。在不断探索源码逻辑的过程中,对这种自动为类注册合适的方法的功能进行了梳理——xmlchemy这个模块设计的真好!!!

大体逻辑如下:

  1. CT_P中包含类属性“r”, 该类属性存储的是ZeroOrMore实例对象——docx.oxml.xmlchemy模块中定义了ZeroOrMore子元素类对象,以及与之相似的OneAndOnlyOne、OneOrMore、ZeroOrOne、ZeroOrOneChoice等子元素类对象。这些类对象均继承_BaseChildElement,并重新定义了populate_class_members方法。正是该方法为许多BaseOxmlElement类对象自动化添加合适的方法。
  2. CT_P继承BaseOxmlElement, BaseOxmlElement是MetaOxmlElement类型,因此在创建CT_P时,会调用MetaOxmlElement的初始化方法,该初始化方法会检查新建类的属性字典,并判断属性字典中的值是否是ZeroOrMore等子元素类,如果是则调用populate_class_members,为新建的类注册合适的方法。

本文以docx.oxml.text.paragraph.CT_P类创建为例,将重点对MetaOxmlElement元类、_BaseChildElement类的功能进行详细记录。注意:本文档参考的版本信息为python_docx=1.1.0

MetaOxmlElement

MetaOxmlElement元类的源码定义如下:

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)
  1. 元类是创建类的类,其功能|角色与type类似。
  2. clsname是待创建的类对象名称,base是待创建类对象的父类,namespace是待创建类对象的namespace,可简单理解为待创建类对象的属性字典。
  3. for key, value in namespace.items()迭代过程中,如果待创建类对象的属性值为dispatchable中的某种类型,则调用populate_class_members方法,注意传入的cls是指父节点,key是dispatchable对象对应的名称

BaseOxmlElement

BaseOxmlElement基础类是docx.oxml子包中所有元素类的基础类,其角色与etree.ElementBase类似,源码定义如下:

class BaseOxmlElement(  # pyright: ignore[reportGeneralTypeIssues]
    etree.ElementBase, metaclass=MetaOxmlElement
):
    """Effective base class for all custom element classes.

    Adds standardized behavior to all classes in one place.
    """
  1. BaseOxmlElement继承etree.ElementBase,因此可以直接使用etree.ElementBase中的find、findall等方法。
  2. BaseOxmlElement是MetaOxmlElement类型,如果新创建一个基于BaseOxmlElement的子类,则子类的类型任然是MetaOxmlElement,并且该子类创建时会调用MetaOxmlElement.__init__,但是实例化创建的子类,会调用etree.ElementBase的初始化方法。

_BaseChildElement

_BaseChildElement是所有子元素的基础类对象,ZeroOrMore等类均继承该类。在该类中定义了诸多公用的方法,下面先介绍一部分,后续将结合CT_P创建过程逐步介绍。

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. 初始化方法中,需要传入命名空间前缀的元素标签名,以及该元素的先驱元素——就是在一个父节点下,排在当前节点之前的所有其它子节点。比如段落对象中,一般段落属性对象会排在第一位。
  2. populate_class_members方法中,element_cls传入的实参是该节点的父节点元素,父节点的类型是MetaOxmlElement,prop_name表示属性名。从该方法的注释可以看出,该方法是为父节点对象添加合适的方法

ZeroOrMore

ZeroOrMore是一种子元素类,其表示某一父节点允许拥有任意多个该种子节点对象。在word文档中,这是最常见的一种子节点元素了,比如word文档允许包含任意多个paragraph,单个paragraph允许包含任意多个run节点。ZeroOrMore的源码定义如下:

class ZeroOrMore(_BaseChildElement):
    """Defines an optional repeating child element for MetaOxmlElement."""

    def populate_class_members(
        self, element_cls: MetaOxmlElement, prop_name: str
    ) -> None:
        """Add the appropriate methods to `element_cls`."""
        super(ZeroOrMore, self).populate_class_members(element_cls, prop_name)
        self._add_list_getter()
        self._add_creator()
        self._add_inserter()
        self._add_adder()
        self._add_public_adder()
        delattr(element_cls, prop_name)

继承_BaseChildElement,并实现自定义的populate_class_members——为父节点添加合适的方法。

  1. super(ZeroOrMore, self).populate_class_members调用父类的方法,将父节点、属性名称存储进实例对象。
  2. _add_*等一组方法表示为父节点添加对象的方法,后续详细介绍。
  3. delattr语句删除父节点中的属性名。

CT_P创建过程分解

CT_P表示<w:p>元素,是word文档中的核心元素类。其在oxml中的源码定义如下:

class CT_P(BaseOxmlElement):
    """`<w:p>` element, containing the properties and text for a paragraph."""

    add_r: Callable[[], CT_R]
    get_or_add_pPr: Callable[[], CT_PPr]
    hyperlink_lst: List[CT_Hyperlink]
    r_lst: List[CT_R]

	...
    r = ZeroOrMore("w:r")
    ...
  1. CT_P继承BaseOxmlElement,因此CT_P是MetaOxmlElement类型。在创建CT_P类的过程中,python解释器会遍历CT_P的定义,收集所有类属性——在CT_P定义中打上断点、进行调试。然后执行MetaOxmlElement.__init__(cls, clsname="CT_P", bases=(BaseOxmlElement,), namespace={...r: ZeroOrMore...}注意MetaOxmlElement初始化时传入的cls是CT_P,即待创建的类对象。namespace是一个字典,存储CT_P中定义的所有类属性与方法、以及一些模块信息,这里简化了,因为本文主要关注如何为CT_P自动添加合适的方法。
  2. 在执行MetaOxmlElement初始化方法中,当key="r" and value=ZeroOrMore("w:r") 时,就会调用ZeroOrMore的populate_class_members(CT_P, "r")。下述分项记录一下五条语句:
    def populate_class_members(
        self, element_cls: MetaOxmlElement, prop_name: str
    ) -> None:
        """Add the appropriate methods to `element_cls`."""
		...
        self._add_list_getter()
        self._add_creator()
        self._add_inserter()
        self._add_adder()
        self._add_public_adder()
		...

self._add_list_getter

_add_list_getter方法定义在_BaseChildElement中,其定义如下:

    def _add_list_getter(self):
        """Add a read-only ``{prop_name}_lst`` property to the element class to retrieve
        a list of child elements matching this type."""
        prop_name = "%s_lst" % self._prop_name
        property_ = property(self._list_getter, None, None)
        setattr(self._element_cls, prop_name, property_)

此时,self._prop_name存储的属性名称为“r”,即prop_name等于“r_lst”。第三句中的self._element_cls此时存储的父节点为“CT_P”,即第三句将self._list_getter方法设置为CT_P的可读特性。self._list_getter同样定义在_BaseChildElement中:

    @property
    def _list_getter(self):
        """Return a function object suitable for the "get" side of a list property
        descriptor."""

        def get_child_element_list(obj: BaseOxmlElement):
            return obj.findall(qn(self._nsptagname))

        get_child_element_list.__doc__ = (
            "A list containing each of the ``<%s>`` child elements, in the o"
            "rder they appear." % self._nsptagname
        )
        return get_child_element_list
  1. 由于在CT_P中定义r = ZeroOrMore("w:r"),因此self._nsptagname等于“w:r”,qn函数是将命名空间前缀名称转换为限定性名称,即将“w:r”转换为“{http://schemas.openxmlformats.org/wordprocessingml/2006/main}r”
  2. findall是etree.BaseElement的方法,即查找CT_P节点下的所有CT_R子节点。

self._add_creator

_add_creator方法同样定义在_BaseChildElement内,其功能是为父节点添加一个合适的创建子节点的方法。源码定义如下:

    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

    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)
  1. 在此处,会为CT_P新增一个_new_r方法,即在CT_P下创建一个空的CT_Run节点。
  2. self._creator是_BaseChildElement类的一个特性,该特性返回一个可调用对象,可调用对象的输入为BaseOxmlElement对象,输出是一个空的BaseOxmlElement实例对象。
  3. _add_to_class方法将_creator方法绑定到CT_P的_new_r特性上。

self._add_inserter

    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)
  1. 此处即为CT_P新增一个_insert_r方法。
  2. 此处_insert_r中的obj应该是CT_P实例,child应是CT_Run,即将实参CT_Run插入到CT_P中合适的位置。

self._add_adder

    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)
  1. _add_adder为CT_P新增一个_add_r方法
  2. 该方法会综合利用之前新增的_new_r与_insert_r方法。在_add_child执行逻辑中,new_method新建一个CT_Run实例,然后为新建的CT_Run设置属性值,最后调用_insert_r将新创建CT_Run插入到CT_P中的合适位置并返回。

self._add_public_adder

    def _add_public_adder(self):
        """Add a public ``add_x()`` method to the parent element class."""

        def add_child(obj: BaseOxmlElement):
            private_add_method = getattr(obj, self._add_method_name)
            child = private_add_method()
            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._public_add_method_name, add_child)
  1. 为CT_P新增一个add_r方法
  2. add_r方法的本质就是获取_add_r、并执行,得到一个新创建的CT_Run节点。
  3. 下图中显示CT_P中已经包含_new_r,_insert_r,_add_r, add_r四个自动新增的方法:
    为CT_P自动注册与CT_Run相关的方法
  • 14
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值