面试准备 - 操作系统 / Python

数据结构与算法

常见排序

在这里插入图片描述

B树,B+树,红黑树

B树和平衡二叉树稍有不同的是B树属于多叉树又名平衡多路查找树(查找路径不只两个),不属于二叉搜索树的范畴,因为它不止两路,存在多路。

B+树是B树的一个升级版,B+树是B树的变种树,有n棵子树的节点中含有n个关键字,每个关键字不保存数据,只用来索引,数据都保存在叶子节点。是为文件系统而生的。

红黑树是一种二叉查找树,但在每个结点上增加了一个存储位表示结点的颜色,可以是RED或者BLACK。通过对任何一条从根到叶子的路径上各个着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。当二叉查找树的高度较低时,这些操作执行的比较快,但是当树的高度较高时,这些操作的性能可能不比用链表好。红黑树(red-black tree)是一种平衡的二叉查找树,它能保证在最坏情况下,基本的动态操作集合运行时间为O(lgn)。

  • 红黑树必须要满足的五条性质:
    • 性质一:节点是红色或者是黑色; 在树里面的节点不是红色的就是黑色的,没有其他颜色,要不怎么叫红黑树呢,是吧。
    • 性质二:根节点是黑色; 根节点总是黑色的。它不能为红。
    • 性质三:每个叶节点(NIL或空节点)是黑色;
    • 性质四:每个红色节点的两个子节点都是黑色的(也就是说不存在两个连续的红色节点); 就是连续的两个节点不能是连续的红色,连续的两个节点的意思就是父节点与子节点不能是连续的红色。
    • 性质五:从任一节点到其每个叶节点的所有路径都包含相同数目的黑色节点。从根节点到每一个NIL节点的路径中,都包含了相同数量的黑色节点。
    • 红黑树的应用场景:红黑树是一种不是非常严格的平衡二叉树,没有AVLtree那么严格的平衡要求,所以它的平均查找,增添删除效率都还不错。广泛用在C++的STL中。如map和set都是用红黑树实现的。
    • STL中set、multiset、map、multimap底层是红黑树实现的,而unordered_map、unordered_set 底层是哈希表实现的。

  • 需要在某个容器中频繁查找以及替换最大值时,使用堆或者红黑树等树结构
  • 对于数据流中位数,可以使用一个最大堆存储较小的一部分,最小堆较大的一部分,进行求解

操作系统

进程和线程以及它们的区别。

  • 进程是具有一定功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源调度和分配的一个独立单位。
    • 组成:程序、数据、程序控制块PCB
    • PCB:标志信息、处理机状态、处理机控制信息、优先级
  • 线程是进程的实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位,不拥有系统资源。
    • 组成:线程ID、程序计数器、寄存器集合、堆栈
  • 一个进程可以有多个线程,多个线程也可以并发执行
    • 详细方面:
      • 调度:线程是独立调度基本单位,进程是拥有资源的基本单位。线程切换/进程切换。
      • 拥有资源:进程是拥有资源的基本单位,线程不拥有但是可以访问隶属进程的系统资源。
      • 并发性:都可以并发。
      • 系统开销:创建和撤销进程时都需要分配回收资源。线程只需要设置保存和设置少量寄存器内容,并且同步和通信不需过多设置(共享资源的原因)。
      • 空间地址和其他资源(比如文件):进程独立,线程共享同一进程内资源。
      • 通信方面:进程需要同步互斥辅助保证数据一致性,线程可以直接读写数据段(如全局变量)。

协程

“子程序就是协程的一种特例。”

  • 协程,又称微线程,纤程。英文名Coroutine。
  • 子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。
  • 所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。
  • 子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。
  • 协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
  • 注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。
def A():
    print '1'
    print '2'
    print '3'

def B():
    print 'x'
    print 'y'
    print 'z'
  • 假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:
1
2
x
y
3
z
  • 但是在A中是没有调用B的,所以协程的调用比函数调用理解起来要难一些。
  • 看起来A、B的执行有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何优势?
    • 最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
    • 第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
  • 因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
  • Python对协程的支持还非常有限,用在generator中的yield可以一定程度上实现协程。虽然支持不完全,但已经可以发挥相当大的威力了。
  • 来看例子:
    • 传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
    • 如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:

线程同步的方式有哪些?

  • 互斥量:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
  • 信号量(PV操作):它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
  • 事件(信号):通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
  • 同步相关:
    • 互斥:对临界资源的控制(一次仅允许一个进程使用):进入区、临界区、退出区、互斥区
    • 前驱后继关系:也可以使用PV操作进行

说一说进程同步有哪几种机制。

  • 信号量机制PV操作(生产者消费者/哲学家进餐)
  • 自旋锁:循环确认是否占用
  • 管程:它是抽象的软件模块:我们可以抽象描述系统中各种软硬件资源,即可以用少量信息和操作来表征该资源,而忽略细节实现。管程就是定义在这种数据结构之上的软件模块,能改变管程中的数据以及同步进程。
  • 会合
  • 分布式系统

常见锁

  • 互斥锁
  • 多重入锁(允许一个进程/线程多次拿到锁)
  • 自旋锁 (CPU不断检查锁是否可用)
  • 事件
  • 条件
  • 信号量 (一次允许多个线程操作锁对象)
  • 读写锁 (一次只有一个写者,多个读者

进程的通信方式

  • 为什么需要进程通信?
    进程通信就是指进程之间的信息交换。进程是分配系统资源的单位,因此各进程拥有的内存地址空间相互独立。但进程之间的信息交换是必须实现的。

  • 进程通信的三种方式:

    • 共享存储
    • 消息传递
    • 管道通信
  • 共享存储:

    • 操作系统负责提供共享空间和同步互斥工具,进程对共享空间的访问必须是互斥的。
    • 基于数据结构的共享:比如共享空间 只能放一个长度为10的数组。这种共享方式速度慢,限制多,是一种低级通信方式
    • 基于存储区的共享:在内存中画出一块共享存储区,数据的形式、存放位置都由进程控制,而不是操作系统。相比之下,这种共享方式速度更快。是一种高级通信方式。
  • 管道通信

    • “管道”是用于连接读写进程的一个共享文件,其实就是在内存中开辟一个大小固定的缓冲区
    • 管道是半双工通信,只能实现单向传输
    • 当管道写满时,写进程将被阻塞,等待读进程将数据读走。如果管道为空,读进程将被阻塞,等待写进程写入数据
    • 如果没写满,就不允许读。如果没读空,就不允许写
  • 消息队列

    • 进程间的数据交换以格式化的消息(message)为单位。进程通过操作系统提供的“发送消息/接收消息”两个原语进行数据交换。
  • 信号

    • 信号的安装:进程要处理某一信号,要先注册确定信号值及进程针对该信号值的动作之间的映射关系信号的注册:内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位。
    • 信号的执行:当其由于被信号唤醒或者正常调度重新获得CPU时,在其从内核空间返回到用户空间时会检测是否有信号等待处理。
  • 信号量使用struct semaphore 实现,包括count是共享计数值、sleepers是等待当前信号量进程个数、wait是当前信号量的等待队列。内核保证正在访问临界区的进程数小于或等于初始化的共享计数值,获取信号量失败的进程将进入不可中断的睡眠状态,在信号量的等待队列中进行等待。当进程释放信号量的时候就会唤醒等待队列中的第一个进程。

  • 套接字(socket)

  • 消息传递的方式

    • 直接通信方式:消息直接挂到接收进程的消息缓冲队列上
    • 间接通信方式:消息先发送到中间实体中,因此也称“信箱通信方式”。就像邮件系统。

线程通信方式

每个进程有自己的地址空间,所以进程间的通信是通过操作系统进行;多线程共享地址空间和数据空间,所以多线程通信可以直接多线程使用,而不必通过操作系统。
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。

  • 锁机制:包括互斥锁、条件变量、读写锁
  • 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
  • 信号机制(Signal):类似进程间的信号处理

进程的三级调度

  • 作业调度-高级调度:内存与辅存之间调度,建立进程
  • 中级调度-内存调度:提高内存利用率和系统吞吐量,不运行的调度至外存挂起,满足条件的就绪进程调入内存
  • 进程调度-低级调度:处理机分配

进程调度算法有哪几种?

  • FCFS(先来先服务)
  • 短作业优先SJF
  • 优先级:抢占式/非抢占式,静态/动态优先级
  • 高响应比优先
  • 时间片轮转
  • 多级反馈(优先级影响时间片大小、)

进程有哪几种状态?

  • 就绪状态:进程已获得除处理机以外的所需资源,等待分配处理机资源
  • 运行状态:占用处理机资源运行,处于此状态的进程数小于等于CPU数
  • 阻塞状态: 进程等待某种条件,在条件满足之前无法执行

什么是死锁?死锁产生的条件?

  • 在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的讲就是两个或多个进程无限期的阻塞、相互等待的一种状态。
  • 死锁产生的四个条件(有一个条件不成立,则不会产生死锁)
    • 互斥条件:一个资源一次只能被一个进程使用
    • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得资源保持不放
    • 不剥夺条件:进程获得的资源,在未完全使用完之前,不能强行剥夺
    • 循环等待条件:若干进程之间形成一种头尾相接的环形等待资源关系

死锁的处理基本策略和常用方法。

  • 解决死锁的基本方法如下:

    • 预防死锁、避免死锁、检测死锁、解除死锁
  • 解决四多的常用策略如下:

    • 鸵鸟策略:出现问题手动解决
    • 预防策略:四大条件无效化:互斥、请求保持、不剥夺、循环等待
    • 避免策略:银行家算法(Available,Max,Allocation,Need矩阵)、系统安全状态
    • 检测与解除死锁:
      • 检测死锁:资源分配图
      • 解除死锁:资源剥夺法、撤销进程法、进程回退法

程序链接和装入

  • 静态链接:将目标模块和库函数链接成完整应用程序,以后不再拆开
  • 动态链接:将目标源程序编译后得到目标模块,装入内存时边装入边链接
  • 运行时动态链接:某些目标模块,运行时才进行链接,便于修改和更新,便于实现目标模块共享
  • 绝对装入:逻辑地址和物理地址完全相同
  • 可重定位装入:相对于起始地址
  • 动态重定位:地址转换推迟到程序真正要执行,需要重定位寄存器支持

动态分区分配算法

  • 首次适应:地址递增
  • 最佳适应:容量递增分区链,最多碎片
  • 最坏适应(最大适应):容量递减分区链
  • 临近适应:循环首次适应

非连续分配:分页和分段

  • 段是信息的逻辑单位,它是根据用户的需要划分的,因此段对用户是可见的 ;页是信息的物理单位,是为了管理主存的方便而划分的,对用户是透明的。
  • 段的大小不固定,有它所完成的功能决定;页大大小固定,由系统决定
  • 段向用户提供二维地址空间;页向用户提供的是一维地址空间
  • 段是信息的逻辑单位,便于存储保护和信息的共享,页的保护和共享受到限制。

虚拟内存思想:局部性原理

  • 时间局部性:指令可能再次执行
  • 空间局部性:临近空间将被访问
  • 多道程序需要很多内存,物理内存有限,在逻辑上扩充内存

虚拟内存实现

  • 建立在离散分配的内存管理方式基础上
  • 实现方式:
    • 请求分页存储管理
    • 请求分段存储管理
    • 请求段页式存储管理
  • 硬件支持:内存外存、页/段表机制、中断机构、地址变换机构

虚拟内存页面置换算法

  • 最佳置换OPT:被淘汰页面是以后永不使用的页面,无法实现
  • 先进先出FIFO:Belady异常
  • 最近最久未使用置换LRU
  • 时钟置换CLOCK/最近未使用NRU:每一帧附加位
  • 抖动:刚被换出的页面立即被换入内存

什么是缓冲区溢出?有什么危害?其原因是什么?

  • 缓冲区溢出是指当计算机向缓冲区填充数据时超出了缓冲区本身的容量,溢出的数据覆盖在合法数据上。
  • 危害有以下两点:
    • 程序崩溃,导致拒绝额服务
    • 跳转并且执行一段恶意代码
  • 造成缓冲区溢出的主要原因是程序中没有仔细检查用户输入。
    • 数组越界检查机制

I/O控制方式

  • 程序直接控制方式
  • 中断驱动方式
  • DMA方式
  • 通道控制方式

SPOOLing技术(假脱机技术)

  • 利用专门的外围控制机,将低速IO设备的数据传到高速磁盘上
  • 将独占设备改造成共享设备
  • 输入输出井

Python特性 / 语言基础

编译型语言和解释型语言 / Python如何实现跨平台

  • 编译型语言的编译器,一般完整包含前端、优化器和后端。前端的主要工作就是将输入的代码,经过词法分析器和语法分析器,生成ast,然后转化成ir,经过优化器优化后,交给编译器后端生成目标机器的机器码(与cpu指令集架构相关)。简单的来说,编译器的主要工作,就是将源码编译成机器码,然后将这些能在目标机器上执行的二进制代码文件,存入磁盘。需要用到的时候,再启动进程,加载代码再执行。
  • 解释器则不一样,我们启动解释器以后,可以直接加载代码,在内部编译后直接执行,中间不会生成任何文件,比如lua。而现代解释器,一般将编译器和虚拟机整合在一起了。也就是首先它用较低级的语言(比如c语言)写了虚拟机,虚拟机一般会定义自己的虚拟机指令,解释器内部的编译器,一般只包含前端,也就是包含词法分析器和语法分析器,将源码编译成虚拟机指令。这样就可以在虚拟机里运行了,因为对应的指令,都在虚拟机里,用较低的语言实现了逻辑。
  • 由于Python是动态编译的语言,和C/C++、Java或者Kotlin等静态语言不同,它是在运行时一句一句代码地边编译边执行的,而Java是提前将高级语言编译成了JVM字节码,运行时直接通过JVM和机器打交道,所以进行密集计算时运行速度远高于动态编译语言。
  • Python的不同实现的过程是不一样的,拿CPython举例,他是有编译流程的,CPython会将Python代码先编译为字节码,然后由其运行时解释执行。所以Python的准确表述为“动态语言”,当然说其为解释性语言也可,因为针对标准实现CPython来讲,他是解释执行的,但不能一定确定Python没有进行编译。

在这里插入图片描述

内存管理与GC

  • Python有一个私有堆空间来保存所有的对象和数据结构。作为开发者,我们无法访问它,是解释器在管理它。但是有了核心API后,我们可以访问一些工具。Python内存管理器控制内存分配。另外,内置垃圾回收器会回收使用所有的未使用内存,所以使其适用于堆空间。
  • 内存管理机制:引用计数、垃圾回收、内存池。
  • 引用计数:引用计数是一种非常高效的内存管理手段, 当一个 Python 对象被引用时其引用计数增加1, 当其不再被一个变量引用时则计数减 1. 当引用计数等于0时对象被删除。
  • GC:
    • 引用计数引用计数也是一种垃圾收集机制,而且也是一种最直观,最简单的垃圾收集技术。当 Python 的某个对象的引用计数降为 0 时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。比如某个新建对象,它被分配给某个引用,对象的引用计数变为 1。如果引用被删除,对象的引用计数为 0,那么该对象就可以被垃圾回收。不过如果出现循环引用的话,引用计数机制就不再起有效的作用了
    • 标记清除如果两个对象的引用计数都为 1,但是仅仅存在他们之间的循环引用,那么这两个对象都是需要被回收的,也就是说,它们的引用计数虽然表现为非 0,但实际上有效的引用计数为 0。所以先将循环引用摘掉,就会得出这两个对象的有效计数。
    • 分代回收从前面“标记-清除”这样的垃圾收集机制来看,这种垃圾收集机制所带来的额外操作实际上与系统中总的内存块的数量是相关的,当需要回收的内存块越多时,垃圾检测带来的额外操作就越多,而垃圾回收带来的额外操作就越少;反之,当需回收的内存块越少时,垃圾检测就将比垃圾回收带来更少的额外操作。
  • 内存池:
    • Python 的内存机制呈现金字塔形状,-1,-2 层主要由操作系统进行操作;
    • 第 0 层是 C 中的 malloc,free 等内存分配和释放函数进行操作;
    • 第1 层和第 2 层是内存池,有 Python 的接口函数 PyMem_Malloc 函数实现,当对象小于 256K 时有该层直接分配内存;
    • 第3层是最上层,也就是我们对 Python 对象的直接操作;Python 在运行期间会大量地执行 malloc 和 free 的操作,频繁地在用户态和核心态之间进行切换,这将严重影响 Python 的执行效率。为了加速Python 的执行效率,Python 引入了一个内存池机制,用于管理对小块内存的申请和释放。
    • Python 内部默认的小块内存与大块内存的分界点定在 256 个字节,当申请的内存小于 256 字节时,PyObject_Malloc会在内存池中申请内存;当申请的内存大于 256 字节时,PyObject_Malloc 的行为将蜕化为 malloc 的行为。当然,通过修改 Python 源代码,我们可以改变这个默认值,从而改变 Python 的默认内存管理行为。
    • 调优手段:
      • 1.手动垃圾回收
      • 2.调高垃圾回收阈值
      • 3.避免循环引用(手动解循环引用和使用弱引用)

*args,**kwargs

  • 当函数的参数前面有一个星号*号的时候表示这是一个可变的位置参数,两个星号表示这个是一个可变的关键词参数。一个星号把序列或者集合解包(unpack)成位置参数,两个星号把字典解包成关键词参数。
  • 可迭代对象前放一个星号能解包该对象内的所有参数
  • 两个星号能解包字典,将关键字与函数参数进行匹配

装饰器

  • 装饰器本质上是一个高级Python函数,它可以让其它函数在不作任何变动的情况下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景。比如:插入日志、性能测试、事务处理、缓存、权限校验等。有了装饰器我们就可以抽离出大量的与函数功能无关的雷同代码进行重用。

多线程

  • 在Python多线程下,每个线程的执行方式:
    1.获取GIL
    2.执行代码直到sleep或者是python虚拟机将其挂起。
    3.释放GIL
  • 经过GIL的处理,会增加执行的开销。这就意味着如果你先要提高代码执行效率,使用threading不是一个明智的选择,当然如果你的代码是IO密集型,多线程可以明显提高效率,相反如果你的代码是CPU密集型的这种情况下多线程大部分是鸡肋。
  • 在我的爬虫项目中代码显然是是IO密集型的(每一个网页对应一个爬取模型的对象),同时使用threading模块的lock保证插入顺序的正常。
  • ulimit -s 返回线程栈大小,默认是8192, 用内存大小除以线程栈大小就得到理论上的线程数

_thread.stack_size([size])

  • 返回新建线程时使用的堆栈大小。可选参数 size 指定之后新建的线程的堆栈大小,而且一定要是0(根据平台或者默认配置)或者最小是32,768(32KiB)的一个正整数。如果size没有指定,默认是0。如果不支持改变线程堆栈大小,会抛出 RuntimeError 错误。如果指定的堆栈大小不合法,会抛出 ValueError 错误并且不会修改堆栈大小。32KiB是当前最小的能保证解释器足够堆栈空间的堆栈大小。需要注意的是部分平台对于堆栈大小会有特定的限制,例如要求大于32KiB的堆栈大小或者需要根据系统内存页面的整数倍进行分配 - 应当查阅平台文档有关详细信息(4KiB页面比较普遍,在没有更具体信息的情况下,建议的方法是使用4096的倍数作为堆栈大小)
  • 可用性: Windows,具有 POSIX 线程的系统。

线程安全

Python线程安全

  • 线程不安全:我们的操作不是原子操作,才会导致的线程不安全。
    • 比如counter+=1,对于这样的操作其实写作number=number+1,也就是包含取值,运算,赋值三步 ;这样就导致多个线程同时读取时,有可能读取到同一个 number 值,读取两次,却只加了一次,最终导致自增的次数小于预期。
  • 原子操作
    • 原子操作(atomic operation),指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会切换到其他线程。它有点类似数据库中的 事务。
# 常见原子操作
L.append(x)
L1.extend(L2)
x = L[i]
x = L.pop()
L1[i:j] = L2
L.sort()
x = y
x.field = y
D[x] = y
D1.update(D2)
D.keys()
  • 实现人工原子操作
    • 在多线程下,我们并不能保证我们的代码都具有原子性,因此如何让我们的代码变得具有 “原子性” ,就是一件很重要的事。
    • 方法也很简单,就是当你在访问一个多线程间共享的资源时,加锁可以实现类似原子操作的效果,一个代码要嘛不执行,执行了的话就要执行完毕,才能接受线程的调度。
    • 因此,我们使用加锁的方法,对例子一进行一些修改,使其具备原子性。
lock=lock()

# few codes
with lock:
	number+=1
# more codes
  • 为什么 Queue 是线程安全的?
    • 其根本原因就是 Queue 实现了锁原语,因此他能像第三节那样实现人工原子操作。

GIL和线程安全

在这里插入图片描述

  • Python GIL底层实现原理
    • 上面这张图,就是 GIL 在 Python 程序的工作示例。其中,Thread 1、2、3 轮流执行,每一个线程在开始执行时,都会锁住 GIL,以阻止别的线程执行;同样的,每一个线程执行完一段后,会释放 GIL,以允许别的线程开始利用资源。
    • 为什么 Python 线程会去主动释放 GIL 呢?如果仅仅要求 Python 线程在开始执行时锁住 GIL,且永远不去释放 GIL,那别的线程就都没有运行的机会。CPython 中还有另一个机制,叫做间隔式检查(check_interval), CPython 解释器会去轮询检查线程 GIL 的锁住情况,每隔一段时间,Python 解释器就会强制当前线程去释放 GIL,这样别的线程才能有执行的机会。
    • 注意,不同版本的 Python,其间隔式检查的实现方式并不一样。早期的 Python 是 100 个刻度(大致对应了 1000 个字节码);而 Python 3 以后,间隔时间大致为 15 毫秒。当然,我们不必细究具体多久会强制释放 GIL,读者只需要明白,CPython 解释器会在一个“合理”的时间范围内释放 GIL 就可以了。
  • Python GIL不能绝对保证线程安全
    • 注意,有了 GIL,并不意味着 Python 程序员就不用去考虑线程安全了,因为即便 GIL 仅允许一个 Python 线程执行,但别忘了 Python 还有 check interval 这样的抢占机制。所以这就是上面提到的原语/锁机制所解决的问题。

os和sys模块

  • os模板提供了一种方便的使用操作系统函数的方法
  • sys模板可供访问由解释器使用或维护的变量和与解释器交互的函数

lambda表达式

  • lambda表达式通常是当你需要使用一个函数,但是又不想费脑袋去命名一个函数的时候使用,也就是通常所说的匿名函数。
  • 和c++内联函数的区别:内联函数是预编译的,lambda只是匿名函数
listC = [('e', 4), ('o', 2), ('!', 5), ('v', 3), ('l', 1)]
print(sorted(listC, key=lambda x: x[1]))

拷贝

  • Python中对象之间的赋值是按引用传递的,如果要拷贝对象需要使用标准模板中的copy
  • copy.copy:浅拷贝,只拷贝父对象,不拷贝父对象的子对象。
  • copy.deepcopy:深拷贝,拷贝父对象和子对象。
  • 如 [obj1,obj2] 若为浅拷贝,则拷贝的list对象是新建的,但是其中的obj1和obj2却还是指向原来的对象

dict:映射、字典、散列表

  • dict采用了哈希表,最低能在 O(1)时间内完成搜索。
  • 同样的java的HashMap也是采用了哈希表实现,不同是dict在发生哈希冲突的时候采用了开放寻址法,而HashMap采用了链接法。
  • 鉴于Python没有内置case语句,使用字典实现也是不错的选择。
  • MappingProxyType创建只读字典视图

迭代器,生成器

  • 迭代器:
    • 迭代器是一个可以记住遍历的位置的对象。
    • 迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。
    • 迭代器有两个基本的方法:iter() 和 next()。
  • 在 Python 中,使用了 yield 的函数被称为生成器(generator)。
    • 跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器就是一个迭代器。
    • 在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行next() 方法时从当前位置继续运行。
    • 调用一个生成器函数,返回的是一个迭代器对象。

词法闭包

  • 函数中的内部函数可以捕捉并且携带父函数的某些状态
  • 闭包在程序流不在闭包范围内的情况下,记住封闭作用域内的值
def speak(text,vol):
	def whisper():
		return text.lower()+'...'
	def yell():
		return text.upper()+'!'
	if vol>0.5:
		return yell
	else:
		return whisper
# 可以看到yell和whisper都没有text参数,但是他们仍然可以访问父函数的text参数

代码部分

lambda表达式运用

  • 排序列表中的字典,排序依据是某个键对应的值
target_list=[{
    
    'name':'tom','age':12},{
    
    'name':'cat','age':50}]
sorted_list=sorted(target_list,key=lambda x:x['age'])
print(sorted_list)

装饰器实现一个函数缓存

  • functools
    • lru_cache实现LRU缓存
    • functools.wraps,wraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器里面的 func 函数中,这使得装饰器里面的 func 函数也有和原函数 foo 一样的元信息了。
# functools模块调用实现
from functools import lru_cache

@lru_cache(maxsize=32)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

>>> print([fib(n) for n in range(10)])
# Output: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]


# 我们也可以轻松地对返回值清空缓存,通过这样:
fib.cache_clear()
  • 使用warps保存元数据,更细致的实现
from functools import wraps

def memorize(function):
    memo = {
    
    }
    @wraps(function)
    def wrapper(*args):
        if args in memo:
            return memo[args]
        else:
            rv = function(*args)
            memo[args] = rv
            return rv
    return wrapper

软件工程/系统分析设计/软件测试

设计模式

在这里插入图片描述

  • 创建型模式:对象实例化的模式,创建型模式用于解耦对象的实例化过程。
  • 结构型模式:把类或对象结合在一起形成一个更大的结构。
  • 行为型模式:类和对象如何交互,及划分责任和算法。

黑盒测试:等价类测试,边界值分析

  • 等价类是离散数学中的一个概念,其基本思想是将一个集合按照一定的标准划分为若干个子集合,其中每个元素的归属依赖于指定功能下具体的行为。
  • 对于数值型的集合,可根据输入变量的取值范围,产生一个有效等价类和两个无效等价类。
  • 对于非数值类型的集合稍微复杂一些,比如对于枚举类型,假设合理的取值包括red、yellow和blue,则可以简单的将这些取值分别对应一个等价类,如果不允许其它值作为输入数据,则不存在它的无效等价类,例如Enumeration类型就是这样的情况。否则可以将所有其它输入对应一个无效等价类,例如包含任何符号的文本输入。
  • 对于等价类的组合方式:
    • 一种组合方式是对于有效等价类要尽可能采用少的测试用例进行覆盖,比如对于E1、E4和E6三个有效等价类可使用一个测试用例同时覆盖。
    • 对于无效等价类则要慎重一些,其覆盖的规则是每个无效等价类必须与其它有效等价类组合测试,以此保证能够触发该无效值对应的专门处理过程。

白盒测试:基于控制流的测试

  • 语句覆盖
  • 分支覆盖
  • 条件覆盖
  • 多条件组合
  • 路径覆盖

猜你喜欢

转载自blog.csdn.net/qq_42739587/article/details/114936160