bd3.2 Python高级
四.设计模式
1. 单例
1.1 请手写一个单例
class SingleMode(object): _instance = None def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = object.__new__(cls) return cls._instance else: return cls._instance
1.2 单例模式的应用场景有哪些?
单例模式应用的场景一般发现在以下条件下: (1) 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如日志文件,应用配置。 (2) 控制资源的情况下,方便资源之间的互相通信。如线程池等。
1.网站的计数器 2.应用配置 3.多线程池 4. 数据库配置,数据库连接池 5.应用程序的日志应用....
2.工厂函数
3. 装饰器
1 对装饰器的理解 ,并写出一个计时器记录方法执行性能的装饰器?
装饰器本质上是一个 Python 函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。
import time def timeit(func): def wrapper(): start = time.clock() func() end = time.clock() print("use time:",end-start) return wrapper() @timeit def test(): for i in range(100): print(i)
3.2 解释一下什么是闭包? 在函数内部再定义一个函数,并且这个函数用到了外边函数的变量,那么将这个函数以及用到的一些变量称之为闭包。 3.3 函数装饰器有什么作用? 装饰器本质上是一个Python 函数,它可以在让其他函数在不需要做任何代码的变动的前提下增加额外的功能。
装饰器的返回值也是一个函数的对象,它经常用于有切面需求的场景。
比如:
插入日志、性能测试、事务处理、缓存、权限的校验等场景
有了装饰器就可以抽离出大量的与函数功能本身无关的雷同代码并发并继续使用。
4. 生成器
迭代器是一个更抽象的概念,任何对象,如果它的类有 next 方法和 iter 方法返回自己本身,对于 string、list、dict、tuple 等这类容器对象,使用 for 循环遍历是很方便的。在后台 for 语句对容器对象调用 iter()函数,iter() 是 python 的内置函数。
iter()会返回一个定义了 next()方法的迭代器对象,它在容器中逐个访问容器内元素,next()也是 python 的内置函数。在没有后续元素时,next()会抛出一个 StopIteration 异常。
生成器(Generator)是创建迭代器的简单而强大的工具。它们写起来就像是正规的函数,只是在需要返回数据的时候使用 yield 语句。每次 next()被调用时,生成器会返回它脱离的位置(它记忆语句最后一次执行的位置 和所有的数据值)
区别:
生成器能做到迭代器能做的所有事,而且因为自动创建了 iter()和 next()方法,生成器显得特别简洁,而且生成器也是高效的,使用生成器表达式取代列表解析可以同时节省内存。除了创建和保存程序状态的自动方法,当 发生器终结时,还会自动抛出 StopIteration 异常。
4.2 X 是什么类型? 1. X = (for i in ramg(10)) 答:X 是 generator 类型。
4.3 请尝试用“一行代码”实现将 1-N的整数列表以 3 为单位分组,比如 1-100 分组后为?
In [30]: print([[x for x in range(1,100)][i:i+3] for i in range(0,100,3)])
[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12], [13, 14, 15], [16, 17, 18], [19, 20, 21], [22, 23, 24], [25, 26, 27], [28, 29, 30], [31, 32, 33], [34, 35, 36], [37, 38, 39], [40, 41, 42], [43, 44, 45], [46, 47, 48], [49, 50, 51], [52, 53, 54], [55, 56, 57], [58, 59, 60], [61, 62, 63], [64, 65, 66], [67, 68, 69], [70, 71, 72], [73, 74, 75], [76, 77, 78], [79, 80, 81], [82, 83, 84], [85, 86, 87], [88, 89, 90], [91, 92, 93], [94, 95, 96], [97, 98, 99], []]
4.4 Python 中 yield 的用法?
yield 就是保存当前程序执行状态。你用 for 循环的时候,每次取一个元素的时候就会计算一次。用 yield 的函数 叫 generator,和 iterator 一样,它的好处是不用一次计算所有元素,而是用一次算一次,可以节省很多空间。generator 每次计算需要上一次计算结果,所以用 yield,否则一 return,上次计算结果就没了
>>> def createGenerator(): mylist = range(3) for i in mylist: ... yield i*i 5. ... >>> mygenerator = createGenerator() # create a generator >>> print(mygenerator) # mygenerator is an object! <generator object createGenerator at 0xb7555c34> >>> for i in mygenerator: ... print(i) 0 1 4
五.面向对象
1.1 Python 中的可变对象和不可变对象? 不可变对象,该对象所指向的内存中的值不能被改变。当改变某个变量时候,由于其所指的值不能被改变,相当于把原来的值复制一份后再改变,这会开辟一个新的地址,变量再指向这个新的地址。 可变对象,该对象所指向的内存中的值可以被改变。变量(准确的说是引用)改变后,实际上是其所指的值直接发生改变,并没有发生复制行为,也没有开辟新的出地址,通俗点说就是原地改变。 Python 中,数值类型(int 和float)、字符串str、元组tuple 都是不可变类型。而列表 list、字典 dict、集合set 是可变类型。 1.2 Python 中 is 和==的区别? is 判断的是a 对象是否就是b 对象,是通过 id 来判断的。 ==判断的是 a 对象的值是否和 b 对象的值相等,是通过 value 来判断的。
1.3 Python 的魔法方法
魔法方法就是可以给你的类增加魔力的特殊方法,如果你的对象实现 (重载)了这些方法中的某一个,那么这个方法就会在特殊的情况下被 Python 所调用,你可以定义自己想要的行为,而这一切都是自动发生的。
它们经常是两个下划线包围来命名的(比如__init__ ,__lt__),Python 的魔法方法是非常强大的,所以了解其使用方法也变得尤为重要!
__init__ 构造器,当一个实例被创建的时候初始化的方法。但是它并不是实例化调用的第一个方法。
__new__ 才是实例化对象调用的第一个方法,它只取下cls参数,并把其他参数传给__init__ 。 __new__很少使用,但是也有它适合的场景,尤其是当类继承自一个像元组或者字符串这样不经常改变的类型的时候。
__call__ 允许一个类的实例像函数一样被调用。
__getitem__ 定义获取容器中指定元素的行为,相当于 self[key] 。
__getattr__ 定义当用户试图访问一个不存在属性的时候的行为 。
__setattr__ 定义当一个属性被设置的时候的行为 。
__getattribute__ 定义当一个属性被访问的时候的行为 。
1.4 面向对象中怎么实现只读属性?
将对象私有化,通过共有方法提供一个读取数据的接口
class Person(object): def __init__(self): self.__age = 10 # def age(self): # return self.__age @property # 以属性访问的方式来访问私有属性 def age(self): return self.__age p1 = Person() print(p1.age)
2.5 谈谈你对面向对象的理解?
面向对象是相对于面向过程而言的。面向过程语言是一种基于功能分析的、以算法为中心的程序设计方法;而面向对象是一种基于结构分析的、以数据为中心的程序设计思想。在面向对象语言中有一个有很重要东西,叫做类。
面向对象有三大特性:封装、继承、多态。
六.正则表达式
1. Python 里 match 与 search 的区别?
match()函数只检测 RE 是不是在 string 的开始位置匹配,
search()会扫描整个 string 查找匹配;
也就是说 match()只有在0位置匹配成功的话才有返回,
如果不是开始位置匹配成功的话,match()就返回 none。
2. Python 字符串查找和替换?
re.findall(r’目的字符串’,’原有字符串’) #查询 re.findall(r'cast','itcast.cn')[0] re.sub(r‘要替换原字符’,’要替换新字符’,’原始字符串’) re.sub(r'cast','heima','itcast.cn')
In [1]: import re In [2]: re.findall(r'cast','itcast.cn') Out[2]: ['cast'] In [3]: re.findall(r'cast','itcast.cn')[0] Out[3]: 'cast' In [4]: re.sub(r'cast','heima','itcast.cn') Out[4]: 'itheima.cn'
3. 用 Python 匹配 HTML tag 的时候,<.*> 和 <.*?> 有什么区别?
<.*>是贪婪匹配,会从第一个“<”开始匹配,直到最后一个“>”中间所有的字符都会匹配到,中间可能会包含“<>”。 <.*?>是非贪婪匹配,从第一个“<”开始往后,遇到第一个“>”结束匹配,这中间的字符串都会匹配到,但是不会有“<>”。
请写出下列正则关键字的含义?
语法 |
说明 |
表达式 实例 |
完整匹配的 字符串 |
字符 |
|||
一般字符 |
匹配自身 |
abc |
abc |
. |
匹配任意除换行符"\n"外的字符。在 DOTALL 模式中也能匹配换行 符。 |
a.c |
abc |
\ |
转义字符,使后一个字符改变原来的意思。 如果字符串中有字符*需要匹配,可 以使用\*或者字符集[*] |
a\.c a\\c |
a. c a\c |
[...] |
字符集(字符类)。对应的位置可以是字符集中任意字符。字符集中的字符可以逐个列出,也可以给出范围, 如[abc]或[a-c]。第一个字符如果是^ 则表示取反,如[^abc]表示不是 abc 的其他字符。 所有的特殊字符在字符集中都失去其原有的特殊含义。在字符集中如果要使用]、-或^,可以在前面加上反斜杠,或把]、-放在第一个字符,把^ 放在非第—个字符。 |
a[bcd]e |
abe ace ade |
预定义字符集(可以写在字符集[…]中) |
|||
\d |
数字:[0-9] |
a\dc |
a1c |
\D |
非数字:[^\d] |
a\Dc |
abc |
\s |
空白字符:[<空格>\ t\r\n\f\v] |
a\sc |
ac |
\S |
非空白字符:[^\s] |
a\Sc |
abc |
\w |
单词字符:[A-Za-z0-9_] |
a\wc |
abc |
\W |
非单词字符:[^\W] |
a\Wc |
ac |
数量词(用在字符或(…)之后) |
|||
* |
匹配前一个字符 0 或无限次。 |
abc* |
ab abccc |
+ |
匹配前一个字符 1 次或无限次 |
abc+ |
abc abccc |
? |
匹配前一个字符 0 次或 1 次。 |
abc? |
Ab abc |
{m} |
匹配前一个字符 m 次 |
ab{2}c |
abbc |
七.系统编程
1. 进程总结 进程:程序运行在操作系统上的一个实例,就称之为进程。进程需要相应的系统资源:内存、时间片、pid。
创建进程: 1.首先要导入 multiprocessing 中的 Process; 2.创建一个 Process 对象; 3.创建 Process 对象时,可以传递参数;
1.p = Process(target=XXX, args=(元组,) , kwargs={key:value}) 2.target = XXX 指定的任务函数,不用加() 3.args=(元组,) , kwargs={key:value} 给任务函数传递的参数
4.使用 start()启动进程; 5.结束进程。
Process 语法结构: Process([group [, target [, name [, args [, kwargs]]]]]) target:如果传递了函数的引用,可以让这个子进程就执行函数中的代码 args:给 target 指定的函数传递的参数,以元组的形式进行传递 kwargs:给 target 指定的函数传递参数,以字典的形式进行传递 name:给进程设定一个名字,可以省略 group:指定进程组,大多数情况下用不到 Process 创建的实例对象的常用方法有: start():启动子进程实例(创建子进程) is_alive():判断进程子进程是否还在活着 join(timeout):是否等待子进程执行结束,或者等待多少秒 terminate():不管任务是否完成,立即终止子进程
Process 创建的实例对象的常用属性: name:当前进程的别名,默认为 Process-N,N 为从 1 开始递增的整数 pid:当前进程的 pid(进程号)
给子进程指定函数传递参数 Demo:
import os
from multiprocessing import Process import time def pro_func(name, age, **kwargs): for i in range(5): print("子进程正在运行中,name=%s, age=%d, pid=%d" %(name, age, os.getpid())) print(kwargs) time.sleep(0.2) if __name__ == '__main__': # 创建 Process 对象 p = Process(target=pro_func, args=('小明',18), kwargs={'m': 20}) # 启动进程 p.start() time.sleep(1) # 1 秒钟之后,立刻结束子进程 p.terminate() p.join() 注意:进程间不共享全局变量。
进程之间的通信-Queue 在初始化 Queue()对象时,(例如 q=Queue(),若在括号中没有指定最大可接受的消息数量,或数量为负值时,那么就代表可接受的消息数量没有上限-直到内存的尽头)
Queue.qsize():返回当前队列包含的消息数量。 Queue.empty():如果队列为空,返回 True,反之 False。 Queue.full():如果队列满了,返回 True,反之 False。 Queue.get([block[,timeout]]):获取队列中的一条消息,然后将其从队列中移除,block 默认值为True。如果 block 使用默认值,且没有设置 timeout(单位秒),消息列队如果为空,此时程序将被阻塞(停在读取状态),直到从消息列队读到消息为止,如果设置了 timeout,则会等待 timeout 秒,若还 没读取到任何消息,则抛出"Queue.Empty"异常;如果 block 值为 False,消息列队如果为空,则会立刻抛出"Queue.Empty"异常;
Queue.get_nowait():相当 Queue.get(False); Queue.put(item,[block[, timeout]]):将 item 消息写入队列,block 默认值为 True;如果 block 使用默认值,且没有设置 timeout(单位秒),消息列队如果已经没有空间可写入,此时程序将被阻塞(停在写入状态),直到从消息列队腾出空间为止,如果设置了 timeout,则会等待 timeout 秒,若还没空间,则抛出"Queue.Full"异常;如果 block 值为 False,消息列队如果没有空间可写入,则会立刻抛出"Queue.Full"异常; Queue.put_nowait(item):相当 Queue.put(item, False);
进程间通信 Demo: from multiprocessing import Process, Queue
import os, time, random # 写数据进程执行的代码:
def write(q): for value in ['A', 'B', 'C']: print('Put %s to queue...' % value) q.put(value) time.sleep(random.random()) # 读数据进程执行的代码:
def read(q): while True: if not q.empty(): value = q.get(True) print('Get %s from queue.' % value) time.sleep(random.random()) else: break if __name__=='__main__': # 父进程创建 Queue,并传给各个子进程: q = Queue() pw = Process(target=write, args=(q,)) pr = Process(target=read, args=(q,)) # 启动子进程 pw,写入: pw.start() # 等待 pw 结束: pw.join() # 启动子进程 pr,读取: pr.start() pr.join() # pr 进程里是死循环,无法等待其结束,只能强行终止: print('') print('所有数据都写入并且读完')
进程池 Pool
# -*- coding:utf-8 -*- from multiprocessing
import Poolimport os, time, random def worker(msg): t_start = time.time() print("%s 开始执行,进程号为%d" % (msg,os.getpid())) # random.random()随机生成 0~1 之间的浮点数 time.sleep(random.random()*2) t_stop = time.time() print(msg,"执行完毕,耗时%0.2f" % (t_stop-t_start)) po = Pool(3) # 定义一个进程池,最大进程数 3 for i in range(0,10): # Pool().apply_async(要调用的目标,(传递给目标的参数元祖,)) # 每次循环将会用空闲出来的子进程去调用目标 po.apply_async(worker,(i,)) print("----start----") po.close() # 关闭进程池,关闭后 po 不再接收新的请求 po.join() # 等待 po 中所有子进程执行完成,必须放在 close 语句之后 print("-----end-----")
multiprocessing.Pool 常用函数解析:
apply_async(func[, args[, kwds]]) :使用非阻塞方式调用 func(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args 为传递给 func 的参数列表,kwds 为传递给 func的关键字参数列表;
close():关闭 Pool,使其不再接受新的任务;
terminate():不管任务是否完成,立即终止;
join():主进程阻塞,等待子进程的退出, 必须在 close 或 terminate 之后使用;
进程池中使用 Queue
如果要使用 Pool 创建进程,就需要使用 multiprocessing.Manager()中的 Queue(),而不是multiprocessing.Queue(),否则会得到一条如下的错误信息:
RuntimeError: Queue objects should only be shared between processes through inheritance
from multiprocessing
import Manager,Poolimport os,time,random def reader(q): print("reader 启动(%s),父进程为(%s)" % (os.getpid(), os.getppid())) for i in range(q.qsize()): print("reader 从 Queue 获取到消息:%s" % q.get(True)) def writer(q): print("writer 启动(%s),父进程为(%s)" % (os.getpid(), os.getppid())) for i in "itcast": q.put(i) if __name__=="__main__": print("(%s) start" % os.getpid()) q = Manager().Queue() # 使用 Manager 中的 Queue po = Pool() po.apply_async(writer, (q,)) time.sleep(1) # 先让上面的任务向 Queue 存入数据,然后再让下面的任务开始从中取数据 po.apply_async(reader, (q,)) po.close() po.join() print("(%s) End" % os.getpid())
2. 谈谈你对多进程,多线程,以及协程的理解,项目是否用? 这个问题被问的概率相当之大,其实多线程,多进程,在实际开发中用到的很少,除非是那些对项目性能要求特别高的,有的开发工作几年了,也确实没用过,你可以这么回答,给他扯扯什么是进程, 线程(cpython 中是伪多线程)的概念就行,
实在不行你就说你之前写过下载文件时,用过多线程技术,或者业余时间用过多线程写爬虫,提升效率。 进程:一个运行的程序(代码)就是一个进程,没有运行的代码叫程序,进程是系统资源分配的最小单位,进程拥有自己独立的内存空间,所以进程间数据不共享,开销大。 线程:调度执行的最小单位,也叫执行路径,不能独立存在,依赖进程存在一个进程至少有一个线程,叫主线程,而多个线程共享内存(数据共享,共享全局变量),从而极大地提高了程序的运行效率。
协程:是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,
直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。 3. 什么是多线程竞争? 线程是非独立的,同一个进程里线程是数据共享的,当各个线程访问数据资源时会出现竞争状态即:数据几乎同步会被多个线程占用,造成数据混乱,即所谓的线程不安全 那么怎么解决多线程竞争问题?-- 锁。锁的好处: 确保了某段关键代码(共享数据资源)只能由一个线程从头到尾完整地执行,能解决多线程资源竞争下的原子操作问题。 锁的坏处: 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
锁的致命问题:死锁。
4. 解释一下什么是锁,有哪几种锁? 锁(Lock)是 Python 提供的对线程控制的对象。有互斥锁、可重入锁。 5. 什么是死锁呢? 若干子线程在系统资源竞争时,都在等待对方对某部分资源解除占用状态,结果是谁也不愿先解锁,互相干等着,程序无法执行下去,这就是死锁。 GIL 锁(有时候,面试官不问,你自己要主动说,增加 b 格,尽量别一问一答的尬聊,不然最后等到的一句话就是:你还有什么想问的么?) GIL 锁 全局解释器锁(只在 cpython 里才有) 作用:限制多线程同时执行,保证同一时间只有一个线程执行,所以 cpython 里的多线程其实是伪多线程! 所以 Python 里常常使用协程技术来代替多线程,协程是一种更轻量级的线程, 进程和线程的切换时由系统决定,而协程由我们程序员自己决定,而模块 gevent 下切换是遇到了耗时操作才会切换。 三者的关系:进程里有线程,线程里有协程。 6.什么是线程安全,什么是互斥锁? 每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。 同一个进程中的多线程之间是共享系统资源的,多个线程同时对一个对象进行操作,一个线程操作尚未结束,另一个线程已经对其进行操作,导致最终结果出现错误,此时需要对被操作对象添加互斥锁, 保证每个线程对该对象的操作都得到正确的结果。
7.说说下面几个概念:同步,异步,阻塞,非阻塞? 同步(IO):多个任务之间有先后顺序执行,一个执行完下个才能执行。 异步:多个任务之间没有先后顺序,可以同时执行有时候一个任务可能要在必要的时候获取另一个同时执行的任务的结果,这个就叫回调!
阻塞(函数调用的方式):如果卡住了调用者,调用者不能继续往下执行,就是说调用者阻塞了。
非阻塞:如果不会卡住,可以继续执行,就是说非阻塞的。 同步异步相对于多任务而言,阻塞非阻塞相对于代码执行而言。
8. 什么是僵尸进程和孤儿进程?怎么避免僵尸进程? 孤儿进程:父进程退出,子进程还在运行的这些子进程都是孤儿进程,孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。 僵尸进程:进程使用 fork 创建子进程,如果子进程退出,而父进程并没有调用 wait 或 waitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中的这些进程是僵尸进程。 避免僵尸进程的方法: 1. fork 两次用孙子进程去完成子进程的任务; 2. 用 wait()函数使父进程阻塞; 3. 使用信号量,在 signal handler 中调用 waitpid,这样父进程不用阻塞。
9. Python 中的进程与线程的使用场景? 多进程适合在 CPU 密集型操作(cpu 操作指令比较多,如位数多的浮点运算)。多线程适合在 IO 密集型操作(读写数据操作较多的,比如爬虫)。 10. 线程是并发还是并行,进程是并发还是并行? 线程是并发,进程是并行; 进程之间相互独立,是系统分配资源的最小单位,同一个线程中的所有线程共享资源。 11. 并行(parallel)和并发(concurrency)? 并行:同一时刻多个任务同时在(多个cpu上)运行。 并发:在同一时间间隔内多个任务都在(一个cpu)运行,但是并不会在同一时刻同时运行,存在交替执行的情况。 实现并行的库有:multiprocessing 实现并发的库有:threading 程序需要执行较多的读写、请求和回复任务的需要大量的 IO 操作,IO 密集型操作使用并发更好。 CPU 运算量大的程序程序,使用并行会更好。 12. IO 密集型和CPU 密集型区别? IO 密集型:系统运作,大部分的状况是 CPU 在等 I/O (硬盘/内存)的读/写。 CPU 密集型:大部份时间用来做计算、逻辑判断等 CPU 动作的程序称之 CPU 密集型。