2019 面试记录
# FunPlus (小视频业务)
# 数据库的事务隔离机制
# 隔离级别
- READ UNCOMMITTED(未提交读)
这个级别,事务中的修改,即使没有提交,对其他事务也都是可见的。事务可以读取未提交的数据,被称为脏读(Dirty Read),这个级别性能不会比其他级别好太多,但缺乏其他级别的很多好处,一般很少使用。
- READ COMMITTED(提交读)
这个级别是大多数数据库系统的默认隔离级别(但 MySQL 不是)。提交读是数据库隔离级别中的一种,它指的是一个事务在读取数据时,不会读取到其他并发事务对同一数据所做的任何修改,即只能读取到已经提交的数据。
在提交读隔离级别下,事务可以读取到其他事务已经提交的数据,但是不能读取到其他事务尚未提交的数据。这意味着一个事务在读取数据时,不会受到其他事务的影响,可以保证读取到的数据是一致的。
提交读隔离级别可以提供较高的并发性能,因为事务可以并发地读取已经提交的数据,而不需要等待其他事务的提交。然而,由于事务在读取数据时不会考虑其他事务的修改,可能会导致不可重复读(nonrepeatable read)(因为两次执行同样的查询,可能会得到不一样的结果。)的问题。
因此,在使用提交读隔离级别时,需要注意处理并发事务可能导致的数据一致性问题,例如通过锁机制或者乐观并发控制来解决。
- REPEATABLE READ(可重复读)
可重复读是数据库隔离级别中的一种,它指的是一个事务在执行期间,多次读取同一数据时,能够保证读取到的数据是一致的,即不会受到其他并发事务对同一数据所做的修改的影响。
在可重复读隔离级别下,事务在读取数据时会获取一个快照(snapshot)来保留事务开始时的数据状态,之后的读取操作都是基于这个快照进行的,而不会受到其他事务的修改的影响。这意味着一个事务在执行期间,无论其他事务如何修改数据,它读取到的数据都是一致的。
可重复读隔离级别可以提供较高的数据一致性,因为事务执行期间的读取操作都是基于一个快照,不会受到其他事务的干扰。但是,由于事务在执行期间可能会有其他事务对同一数据进行修改,可能会导致幻读(Phantom Read)(读取到其他事务插入的数据)的问题。
幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行(Phantom Row)。
因此,在使用可重复读隔离级别时,需要注意处理并发事务可能导致的幻读问题,例如通过锁机制或者多版本并发控制(MVCC)来解决。可重复读是 MySQL 的默认事务隔离级别。
- SERIALIZABLE(可串行化)
串行化是数据库隔离级别中最严格的一种级别。在串行化隔离级别下,事务串行执行,即每个事务在执行时都会对数据进行排他性锁定,确保同时只有一个事务能够对同一数据进行读取或写入操作。
在串行化隔离级别下,事务之间完全隔离,不会出现任何并发冲突。每个事务在执行期间,会对读取和写入的数据加上锁,其他事务无法同时对同一数据进行读取或写入操作,直到当前事务完成。
串行化隔离级别能够提供最高的数据一致性和完整性,因为事务之间完全隔离,不会出现任何并发冲突或数据不一致的情况。但是,由于事务串行执行,可能会导致较低的并发性能,因为每个事务需要等待其他事务释放锁才能执行。
因此,在使用串行化隔离级别时,需要权衡数据一致性和并发性能之间的关系,根据具体的业务需求来选择合适的隔离级别。
所有隔离级别
read uncommitted : 读取尚未提交的数据 :哪个问题都不能解决
read committed:读取已经提交的数据 :可以解决脏读 —- oracle 默认的
repeatable read:可重复读:可以解决脏读 和 不可重复读 —- mysql 默认的
serializable:串行化:可以解决 脏读 不可重复读 和 虚读—-相当于锁表
事务隔离级别 | 脏读 | 可重复读 | 幻读 |
---|---|---|---|
未提交读(read uncommited) | × | × | × |
提交读(read commited) | √ | × | × |
可重复读(repeatable read) | √ | √ | × |
串行化(serialziable) | √ | √ | √ |
注:×表示有该问题,√表示解决该问题。
脏读:事务 A 读取了事务 B 更新的数据,然后 B 进行回滚操作,那么 A 读取到的数据是脏数据;
不可重复读:事务 A 多次读取同一数据,事务 B 在事务 A 多次读取的过程中,对数据作了更新并提交,导致事务 A 多次读取同一数据时,结果不一致;
幻读:“当事务 A 要对数据表中某个字段的所有值进修改操作,此时有一个事务是插入一条记录并提交给数据库,当提交事务 A 的用户再次查看时就会发现有一行数据未被修改,其实是事务 B 刚刚添加进去的”,这就是幻读;
隔离级别越高,越能保证数据的完整性和统一性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为 Read Committed。它能够避免脏读,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
- MySQL 事务隔离机制&锁 (opens new window)
- MySQL 隔离级别 (opens new window)
- 数据库事务和四种隔离级别 (opens new window)
- RR(REPEATABLE-READ) 与 RC(READ-COMMITED) 隔离级别的异同 (opens new window)
# flask 组件及源码剖析
- Flask 自带的常用组件介绍 (opens new window)
- session
- flash,消息闪现
- jsonify,返回 json 化数据
- blueprint,构建大型应用条理化
- g,Flask 中的全局变量 g ,可以为特定请求临时存储任何需要的数据并且是线程安全的,当请求结束时,这个对象会被销毁,下一个新的请求到来时又会产生一个新的 g。
- abort,自定义错误
- current_app,应用上下文
- 一个 Flask 应用运行过程剖析 (opens new window)
- Flask 的请求处理流程和上下文 (opens new window)
- flask 源码解析 (opens new window)
- Flask 源码解析:Flask 应用执行流程及原理 (opens new window)
- Flask 面试题 (opens new window)
- Flask 源码解读 | 浅谈 Flask 基本工作流程 (opens new window)
# redis 中的数据类型,其中列表和有序集合有什么区别
Redis 中的列表和有序集合是两种不同的数据结构,具有以下区别:
有序性:列表(List)是一个有序的字符串列表,按照插入顺序进行排序。而有序集合(Sorted Set)是一个有序的字符串集合,每个成员都关联着一个分数,通过分数来进行排序。
元素的唯一性:列表中的元素可以重复,而有序集合中的元素必须是唯一的。
操作的复杂度:列表的插入、删除和查找操作的复杂度是 O(n),其中 n 是列表的长度。而有序集合的插入、删除和查找操作的复杂度是 O(log(n)),其中 n 是有序集合中的元素数量。当数据量特别大的时候,插入操作会特别耗时。
使用场景:列表适合用于实现队列、栈等数据结构,可以通过左端或右端进行插入和删除操作。有序集合适合用于实现排行榜、计数器等场景,可以根据分数进行排序和检索。
总的来说,列表适用于需要保持元素插入顺序的场景,而有序集合适用于需要根据分数进行排序和检索的场景。
# list 列表
List 内部数据结构是双向链表,可以在链表左、右两边分别操作,所以插入数据的速度很快。
也可以把 list 看成一种队列,所以在很多时候可以用 redis 用作消息队列,这个时候它的作用类似于 activeMq;
但是缺点就是在数据量比较大的时候,访问某个数据的时间可能会很长,但针对这种情况,可以使用 zset。
应用案例有时间轴数据,评论列表,消息传递等等,它可以提供简便的分页,读写操作。
# Set 集合
Set 就是一个集合,内部数据结构是整数集合(intset)、HASH 表,集合的概念就是一堆不重复值的组合。利用 Redis 提供的 Set 数据结构,可以存储一些集合性的数据。
比如在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。
因为 Redis 非常人性化的为集合提供了求交集、并集、差集等操作,那么就可以非常方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。
- 共同好友、二度好友
- 利用唯一性,可以统计访问网站的所有独立 IP
- 好友推荐的时候,根据 tag 求交集,大于某个阈值(threshold)就可以推荐
# Zset 集合(Sorted Sets)
Sorted Set 有点像 Set 和 Hash 的结合体。
和 Set 一样,它里面的元素是唯一的,但是 Set 里面的元素是无序的,而 Sorted Set 里面的元素都带有一个浮点值,叫做分数(score),内部数据结构跳跃表,所以这一点和 Hash 有点像,因为每个元素都映射到了一个值。 使它在 set 的基础上增加了一个顺序属性,这一属性在添加修改元素的时候可以指定,每次指定后,zset 会自动重新按新的值调整顺序。可以对指定键的值进行排序权重的设定,它应用排名模块比较多。
比如一个存储全班同学成绩的 Sorted Sets,其集合 value 可以是同学的学号,而 score 就可以是其考试得分,这样在数据插入集合的时候,就已经进行了天然的排序。另外还可以用 Sorted Sets 来做带权重的队列,比如普通消息的 score 为 1,重要消息的 score 为 2,然后工作线程可以选择按 score 的倒序来获取工作任务,让重要的任务优先执行。
zset 集合可以完成有序执行、按照优先级执行
的情况;
- redis 五种数据结构详解(string,list,set,zset,hash) (opens new window)
- Redis 实战 - list、set 和 Sorted Set (opens new window)
# 独到科技
# 进程、线程、协程及其区别
理解进程、线程和协程是掌握并发编程的关键。下面详细解释它们的核心概念和区别:
核心概念:
进程 (Process):
- 定义: 操作系统进行资源分配和调度的基本单位。可以理解为一个正在运行的程序实例。
- 关键特性:
- 独立性: 拥有独立的虚拟地址空间(内存空间)、代码、数据、堆栈、文件描述符、环境变量等系统资源。一个进程崩溃通常不会直接影响其他进程。
- 隔离性: 进程间通信(IPC)需要特殊机制(如管道、消息队列、共享内存、套接字等),因为内存空间不共享。
- 资源开销大: 创建、销毁、切换进程需要操作系统进行复杂的资源管理(分配/回收内存、文件句柄等),开销较大。
- 稳定性: 进程间的隔离提供了更好的安全性和稳定性。
- 类比: 一个独立的工厂,拥有自己的土地(内存空间)、厂房(代码)、原材料(数据)、工人(线程)、独立的供电供水系统(系统资源)。工厂之间需要通过特定渠道(IPC)沟通。
线程 (Thread):
- 定义: 操作系统进行CPU 调度、分派和执行的基本单位。是进程中的一个执行流。一个进程可以包含多个线程。
- 关键特性:
- 共享性: 同一进程内的所有线程共享该进程的内存地址空间和系统资源(如全局变量、堆内存、打开的文件等)。这使得线程间通信非常高效(直接读写共享内存即可),但也带来了同步问题(需要使用互斥锁、信号量等机制防止数据竞争)。
- 轻量级: 创建、销毁、切换线程的开销比进程小得多,因为不需要分配/回收大量新资源(主要是栈和寄存器)。
- 并发性: 线程是CPU调度的基本单位。多线程允许程序在单个进程中并发执行多个任务(在多核CPU上可能真正并行)。
- 依赖进程: 线程不能独立存在,必须依附于一个进程。如果进程退出,其所有线程也会被强制终止。
- 类比: 工厂(进程)里的工人(线程)。工人共享工厂的资源(车间、仓库、工具),可以同时在不同的流水线上工作(并发执行)。工人之间沟通方便(共享空间),但需要协调避免冲突(同步)。工厂倒闭(进程结束),所有工人失业(线程终止)。
协程 (Coroutine):
- 定义: 用户态的轻量级线程。是一种协作式的多任务处理机制,由程序员在用户空间显式控制调度,而非依赖操作系统内核调度器。
- 关键特性:
- 用户态调度: 协程的创建、销毁、切换完全在用户空间由程序(或协程库)控制,不涉及操作系统内核态切换。这使其开销极低(通常只是保存/恢复少量寄存器),可以轻松创建成千上万甚至百万级的协程。
- 协作式非抢占: 协程主动让出执行权(通过
yield
或await
等操作)给其他协程,而不是像线程那样被操作系统强制剥夺CPU时间片(抢占式)。一个协程如果不主动让出,会一直占用其所在的线程。 - 栈内存小/可定制: 协程栈通常非常小(如几KB)且可以动态增长,或者使用共享栈,内存占用远小于线程(MB级)。
- 依赖线程: 协程运行在线程之上。一个线程可以运行多个协程(由用户态调度器调度)。协程阻塞会导致其所在的线程阻塞,进而阻塞该线程上的所有其他协程(因此协程中应避免阻塞式IO,改用异步IO)。
- 高效IO: 协程特别适合处理高并发IO密集型任务(如网络服务器),可以在等待IO时高效地切换执行其他协程,极大提高单线程的吞吐量。
- 类比: 工厂(进程)里某个工人(线程)掌握的多个精细技能(协程)。工人可以在组装一个零件(协程A)时,遇到需要等待油漆干燥的情况(IO阻塞),他主动放下这个零件,转而去打磨另一个零件(协程B)。切换做什么完全由工人自己决定(用户调度),不需要车间主任(OS调度器)来安排。工人本身(线程)并没有换人。
三者之间的核心区别总结:
特性 | 进程 (Process) | 线程 (Thread) | 协程 (Coroutine) |
---|---|---|---|
定义 | 资源分配的基本单位 | CPU调度的基本单位 | 用户态轻量级线程 / 协作式任务 |
拥有者 | 操作系统 | 进程 | 线程 / 用户程序 |
资源 | 独立地址空间和系统资源 | 共享进程的内存和资源 | 共享线程的资源 |
创建开销 | 大 (涉及资源分配) | 中 (主要分配栈/寄存器) | 极小 (用户态切换) |
切换开销 | 大 (涉及内核切换、TLB刷新) | 中 (涉及内核切换) | 极小 (用户态切换寄存器) |
通信方式 | IPC (管道、共享内存、Socket等) | 共享内存 (需同步) | 共享内存 (通常无需复杂同步) |
并发性 | 进程间并发 | 线程间并发 (可多核并行) | 协程间协作式并发 (单线程内) |
调度者 | 操作系统内核 (抢占式) | 操作系统内核 (抢占式) | 用户程序自身 (协作式) |
独立性 | 高 (崩溃不影响其他进程) | 低 (线程崩溃可能拖垮进程) | 低 (依赖所在线程) |
内存占用 | 大 (独立地址空间) | 中 (主要是栈, MB级) | 极小 (栈通常KB级) |
主要优势 | 隔离性、稳定性、安全性 | 共享内存通信高效、利用多核 | 极高并发 (IO密集型)、极低开销 |
主要场景 | 隔离应用、系统服务 | 计算密集型、利用多核 | 高并发IO密集型 (网络、爬虫等) |
进阶理解:
- 协程与事件循环: 在单线程模型中(如Node.js, Python asyncio),协程通常与事件循环配合使用。事件循环监听IO事件,当某个协程发起的IO操作完成时,事件循环通知调度器恢复该协程的执行。
- M:N模型: 一些协程库(如Go的goroutine)实现了M:N模型,即M个协程映射到N个操作系统线程上执行。用户态调度器负责在协程间切换,操作系统调度器负责在线程间切换。这结合了协程的轻量和线程的多核并行能力。
- 协程阻塞问题: 协程中如果进行阻塞式系统调用(如
sleep
, 同步读写磁盘/网络),会导致其所在的整个线程被操作系统挂起,该线程上的所有其他协程都无法执行。因此,协程编程必须使用非阻塞IO或异步IO,并在等待IO时主动让出控制权。
总结:
- 进程是资源分配的堡垒,提供最强的隔离和安全,但开销最大。
- 线程是CPU调度的主力,共享内存通信高效,能利用多核,但创建切换仍有开销,同步复杂。
- 协程是用户态调度的轻骑兵,开销极小,能实现极高的并发(尤其是IO密集型),但需要协作式编程和避免阻塞操作。
选择哪种模型取决于应用的具体需求:需要强隔离用进程;需要利用多核计算或适中的并发用线程;需要超高并发处理大量IO连接(如Web服务器)则协程是首选。现代高并发系统(如Go, Node.js, Python asyncio)大量依赖协程(或类似概念如goroutine)来提升性能。
进程和线程、协程的区别 (opens new window)
# 二叉树的深度优先算法和广度优先算法
二叉树的深度优先遍历的非递归的通用做法是采用栈,广度优先遍历的非递归的通用做法是采用队列。 1:树的深度优先遍历主要分为:前序遍历、中序遍历以及后序遍历
- 前序遍历:若二叉树为空则结束,否则依次先访问根节点,然后访问左子树,最后访问右子树。
- 中序遍历:若二叉树为空则结束,否则先访问根节点的左子树,然后访问根节点,最后访问右子数。
- 后序遍历:若二叉树为空则结束,否则先访问根节点的左子树,然后访问右子数,最后访问根节点。 深度优先一般采用递归的方式实现,递归的深度为树的高度。
2:树的广度优先算法:广度优先是按照层次来遍历树的节点,先是根节点,然后依次遍历第二层子节点,当第二层子节点遍历完后,在依次遍历第三层子节点。广度优先采用队列来记录当前可遍历的节点,当遍历某个节点时,将其左孩子和右孩子结点依次入队,待该层遍历完了以后,再依次遍历下一层儿子结点。
3:非递归实现特点: 深度优先一般采用递归实现,如改用非递归,则可需要来模拟栈,当需要先遍历当前节点的儿子结点时(例如中序遍历)需要将其压入栈中,先遍历其儿子结点,然后再将其弹出栈,遍历当前节点。广度优先一般采用非递归来实现,用一个队列来保存依次需要遍历的节点。
- 二叉树深度优先遍历(DFS)和广度优先遍历(BFS) (opens new window)
- 简述树的深度优先算法、广度优先算法,及非递归实现的特点 (opens new window)
- 广度优先搜索(BFS)和深度优先搜索(DFS) (opens new window)
https://nullcc.github.io/2018/06/07/广度优先搜索(BFS)和深度优先搜索(DFS)/ (opens new window)
# Python 垃圾回收机制
引用计数
标记-清除机制
分代技术
# Python 传值还是传引用
python 参数传递采用的是“传对象引用”的方式。这种方式相当于传值和传引用的一种综合。如果函数收到的是一个不可变对象(数字、字符或元组)的引用,就不能直接修改原始对象——相当于通过‘值传递’来传递对象。如果函数收到的是一个可变对象(字典、列表)的引用,就能修改对象的原始值——相当于‘传引用’来传递对象。
Python 传值还是传引用?| 通过对象引用传递 (opens new window)
# 中天联科
# 类属性和实例属性的区别,如何判断类 A 是否有属性 x
class A:
X = 'Hello' # 类属性
def __init__(self,name='World'):
self.name = name # 实例属性
a = A('foo')
print(A.X) # Hello
print(a.name) #foo
print(hasattr(a,'X')) # True
print(hasattr(A,'X')) # True
print(hasattr(A,'name')) # False
print(hasattr(a,'name')) # True
setattr(a,'name','bar')
setattr(A,'X','Bye')
print(a.name) # bar
print(A.X) # Bye
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 什么是列表推导式?如何用一行代码判断一个文件夹下面文件数大于 10 个的子目录
import os
# 不包括嵌套层
subdirs = [subdir for subdir in os.listdir('path_to_folder') if os.path.isdir(os.path.join('path_to_folder', subdir)) and len(os.listdir(os.path.join('path_to_folder', subdir))) > 10]
# 包括嵌套层
subdirs = [dirpath for dirpath, _, filenames in os.walk('D:\codes') if
len(filenames) > 10]
2
3
4
5
6
7
import os
def find_file_more_than_10(root_dir_fp=None, limit=10):
if root_dir_fp is None:
root_dir_fp = os.path.split((os.path.abspath(__file__)))[0]
return [dirpath for dirpath, _, filenames in os.walk(root_dir_fp) if
len(filenames) > limit]
if __name__ == '__main__':
print(find_file_more_than_10())
2
3
4
5
6
7
8
9
10
11
12
# 什么是协程?什么是生成器
- 生成器
yield
和生成器表达式(i for i in range(10))
。
# 金山云
# 单例模式,如何实现?以及如何判断只有这一个实例
class Singleton:
_instance = {}
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super().__new__(cls, *args, **kwargs)
return cls._instance
# 装饰器实现法
def deco_singleton(cls):
_instance = {}
def wrapper(*args, **kwargs):
if not cls in _instance:
_instance[cls] = cls(*args, **kwargs)
return _instance[cls]
return wrapper
@deco_singleton
class A:
pass
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 死锁产生的原因
# ping 域名的过程
ping 某域名的过程详解 (opens new window)
# select 和 epoll 区别
- Python 全栈之路系列之 IO 多路复用 (opens new window)
- select、poll、epoll 之间的区别总结[整理] (opens new window)
- Python 异步非阻塞 IO 多路复用 Select/Poll/Epoll 使用 (opens new window)
- Python 使用 select 和 epoll 实现 IO 多路复用实现并发服务器 (opens new window)
- How To Use Linux epoll with Python (opens new window)
# 艾普艾
# Redis的数据类型都有哪些,如果要实现计数器功能,应该选用哪种数据类型?使用 Redis,如果内存满了会怎么样
- 数据类型 string,list,set,zset,hash
- 计数器/限流器功能
- 可以选用
string
类型,调用incr()
方法,参见INCR (opens new window) 每次自增加 1
redis> SET mykey "10"
"OK"
redis> INCR mykey
(integer) 11
redis> GET mykey
"11"
2
3
4
5
6
- 可以选用
hash
类型,调用hincrby()
方法,参见HINCRBY (opens new window) 对关联的统计项进行统一管理;
redis> HSET myhash field 5
(integer) 1
redis> HINCRBY myhash field 1
(integer) 6
redis> HINCRBY myhash field -1
(integer) 5
redis> HINCRBY myhash field -10
(integer) -5
2
3
4
5
6
7
8
- 可以选用
set
类型,调用sadd()
方法,参见SADD (opens new window) 多次调用只加一,防作弊刷数据等;
redis> SADD myset "Hello"
(integer) 1
redis> SADD myset "World"
(integer) 1
redis> SADD myset "World"
(integer) 0
redis> SMEMBERS myset
1) "Hello"
2) "World"
2
3
4
5
6
7
8
9
更多 Python 实例应用:Redis 多方式实现计数器功能(附代码) (opens new window)
# 内存满了
此时不能继续写入数据,而且系统的其他操作任务也会受到影响。为防止这种现象发生,应该启用内存淘汰策略。
# 更多
10 个常见的 Redis 面试"刁难"问题 (opens new window)
# 常见状态码错误?301、302 错误及区别?502 错误出现时应该怎么解决
- 301/302 跳转,301 redirect: 301 代表永久性转移(Permanently Moved);302 redirect: 302 代表暂时性转移(Temporarily Moved )
参见http 状态码 301 和 302 详解及区别——辛酸的探索之路 (opens new window)
- 502 Bad Gateway Error
对用户访问请求的响应超时错误
- DNS 测试,ping 测试
- 检查防火墙端口,检查防火墙日志
- 数据库调用延迟
- 网络服务进程是否正常
# 参考
# 华胜天成
# 类属性的继承
class Parent:
x = 10
class Child1(Parent):
pass
class Child2(Parent):
pass
a = Parent()
b = Child1()
c = Child2()
print(a.x,b.x,c.x) # (10, 10, 10)
a.x = 20
print(a.x,b.x,c.x) # (20, 10, 10)
b.x = 30
print(a.x,b.x,c.x) # (20, 30, 10)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A:
x = 'a'
class B:
x = 'b'
class C(A,B):
pass
class D(B,A):
pass
print(A.x,B.x,C.x,D.x) # ('a', 'b', 'a', 'b')
A.x = 'a1'
print(A.x,B.x,C.x,D.x) # ('a1', 'b', 'a1', 'b')
B.x = 'b1'
print(A.x,B.x,C.x,D.x) # ('a1', 'b1', 'a1', 'b1')
C.x = 'c'
print(A.x,B.x,C.x,D.x) # ('a1', 'b1', 'c', 'b1')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在多继承中,当一个类有多个父类时,它会按照从左到右的顺序搜索属性和方法。优先找到谁就继承谁的属性。