【日常学习】2025-8-22 类属性和实例属性+小白学调试
一、同名类属性和实例属性:普通类
1. 类属性的作用:定义 “默认值” 和 “类型提示”
类中定义的menu_path: list = None
、locator = None
等类属性,主要有两个作用:
- 类型提示:通过
menu_path: list
告诉开发者,这个属性预期是list
类型(提高代码可读性和 IDE 提示能力); - 默认初始值:当实例未显式设置该属性时,默认值为
None
(明确属性的初始状态)。
2. 实例属性的作用:存储 “实例个性化值”
构造方法中self.menu_path = menu_path
等赋值,是为了将每个实例特有的参数存储到实例中。即使与类属性同名,实例属性也会覆盖类属性(面向对象中,实例属性的优先级高于类属性)。
# 类属性默认值为 None
print(PageElement.menu_path) # 输出: None# 创建实例时传递个性化值
elem = PageElement(menu_path=["首页", "股票"],locator=Locator(By.ID, "stock")
)# 实例属性覆盖了类属性
print(elem.menu_path) # 输出: ["首页", "股票"](实例自己的值)
- 类属性是 “模板级的默认配置”,实例属性是 “对象级的个性化配置”;
- 当访问
实例.属性
时,Python 会先找实例属性,找不到才会用类属性(形成 “默认值 fallback” 机制)。
二、不同名类属性和实例属性:基类
之前的ElementBase
中,类属性cache
与实例属性p_cache
不同名,是因为它们代表不同概念
要彻底理解 ElementBase
中类属性与实例属性的 “概念差异”,需要结合 UI 自动化测试的业务场景 和 属性的实际功能—— 它们不是 “默认值与覆盖值” 的关系,而是 “全局规则” 与 “实例状态” 的分层设计,各自负责不同的功能维度。
ElementBase
是 UI 自动化框架中 “页面元素” 的基类(比如按钮、输入框、列表等元素都继承它)。类属性和实例属性的设计,对应了自动化测试中两个核心需求:
- 类属性:定义 “所有同类元素的通用规则 / 模板”(比如 “默认是否允许缓存”“基础定位器是什么”);
- 实例属性:记录 “单个元素实例的运行时状态”(比如 “实际找到的元素对象”“该实例是否真的启用了缓存”)。
比如ElementBase
中:
- 类属性负责 “规则定义”(是否允许缓存、如何定位);
- 实例属性负责 “执行状态”(是否实际缓存、找到的元素对象)。
它们是 不同维度的概念,就像 “交通规则(限速 120)” 和 “汽车实际速度(100km/h)”—— 规则和实际状态是两回事,不能说 “汽车速度是规则的默认值”。
1. cache
(类属性) vs self.p_cache
(实例属性):普通类
类属性
cache = False
:这是 “框架级的默认规则”,表示 “所有继承
ElementBase
的元素类,默认不允许缓存元素”(缓存指:找到元素后保存起来,避免每次操作都重新查找,可能影响动态页面的准确性)。
它是给 “类” 设定的规则,比如:class Button(ElementBase):cache = True # 按钮类单独修改规则:允许缓存(覆盖类属性的默认值)
这里的
cache
控制的是 “某类元素是否允许缓存”(规则层面)。实例属性
self.p_cache = False
:这是 “实例级的实际状态”,表示 “当前这个元素实例是否真的启用了缓存”(执行层面)。
它可以根据实例的具体场景,覆盖类的规则,比如:btn = Button() btn.p_cache = False # 虽然按钮类允许缓存,但这个按钮实例临时关闭缓存
这里的
p_cache
控制的是 “这个实例是否实际使用缓存”(状态层面)。
核心差异:cache
是 “是否允许”(规则),p_cache
是 “是否实际使用”(状态),两者是 “规则 vs 执行” 的不同维度,而非 “默认 vs 覆盖”。
2. locator
(类属性) vs self.fullLocator
(实例属性):基类
类属性
locator = None
:
这是 “元素定位的基础模板”,子类需要重写它来定义 “该类元素的基础定位方式”。比如:class SearchInput(ElementBase):locator = Locator(By.ID, "search-input") # 搜索框的基础定位器
它是 “某类元素应该如何查找” 的通用规则(比如所有搜索框都通过
id="search-input"
定位)。实例属性
self.fullLocator = None
:
这是 “实例运行时的完整定位路径”,可能结合了父元素的定位器动态生成。比如:
父元素是表单(locator = By.ID, "form"
),子元素是输入框(基础locator = By.NAME, "username"
),则fullLocator
可能是By.ID, "form" → By.NAME, "username"
(完整路径)。
核心差异:locator
是 “基础定位模板”(静态规则),fullLocator
是 “动态拼接的实际定位路径”(运行时结果),两者是 “模板 vs 结果” 的不同概念。
3. page_container_loc
/page_title_loc
(类属性) vs self.element
(实例属性)
类属性
page_container_loc
等:
这是 “页面级通用元素的定位器”(比如所有股票页面都有stock-view-page-container
容器),是框架预定义的 “公共定位规则”,供所有实例共享使用(比如任何元素都可以通过它找到页面容器)。实例属性
self.element = None
:
这是 “实例实际找到的 Web 元素对象”(比如 Selenium 的WebElement
或 Appium 的MobileElement
),是执行find_element
后的实际结果,每个实例的element
指向自己对应的页面元素(比如 “登录按钮实例” 的element
指向页面上的登录按钮,“注册按钮实例” 的element
指向注册按钮)。
核心差异:page_container_loc
是 “找元素的规则”,self.element
是 “找到的元素本身”,两者是 “方法 vs 结果” 的不同概念。
- 若类属性和实例属性代表同一概念(只是默认值 vs 个性化值)
- 若代表不同概念(如 “全局配置” vs “实例状态”)
三、子类的类属性重写
类属性可以被子类重写,这是面向对象编程中继承特性的重要体现。子类重写父类属性后,会用自己的属性值覆盖父类的默认值,让子类拥有更贴合自身需求的配置。
假设ElementBase
是父类,我们创建一个子类ButtonElement
(按钮元素),重写它的locator
和cache
属性:
# 父类:ElementBase
class ElementBase(metaclass=ElementMeta):locator = None # 父类默认定位器为Nonecache = False # 父类默认不缓存# 其他属性和方法...# 子类:ButtonElement(继承自ElementBase)
class ButtonElement(ElementBase):# 重写父类的locator:按钮有自己的定位规则locator = Locator(By.XPATH, ".//button")# 重写父类的cache:按钮允许缓存cache = True
原来如此,基类的locator就是一个规则,=None是在预留,搭个框架初始化而已。
子类可以自己重写:是否需要定位功能,缓存功能?具体定位规则是什么,具体怎么缓存的。
属性是小写开头locator,重写调用的就是diver包下面的同名首字母大写函数制定定位规则。
所以想要理解这个代码为什么这么写,一定需要结合具体的框架内容甚至业务逻辑,内容。
- 父类
ElementBase
定义通用默认规则(比如cache = False
); - 子类(按钮、输入框、下拉框等)根据自身特性重写规则(比如按钮可以缓存
cache = True
,动态生成的输入框不缓存cache = False
)。
这样既保证了所有元素有统一的基础配置,又允许子类灵活定制,符合 “通用父类 + 个性化子类” 的设计思想。这是子类重写基类同名类属性!
子类重写的是类属性(属于子类本身),而之前说的实例属性(如self.p_cache
)是每个实例自己的状态。
# 父类:ElementBase
class ElementBase(metaclass=ElementMeta):locator = None # 父类默认定位器为Nonecache = False # 父类默认不缓存# 其他属性和方法...# 子类:ButtonElement(继承自ElementBase)
# 子类重写类属性
class ButtonElement(ElementBase):# 重写父类的locator:按钮有自己的定位规则locator = Locator(By.XPATH, ".//button")# 重写父类的cache:按钮允许缓存cache = True # 类属性:所有按钮默认允许缓存# 实例修改自己的状态
btn1 = ButtonElement()
btn1.p_cache = False # 实例属性:这个按钮实例不实际使用缓存
- 子类重写的
cache
是 “该类元素的默认规则”; 说明这个子类的规则是允许缓存的。(类属性代表着一种大多数都遵循这个规则的含义) - 实例的
p_cache
是 “这个实例的实际执行状态”。子类是允许缓存的,但那是给实例权力,但实例完全可以不执行啊,这个按钮实例它就不使用缓存,就是说可以缓存,但是我这个实例不需要这个功能。这是子类不同名的实例属性!
两者配合,既实现了 “类级别的规则定制”,又支持 “实例级别的灵活调整”,非常适合复杂框架的设计。
总结
子类完全可以重写父类的属性,这是面向对象中 “继承 + 定制” 的核心用法。在你的框架中,这种机制让不同类型的元素(按钮、输入框等)能在共享父类基础功能的同时,拥有自己的专属配置,既减少重复代码,又保证了灵活性。
四、调试有学问!
调试是理解代码流程最直接有效的方法,尤其是对于复杂框架。作为 “调试新手”,掌握一些关键细节和技巧能让你效率翻倍。
【1】调试前的 3 个准备工作(关键!)
明确目标:先想清楚 “我要搞懂什么”
- 比如:“这个
show_case_step()
装饰器是怎么给方法加日志的?”“ElementBase
的__init__
执行时,parent
属性是从哪来的?” - 带着具体问题调试,避免漫无目的地单步执行。
- 比如:“这个
找到入口点:从 “触发函数的第一行代码” 开始
- 比如框架中某个测试用例的
test_xxx()
方法,或者你知道的某个公开接口(如driver.find_element()
),从这里打第一个断点。
- 比如框架中某个测试用例的
简化场景:用最小化的代码复现流程
- 比如在框架里写一个最简单的测试用例(只调用你要研究的函数),减少无关代码干扰(比如注释掉其他测试步骤)。简单的脚本写想要调试的函数,或者注释掉别的行。
【2】调试核心操作:断点 + 变量查看(以 PyCharm 为例,其他 IDE 逻辑类似)
1. 断点怎么打?3 种常用断点类型
普通断点:点击代码行号右侧的空白处(会出现红色圆点),程序执行到这里会暂停。
✅ 适合:函数入口(如def __init__
的第一行)、关键逻辑分支(if
/else
)。条件断点:右键断点→设置条件(如
menu_path == ["首页"]
),只有满足条件时才暂停。
✅ 适合:循环中只想看特定情况(比如第 5 次循环)、多分支中只关注某条路径。异常断点:PyCharm 顶部
Run
→View Breakpoints
→点击+
→选Python Exception Breakpoint
,输入异常类型(如AttributeError
),程序抛出该异常时会自动暂停。
✅ 适合:定位 “不知道在哪报错” 的问题(比如框架中偶尔出现的NoneType
错误)。
2. 暂停后该看什么?4 个核心信息
程序暂停后,重点关注这几个窗口(PyCharm 底部):
窗口 / 操作 | 作用 | 新手必看内容 |
---|---|---|
Variables | 显示当前作用域的所有变量(局部变量、全局变量、实例属性等) | 实例的self.xxx 属性(如self.element 的值)、函数参数值 |
Call Stack | 显示函数调用链(谁调用了当前函数,当前函数又调用了谁) | 从上到下是调用顺序,点击某一行可跳转到对应函数代码 |
Watches | 手动添加变量 / 表达式(如self.parent.locator ),实时查看其值 | 跟踪复杂属性(比如多层嵌套的locator ) |
Console | 调试时的交互式命令行,可直接输入代码执行(如print(self.menu_path) ) | 临时验证猜想(比如 “这个变量是不是None ”) |
3. 单步执行:4 个按钮怎么用?
暂停后,用这 4 个按钮控制执行节奏(PyCharm 调试工具栏):
按钮(图标) | 功能 | 什么时候用? |
---|---|---|
单步执行(Step Over, F8) | 执行当前行,不进入当前行调用的函数内部 | 只想看当前函数的流程,跳过子函数 |
单步进入(Step Into, F7) | 执行当前行,进入当前行调用的函数内部(如果是自定义函数) | 想钻进子函数看细节(比如show_case_step() 装饰器的逻辑) |
单步跳出(Step Out, Shift+F8) | 从当前函数内部跳出,回到调用它的地方 | 子函数看得差不多了,想回到上层函数 |
继续执行(Resume Program, F9) | 从当前断点继续执行,直到遇到下一个断点 | 想直接跳到下一个断点(比如跳过循环中间步骤) |
【3】框架代码调试的实战技巧
结合你提到的ElementBase
和元类代码,举几个具体场景:
1. 想知道 “元类ElementMeta
如何修改类属性”
- 断点位置:
ElementMeta
的__new__
方法第一行(def __new__(cls, name, base, attr):
)。 - 看什么:
name
:当前正在创建的类名(比如ElementBase
或它的子类);attr
:类的属性字典(里面有locator
、cache
等,调试时能看到元类如何给attr
添加element
和parent
);- 执行到
attr[attr_name] = show_case_step()(attr_value)
时,用Step Into
钻进show_case_step()
装饰器,看它如何包装方法。
2. 想跟踪 “self.element
是何时被赋值的”
- 断点位置:
ElementBase
的__init__
方法(self.element = None
这行),以及所有可能修改self.element
的地方(比如搜索self.element =
在框架中的所有出现位置)。 - 操作:
- 先在
__init__
断点确认初始值是None
; - 用
Resume Program
跳到下一个修改self.element
的断点,看是哪个函数(比如find_element()
)给它赋了值; - 在
Variables
窗口观察self.element
的类型(比如是不是 Selenium 的WebElement
)。
- 先在
3. 看不懂 “函数调用链”(比如 “测试用例→页面元素→元类” 的调用顺序)
- 用
Call Stack
窗口:- 比如执行测试用例时暂停,
Call Stack
顶部是当前函数(比如ElementBase.__init__
),下面一行是调用它的函数(比如Button.__init__
),再下面是更上层的调用(比如test_case()
); - 点击
Call Stack
中的任意一行,能直接跳转到对应的调用位置,轻松理清 “谁调用了谁”。
- 比如执行测试用例时暂停,
【4】调试习惯:边调边记
调试时拿个笔记(或记事本)记录:
- 关键函数的调用顺序(比如
元类__new__
→ElementBase.__init__
→子类方法
); - 变量的变化节点(比如
self.element
在find_element
后从None
变成了具体对象); - 自己的猜想和验证结果(比如 “猜
p_cache
是cache
的实例化,调试后发现确实在__init__
里用了self.p_cache = ...
”)。
记下来的内容就是你理解框架的 “线索”,比单纯调试更有收获。
这一点也同样说明了学习或者刷题不要一上来就看答案,这种不思考直接接受的方式会让我们忽略很多思考的关键点和细节,细节决定成败,懂了细节才能理解到脑子里去。
五、类的初始化和实例初始化
【1】类初始化的触发时机
首先要明确:类不是一开始就加载到内存的,而是在程序第一次 “需要用到这个类” 时才会触发初始化。比如:
- 当你写
obj = ElementBase()
(创建类的实例); - 当你访问类的静态属性 / 方法(比如
ElementBase.page_container_loc
); - 当子类初始化时(因为子类依赖父类,会先加载父类)。
【2】类初始化时具体加载什么?
以你的ElementBase
类为例,初始化过程会依次处理这些内容:
1. 加载类的 “结构信息”
把类的定义(比如类名、父类、方法列表)加载到内存,让程序知道 “这个类是什么样的”。
- 比如程序会记录:
ElementBase
的父类是object
(默认),它有locator
、cache
这些类属性,有__init__
这个实例方法。
2. 初始化 “类属性”(静态属性)
类中定义的类属性(不是实例属性)会在此时被创建并赋值。
- 比如你的
ElementBase
中:class ElementBase(metaclass=ElementMeta):locator = None # 类属性cache = False # 类属性page_container_loc = Locator(...) # 类属性
类初始化时,会给locator
、cache
、page_container_loc
这些属性分配内存,并设置初始值(None
、False
、Locator
对象)。 - 这些类属性属于类本身,所有实例共享(比如
ElementBase.cache
和obj.cache
在未被实例覆盖时是同一个值)。
3. 执行类级别的代码块(如果有)
如果类中存在直接写在类里的代码(不是方法内的),会在此时执行。
- 比如:
class ElementBase:print("正在初始化ElementBase类") # 类级代码块cache = False
类初始化时,会先打印这句话,再给cache
赋值。
4. 处理元类(如果有)
你的类用了metaclass=ElementMeta
,元类会在类初始化时 “干预” 类的创建过程。
- 元类可以动态修改类的属性、方法,甚至决定类是否能被创建(比如校验类属性是否符合规范)。
- 这一步是 Python 特有的,比如
ElementMeta
可能会在类加载时自动处理locator
属性的格式。
5. 初始化静态方法 / 类方法(如果有)
静态方法(@staticmethod
)和类方法(@classmethod
)属于类本身,会在类初始化时加载到内存,此时就可以通过 “类名。方法名” 调用(比如ElementBase.static_method()
)。
【3】和 “实例初始化” 的区别(关键!)
很多人会混淆 “类初始化” 和 “实例初始化”,这里明确:
类初始化(类加载) | 实例初始化(__init__ 方法) |
---|---|
针对 “类本身”,只执行 1 次 | 针对 “每个实例”,每次new /创建实例 时执行 |
加载类属性、静态方法等 | 初始化实例属性(比如self.element = None ) |
完成后类就 “可用” 了 | 完成后得到一个具体的实例对象 |
比如你的ElementBase
:
- 当第一次用到
ElementBase
时(比如obj = ElementBase()
),先触发类初始化(加载locator
等类属性); - 类初始化完成后,才会执行
__init__
方法,创建obj
这个实例,初始化self.element
等实例属性。