【日常学习】2025-8-20 框架中控件子类实例化设计
一、嵌套函数
def expand(self, data=None): # 外部函数:处理展开的整体逻辑def extend_button(btn_loc): # 内部函数:封装“点击展开按钮”的重复逻辑# 检查按钮是否显示、判断文本、点击、等待展开...# 外部函数的主逻辑:根据不同条件找到按钮,调用内部函数处理if 条件1:找到按钮Aextend_button(按钮A) # 调用内部函数elif 条件2:找到按钮Bextend_button(按钮B) # 调用内部函数elif 条件3:找到按钮C并点击
内部函数是外部函数的 “专属工具”,专门为外部函数服务,不需要暴露给外部使用。
为什么要在expand
里定义extend_button
并调用它?
核心原因是 “复用代码”:expand
函数的不同分支(if
/elif
)都需要执行 “检查按钮是否显示、判断文本、点击按钮、等待展开” 的逻辑,如果不封装成内部函数,这些代码会在每个分支中重复写多次,导致代码冗余、难维护。
二、语法概念要清楚
self.展开 = FilterSwitchCtrl.instance(self)
这一步中,子类调用父类方法的说法是对的,但它不属于向上转型,也不涉及动态绑定。
FilterSwitchCtrl
是子类(继承自 ActionCtrlBase
→ ElementBase
),而 instance
方法是在父类 ElementBase
中定义的类方法(@classmethod
)。由于子类会继承父类的类方法,因此 FilterSwitchCtrl.instance(self)
本质是子类通过继承关系调用了父类中定义的 instance
方法,这是典型的 “子类复用父类方法” 的场景。
向上转型(Upcasting)的核心是 “将子类对象赋值给父类类型的引用”,目的是利用多态性,让父类引用可以指向任意子类对象。
class Parent:passclass Child(Parent):pass# 向上转型:子类对象赋值给父类引用
parent_ref: Parent = Child() # 正确,Parent 是 Child 的父类
动态绑定(Dynamic Binding,也叫运行时绑定)的核心是 “调用方法时,在运行时根据对象的实际类型确定要执行的方法”,通常发生在 “父类引用指向子类对象,且子类重写了父类方法” 的场景。
class Parent:def do_something(self):print("Parent 做事")class Child(Parent):def do_something(self): # 重写父类方法print("Child 做事")# 父类引用指向子类对象
parent_ref: Parent = Child()
parent_ref.do_something() # 运行时绑定到 Child 的 do_something,输出“Child 做事”
而 FilterSwitchCtrl.instance(self)
调用的是类方法(@classmethod
),类方法的绑定是 “编译时绑定”(静态绑定)—— 即调用哪个类的类方法,在代码编写时就已确定(这里明确是 FilterSwitchCtrl
调用继承自父类的 instance
方法,且子类没有重写 instance
方法)。
此外,动态绑定针对的是实例方法(依赖对象的实际类型),而类方法依赖的是 “调用它的类”,因此这里不涉及动态绑定。
三、类是元类的实例
在 Python 中,元类(metaclass)是 “创建类的类”,是类的 “模板”。
类用于创建实例(对象);
而元类则用于创建类本身 —— 类是元类的实例。
在 Python 中,“一切皆对象”:
- 整数、字符串、列表是对象(由内置类
int
、str
、list
创建); - 类本身也是对象(比如
class Person: ...
,Person
这个类本身就是一个对象)。
而创建 “类对象” 的工具,就是元类。
默认情况下,Python 中所有类的元类是内置的type
(可以理解为 “元类之母”)。
最根本的父类是object
,而所有类(包括object
)都是元类type
的实例。
四、元类与基类的关系
ElementBase
类通过metaclass=ElementMeta
指定了自己的元类是ElementMeta
。在 Python 中,元类是 “类的类”—— 普通类用于创建实例,而元类用于创建类。因此:
- 当
ElementBase
被定义时,Python 会调用ElementMeta
的__new__
方法来 “创建”ElementBase
类本身; - 当
ElementBase
的子类(比如ButtonElement
、InputElement
等)被定义时,同样会由ElementMeta
的__new__
方法来创建这些子类。
⭐ 元类ElementMeta
的作用:统一干预类的创建过程
1. 强制添加类属性element
和parent
attr['element'] = None
attr['parent'] = None
attr
是类的属性 / 方法字典(比如类中定义的变量、函数都会存在这里)。元类强制给所有通过它创建的类(ElementBase
及其子类)添加element
和parent
两个类属性,初始值为None
。
目的:确保所有继承自ElementBase
的元素类(比如 UI 自动化中的按钮、输入框等元素)都有这两个基础属性,用于后续存储 “元素实例”(element
)和 “父元素”(parent
),避免子类重复定义,实现标准化。
2. 自动装饰子类的方法(排除ElementBase
自身)
if name != 'ElementBase': # 排除基类本身for attr_name, attr_value in attr.items():# 筛选出“非特殊方法”(不以__开头/结尾)的函数/方法if (type(attr_value).__name__ in ['method', 'function']) and not attr_name.startswith('__') and not attr_name.endswith('__') and attr_name!='':attr[attr_name] = show_case_step()(attr_value) # 用装饰器包装
这段逻辑会遍历类中定义的方法,对 “非特殊方法”(比如click()
、input_text()
等业务方法)用show_case_step()
装饰器进行包装 —— 但只对ElementBase
的子类生效,不对ElementBase
本身生效。
目的:这是一种 “面向切面编程(AOP)” 的思想,通过元类自动给所有子类的业务方法添加统一功能。show_case_step()
是用于记录测试步骤、打印日志、截图等功能(比如执行click()
时,自动记录 “点击按钮” 的步骤到测试报告中)。
通过元类自动装饰,避免了每个子类手动添加装饰器,减少重复代码,同时确保所有元素类的方法都遵循统一的步骤记录逻辑。
⭐ 基类ElementBase
的作用:定义通用属性和初始化逻辑
ElementBase
作为所有元素类的基类,主要提供基础属性定义和实例初始化逻辑,为子类提供统一的继承基础:
1. 定义通用类属性
locator = None; # 元素定位器(比如By.XPATH、By.ID)
cache = False # 是否缓存元素
page_container_loc = Locator(...) # 页面容器定位器(通用元素)
page_title_loc = Locator(...) # 页面标题定位器(通用元素)
2. 实例初始化方法
def __init__(self) -> object:self.element = None # 实例属性:存储当前元素的WebElement/AppiumElement对象self.parent = None # 实例属性:存储父元素实例self.p_cache = False # 实例属性:控制当前实例的缓存逻辑self.fullLocator = None # 实例属性:完整的定位路径(可能用于复杂元素定位)
在这段代码设计中,类属性(cache
、locator
等)和实例属性(self.p_cache
等)命名不同,是刻意为之的设计选择,背后反映了 “类级通用配置” 和 “实例级动态状态” 的分离。
类型 | 作用域 | 设计目标 | 典型场景 |
---|---|---|---|
类属性 | 所有实例共享 | 定义通用配置、默认行为 | 元素默认是否缓存(cache )、通用定位器(locator ) |
实例属性 | 单个实例独有 | 存储实例的动态状态、个性化数据 | 某个元素实例的缓存开关(self.p_cache )、实际元素对象(self.element ) |
- 类属性
cache
:是所有子类实例的 “默认配置”。比如框架设计者希望 “大部分元素默认不缓存”,就把cache = False
作为类属性 —— 所有继承ElementBase
的子类,若不单独修改cache
,都会继承这个默认值。 - 实例属性
self.p_cache
:是单个实例的 “运行时状态”。允许实例在初始化或运行中,覆盖类的默认配置,实现 “这个按钮实例需要缓存,那个输入框实例不需要缓存” 的个性化控制。
举个实际场景:
class Button(ElementBase):cache = True # 所有按钮默认缓存(修改类属性)# 实例化时,可覆盖默认值
btn1 = Button()
btn1.p_cache = False # 这个按钮实例不缓存(修改实例属性)
locator
(类属性) vs 无直接同名实例属性(但有 self.fullLocator
)
- 类属性
locator
:是元素定位的 “基础规则”,定义元素的默认定位方式(如By.XPATH, "//button"
)。子类继承后,可直接复用或重写这个定位规则。 - 实例属性
self.fullLocator
:是定位逻辑的 “运行时产物”。可能在实例化时,结合父元素、动态参数,拼接出完整的定位路径(比如父元素是表单,子元素是输入框,fullLocator
可能是表单定位 + 输入框定位
)。
设计上,locator
更偏向 “静态配置”,fullLocator
更偏向 “动态计算结果”—— 前者是规则,后者是规则运行后的产物,因此命名不同。
命名差异的本质是 “类级配置” 和 “实例级状态” 的分离设计—— 类属性定规则、实例属性存状态,既保证了框架的通用性(默认配置可复用),又支持了实例的个性化(动态状态可调整)。这种设计在面向对象编程(尤其是框架开发)中很常见,核心是通过分层控制让代码更灵活、更易维护。
OOP 基类 通过封装、继承、多态解决纵向代码复用,AOP 元类 通过切面解决横向代码复用,共同提升代码的可维护性和可读性。
ElementMeta
(元类):负责在 “类创建阶段” 统一注入属性和装饰方法,控制类的行为规范;ElementBase
(基类):负责定义 “实例层面” 的通用属性和初始化逻辑,为子类提供继承基础。
四、控件依赖父容器的好处
父类ElementBase
将instance
设计为类方法(@classmethod
),而不是让子类通过super().__init__()
调用父类构造函数,核心原因是为了统一管控实例的创建逻辑,解决 UI 自动化框架中 “控件与页面的关联”“实例复用”“初始化一致性” 等特定问题。具体来说,类方法instance
相比单纯的super().__init__()
有三个关键优势:
⭐ 类方法instance
能统一处理 “父对象关联”,避免子类重复编码
在 UI 自动化中,控件(如FilterSwitchCtrl
)必须依赖所属的页面(如HistoryPage
)才能工作(比如需要共享页面的浏览器驱动driver
、定位上下文等)。这种 “控件 - 页面” 的关联关系,需要在实例化控件时就明确绑定。
如果用super().__init__()
的方式,子类需要手动传递父页面实例并绑定,例如:
# 假设用构造函数关联父对象,子类必须手动实现
class FilterSwitchCtrl(ActionCtrlBase):def __init__(self, parent):super().__init__() # 调用父类构造函数self.parent = parent # 手动绑定父页面self.driver = parent.driver # 手动获取父页面的驱动# 页面中实例化时,需要显式传入self(页面实例)
self.展开 = FilterSwitchCtrl(self)
self. 展开=FilterSwitchCtrl(self),的self指的是调用控件实例化的页面类history,不是init函数里面的参数self,而是参数parent(父容器)。
这种方式的问题是:每个子类都要重复编写 “接收 parent、绑定 driver” 的代码,一旦框架调整关联逻辑(比如新增context
属性需要绑定),所有子类都要修改,维护成本极高。
而类方法instance
将这种关联逻辑封装在父类中,子类无需关心:
class ElementBase:@classmethoddef instance(cls, parent): # 父类统一实现关联逻辑obj = cls() # 创建子类实例obj.parent = parent # 绑定父页面obj.driver = parent.driver # 共享驱动obj.context = parent.context # 共享上下文(框架扩展时只需改这里)return obj# 子类无需重写,直接继承使用
class FilterSwitchCtrl(ActionCtrlBase):pass # 无需写__init__,也无需处理parent关联# 页面中实例化时,只需调用instance并传入self
self.展开 = FilterSwitchCtrl.instance(self)
所有控件的 “父对象关联” 逻辑都在父类ElementBase
的instance
中实现,子类只需继承,既减少重复代码,又保证了关联逻辑的一致性。直接控件类名.instance方法即可
⭐ 类方法instance
支持 “实例复用 / 单例”,避免重复创建对象
在 UI 自动化中,同一个页面的同一个控件通常只需要一个实例(比如页面上的 “展开按钮” 不会同时存在多个,重复创建实例会浪费资源)。
类方法instance
可以轻松实现 “单例模式” 或 “实例缓存”,例如:
class ElementBase:_instance_cache = {} # 缓存实例,key是(类名+父页面标识)@classmethoddef instance(cls, parent):# 生成唯一标识:当前类 + 父页面(避免不同页面的同控件混淆)cache_key = (cls.__name__, id(parent))if cache_key not in cls._instance_cache:# 缓存中没有则创建新实例obj = cls()obj.parent = parentcls._instance_cache[cache_key] = obj# 返回缓存的实例(复用)return cls._instance_cache[cache_key]
这样,当页面多次调用FilterSwitchCtrl.instance(self)
时,只会创建一个实例,避免重复初始化。
而如果用super().__init__()
,每次调用FilterSwitchCtrl(self)
都会创建新实例,无法实现复用,可能导致资源浪费(比如重复定位同一元素)。
但!
框架由于采用了 “父容器实例的属性直接绑定控件实例” 的方式( self.展开 = FilterSwitchCtrl.instance(self)
),确实不需要在 ElementBase
中显式实现 _instance_cache
这样的缓存管理,原因如下:
1. 父容器的属性绑定本身就是一种 “天然缓存”
- 每个父容器(如
HistoryPage
实例)会通过self.展开
等属性绑定一个控件实例(FilterSwitchCtrl
实例); - 只要父容器实例存在,
self.展开
就会一直指向同一个控件实例,不会重复创建; - 当父容器实例被销毁时,绑定的控件实例也会被自动回收(Python 的垃圾回收机制)。
这种方式下,一个父容器对应一套控件实例,天然避免了 “同一页面内重复创建相同控件实例” 的问题,无需额外的缓存机制。
2. 框架设计可能更注重 “控件与父容器的强关联”
UI 自动化框架中,控件通常与所属的页面(父容器)强绑定(比如依赖页面的驱动、定位上下文)。如果采用全局缓存(如 _instance_cache
),反而可能出现 “控件实例与父容器脱离关联” 的风险(比如页面已销毁,但控件实例仍被缓存占用)。
而 “父容器属性绑定” 的方式,让控件实例的生命周期与父容器完全一致:
- 页面存在 → 控件存在;
- 页面销毁 → 控件随属性被回收。
这种设计更符合 UI 自动化中 “页面 - 控件” 的从属关系,比全局缓存更安全、更贴合业务场景。
3. 何时需要显式缓存(_instance_cache
)?
如果框架中存在 “跨父容器复用同一控件” 的场景(比如多个页面共享一个全局控件),则需要 _instance_cache
来管理。但在你的场景中:
- 控件是页面的一部分(如 “展开按钮” 属于某个特定页面);
- 不同页面的 “展开按钮” 是独立的(即使控件类相同,实际操作的元素也不同)。
因此,“父容器属性绑定” 已足够满足需求,无需额外缓存。
五、初始化的调用流程
谁调用的instance,instance方法里面的cls()就是他。
cls() 等价于 FilterSwitchCtrl(),即创建 FilterSwitchCtrl 的实例。
由于 FilterSwitchCtrl 没有自己的 __init__ 方法,Python 会自动调用其父类的 __init__ 方法(即 ElementBase 的 __init__)。在 Python 中,当一个类(子类)没有定义自己的 __init__
方法时,创建该类的实例时,Python 会自动向上查找父类的 __init__
方法并调用。这是面向对象继承机制的基本规则之一。
当 History
页面执行 self.展开 = FilterSwitchCtrl.instance(self)
时,完整流程如下:
1. 调用 FilterSwitchCtrl.instance(self)
FilterSwitchCtrl
没有自己的 instance
方法,因此继承并调用父类 ElementBase
的 instance
类方法。此时 instance
方法中的 cls
代表 FilterSwitchCtrl
(因为是通过 FilterSwitchCtrl
调用的,类方法的 cls
参数指向调用者类)。
2. 执行 obj = cls()
:创建子类实例,触发 __init__
cls()
等价于 FilterSwitchCtrl()
,即创建 FilterSwitchCtrl
的实例。由于 FilterSwitchCtrl
没有自己的 __init__
方法,Python 会自动调用其父类的 __init__
方法(即 ElementBase
的 __init__
)。
# 触发 ElementBase 的 __init__
self.element = None
self.driver = None
print("ElementBase __init__ 执行")
这一步已经完成了父类定义的基础初始化。
3. 执行 instance
方法的补充初始化
创建实例(obj = cls()
)后,instance
方法继续执行后续逻辑,为实例绑定父页面、共享驱动等:
obj.parent = parent # parent 是 History 页面的 self
obj.driver = parent.driver # 从父页面获取驱动,覆盖 __init__ 中设置的 None
print("instance 补充初始化完成")
4. 返回实例并赋值
instance
方法返回初始化完成的 FilterSwitchCtrl
实例,赋值给 self.展开
。此时 self.展开
已经包含:
- 父类
__init__
初始化的属性(element
等); instance
方法补充的属性(parent
、driver
等)。
六、instance和init协同的好处
我觉得是解耦
elementbase的init函数创建对象只需要规定好有什么属性即可
instance这个类方法,关联的是调用它的类控件,需要和父容器进行关联生成实例,这里就要传入父容器的上下文。
所以elementbase的init函数主要是规定这个对象是什么样的什么结构的,是一个初始化。
但instance把父容器给到了控件实例,完成了具体的框架业务这方面的实例化。
__init__
负责 “基础属性初始化”,instance
负责 “框架级实例装配”。
ElementBase
中:
__init__
方法:仅初始化了最基础的属性(element=None
、parent=None
、p_cache=False
等),不依赖任何外部参数(没有parent
、locator
等入参)。instance
类方法:接收parent
、locator
等外部参数,创建实例后手动设置inst.parent = parent
、inst.locator = locator
,负责将实例与外部环境(父容器、定位器)关联。
1. 保持__init__
的 “通用性”,避免与框架强绑定
__init__
是 Python 中所有类的 “基础构造入口”,它的职责应该是初始化类自身的核心属性(与框架无关的部分)。
元素基本类设计中,__init__
只做 “无参数的基础初始化”,确保任何场景下都能安全创建实例(哪怕暂时不关联父容器),更通用、更灵活。
2. 通过instance
统一管控 “框架级装配逻辑”,保证一致性
instance
的核心作用是作为 “框架规定的实例创建入口”,所有控件实例必须通过它创建,从而强制执行 “关联父容器、设置定位器” 等框架必需的逻辑。
3. 方便扩展框架功能(如缓存、日志、权限校验等)
instance
作为 “实例创建的中间层”,可以很容易地添加框架级功能,而不需要修改__init__
或子类代码。
总结:instance
是框架的 “标准化装配线”
__init__
像 “零件加工厂”:负责生产最基础的 “零件”(实例的核心属性),不关心这个零件最终要装到哪个 “机器”(父容器)上。instance
像 “总装车间”:接收外部 “订单”(parent
、locator
),把零件(实例)装配成可用的 “成品”(关联父容器、设置定位器),并确保所有成品符合框架的标准。