概述
在docx.oxml.text.paragraph模块中定义了CT_P段落对象元素类,但是CT_P中并未定义add_r等与CT_Run相关的方法。在不断探索源码逻辑的过程中,对这种自动为类注册合适的方法的功能进行了梳理——xmlchemy这个模块设计的真好!!!
大体逻辑如下:
- CT_P中包含类属性“r”, 该类属性存储的是ZeroOrMore实例对象——docx.oxml.xmlchemy模块中定义了ZeroOrMore子元素类对象,以及与之相似的OneAndOnlyOne、OneOrMore、ZeroOrOne、ZeroOrOneChoice等子元素类对象。这些类对象均继承_BaseChildElement,并重新定义了populate_class_members方法。正是该方法为许多BaseOxmlElement类对象自动化添加合适的方法。
- 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)
- 元类是创建类的类,其功能|角色与type类似。
- clsname是待创建的类对象名称,base是待创建类对象的父类,namespace是待创建类对象的namespace,可简单理解为待创建类对象的属性字典。
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.
"""
- BaseOxmlElement继承etree.ElementBase,因此可以直接使用etree.ElementBase中的find、findall等方法。
- 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
- 初始化方法中,需要传入命名空间前缀的元素标签名,以及该元素的先驱元素——就是在一个父节点下,排在当前节点之前的所有其它子节点。比如段落对象中,一般段落属性对象会排在第一位。
- 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——为父节点添加合适的方法。
- super(ZeroOrMore, self).populate_class_members调用父类的方法,将父节点、属性名称存储进实例对象。
- _add_*等一组方法表示为父节点添加对象的方法,后续详细介绍。
- 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")
...
- 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自动添加合适的方法。 - 在执行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
- 由于在CT_P中定义
r = ZeroOrMore("w:r")
,因此self._nsptagname等于“w:r”,qn函数是将命名空间前缀名称转换为限定性名称,即将“w:r”转换为“{http://schemas.openxmlformats.org/wordprocessingml/2006/main}r” - 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)
- 在此处,会为CT_P新增一个_new_r方法,即在CT_P下创建一个空的CT_Run节点。
- self._creator是_BaseChildElement类的一个特性,该特性返回一个可调用对象,可调用对象的输入为BaseOxmlElement对象,输出是一个空的BaseOxmlElement实例对象。
- _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)
- 此处即为CT_P新增一个_insert_r方法。
- 此处_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)
- _add_adder为CT_P新增一个_add_r方法
- 该方法会综合利用之前新增的_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)
- 为CT_P新增一个add_r方法
- add_r方法的本质就是获取_add_r、并执行,得到一个新创建的CT_Run节点。
- 下图中显示CT_P中已经包含_new_r,_insert_r,_add_r, add_r四个自动新增的方法: