当前位置: 首页 > news >正文

【日常学习】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 中,“一切皆对象”:

  • 整数、字符串、列表是对象(由内置类intstrlist创建);
  • 类本身也是对象(比如class Person: ...Person这个类本身就是一个对象)。

而创建 “类对象” 的工具,就是元类。

默认情况下,Python 中所有类的元类是内置的type(可以理解为 “元类之母”)。

最根本的父类是object,而所有类(包括object)都是元类type的实例。

四、元类与基类的关系 

ElementBase类通过metaclass=ElementMeta指定了自己的元类是ElementMeta。在 Python 中,元类是 “类的类”—— 普通类用于创建实例,而元类用于创建类。因此:

  • ElementBase被定义时,Python 会调用ElementMeta__new__方法来 “创建”ElementBase类本身;
  • ElementBase的子类(比如ButtonElementInputElement等)被定义时,同样会由ElementMeta__new__方法来创建这些子类。

⭐ 元类ElementMeta的作用:统一干预类的创建过程

1. 强制添加类属性elementparent
attr['element'] = None
attr['parent'] = None

attr是类的属性 / 方法字典(比如类中定义的变量、函数都会存在这里)。元类强制给所有通过它创建的类(ElementBase及其子类)添加elementparent两个类属性,初始值为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   # 实例属性:完整的定位路径(可能用于复杂元素定位)

在这段代码设计中,类属性(cachelocator 等)和实例属性(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(基类):负责定义 “实例层面” 的通用属性和初始化逻辑,为子类提供继承基础。

四、控件依赖父容器的好处 

父类ElementBaseinstance设计为类方法(@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)

所有控件的 “父对象关联” 逻辑都在父类ElementBaseinstance中实现,子类只需继承,既减少重复代码,又保证了关联逻辑的一致性。直接控件类名.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 方法补充的属性(parentdriver 等)。

六、instance和init协同的好处 

我觉得是解耦

elementbase的init函数创建对象只需要规定好有什么属性即可

instance这个类方法,关联的是调用它的类控件,需要和父容器进行关联生成实例,这里就要传入父容器的上下文。

所以elementbase的init函数主要是规定这个对象是什么样的什么结构的,是一个初始化。

但instance把父容器给到了控件实例,完成了具体的框架业务这方面的实例化。

__init__负责 “基础属性初始化”,instance负责 “框架级实例装配”

ElementBase中:

  • __init__方法:仅初始化了最基础的属性(element=Noneparent=Nonep_cache=False等),不依赖任何外部参数(没有parentlocator等入参)。
  • instance类方法:接收parentlocator等外部参数,创建实例后手动设置inst.parent = parentinst.locator = locator负责将实例与外部环境(父容器、定位器)关联

1. 保持__init__的 “通用性”,避免与框架强绑定

__init__是 Python 中所有类的 “基础构造入口”,它的职责应该是初始化类自身的核心属性(与框架无关的部分)。

元素基本类设计中,__init__只做 “无参数的基础初始化”,确保任何场景下都能安全创建实例(哪怕暂时不关联父容器),更通用、更灵活。

2. 通过instance统一管控 “框架级装配逻辑”,保证一致性

instance的核心作用是作为 “框架规定的实例创建入口”,所有控件实例必须通过它创建,从而强制执行 “关联父容器、设置定位器” 等框架必需的逻辑。

3. 方便扩展框架功能(如缓存、日志、权限校验等)

instance作为 “实例创建的中间层”,可以很容易地添加框架级功能,而不需要修改__init__或子类代码。

总结:instance是框架的 “标准化装配线”

  • __init__像 “零件加工厂”:负责生产最基础的 “零件”(实例的核心属性),不关心这个零件最终要装到哪个 “机器”(父容器)上。
  • instance像 “总装车间”:接收外部 “订单”(parentlocator),把零件(实例)装配成可用的 “成品”(关联父容器、设置定位器),并确保所有成品符合框架的标准。

http://www.dtcms.com/a/344858.html

相关文章:

  • FPGA学习笔记——简单的IIC读写EEPROM
  • LeetCode 3195.包含所有 1 的最小矩形面积 I:简单题-求长方形四个范围
  • 化工生产场景下设备状态监测与智能润滑预测性维护路径
  • 校园作品互评管理移动端的设计与实现
  • Boost库中boost::random::normal_distribution(正态分布)详解和实战示例
  • 腾讯云EdgeOne安全防护:快速上手,全面抵御Web攻击
  • 如何优雅的监听dom的变化(尺寸)
  • php apache无法接收到Authorization header
  • JDK17 升级避坑指南:技术原理与解决方案详解
  • 【学习记录】structuredClone,URLSearchParams,groupBy
  • 【大语言模型 14】Transformer权重初始化策略:从Xavier到GPT的参数初始化演进之路
  • 网络编程8.22
  • Python面试常考函数
  • 技术分析 剖析一个利用FTP快捷方式与批处理混淆的钓鱼攻击
  • RSS与今日头条技术对比分析
  • Unreal Engine UObject
  • 嵌入式-EXTI的工作原理和按钮实验-Day19
  • 6口千兆图像采集卡:突破多路高清视觉系统的传输瓶颈
  • DFS序与树链剖分入门
  • RORPCAP: retrieval-based objects and relations prompt for image captioning
  • 多元函数积分学
  • kafka生产者 消费者工作原理
  • 线性回归8.21
  • 椭圆、双曲线、抛物线总对比表
  • Java 对象内存布局详解
  • Docker容器化部署实战:Tomcat与Nginx服务配置指南
  • 大模型推理-MTK Neurapilot sdk了解与环境配置-1
  • Unreal Engine UPrimitiveComponent
  • QT5 UI界面上Scroll Area控件显示滚动条
  • 浏览器开发CEFSharp+X86+win7(十三)之Vue架构自动化——仙盟创梦IDE