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

C++:迭代器的封装思想

C++:迭代器的封装思想

    • list迭代器实现
    • 反向迭代器实现


本博客将通过实现list的迭代器,以及它的反向迭代器,来帮助大家理解迭代器的底层逻辑,以及封装思想。


list迭代器实现

迭代器是一个遍历容器的工具,其可以通过自增自减来改变位置,通过解引用来访问节点,也就是模仿指针的行为。这为算法库提供了统一的方式来访问容器,也降低了用户的学习成本,可以用相同的方式来访问不同的容器。

那么迭代器是如何做到模仿指针行为的?
这还要归功于运算符重载这一设计,在类中,我们可以通过运算符重载来决定一个类在面对不同运算符时的行为,那么我们是否可以将迭代器封装为一个类,然后利用运算符重载,使得迭代器可以使用++,–,*这样的操作符,从而模仿指针的行为呢?
STL的底层迭代器就是利用这样的方式实现的。

我们先来看到list的基本结构:

list节点:

template <class T>
struct list_node
{list_node<T>* _next;list_node<T>* _prev;T _data;
};

这是一个list的节点list_node,其有三个成员:_prev指向上一个节点,_next指向下一个节点,_data存储当前节点的数据。

template <class T>class list{typedef list_node<T> node;private:node* _head;};

这是一个list类,其将节点list_node<T>重命名为了node,方便后续使用。
唯一一个成员变量是哨兵位头节点指针_head

接下来我们就讨论要如何设计这个list的迭代器:
毫无疑问,我们需要把这个迭代器封装为一个类,而这个类的内部我们需要什么,才能让迭代器访问不同的节点呢?
想要访问不同的节点,毫无疑问就需要不同节点的指针,所以我们的迭代器需要把一个指向节点的指针给封装起来,然后利用运算符重载,改变这个指针的行为,从而实现迭代器

现在我们将这个迭代器命名为__list_iterator,先看看基础结构:

template <class T>
struct __list_iterator
{typedef list_node<T> node;node* _node;
};

为了方便我们使用节点的指针,我们重命名list_node<T>node,而迭代器内部指向节点的指针名为_node


构造函数:
代码如下:

__list_iterator(node* n): _node(n)
{}

这就是一个很简单的初始化过程,这里不过多赘述了,那么我们要如何使用这个迭代器呢?

看看list类中获取迭代器的函数:

typedef __list_iterator<T> iterator;iterator begin()
{return iterator(_head->_next);
}iterator end()
{return iterator(_head);
}

先将我们的迭代器重命名为iterator,方便后续使用。
可以看到,我们的begin()函数就是返回了哨兵位节点的后一个节点,也就是第一个节点的指针,但是普通的指针的++,–操作不符合我们的要求,于是我们利用iterator(_head->_next)这个方式,利用这个指针来构造一个匿名对象,也就是迭代器的匿名对象,此时我们就完成了指针的封装,得到的指针就是被我们修改了行为后的迭代器了。

end()函数同理,iterator(_head)构造一个迭代器,将指针进行封装,修改其行为。

接下来我们回到迭代器的实现:


operator++:
我们希望我们的迭代器可以在++的时候到达下一个节点的位置,也就是_node = _node->_next这样的行为。
所以我们的需求就明确了,当迭代器++的时候,实际发生的是_node = _node->_next而不是简单的指针偏移。

先看到我们的实现:

__list_iterator<T> & operator++()
{_node = _node->_next;return *this;
}

++是要进行返回的,将自增后的节点作为返回值,所以最后我们还要return *this;,将修改后的节点作为返回值。

后置++同理:

__list_iterator<T>& operator++(int)
{__list_iterator<T> tmp(*this);_node = _node->_next;return tmp;
}

由于后置++需要将自增前的值作为返回值,所以我们要先拷贝一份原先的值tmp,自增完成后,将原先的值tmp返回。

为了简化代码,我们可以将__list_iterator<T>重命名为self代表迭代器自己的类型。

最后代码如下:

typedef __list_iterator<T> self;self& operator++()
{_node = _node->_next;return *this;
}self& operator++(int)
{self tmp(*this);_node = _node->_next;return tmp;
}

这样一来,我们的迭代器++,表面上和指针自增一样,但是底层却是_node = _node->_next到达下一个节点的位置了。


operator–:
迭代器–同理,我们需要实现迭代器到达上一个节点,其实就是_node = _node->_prev
实现如下:

self& operator--()
{_node = _node->_prev;return *this;
}self& operator--(int)
{self tmp(*this);_node = _node->_prev;return tmp;
}

迭代器判断相等:
想要判断两个迭代器是否相等,其实就是判断其内部封装的指针_node是否相等。

代码如下:

bool operator!=(const self& s)
{return _node != s._node;
}bool operator==(const self& s)
{return _node == s._node;
}

这个比较简单,不做赘述了。


operator*:

*是迭代器中十分重要的操作符,指针就是利用解引用来访问指向内容的,迭代器就模仿指针,也通过解引用来访问节点值。
迭代器内部的成员_node是指向节点的指针,那么访问这个节点的值就是:_node->_data了。

所以解引用代码如下:

T& operator*()
{return _node->_data;
}

但是这个代码有一个问题,那就是:如果我们的listconst修饰了怎么办?
这样的话,由于我们返回的是T&,而这个_data就有可能会被修改,这就不符合const的行为了。

是否我们要写一个重载,比如这样:

T& operator*()
{return _node->_data;
}const T& operator*()
{return _node->_data;
}

这是不行的,因为两个函数只有返回值不同,并不构成重载。
我们只希望返回值的类型不同,这要怎么办?

注意:我们的迭代器是利用模板生成的,而模板最大的用处就是泛型编程,可以无视类型的限制,我们完全可以给模板多传一个参数,让第二个参数作为operator*的返回值

//-----迭代器-----
template <class T, class Ref>
struct __list_iterator
{typedef __list_iterator<T, Ref> self;Ref operator*(){return _node->_data;}
};

这样,当我们需要普通迭代器,第二个参数Ref就传入T&,如果需要const迭代器,那么第二个参数就传入const T&
不过要注意的是,刚刚我们重命名了一个self, 即typedef __list_iterator<T> self,此时由于模板参数改变,这个重命名也要一起改变:typedef __list_iterator<T, Ref> self

现在我们就可以在list中加入const迭代器了,接下来让类中定义的迭代器一起改变:

template <class T>
class list
{typedef __list_iterator<T, T&> iterator;typedef __list_iterator<T, const T&> const_iterator;//普通迭代器iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}//const迭代器const_iterator begin() const{return const_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}
};

看到两行typedef

typedef __list_iterator<T, T&> iterator;
typedef __list_iterator<T, const T&> const_iterator;

当我们传入的第二个模板参数为T&,那么解引用的返回值就是可以修改的,此时就是一般的迭代器iterator
当我们传入的第二个模板参数为const T&,那么解引用的返回值就是不可以修改的,此时就是const迭代器const_iterator

相比于开始,我们现在多了一个迭代器const_iterator;,我们的this指针被const修饰时,说明此时需要调用const迭代器,那么我们就返回const_iterator类型的迭代器。

const_iterator begin() const
{return const_iterator(_head->_next);
}const_iterator end() const
{return const_iterator(_head);
}

operator->:
接下面我们看到最后一个问题,那就是类的嵌套问题,假设我们的list内部是一个A类:

class A
{
public:int num;
};

那么如果我们想要通过迭代器访问A类的num成员,要怎么做?

假设我们有一个list的迭代器it,接下来我用it尝试访问这个A的成员num

(*it).num

先解引用迭代器(*it),此时就得到了list内部的A类,然后再利用.来访问内部的num成员。

这样访问是没有问题的,但是我们的迭代器是模仿指针的行为,我们会对一个指针先解引用,再用点操作符访问成员吗?
我们完全可以指针 -> 成员这样访问。

也就是说,我们还要对迭代器重载一个->操作符,实现这种直接访问的方式

代码:

T* operator->()
{return &_node->_data;
}

我们的返回值是_data的地址,而_data就是A类,所以我们可以就得到了一个A类的指针,此时T*就是其实就是A*
那么我们看看现在要如何访问:

list.operator->()->num

你也许不是很能理解以上代码,我们拆解一下:
list.operator->()返回了一个A*类型的指针,而A*类型的指针可以直接访问num

A* ptr;
ptr->num

所以以上代码list.operator->()->num就可以访问到num这个成员了。
.operator->()其实就是->操作符,所以可以变形为以下代码:
list->->num
连用->->不太美观,也容易让人费解,但是编译器会将两个->优化为一个->,所以此时我们已经可以直接像指针一样访问成员了:

list->num

我们再回顾一遍推导过程:

list.operator->()->num == list->->num == list->num

这个访问和刚刚的operator*会遇到同样的问题,那就是const问题,所以我们要传入第三个模板参数Ptr,来控制operator->的返回值:

template <class T, class Ref, class Ptr>
struct __list_iterator
{Ptr operator->(){return &_node->_data;}
};

list中:

typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;

至此,我们就实现了一个list的迭代器了。


代码展示:

template <class T, class Ref, class Ptr>
struct __list_iterator
{typedef list_node<T> node;typedef __list_iterator<T, Ref, Ptr> self;node* _node;__list_iterator(node* n): _node(n){}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}self& operator++(){_node = _node->_next;return *this;}self& operator++(int){self tmp(*this);_node = _node->_next;return tmp;}self& operator--(){_node = _node->_prev;return *this;}self& operator--(int){self tmp(*this);_node = _node->_prev;return tmp;}bool operator!=(const self& s){return _node != s._node;}bool operator==(const self& s){return _node == s._node;}
};

反向迭代器实现

那么我们要如何实现反向迭代器呢?

一开始我们实现正向迭代器的时候,遇到的问题就是:原生指针的行为不符合我们的要求,于是我们将原生指针进行了封装,利用运算符重载来修改其行为。

那么我们的反向迭代器还需要封装一个原生指针吗?
其实我们可以换一个思路:现在我们有正向迭代器,而正向迭代器并不符合反向迭代器的预期行为,我们不妨对正向迭代器进行一次封装,让其++变成–,–变成++,这不就是一个反向迭代器了吗?

结构如下:

template <class Iterator, class Ref, class Ptr>
struct ReverseIterator
{Iterator _cur;
};

我们定义了一个反向迭代器的类ReverseIterator,在内部封装了一个迭代器Iterator _cur,接下来我们就重载这个反向迭代器,让其行为符合预期:

先看看我们在list中是如何定义反向迭代器的:

	template <class T>class list{typedef ReverseIterator<iterator, T&, T*>  reverse_iterator;//反向迭代器reverse_iterator rbegin(){return reverse_iterator(end());}reverse_iterator rend(){return reverse_iterator(begin());}};

首先我们重命名了反向迭代器typedef ReverseIterator<iterator, T&, T*> reverse_iterator;,其第一个模板参数为iterator,也就是我们封装的正向迭代器。
随后在rbegin()中,我们直接将end()返回的末尾迭代器,作为我们反向迭代器的起始迭代器rbegin()
begin()返回的正向迭代器的起始迭代器,作为我们反向迭代器的末尾迭代器。

注意,end()返回的是最后一个元素的后面一个位置,所以我们在后续++,–操作时,要进行其他的操作,来矫正这个位置的偏差


operator++:
现在由于我们是反向迭代器,我们的反向迭代器++,其实就是正向迭代器的–。

代码如下:

self& operator++()
{--_cur;return *this;
}

operator*

由于我们的迭代器位置是往后偏一位的,所以我们要返回前一个位置的迭代器而不是当前位置的迭代器:

Ref operator*()
{Iterator tmp = _cur;--tmp;return *tmp;
}

我们用tmp拷贝了一份当前的迭代器,随后--tmp,将其往前走一个位置,再返回tmp位置的迭代器,这样就可以矫正我们起初的位置偏差了。


而其它的操作符重载,几乎没有太大的差别,基本就围绕以上两种类型的改动,此处我们直接展示代码了:

	template <class Iterator, class Ref, class Ptr>struct ReverseIterator{typedef ReverseIterator<Iterator, Ref, Ptr> self;Iterator _cur;ReverseIterator(Iterator it):_cur(it){}Ref operator*(){Iterator tmp = _cur;--tmp;return *tmp;}Ptr operator->(){Iterator tmp = _cur;--tmp;return &tmp->_data;}self& operator++(){--_cur;return *this;}self& operator++(int){self tmp(*this);--_cur;return tmp;}self& operator--(){++_cur;return *this;}self& operator--(int){self tmp(*this);++_cur;return tmp;}bool operator!=(const self& s){return _cur != s._cur;}bool operator==(const self& s){return _cur == s._cur;}};

这个反向迭代器的实现,可以作用于任何正向迭代器,也就是说,不论是listvector等等各种容器,都可以使用这一套反向迭代器来封装原本的迭代器,从而获得反向迭代器,


相关文章:

C++:迭代器的封装思想

C&#xff1a;迭代器的封装思想 list迭代器实现反向迭代器实现 本博客将通过实现list的迭代器&#xff0c;以及它的反向迭代器&#xff0c;来帮助大家理解迭代器的底层逻辑&#xff0c;以及封装思想。 list迭代器实现 迭代器是一个遍历容器的工具&#xff0c;其可以通过自增自…...

飞天使-k8s知识点17-kubernetes实操2-pod探针的使用

文章目录 探针的使用容器探针启动实验1-启动探针的使用-startupprobeLiveness Probes 和 Readiness Probes演示若存在started.html 则进行 探针的使用 kubectl edit deploy -n kube-system corednslivenessprobe 的使用 livenessProbe:failureThreshold: 5httpGet:path: /heal…...

tee漏洞学习-翻译-3:TrustZone exploit for MSM8974

原文&#xff1a;http://bits-please.blogspot.com/2015/08/full-trustzone-exploit-for-msm8974.html 在这篇博文中&#xff0c;我们将介绍利用上一篇文章中描述的 TrustZone 漏洞的完整过程。 在开发此漏洞时&#xff0c;我只使用了我值得信赖的&#xff08;个人&#xff0…...

rust递归遍历磁盘目录及文件

Std库实现 //遍历dir目录&#xff0c;找出修改日期距离当前超过age天的文件名称&#xff0c;存入file_list中 fn visit_dir(dir: &Path, file_list: &mut Vec<String>, age: u64) -> io::Result<()> {if dir.is_dir() {for entry in fs::read_dir(dir)…...

C语言每日一题(56)平衡二叉树

力扣网 110 平衡二叉树 题目描述 给定一个二叉树&#xff0c;判断它是否是高度平衡的二叉树。 本题中&#xff0c;一棵高度平衡二叉树定义为&#xff1a; 一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,…...

Flutter Android开发 梳理Google Material Design颜色体系

前言 做安卓开发&#xff08;Kotlin语言&#xff09;&#xff0c;Flutter开发的人员应该都听说过谷歌一直推崇的Material Design&#xff0c;而Material Design Color是其推崇的颜色体系&#xff0c;具体来说&#xff0c;Material Design Color是一套旨在帮助设计师和开发者创…...

每日五道java面试题之java基础篇(六)

目录&#xff1a; 第一题&#xff1a;Java 创建对象有哪⼏种⽅式&#xff1f;第二题 .Integer a 127&#xff0c;Integer b 127&#xff1b;Integer c 128&#xff0c;Integer d 128&#xff1b;相等吗?第三题.Object 类的常⻅⽅法?第四题 List和Set的区别第五题 ArrayList和…...

c++ STL系列——(五)map

目录 引言 特点 包含头文件 基本特性 基本操作 插入元素 访问元素 移除元素 检查是否包含某个键 获取元素数量 高级特性 迭代器 自定义比较函数 实际应用 统计字符出现次数 缓存最近访问的元素 总结 引言 在C中&#xff0c;标准模板库&#xff08;STL&#xf…...

Huggingface 文档翻译完毕

Accelerate 0.27 中文文档音频课程文档AutoTrain 中文文档AWS 中文文档竞赛中文文档Diffusers 0.26 中文文档深度强化学习课程文档数据集服务器中文文档Datasets 2.17 中文文档 Evaluate 0.4 中文文档Huggingface.js 中文文档Hub 中文文档Hub 客户端库 JS 0.20 中文文档推理 AP…...

C++中类的6个默认成员函数 【拷贝构造函数】

文章目录 拷贝构造函数的使用拷贝构造对于自定义类型【浅拷贝】深拷贝拷贝构造函数典型调用场景 拷贝构造函数的使用 在前几章学习对象的时候&#xff0c;我们有的时候需要一个与已存在对象一某一样的新对象 那在创建对象时&#xff0c;可否创建一个与已存在对象一某一样的新对…...

【前端高频面试题--Vuex下篇】

&#x1f680; 作者 &#xff1a;“码上有前” &#x1f680; 文章简介 &#xff1a;前端高频面试题 &#x1f680; 欢迎小伙伴们 点赞&#x1f44d;、收藏⭐、留言&#x1f4ac;前端高频面试题--Vuex篇 往期精彩内容Vuex 的原理Vuex中action和mutation的区别Vuex 和 localStor…...

MySQL性能调优篇(4)-查询语句的优化与重构

MySQL数据库查询语句的优化与重构 MySQL是一种常用的关系型数据库管理系统&#xff0c;广泛应用于Web开发中。在实际应用中&#xff0c;对数据库查询语句的优化和重构是提高应用性能和响应速度的重要手段。本文将介绍一些常见的优化技巧和重构方法&#xff0c;帮助开发者提高数…...

LInux、源码编译安装

步骤&#xff1a; 步骤1&#xff1a;安装开发工具gcc与make&#xff0c;释放源代码至指定目录 yum -y install gcc make 步骤2&#xff1a;tar解包&#xff0c;释放源代码至指定目录 tar -xf /root/tools.tar.gz -C /usr/local 步骤3&#xff1a;./configure 配置&#xff0c;…...

wordpress好的网站主题

有什么好的网站主题&#xff0c;都分享在这里了。 蓝色风格的wordpress模板&#xff0c;好的wordpress网站主题&#xff0c;需要既好看&#xff0c;又好用。 https://www.zhanyes.com/qiye/6305.html 血红色的好看的wordpress主题&#xff0c;布局经典&#xff0c;设计好的&am…...

【Java多线程】对进程与线程的理解

目录 1、进程/任务&#xff08;Process/Task&#xff09; 2、进程控制块抽象(PCB Process Control Block) 2.1、PCB重要属性 2.2、PCB中支持进程调度的一些属性 3、 内存分配 —— 内存管理&#xff08;Memory Manage&#xff09; 4、线程&#xff08;Thread&#xff09;…...

C# CAD交互界面-自定义面板集-查找定位(六)

运行环境 vs2022 c# cad2016 调试成功 一、代码说明 1. 类成员变量声明&#xff1a; List<ObjectId> objectIds new List<ObjectId>(); // 用于存储AutoCAD实体对象的ObjectId列表 private static Autodesk.AutoCAD.Windows.PaletteSet _ps2; // 自定义浮动面板…...

5.7 BCC工具之disksnoop.py解读

一,disksnoop.py简介 disksnoop工具用于追踪块设备的I/O操作的延迟,它会在每次I/O执行完成后打印一行摘要信息。我们根据这些摘要日志,来分析当前的I/O操作是否存在延迟,以判断I/O是否达到了瓶颈。 二,代码示例 #!/usr/bin/python # # disksnoop.py Trace block device…...

QT:实现图片选择器

一、效果图 二、用到的类 qApp&#xff1a;可以快速获取到项目目录位置。 QSettings &#xff1a;编写config文件&#xff0c;记录上次打开图片的位置&#xff0c;下次打开图片会从上次的位置查找图片。 QPixmap&#xff1a;用于图片的缩放&#xff0c;防止图片过小&#xff0…...

LLM大模型相关问题汇总---包括问题与答案

一、基础篇 1. 目前主流的开源模型体系有哪些&#xff1f; - Transformer体系&#xff1a;由Google提出的Transformer模型及其变体&#xff0c;如BERT、GPT等。 - PyTorch Lightning&#xff1a;一个基于PyTorch的轻量级深度学习框架&#xff0c;用于快速原型设计和实验…...

自动化测试定位不到元素怎么办?

1.动态id定位不到元素 分析原因&#xff1a;每次打开页面&#xff0c;ID都会变化。用ID去找元素&#xff0c;每次刷新页面ID都会发生变化。 解决方案&#xff1a;推荐使用xpath的相对路径方法或者cssSelector查找到该元素。 2.iframe原因定位不到元素 分析原因&#xff1a;…...

论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(二)

HoST框架核心实现方法详解 - 论文深度解读(第二部分) 《Learning Humanoid Standing-up Control across Diverse Postures》 系列文章: 论文深度解读 + 算法与代码分析(二) 作者机构: 上海AI Lab, 上海交通大学, 香港大学, 浙江大学, 香港中文大学 论文主题: 人形机器人…...

跨链模式:多链互操作架构与性能扩展方案

跨链模式&#xff1a;多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈&#xff1a;模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展&#xff08;H2Cross架构&#xff09;&#xff1a; 适配层&#xf…...

Qt Http Server模块功能及架构

Qt Http Server 是 Qt 6.0 中引入的一个新模块&#xff0c;它提供了一个轻量级的 HTTP 服务器实现&#xff0c;主要用于构建基于 HTTP 的应用程序和服务。 功能介绍&#xff1a; 主要功能 HTTP服务器功能&#xff1a; 支持 HTTP/1.1 协议 简单的请求/响应处理模型 支持 GET…...

Rust 异步编程

Rust 异步编程 引言 Rust 是一种系统编程语言,以其高性能、安全性以及零成本抽象而著称。在多核处理器成为主流的今天,异步编程成为了一种提高应用性能、优化资源利用的有效手段。本文将深入探讨 Rust 异步编程的核心概念、常用库以及最佳实践。 异步编程基础 什么是异步…...

【RockeMQ】第2节|RocketMQ快速实战以及核⼼概念详解(二)

升级Dledger高可用集群 一、主从架构的不足与Dledger的定位 主从架构缺陷 数据备份依赖Slave节点&#xff0c;但无自动故障转移能力&#xff0c;Master宕机后需人工切换&#xff0c;期间消息可能无法读取。Slave仅存储数据&#xff0c;无法主动升级为Master响应请求&#xff…...

VM虚拟机网络配置(ubuntu24桥接模式):配置静态IP

编辑-虚拟网络编辑器-更改设置 选择桥接模式&#xff0c;然后找到相应的网卡&#xff08;可以查看自己本机的网络连接&#xff09; windows连接的网络点击查看属性 编辑虚拟机设置更改网络配置&#xff0c;选择刚才配置的桥接模式 静态ip设置&#xff1a; 我用的ubuntu24桌…...

【Go语言基础【12】】指针:声明、取地址、解引用

文章目录 零、概述&#xff1a;指针 vs. 引用&#xff08;类比其他语言&#xff09;一、指针基础概念二、指针声明与初始化三、指针操作符1. &&#xff1a;取地址&#xff08;拿到内存地址&#xff09;2. *&#xff1a;解引用&#xff08;拿到值&#xff09; 四、空指针&am…...

MFC 抛体运动模拟:常见问题解决与界面美化

在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...

【堆垛策略】设计方法

堆垛策略的设计是积木堆叠系统的核心&#xff0c;直接影响堆叠的稳定性、效率和容错能力。以下是分层次的堆垛策略设计方法&#xff0c;涵盖基础规则、优化算法和容错机制&#xff1a; 1. 基础堆垛规则 (1) 物理稳定性优先 重心原则&#xff1a; 大尺寸/重量积木在下&#xf…...

Python网页自动化Selenium中文文档

1. 安装 1.1. 安装 Selenium Python bindings 提供了一个简单的API&#xff0c;让你使用Selenium WebDriver来编写功能/校验测试。 通过Selenium Python的API&#xff0c;你可以非常直观的使用Selenium WebDriver的所有功能。 Selenium Python bindings 使用非常简洁方便的A…...