Python面试题

这里是目录

第一章-python基础

一、基础语法

1. 输入与输出

1.1 代码中要修改不可变数据会出现什么问题? 抛出什么异常?
答:代码不会正常运行,抛出TypeError

在这里插入图片描述

1.2 a=1,b=2,不用中间变量交换 a 和 b 的值?
a = 1
b = 2
# 方法一:
a = a + b
b = a - b
a = a - b

# 方法二(^是异或门运算,通过观察可以发现这种运算,a和b能得出c,a和c能得到b,同理b和c能得到a)
a = a ^ b
b = a ^ b
a = a ^ b

# 方法三
a,b = b,a

1.3 print 调用 Python 中底层的什么方法?
print 方法默认调用 sys.stdout.write 方法,即往控制台打印字符串
import sys
sys.stdout.write('xixi')

>>>xixi
1.4 下面这段代码的输出结果将是什么?请解释?

在这里插入图片描述

1.5 简述你对 input()函数的理解?
python3中input是获取用户输入的方法,无论输入什么,获取到的类型都是字符串

而在python2中有raw_input和python3的input一样,而input则是输入什么类型则获取到什么类型
2. 条件与循环

2.1 阅读下面的代码,写出 A0,A1 至 An 的最终值。

A0 = dict(zip(('a','b','c','d','e'),(1,2,3,4,5)))
print(A0,'A0')
A1 = range(10)
print(A1,'A1')
A2 = [i for i in A1 if i in A0]
print(A2,'A2')
A3 = [A0[s] for s in A0]
print(A3,'A3')
A4 = [i for i in A1 if i in A3]
print(A4,'A4')
A5 = {i:i*i for i in A1}
print(A5,'A5')
A6 = [[i,i*i] for i in A1]
print(A6,'A6')

>>>{'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5} A0
>>>range(0, 10) A1
>>>[] A2 # 注意不是None,是[]
>>>[1, 2, 3, 4, 5] A3
>>>[1, 2, 3, 4, 5] A4
>>>{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81} A5
>>>[[0, 0], [1, 1], [2, 4], [3, 9], [4, 16], [5, 25], [6, 36], [7, 49], [8, 64], [9, 81]] A6

# 列表推导式

# 表达式:[expression for iter_val in iterable if cond_expr]
# [expression]:最后的结果
# [for iter_val in iterable]:循环体
# [if cond_expr]:两个for间是不能有判断语句的,判断语句只能在最后;顺序不定,默认是左到右。


# 无条件的列表解析式
lists = [2*i for i in range(5)]
print(lists)
#  >>>[0, 2, 4, 6, 8]
 
# 带条件子句的列表解析式
lists1 = [i for i in range(10) if i > 4]
print(lists1)
# >>>[5, 6, 7, 8, 9]
 
 
# 多重循环的列表解析式
print([(i,j) for i in [1,3,5] for j in [2,4,6]])
# >>>[(1, 2), (1, 4), (1, 6), (3, 2), (3, 4), (3, 6), (5, 2), (5, 4), (5, 6)]

2.2 range 和 xrange 的区别?可迭代对象 | 迭代器 | 生成器
# 非常好的参考 https://kelepython.readthedocs.io/zh/latest/c01/c01_11.html

两者用法相同,不同的是 range 返回的结果是一个列表,而 xrange 的结果是一个生成器,前者是 
直接开辟一块内存空间来保存列表,后者是边循环边使用,只有使用时才会开辟内存空间,所以当列表 
很长时,使用 xrange 性能要比 range 好。

python3 中取消了 range 函数,而把 xrange 函数重命名为 range,所以现在直接用 range 函数即可

# 可迭代对象 | 容器
像列表(list)、集合(set)、序列(tuple)、字典(dict)都是容器。简单的说,容器是一种把多个元素组织在一起的数据结构,可以逐个迭代获取其中的元素。容器可以用in来判断容器中是否包含某个元素,如
'a' in {'a', 'b', 'c'} # 输出 True
'a' in {'a': 1, 'b': 2} # 输出 True
'a' in set(['a', 'b', 'c']) # 输出 True

大多数的容器都是可迭代对象,可以使用某种方式访问容器中的每一个元素

# 迭代器
实现了__iter__和__next__方法的对象都称为迭代器,可以通过iter()等方法对可迭代对象生成迭代器。迭代器是一个有状态的对象,在调用next() 的时候返回下一个值,如果容器中没有更多元素了,则抛出StopIteration异常。如
a = iter([1,2,3])
print(a.__next__())
print(a.__next__())
print(a.__next__())
print(a.__next__())

>>>1
>>>2
>>>3
>>>Traceback (most recent call last):
  File "C:/Users/Administrator/.PyCharmCE2019.2/config/scratches/scratch_47.py", line 86, in <module>
    print(a.__next__())
StopIteration

# 生成器
生成器其实是一种特殊的迭代器,内部自动创建实现__iter__和__next__方法,只需要使用关键字yield就可以,只有在需要返回数据的时候( next()被调用)才使用 yield 语句,返回它上次脱离的位置并生成数据返回。而且生成器在迭代的过程中可以改变当前迭代值

# for循环
for 语句对容器对象调用 iter()函数,iter()是 python 的内置函数。iter()会返回一个定义了 next()方法的迭代器对象,它在容器中逐个访问容器内元素,next()在没有后续元素时,会抛出一个 StopIteration 异常


# 创建生成器
def gen():
    a = 1
    b = 2
    print('第一次返回')
    yield a + b
    print('第二次返回')
    yield a + b

gen1 = gen()
result = gen1.__next__()
print(result)
result = gen1.__next__()
print(result)
result = gen1.__next__()
print(result)

>>>第一次返回
>>>3
>>>第二次返回
>>>3
>>>>StopIteration

# 生成器也可以通过以下方式生成
data_generator = (x*x for x in range(3))
print(data_generator)
for i in data_generator:
    print(i)
print('-------------------------第一次完全迭代完成-------------------')
print('-------------下面进行第二次完全迭代data_generator--------------')
for i in data_generator:
    print(i)
print('第二次迭代data_generator为空,因为生成器的值都是一次性的')

# 列表生成器、推导式
def main1():
	# 列表生成器
    num1 = (x for x in range(10, 15))
    return num1

mian_result = main1()
print(mian_result)
print(mian_result.__next__())
print(mian_result.__next__())
print(mian_result.__next__())
print('------------------------')

def main2():
	# 列表推导式(一次性生成,占内存)
    num1 = [x for x in range(10, 15)]
    return num1
mian_result = main2()
print(mian_result)

>>>10
>>>11
>>>12
>>>------------------------
>>>[10, 11, 12, 13, 14]

2.3 考虑以下 Python 代码,如果运行结束,命令行中的运行结果是什么?
l = []
a = {'num':0}
for i in range(5):
    a['num'] = i
    l.append(a)
    print(l)

>>>[{'num': 0}]
>>>[{'num': 1}, {'num': 1}]
>>>[{'num': 2}, {'num': 2}, {'num': 2}]
>>>[{'num': 3}, {'num': 3}, {'num': 3}, {'num': 3}]
>>>[{'num': 4}, {'num': 4}, {'num': 4}, {'num': 4}, {'num': 4}]

#字典是可变对象,l.append(a)操作是把字典 a 的引用传到列表 l 中,后续操作修改 a[‘num’]的值的时候,l 中的值也会跟着改变,相当于浅拷贝。

2.4 以下 Python 程序的输出?
for i in range(5,0,-1):
    print(i,end=' ')
    
>>> 5 4 3 2 1 

3. 文件操作

3.1 4G 内存怎么读取一个 5G 的数据?
# 方法一:通过生成器分批读取

# 文本里的内容是1234567890
file_path = 'E:/1A爬虫课程 平哥/爬虫课程 平哥/叶森/猿人学-爬虫进阶课/面试题/test.txt'
def take(filePath, chunk_size=2):
    file_object = open(filePath)
    while True:
        data = file_object.read(chunk_size)
        if not data:
            break
        yield data

for chunk_data in take(file_path):
    print (chunk_data)

# 又或者
# take_data = take(file_path)
# result = take_data.__next__()
# print(result)
# result = take_data.__next__()
# print(result)

>>>12 34 56 78 90 

# 方法二
# 通过linux的split命令# https://blog.csdn.net/qq_27870421/article/details/93969416

#使用-l选项根据文件的行数来分割文件,例如把文件分割成每个包含10行的小文件:
split -l 10 date.file

# 将date.file文件分割成大小为10KB的小文件
split -b 10k date.file
3.2 现在考虑有一个 jsonline 格式的文件 file.txt 大小约为 10K,之前处理文件的代码如下所示
def process(e):
    print(e,end='')
    print('here')
    print('假装是处理数据的方法')
    pass

def get_lines():
    l = []
    with open('E:/for_cd/test1.txt','r') as f:
        for eachline in f:
            l.append(eachline)
        return l

if __name__ == '__main__':
    for e in get_lines():
        process(e) #处理每一行数据


# 修改后如下,通过生成器来分段读取
def get_lines():
    l = []
    with open('./phone.txt','r') as f:
        while True:
            data = f.readlines(60)
            if data:
                l.append(data)
                yield l
            else:
                return None

3.3 read、readline 和 readlines 的区别?
# read()不给定readsize的话就从头到尾读出来
file = open('E:/for_cd/test1.txt','r')
result = file.read()
print(result,'-read()')
file.close()
print('==================================')

# readline读取一行,适合读取大文件
file = open('E:/for_cd/test1.txt','r')
result = file.readline()
print(result,'-readline()')
file.close()
print('==================================')

# readlines读取每一行,每一行都是元素放在列表里返回。
file = open('E:/for_cd/test1.txt','r')
result = file.readlines()
print(result,'-readlines()')
file.close()

3.4.补充缺失的代码?
# 这个函数接收文件夹的名称作为输入参数
# 返回该文件夹中文件的路径
# 以及其包含文件夹中文件的路径

import os
def print_directory_contents(sPath):
    for sChild in os.listdir(sPath):
        # 进行路径和文件名的合并
        sChildPath = os.path.join(sPath, sChild)
        # 判断该绝对地址是否目录,是目录的话继续遍历,不是的话就else进行打印
        if os.path.isdir(sChildPath):
            print_directory_contents(sChildPath)
        else:
            print(sChildPath)

print_directory_contents('E:/for_cd')
3.5 储存的大小换算
# 在不同的编码中汉字占的字节大小不同,在utf-8中一个汉字占三个字节,一个英文占一个字节
1B(字节)=8b(位)( 1 Byte(字节)=8 bit(位))
1 KB = 1024 B
1 MB = 1024 KB
1 GB = 1024 MB
1TB = 1024GB

4. 异常

4.1在except中return后还会不会执行finally中的代码?怎么抛出自定义异常?
会继续处理 finally 中的代码;用 raise 方法可以抛出自定义异常。

在这里插入图片描述

# http://m.biancheng.net/view/4593.html(ZeroDivisionError这个位置是要某一种错误的类型)
# raise 语句的基本语法格式为:
raise ZeroDivisionError("除数不能为零")
会继续处理 finally 中的代码;用 raise 方法可以抛出自定义异常
# 捕获所有异常
except:
# 只捕获IO异常
except IOError:

5. 模块与包

5.1 常用的 Python 标准库都有哪些?
内置库:
os操作系统,random随机,sys命令行,hashlib加密,time时间,math处理数字

第三方库:
requests网络请求,flask网络框架,selenium自动化框架,scrapy异步爬虫框架,leveldb key-value数据库(存硬盘) sanidb异步操作mysql的库,lzma亚马逊的压缩算法,re正则

5.2__init__ 和__new__的区别?
# https://blog.csdn.net/weixin_37579123/article/details/89515577
# https://my.oschina.net/lionets/blog/193900
init 在对象创建后,对对象进行初始化。 
new 是在对象创建之前创建一个对象,并将该对象以self返回给 init

创建 foo = test()的过程大概是:
1.创建 foo 这个名字
2.调用 test 类的 __new__() 静态方法,返回一个类实例
3.将__new__() 返回的类实例赋值给 foo
4.这个实例被当做 self 参数传给 __init__(),以完成该实例的初始化工作

class B():
    def __new__(cls):
        print ("__new__方法被执行")
 
    def __init__(self):
        print ("__init__方法被执行")
 
b=B()   
结果:
__new__方法被执行

----------------------------------------------

class B():
    def __new__(cls):
        print ("__new__方法被执行")
        return super(B,cls).__new__(cls)
 
    def __init__(self):
        print ("__init__方法被执行")
 
b=B()
结果:
__new__方法被执行
__init__方法被执行


--------------------------------------------
探究__new__方法的return


class A ():
    x = 2
    def __new__(cls, *args, **kwargs):
        print('A__new__')
        return super(A, cls).__new__(cls)

class B(A):
    x = 1
    def __new__(cls):
        print("__new__方法被执行")
        return super(B, cls).__new__(cls)

    def __init__(self):
        print("__init__方法被执行")

b = B()
print(b.x)
输出j
__new__方法被执行
A__new__
__init__方法被执行
1

由此可见B的__new__的return会调用父类A的__new__,然后A也往上迭代,直到找到python的class对象的原型进行return。然后通过return得到的这个原型构建b对象,并开始调用__init__方法进行b的初始化
5.3 Python 里面如何生成随机数?
import random

a = 0
b = 10
step = 2
sequence = '这是一个序列'

print(random.random())#  生成一个 0-1 之间的随机浮点数;  0.3030436785935363
print(random.uniform(a, b))#  生成[a,b]之间的浮点数;     2.7330575986758676
print(random.randint(a, b))#  生成[a,b]之间的整数;       6
print(random.randrange(a, b, step))#  在指定的集合[a,b)中,以 step 为递增基数(+)随机取一个数;     7
print(random.choice(sequence))#  从特定序列中随机取一个元素,这里的序列可以是字符串,列表, 元组等。   一
5.4 输入某年某月某日,判断这一天是这一年的第几天?(可以用 Python 标准库)
import datetime
year = '2021'
month = '1'
day = '16'

def get_day():
    # year = input("请输入年份:")
    # month = input("请输入月份:")
    # day = input("请输入天:")
    date1 = datetime.date(year=int(year),month=int(month),day=int(day))
    print(date1,'-date1')  # 2021-01-16 -date1
    print(type(date1),'-type(date1)')  # <class 'datetime.date'> -type(date1)
    date2 = datetime.date(year=int(year),month=1,day=1)
    print(date2,'-date2')  # 2021-01-01 -date2
    print(date1 - date2,'date1 - date2')  # 15 days, 0:00:00 date1 - date2
    return (date1 - date2).days + 1

result = get_day()
print(result,'result+')  # 16 result+
5.5 打乱一个排好序的 list 对象 alist?
import random
alist = [1,2,3,4,5,6,7]
random.shuffle(alist)
print(alist,'-alist')
5.6 说明一下 os.path 和 sys.path 分别代表什么?
os.path 是module,包含了各种处理长文件名(路径名)的函数,主要是用于对系统路径文件的操作。 
sys.path 是由目录名构成的列表,主要是对 Python 解释器的环境参数的操作,可动态的改变 Python 解释器搜索路径

# 可以在python 环境下使用sys.path.append(path)添加相关的路径,但在退出python环境后自己添加的路径就会自动消失!
import sys
sys.path.append('c:\\mypythonlib')

5.7 Python 中的 os 模块常见方法?
import os

os.remove() #删除文件
os.rename() #重命名文件
os.walk() #生成目录树下的所有文件名
os.chdir() #改变目录
os.listdir() #列出指定目录的文件
os.getcwd() #取得当前工作目录
os.chmod() #改变目录权限
os.path.basename() #去掉目录路径,返回文件名
os.path.dirname() #去掉文件名,返回目录路径
os.path.join() #将分离的各部分组合成一个路径名
os.path.split() #返回(dirname() #,basename() #) #元组
os.path.splitext() #(返回 filename,extension) #元组
os.path.getsize() #返回文件大小
os.path.exists() #是否存在
os.path.isabs() #是否为绝对路径
os.path.isdir() #是否为目录
os.mkdir/makedirs #创建目录/多层目录
os.rmdir/removedirs #删除目录/多层目录
os.path.getatime\ctime\mtime #分别返回最近访问、创建、修改时间

result = os.walk('E:/for_cd')
for r in result:
    print(r)

result = os.listdir('E:/for_cd')
print(result)
5.8 Python 的 sys 模块常用方法?
import sys

sys.argv  # 返回命令行参数 List,第一个元素是程序本身路径
sys.modules.keys()  # 返回所有已经导入的模块列表
sys.version  # 获取 Python 解释程序的版本信息
sys.modules  # 返回系统导入的模块字段,key 是模块名,value 是模块 
sys.path  # 返回模块的搜索路径,初始化时使用 PYTHONPATH 环境变量的值 
sys.platform  # 返回操作系统平台名称 
sys.stdout  # 标准输出 
sys.stdin  # 标准输入 
sys.stderr  # 错误输出
5.9模块和包是什么?
在 Python 中,模块是搭建程序的一种方式。每一个 Python 代码文件都是一个模块,并可以引用其他的模块,比如对象,方法和属性。

6.Python 特性

6.0 赋值、浅拷贝和深拷贝的区别?
赋值:
在Python中,对象的赋值就是简单的对象引用,当创建一个对象,然后把它赋给另一个变量的时候,python并没有拷贝这个对象,而只是拷贝了这个对象的引用
a = [1,2,[3,4]]
b = a
a.append(5)
print(a,'-a') # [1, 2, [3, 4], 5] -a
print(b,'-b') # [1, 2, [3, 4], 5] -b

浅拷贝:
浅拷贝产生的列表b不再是列表a了,使用id()判断可以发现他们不是同一个对象,因为浅拷贝并非像赋值一样只拷贝原对象本身的引用,而是也拷贝原对象内第一层对象的引用,由于只是对第一层对象的应用进行拷贝,于是第一层的可变类型发生改变时,浅拷贝也会受到影响
浅拷贝有三种形式:
切片操作: b = a[:]
copy函数:b = copy.copy(a)
工厂函数:b = list(a)

import copy
a = [1,2,[3,4]]
b = copy.copy(a)
a.append(5)
print(a,'-a') # [1, 2, [3, 4], 5] -a
print(b,'-b') # [1, 2, [3, 4]] -b

# 当可变类型发生改变时,浅拷贝也会受到影响
a = [1,2,[3,4]]
b = copy.copy(a)
a[2].append(5)
print(a,'-a') # [1, 2, [3, 4, 5]] -a
print(b,'-b') # [1, 2, [3, 4, 5]] -b


深拷贝:
深拷贝不单单像浅拷贝一样拷贝引用,还拷贝其引用指向的数据即使是嵌套的,并开辟新的内存空间进行存放。原对象发生改变,不会对深拷贝对象造成影响
import copy
a = [1,2,[3,4]]
b = copy.deepcopy(a)
a[2].append(5)
print(a,'-a') # [1, 2, [3, 4, 5]] -a
print(b,'-b') # [1, 2, [3, 4]] -b

6.1 可变类型和不可变类型
可变不可变指的是内存中的值是否可以被改变,不可变类型指的是对象所在内存块里面的值不可以 
改变,有数值、字符串、元组;可变类型则是可以改变,主要有列表、字典。

不可变类型:
对不可变类型我的理解是,比如x=1在内存位置8791163524128上的不可变类型的对象的值一旦确定,在被垃圾回收之前,这个位置上的对象的值就不能有任何改变(这点和可变类型就有差别了),如果要改变x指向的值,只能指向在内存其他位置上的对象。
而且值相同的不可变类型的对象在内存上的地址是一样的。正如上面变量x和y所指向的对象都是数字1,所以他们指向的地址都是一样的
x = 1
y = 1
print(id(x),'-x在内存上的位置') #8791163524128 -x在内存上的位置
print(id(y),'-y在内存上的位置') #8791163524128 -y在内存上的位置

可变类型:
对可变类型我的理解是,比如说在例子中,在内存位置30171720上的对象的值是可以被修改的,值从[1,2,3]->[1,2,3,4],而内存位置可以依然不变
还值得注意的一点是,对于可变类型,有相同的值的对象在内存位置是可以不同的,在例子中虽然x和y都有相同的值[1,2,3],但是该值在内存上的位置却是不同的,这点和值类型是不同的。
x = [1,2,3]
y = [1,2,3]
print(id(x),'-x在内存上的位置') #30171720 -x在内存上的位置
print(id(y),'-y在内存上的位置') #30171784 -y在内存上的位置
x.append(4)
print(id(x),'-x在内存上的位置') #30171720 -x在内存上的位置

6.2Python 是强语言类型还是弱语言类型?
Python 是强类型的动态脚本语言
 
强类型:不允许不同类型相加。 不像js可以'1000' + 1 = 1001
动态:不使用显性的数据类型声明,在编译期就确定变量类型的是静态类型语言(如java、c),在运行期才确定变量类型的则是动态类型语言。如python只有到运行时调用函数,才最终确定参数和返回值的类型,所以是动态类型
脚本语言:一般也是解释型语言,运行代码只需要一个解释器,不需要编译。(不像java和c)

在这里插入图片描述

6.3谈一下什么是解释性语言,什么是编译性语言?
计算机不能直接理解高级语言,只能直接理解机器语言,所以必须要把高级语言翻译成机器语言,计算机才能执行高级语言编写的程序。 

解释性语言在运行程序的时候才会进行翻译。这样解释性语言每执行一次就需要逐行翻译一次
编译型语言写的程序在执行之前,需要一个专门的编译过程,把程序编译成机器语言(可执行文件)。以后要运行的话就不用重新翻译了,直接使用编译的结果就行了,执行效率更高。 

6.4 Python 中有日志吗?怎么使用?
Python 自带 logging 模块,调用 logging.basicConfig()方法,配置需要的日志等级和相应的参数,Python 解释器会按照配置的参数生成相应的日志。
相比print的优点:
1.可以通过设置不同的日志等级,过滤信息
2.print 将所有信息都输出到标准输出中,logging 则可以由开发者决定将信息输出到什么地方,以及怎么输出

import logging

logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s %(filename)s %(levelname)s %(message)s',
                    datefmt='%a %d %b %Y %H:%M:%S',
                    filename='my.log',
                    filemode='w')

logging.info('This is a info.')
logging.debug('This is a debug message.')
logging.warning('This is a warning.')

# 输出到同目录下 my.log 文件中的内容:
Wed 05 Jun 2019 22:25:32 test.py INFO This is a info.
Wed 05 Jun 2019 22:25:32 test.py WARNING This is a warning.

6.5Python 是如何进行类型转换的?
内建函数封装了各种转换函数,可以使用目标类型关键字强制类型转换
进制之间的转换可以用int('str',base='n')将特定进制的字符串转换为十进制,再用相应的进制转换函数将十进制转换为目标进制。 
result = int(3.6)
print(result) # 3

result = int('0xa',16)
print(result) # 10

可以使用内置函数直接转换的有: 
list-->tuple tuple(list) 
tuple-->list list(tuple)

6.6 Python2 与 Python3 的区别?
# 编码
Python 2有两种字符串类型:strunicode,Python 3中的字符串默认就是Unicode(utf-8),Python 3中的str相当于Python 2中的unicode。

在Python 2中默认编码ASCII,如果代码中包含非英文字符,需要在代码文件的最开始声明编码,如# -*- coding: utf-8 -*-
在Python 3中,默认的字符串就是Unicode,就省去了这个麻烦

# 异常捕获
Python 2中捕获异常一般用下面的语法
try:
    1/0 
except ZeroDivisionError, e:
    print str(e)
python3必须要as
try:
    1/0 
except ZeroDivisionError as e:
    print str(e)

# 废弃差异
废弃了print语句,改用print函数
废弃xrange,Python 2中有 rangexrange 两个方法。其区别在于,range返回一个listxrange返回一个iterator。Python 3中不再支持 xrange 方法,Python 3中的 range 方法就相当于 Python 2中的 xrange 方法。
废弃不相等操作符"<>",python3统一使用"!="

# 整数相除
在Python 2中,3/2的结果是整数,在Python 3中,结果则是浮点数

6.7 关于 Python 程序的运行方面,有什么手段能提升性能?
1、使用多进程,充分利用机器的多核性能 
2、对于性能影响较大的部分代码,可以使用 C 或 C++编写 
3、对于 IO 阻塞造成的性能影响,可以使用 IO 多路复用来解决 
4、尽量使用 Python 的内建函数 
5、尽量使用局部变量

6.8Python 中的作用域和命名空间?
Python中一个变量的作用域总是由在代码中被赋值的地方所决定。
变量的访问:在一个python程序中,直接访问一个变量,会从内到外依次访问所有的作用域直到找到,否则会报未定义的错误。访问顺序:局部作用域--局部作用域和全局作用域之间--全局作用域--内建域

在这里插入图片描述

命名空间
命名空间(Namespace)是从名称到对象的映射,大部分的命名空间都是通过 Python 字典来实现的。
命名空间提供了在项目中避免名字冲突的一种方法。各个命名空间是独立的,没有任何关系的,所以一个命名空间中不能有重名,但不同的命名空间是可以重名而没有任何影响。

7. Linux 基础和 git

7.1 Linux 的基本命令(怎么区分一个文件还是文件夹)
ls -F 会在文件夹后添加“/”,在文件后面加“*”
7.2 日志以什么格式,存放在哪里?
日志文件的默认路径是:/var/log 
后缀名为.log
7.3 Linux 查看某个服务的端口?
netstat -anp | grep service_name
7.4 centos系统如何设置开机自启动一个程序?
把写好的启动脚本添加到目录/etc/rc.d/init.d/,然后使用命令chkconfig设置开机启动

#将mysql启动脚本放入所有脚本运行目录/etc/rc.d/init.d中
cp /lamp/mysql-5.0.41/support-files/mysql.server /etc/rc.d/init.d/mysqld
#改变权限
chown root.root /etc/rc.d/init.d/mysqld
#所有用户都可以执行,单只有root可以修改
chmod 755 /etc/rc.d/init.d/mysqld
#将mysqld 放入linux启动管理体系中
chkconfig --add mysqld
#查看全部服务在各运行级状态
chkconfig --list mysqld
#只要运行级别3启动,其他都关闭
chkconfig --levels 245 mysqld off

# 开启自启动
chkconfig mysqld on
# 关闭自启动
chkconfig mysqld off
7.5 在 linux 中 find 和 grep 的区别

在这里插入图片描述

grep:
一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行 
打印出来


find:
从指定的起始目录开始,递归地搜索其各个子目录,查找满足寻找条件的文件并对 
之采取相关的操作

按照名字查找忽略大小写
find -iname 文件名
利用关键词查找
find -iname "news*"


简单点说说,grep 是查找匹配条件的行,find 是搜索匹配条件的文件

7.6 Linux 重定向命令有哪些?有什么区别?
1、重定向> 
Linux 允许将命令执行结果重定向到一个文件,本应显示在终端上的内容保存到指定文件中。如:ls > test.txt ( test.txt 如果不存在,则创建,存在则覆盖其内容 )2、重定向>> 
>>这个是将输出内容追加到目标文件中。如果文件不存在,就创建文件;如果文件存在,则将新的内容追加到那个文件的末尾,该文件中的原有内容不受影响。
7.7 软连接和硬链接的区别?
软连接类似 Windows 的快捷方式,当删除源文件时,那么软链接也失效了。硬链接可以理解为源文件的一个别名,多个别名所代表的是同一个文件。当 rm 一个文件的时候,那么此文件的硬链接数减1,当硬链接数为 0 的时候,文件被删除。 (硬链接有点像Python的垃圾回收,引用为0才会回收)
7.8 10 个常用的 Linux 命令?
pwd 显示当前的绝对路径 
ls 查看目录中的文件,-l是详细信息
cd /home 进入 '/ home' 目录' 
cd .. 返回上一级目录  
mkdir dir1 创建一个叫做 'dir1' 的目录' 
rm -f file1 删除一个叫做 'file1' 的文件',-f 参数,忽略不存在的文件,从不给出提示。 
rmdir dir1 删除一个叫做 'dir1' 的目录' 
cat jilu.txt 表示查看所有内容
head -n 2 jilu.txt 表示查看文件前2行的内容
tail -n 2 jilu.txt 表示查看文件末尾2行的内容

解压
# https://www.cnblogs.com/lhm166/articles/6604852.html
tar -xvf file.tar //解压 tar包 
tar -xvf archive.tar -C /tmp //将压缩包释放到 /tmp 目录下
tar -xzvf file.tar.gz //解压tar.gz # x是解压 z是有gzip属性的 v是解压过程可视化 f一定放在最后,后面跟文件名
unrar e file.rar //解压rar
unzip file.zip //解压zip

tar -cvf jpg.tar *.jpg //将目录里所有jpg文件打包成jpg.tar  # c是打包压缩
tar -czf jpg.tar.gz *.jpg   //将目录里所有jpg文件打包成jpg.tar后,并且将其用gzip压缩,生成一个gzip压缩过的包,命名为jpg.tar.gz

7.9 Linux 关机命令有哪些?
reboot 重新启动操作系统 
shutdown –r now 重新启动操作系统,shutdown 会给别的用户提示 
shutdown -h now 立刻关机,其中 now 相当于时间为 0 的状态 
shutdown -h 20:25 系统在今天的 20:25 会关机 
shutdown -h +10 系统再过十分钟后自动关机 
init 0 关机 
init 6 重启

7.91 git的基本操作有哪些?
git


二.数据类型

1. 字典

1.1 现有字典 d={“a”:24,“g”:52,“i”:12,“k”:33}请按字典中的 value 值进行排序?
dict = {"a":24,"g":52,"i":12,"k":33}
result1 = dict.items()  # 返回可遍历的(键, 值) 元组数组。
print(result1,'-result1') # dict_items([('a', 24), ('g', 52), ('i', 12), ('k', 33)]) -result1
result2 = sorted(result1,key=lambda x:x[1],reverse=True) # reverse为True是倒序是大到小,默认是小到大
print(result2,'-result2') # [('g', 52), ('k', 33), ('a', 24), ('i', 12)] -result2

1.2 说一下字典和 json 的区别?
字典是一种数据结构,有很多内置函数和多种调用方法,而json是一种数据打包的一种数据表现形式
字典的 key 值只要是能 hash 的就行(所以数字类型也能做键),json 的必须是是用双引号引起来的字符串

1.3 存入字典里的数据有没有先后排序?
存入的数据不会自动排序,可以使用 sorted 函数对字典进行排序。

1.4字典推导式?
dict = {1:9,3:4,5:6,7:8}
dict2 = {k:v for k,v in dict.items()}
print(dict2,'-dict2')

2. 字符串

2.1 如何理解 Python 中字符串中的\字符?
有三种不同的含义: 
1、转义字符 2、路径名中用来连接路径名 3、编写太长代码手动软换行。

str1 = '111111' \
       '222222'
print(str1)  # 111111222222
2.2 请反转字符串“aStr”?
print('aStr'[::-1])

2.3 将字符串"k:1|k1:2|k2:3|k3:4",处理成 Python 字典:{k:1, k1:2, … } # 字 典里的 K 作为字符串处理
str1 = "k:1|k1:2|k2:3|k3:4" 
def getdict():
    dict = {}
    re_split = str1.split('|')
    for r in re_split:
        k,v = r.split(':')
        dict[k] = v
    return dict
result = getdict()
print(result) # {'k': '1', 'k1': '2', 'k2': '3', 'k3': '4'}

2.4 请按 alist 中元素的 age 由大到小排序
alist = [{'name':'a','age':20},{'name':'b','age':30},{'name':'c','age':25}]
def get_list():
    result = sorted(alist,key=lambda x:x['age'])
    return result

result = get_list()
print(result,'-result') # [{'name': 'a', 'age': 20}, {'name': 'c', 'age': 25}, {'name': 'b', 'age': 30}] -result

3. 列表

3.1 下面代码的输出结果将是什么?
list = [1,2,3,4,5,6,7,8,9]
print(list[10:]) # []
代码将输出[],不会产生IndexError错误。

尝试获取 list[10]之后的成员,会导致 IndexError。
list = [1,2,3,4,5,6,7,8,9]
print(list[10]) # IndexError: list index out of range

3.2 写一个列表生成式,产生一个公差为 11 的等差数列
print([x*11 for x in range(10)])  # [0, 11, 22, 33, 44, 55, 66, 77, 88, 99]

3.3 给定两个列表,怎么找出他们相同的元素和不同的元素?
list1 = [1,2,3] 
list2 = [3,4,5] 
set1 = set(list1) 
set2 = set(list2)
# 交集
print(set1&set2) # {3}
# 差集
print(set1^set2) # {1, 2, 4, 5}
# 并集
print(set1|set2) # {1, 2, 3, 4, 5}

3.4 请写出一段 Python 代码实现删除一个 list 里面的重复元素?
# 方法一
list = [1,2,3,5,4,6,7,7,8,8,4]
result = sorted(set(list),key=list.index)
print(result,'-result')

# 方法二
list2 = []
for l in list:
    if l not in list2:
        list2.append(l)
print(list2,'-list2')

3.5 有如下数组 list = range(10)我想取以下几个数组,应该如何切片?
list = range(10)

# [1,2,3,4,5,6,7,8,9]
print([i for i in list[1:]])

# [1,2,3,4,5,6]
print([i for i in list[1:7]])

# [3,4,5,6]
print([i for i in list[3:7]])

# [9]
print([list[-1]])

# [1,3,5,7,9]
print([i for i in list[1::2]])

3.6 下面这段代码的输出结果是什么?请解释?
def extendlist(val, list=[]): 
    list.append(val)
    return list

list1 = extendlist(10) 
list2 = extendlist(123, [])
list3 = extendlist('a') 

print("list1 = %s" %list1) # list1 = [10, 'a']
print("list2 = %s" %list2) # list2 = [123]
print("list3 = %s" %list3) # list3 = [10, 'a']

形参只在函数被定义的那一刻创建一次,如果没有传入list实参,用的一直是同一个形参对象
3.7.将以下 3 个函数按照执行效率高低排序
def f1(lIn): 
    l1 = sorted(lIn)
    l2 = [i for i in l1 if i<0.5]
    return [i*i for i in l2]


def f2(lIn): 
    l1 = [i for i in lIn if i<0.5]
    l2 = sorted(l1) 
    return [i*i for i in l2]


def f3(lIn): 
    l1 = [i*i for i in lIn]
    l2 = sorted(l1)
    return [i for i in l2 if i<(0.5*0.5)]


import random 
import cProfile 
lIn = [random.random() for i in range(100000)]
cProfile.run('f1(lIn)')
cProfile.run('f2(lIn)')
cProfile.run('f3(lIn)')

按执行效率从高到低排列:f2、f1 和 f3,用Python的cprofile库去测试各个方法的cpu运行时间

3.8 获取 1~100 被 6 整除的最后三个数?
def A(): 
    alist = []
    for i in range(1,100):
        if i % 6 == 0:
            alist.append(i)
            last_num = alist[-3:]
    print(last_num)
A() # [84, 90, 96]

第二章 Python 高级

一.元类

1.Python 中类方法、类实例方法、静态方法有何区别?

类是一个模板,类的作用就是创建实例对象,我们在创建类的时候,系统会给这个类分配空间内存,而由这个类创建出来的实例对象,系统也会为其分配内存,可以这么说,类对象只有一个,而实例对象有多个,实例对象在内存中有自己独立的空间,里面有自己的属性,和方法,所以在这里self参数的含义就是对该实例化对象的内存空间的引用,所以当要调用此内存空间里的属性和方法时,需要以self.xxx进行调用。

静态方法主要是用来存放逻辑性的代码,主要是一些逻辑属于类,但是和类本身没有实际性的交互,但是需要让这个功能成为这个类的成员,那么就可以采用静态方法。在静态方法中,不会涉及到类中的方法和属性的操作,可以理解为将静态方法寄存在该类的命名空间中。


class A():
    x = 1
    # 类方法  # cls即为类自身
    @classmethod
    def class_f(cls):
        print(cls.x)
    #类实例方法
    def self_f(self):
        print(self.x)
    #静态方法
    @staticmethod
    def static_f():
        sx = 3
        print(sx)

# 类实例方法需要实例化对象
a = A()
a.self_f() # 1
# 类方法不需要实例化调用
A.class_f() # 1
# 静态方法不需要实例化调用,但无法访问类和类实例的属性和方法,是一个与世隔绝的静男子
print(A.static_f()) # 3
/类方法实例方法静态方法
类调用
实例调用
访问实例属性
访问类属性
有个地方要注意一下,类实例实际上是将类复制一份然后在这基础上进行一些初始化得到的一个对象,所以对象里的类方法里的cls.类属性,实例方法也是能访问到的,比如

在这里插入图片描述

2.Python 中如何动态获取和设置对象的属性?

class Animal:
    test = 'xixi'
    def __init__(self, sex, name):
        self.sex = sex
        # 单下划线私有属性,其要求只有类本身和子类自己能访问到这些变量
		# 双下划线私有属性,其要求只有类本身能访问、子类不能访问
        self.__name = name

    @property
    def name(self):
        return self.__name

animal1 = Animal('female', 'xiaohua')
# getattr返回animal1对象的name方法的值
print(getattr(animal1, 'name'),'-getattr name')
# 也可以返回对象的属性的值
print(getattr(animal1, 'test'),'-getattr test')
# 可以添加或者修改属性
setattr(animal1, 'age', 25)
print(getattr(animal1, 'age'),'-getattr test1')
# 可以确认是否存在属性
print(hasattr(animal1, 'age'),'-hasattr(animal1, "age")')

3.私有方法,私有属性(前面加__)

私有方法无法在类外部通过调用的方法进行访问,只能通过内部调用的方法完成访问

class Test(object):
    #普通方法
    def test(self):
        self.__test2()
        print("普通方法test")
    #私有方法
    @classmethod
    def __test2(cls):
        print("私有方法__test2方法")

t = Test()
# t.__test() # 会报错,访问不了,但是能通过如下面内部的非私有方法去完成对私有方法的访问
t.test()

----------------------
私有属性同理
class Person(object):
    def __init__(self, name):
        self.__name = name
    def get_name(self):
        return self.__name

xiaoming = Person("sam")
print(xiaoming.get_name())
# print(xiaoming.__name)#会报错

二.内存管理与垃圾回收机制

1. Python 的内存管理机制及调优手段?

# https://blog.csdn.net/bylfsj/article/details/106546448

内存机制
第-1-2层主要由操作系统进行操作。

第0层是由C语言中的malloc,free等内存分配和释放函数进行内存操作

第1层则是在第0层的基础之上对其提供的接口进行了统一的封装。
(这是因为:虽然不同的操作系统都提供标准定义的内存管理接口,但是对于某些特殊的情况,不同的操作系统都有不同的行为,比如说调用malloc(0),有的操作系统会返回NULL,表示内存申请失败;然而有的操作系统会返回一个貌似正常的指针,但是这个指针所指的内存并不是有效的。为了广泛的移植性,Python必须保证相同的语义一定代表相同的运行行为。)

第2层是内存池,由Python的接口函数PyMem_Malloc函数实现。
(这是因为Python为了避免频繁的申请和删除内存所造成系统切换于用户态和核心态的开销,从而引入了内存池机制,专门用来管理小内存的申请和释放。当对象小于256K时有该层直接在内存池中分配内存,大于则退化由低层来进行分配,如由malloc函数进行分配。)

第3层是最上层,也就是我们对Python对象的直接操作。直接面向用户,它提供给我们intlist,string,dict等方法。



内存管理机制
Python中的垃圾回收是以引用计数为主,分代收集为辅。引用计数的缺陷是循环引用的问题,为了解决循环引用的问题,又有了标记-清除技术。
(在Python中,如果一个对象的引用数为0,Python虚拟机就会回收这个对象的内存)

引用计数
当一个对象的引用被创建或者复制时,对象的引用计数加1;当一个对象的引用被销毁时,对象的引用计数减1,当对象的引用计数减少为0时,就意味着对象已经再没有被使用了,可以将其内存释放掉。不过如果出现循环引用的话,引用计数机制就不再起有效的作用了,所以就会有标记-清除

标记-清除
标记-清除只关注那些可能会产生循环引用的对象,显然,像是int、stringt这些不可变对象是不可能产生循环引用的,因为它们内部不可能持有其它对象的引用。在Python中, 所有能够引用其他对象的对象都被称为容器(container)。Python中的循环引用总是发生在container对象之间,也就是能够在内部持有其它对象的对象,比如listdictclass等等。

前面提到过,循环引用使得内存无法被回收,即造成了内存泄漏。下面看一个实例:
class ClassA():
    def __init__(self, x=None):
        self.t = x
        print('object born    id:%s' % str(hex(id(self))))

def f2():
	c1=ClassA()
	c2=ClassA()
	c1.t=c2
	c2.t=c1
	del c1
	del c2
执行f2(),会产生一个循环引用,即是del c1、c2,内存还是没有被释放,如果进程中存在大量的这种情况,那么进程占用的内存会不断增大。
创建了c1,c2后,0x1a29f609390(c1对应的内存,记为内存1,0x1a29f609400(c2对应的内存,记为内存2)这两块内存的引用计数都是1,执行c1.t=c2和c2.t=c1后,这两块内存的引用计数变成2.del c1后,内存1的对象的引用计数变为1,由于不是为0,所以内存1的对象不会被销毁,所以内存2的对象的引用数依然是2,在del c2后,同理,内存1的对象,内存2的对象的引用数都是1。删除了c1,c2之后,这两个对象不可能再从程序中调用,就没有什么用处了。但是由于引用环的存在,这两个对象的引用计数都没有降到0,导致垃圾回收器都不会回收它们,所以就会导致内存泄露。


分代回收
Python同时采用了分代(generation)回收的策略。这一策略的基本假设是,存活时间越久的对象,越不可能在后面的程序中变成垃圾。将系统中的所有内存块根据其存活时间划分为不同的集合,每一个集合就成为一个“代”,Python默认定义了012三代。所有的新建对象都是0代对象,1,2代以此类推,垃圾收集的频率随着“代”的存活时间的增大而减小。如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理。当1代也经历了一定次数的垃圾回收后,那么会启动对012,即对所有对象进行扫描。


三种情况触发垃圾回收
1、调用gc.collect()
2、GC达到阀值时
3、程序退出时


调优手段
1. 手动垃圾回收
  对Python的垃圾回收进行调优的一个最简单的手段便是关闭自动回收, 根据情况手动触发. 例如在用Python开发游戏时, 可以在一局游戏的开始关闭GC, 然后在该局游戏结束后手动调用一次GC清理内存. 这样能完全避免在游戏过程中因此 GC造成卡顿. 但是缺点是在游戏过程中可能因为内存溢出导致游戏崩溃.
# 暂停自动垃圾回收
gc.disable()
# 执行一次完整的垃圾回收, 返回垃圾回收所找到无法到达的对象的数量
gc.collect()

2. 调高垃圾回收阈值
gc.set_threshold()

3. 避免循环引用(手动解循环引用和使用弱引用)
手动解循环引用指在编写代码时写好解开循环引用的代码, 在一个对象使用结束不再需要时调用. 例如:
class A(object):
    def __init__(self):
        self.child = None
    def destroy(self):
        self.child = None

class B(object):
    def __init__(self):
        self.parents = None
    def destroy(self):
        self.parents = None

a = A()
b = B()
# 循环引用
a.child = b
b.parent = a
# 解开引用
a.destroy()
b.destroy()

2.内存泄露是什么?如何避免?

程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。导致程序运行速度减慢甚至系统崩溃等严重后果。 

不使用一个对象时使用:del object 来删除一个对象的引用计数,或者想上面手动解循环引用的操作,不使用对象时候使用在函数内部写好的方法来解循环引用
通过 Python 扩展模块 gc 来查看不能回收的对象的详细信息。
gc.garbage  #gc.garbage是一个list对象,列表项是垃圾收集器发现的不可达(即垃圾对象)、但又不能释放(不可回收)的对象

三.函数

1. 函数参数

1.1 Python 函数调用的时候参数的传递方式是值传递还是引用传递?
如果函数收到的是一个可变类型就是引用传递
收到的是不可变类型就是值传递

1.2 对缺省参数的理解
定义函数时,可以给某个参数指定一个默认值,具有默认值的参数叫做缺省参数。
调用函数时,若没有传入缺省参数的值,则在函数内部使用参数默认值(注意参数只在声明的时候生成一次,后面再调用用的都是同一个)。

1.3 为什么函数名字可以当做参数用?
Python 中一切皆对象,函数名是函数在内存中的空间的引用,也是一个对象

1.4 Python 中 pass 语句的作用是什么?
在写代码的框架思路时候可以先用pass进行占位操作

1.5 有这样一段代码,print c 会输出什么,为什么?
a = 10
b = 20
c = [a]
a = 15

print(c) # [10]
列表里存的是地址,一开始c就已经记录下10的地址
1.6 交换两个变量的值?
a,b = b,a

2. 内建函数

2.1 map 函数和 reduce 函数?
map()函数接收两个参数,一个是函数,一个是序列,map将传入的函数依次作用到序列的每个元素,并把结果作为迭代器返回。
def f(x):
    return x * x
result = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
# 打印
for i in result:
    print(i,end=' ') # 1 4 9 16 25 36 49 64 81 

再看reduce的用法。reduce把一个函数作用在一个序列[x1, x2, x3...]上,而这个函数必须接收两个参数如add(x, y)reduce把结果继续和序列的下一个元素做累积计算
from functools import reduce
def add(x, y):
    return x + y
# reduce(f, [1, 3, 5, 7]) = f(f(f(1, 3), 5), 7)
result = reduce(add, [1, 3, 5, 7])
print(result)

1.从参数方面来讲
map()包含两个参数,第一个参数是一个函数,第二个是序列。其中,函数可以接收一个或多个参数。 
reduce()第一个参数是函数,第二个是序列。但是,其函数必须接收两个参数。

2.从对传进去的数值作用来讲
map()是将传入的函数依次作用到序列的每个元素,每个元素都是独自被函数“作用”一次 。 
reduce()是将传人的函数作用在序列的第一个元素得到结果后,把这个结果继续与下一个元素作用

2.2 递归函数停止的条件?
如果一个函数在内部调用自身本身,这个函数就是递归函数
递归的终止条件一般定义在递归函数内部,在递归调用前要做一个条件判断,根据判断的结果选择是继续调用自身,还是return终止递归。 
终止的条件: 
1. 判断递归的次数是否达到某一限定值 
2. 判断运算的结果是否达到某个范围等,根据设计的目的来选择

2.3 回调函数,如何通信的?
回调函数
把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数

如何通信
把回调函数的指针(地址)作为参数传递给另一个函数,把整个函数当作一个对象,传给调用的函数
2.4 Python 主要的内置数据类型都有哪些? print(dir( ‘a’)) 的输出?
内建类型:布尔类型、数字、字符串、列表、元组、字典、集合;

print(dir( 'a')) 的输出字符串a的内建方法

2.5 print(list(map(lambda x: x * x, [y for y in range(3)]))) 的输出?
print(list(map(lambda x: x * x, [y for y in range(3)]))) # [0, 1, 4]

2.6 hasattr() getattr() setattr() 函数使用详解?
hasattr(object, name)
判断一个对象里面是否有 name 属性或者 name 方法,返回 bool 值,有 name 属性(方法)返回 True,否则返回 False。注意:name 要使用引号括起来。

getattr(object, name[,default]) 
获取对象 object 的属性或者方法,如果存在则打印出来,如果不存在,打印默认值,默认值可选。 
获取属性打印值,获取方法打印内存地址

class A():
    sex = 'female'
    @classmethod
    def test(cls):
    	print('run test')
        pass

# 获取A的sex属性
print(getattr(A, 'sex')) # female
# 获取A的age属性,没有则返回默认值
print(getattr(A, 'age',18)) # 18
# 获取A的test方法,返回内存位置
print(getattr(A, 'test')) # <bound method A.test of <class '__main__.A'>>
# 可以直接运行返回的test的内存地址
getattr(A, 'test')() # run test


setattr(object,name,values)
给对象的属性赋值,若属性不存在,先创建再赋值
setattr(functiondemo, 'age', 18 )

2.7 一句话解决阶乘函数?
from functools import reduce

result = reduce(lambda x,y:x*y ,range(1,10))

3.Lambda

3.1 什么是 lambda 函数? 有什么好处?
匿名函数lambda x: x * x实际上就是:
def f(x):
    return x * x


lambda 函数是一个可以接收任意多个参数(包括可选参数)并且返回单个表达式值的函数

result = lambda x,y:x*y
print(result(4,5)) # 20

好处
1lambda 函数比较轻便,即用即仍,很适合需要完成一项功能,但是此功能只在此一处使用
2、匿名函数,一般用来给map这样的函数式服务;
3.2 下面这段代码的输出结果将是什么?请解释
def multipliers():
    # 把匿名函数放在一个列表里面进行返回,表达式分别是0*x,1*x,2*x,3*x
    # 大概是这个样子[lambda x : 0 * x,lambda x : 1 * x,lambda x : 2 * x,lambda x : 3 * x]
    return [lambda x : i * x for i in range(4)]
# 但因为闭包的原因,在调用时查找到的i值是3,所以应该是3*x,3*x,3*x,3*x
# 这里的m取出来就大概是lambda x : 3 * x这个样子,然后就赋值2
print([m(2) for m in multipliers()]) # [6, 6, 6, 6]

上述问题产生的原因是 Python 闭包的延迟绑定。这意味着内部函数被调用时,i的值在闭包内进行查找。然后在for循环结束,在return时,i的值已经是3,所以结果每个元素都是2*3=6

关于闭包延迟绑定可以通过下面的代码理解,niming1方法在声明时,只是绑定了形参i,这个时候niming1还没有调用,所以niming1(i)的i并没有值,只有到最后return [niming1(i),niming2(i),niming3(i)]的时候,因为i是main_func的变量,需要一起打包进去闭包,然而这个时候i的值已经是2了。因此当调用niming方法的时候,所有的niming方法中的i都是2了,而不是0,1,2def main_func():
    i = 0
    def niming1(i):
        return (i,'-niming1')
    i = 1
    def niming2(i):
        return (i,'-niming2')
    i = 2
    def niming3(i):
        return (i,'-niming3')
    return [niming1(i),niming2(i),niming3(i)]

result = main_func()
print(result,'result') # [(2, '-niming1'), (2, '-niming2'), (2, '-niming3')] result



解决方法:
一种解决方法就是用 Python 生成器。 
def multipliers(): 
	for i in range(4): 
		yield lambda x : i * x 

另外一个解决方案是利用默认函数立即绑定。 (我估计原因是默认参数在声明的时候被创建,所以在return前,默认参数i的值已经确定好)
def multipliers(): 
	# 就像
	# def f(x, i=i):
    # 	return i * x
	return [lambda x, i=i : i * x for i in range(4)] 

四.设计模式

1. 单例

1.1 请手写一个单例
# https://www.jianshu.com/p/6a1690f0dd00
(单例模式就是确保一个类只有一个实例.当你希望整个系统中,某个类只有一个实例时,单例模式就派上了用场.
比如,某个服务器的配置信息存在在一个文件中,客户端通过AppConfig类来读取配置文件的信息.如果程序的运行的过程中,很多地方都会用到配置文件信息,则就需要创建很多的AppConfig实例,这样就导致内存中有很多AppConfig对象的实例,造成资源的浪费.其实这个时候AppConfig我们希望它只有一份,就可以使用单例模式.)

在一个模块文件中实例化
class Singleton(object):
    def foo(self):
        pass
singleton = Singleton()

在其他文件中进行导入即可
from singleton.mysingleton import singleton

1.2 单例模式的应用场景有哪些?
单例模式应用的场景一般发现在以下条件下: 
(1)资源共享的情况下,避免由于资源操作时导致的性能或内存损耗等。如日志文件,应用配置。 
(2)控制资源的情况下,方便资源之间的互相通信。如 1.网站的计数器 2.应用配置 3.多线程池 4. 数据库配置,数据库连接池 5.应用程序的日志应用.... 

3. 装饰器

3.1 对装饰器的理解 ,并写出一个计时器记录方法执行性能的装饰器?
装饰器本质上是一个 Python 函数,它可以让原函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景。比如:插入日志、性能测试等场景,有了装饰器就可以抽离出大量的与函数功能本身无关的雷同代码并发并继续使用。

import time 
def timeit(func): 
    def wrapper(): 
        start = time.clock() 
        func() 
        end = time.clock() 
        print ('used:',end - start )
        return wrapper 
@timeit 
def foo():
    foo()
    print('in foo()')

3.2 解释一下什么是闭包?(2018-3-30-lxy)
在函数内部再定义一个函数,并且这个函数用到了外边函数的变量,那么将这个函数以及用到的一些变量称之为闭包。

4. 生成器

4.1 X = (i for i in range(10)),X 是什么类型?
X 是 generator 生成器类型,

range(10)返回的也是生成器
4.2 请尝试用“一行代码”实现将 1-N 的整数列表以 3 为单位分组,比如 1-100 分组后为?
# 二维列表推导式
result = [[x for x in range(100)][i:i+3] for i in range(1,100,3)]
print(result) # [[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.3Python 中 yield 的用法?
yield 就是保存当前程序执行状态。用 yield 的函数叫 generator,和 iterator 一样,它的好处是不用一次计算所有元素,而是每调用一次__next__()返回一次生成值,可以节省很多内存空间。

五.面向对象

2. 对象

2.1 Python 中 is 和==的区别?
is 判断的是 a 对象是否就是 b 对象,是通过 id 即内存地址来判断的。 
==判断的是 a 对象的值是否和 b 对象的值相等,是通过 value 来判断的。

2.2 Python 的魔法方法
在Python中,所有以“__”双下划线包起来的方法,都统称为魔术方法,是python内置方法,不需要主动调用,存在的目的是为了给python的解释器进行调用。

__new__(cls[, ...])
是在对象实例化的时候所调用的第一个方法

__init__(self[, ...])
构造器,当一个实例被创建的时候调用的初始化方法

__del__(self)
析构器,当一个实例被销毁的时候调用的方法

class A:
    def __del__(self):
        self.del_fun()
    def del_fun(self):
        print('run del ')

a = A()
# 销毁对象
del a

__call__ (self)
允许一个类的实例像函数一样被调用
class A:
    def __call__(self):
        print('run call')

a = A()
a()

2.3 面向对象中怎么实现只读属性?
将对象私有化,通过共有方法提供一个读取数据的接口

class Person(object):
    def __init__(self, name):
        self.__name = name
    def get_name(self):
        return self.__name

xiaoming = Person("sam")
print(xiaoming.get_name())

或者

class Person(object):
    __name = 'xixi'
    # Python内置的@property装饰器可以把类的方法伪装成属性调用的方式。也就是本来是Foo.func()的调用方法,变成Foo.func的方式
    @property
    def get_name(self):
        return self.__name
p = Person()
print(p.get_name)

2.4 谈谈你对面向对象的理解?
面向对象是相对于面向过程而言的。面向过程语言是一种以过程为中心的编程思想;而面向对象是一种是把构成问题事务分解成各个对象的程序设计思想。在面向对象语言中有一个有很重要东西,叫做类。通常会将一类事物的算法和数据结构封装在一个类之中。
面向对象有三大特性:封装、继承、多态。

https://blog.csdn.net/qq_18824345/article/details/104792097
多态
在python中,不同的对象调用同一个接口,表现出不同的状态,称为多态。要实现多态有两个前提:
1.继承:多态必须发生在父类与子类之间
2.重写:子类重写父类方法
class Animal():
    def who(self):
        print("I am an Animal")
class Duck(Animal):
    def who(self):
        print("I am a duck")

class Dog(Animal):
    def who(self):
        print("I am a dog")

class Cat(Animal):
    def who(self):
        print("I am a cat")
if __name__ == "__main__":
    duck=Duck()
    dog=Dog()
    cat=Cat()
    duck.who()
    dog.who()
    cat.who()

六.正则表达式

1. Python 里 match 与 search 的区别?

match()函数只在 string 的开始位置匹配,匹配成功的话才有返回, 
不成功的话,match()就返回 none。 

search()会扫描整个 string 查找匹配;

import re
str1 = 'str123456789'
result1 = re.match('str',str1).group(0)
print(result1,'-result1') # str -result1
result2 = re.search('456',str1).group(0)
print(result2,'-result2') # 456 -result2

2. Python 字符串查找和替换?

import re
str1 = 'str123456789str123456789str123456789'
result1 = re.findall('str',str1)
print(result1,'-result1') # int123456789int123456789int123456789 -result2
result2 = re.sub('str','int',str1)
print(result2,'-result2') # ['str', 'str', 'str'] -result1

3. 用 Python 匹配 HTML g tag 的时候,<.> 和 <.?> 有什么区别?

<.*> 是贪婪匹配,从第一个 < 到最后一个 > 中间的内容都会被匹配到
(.*?)是非贪婪匹配,从第一个 < 到后面遇到的第一个 > 就结束匹配

七.系统编程

2. 谈谈你对多进程,多线程,以及协程的理解

https://blog.csdn.net/m0_50685012/article/details/112973673

3. 什么是多线程竞争?

线程是非独立的,同一个进程里线程是数据共享的,当各个线程访问数据资源时会出现竞争状态即:数据几乎同步会被多个线程占用,造成数据混乱,即所谓的线程不安全 
那么怎么解决多线程竞争问题?-- 锁。 
锁的好处: 
确保了某段关键代码(共享数据资源)只能由一个线程从头到尾完整地执行能解决多线程资源竞争下的操作问题。 
锁的坏处: 
阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了 
锁的致命问题:死锁。

4. 解释一下什么是锁,有哪几种锁?

(Lock)是 Python 提供的对线程控制的对象。有互斥锁、可重入锁、死锁。

5. 什么是死锁呢?

若干子线程在系统资源竞争时,都在等待对方对某部分资源解除占用状态,结果是谁也不愿先解锁,互相干等着,程序无法执行下去,这就是死锁。

6. 什么是线程安全,什么是互斥锁?

线程安全
所谓线程安全,就是说对某个资源的访问在多线程状态下和单线程状态下得到相同的结果,结果不会受到线程调度等因素的影响

互斥锁
同一个进程中的多线程之间是共享系统资源的,多个线程同时对一个对象进行操作,一个线程操作尚未结束,另一个线程已经对其进行操作,导致最终结果出现错误,此时需要对被操作对象添加互斥锁,保证每个线程对该对象的操作都得到正确的结果。

互斥锁:
没上锁(运行结果不正确)
# 线程资源不安全版(没加互斥锁)
from threading import Lock,Thread
n = 10000000
def func():
    global n
    for i in range(1000000):
        n -= 1
t_lst = []
for i in range(10):
    t = Thread(target=func)
    t.start()
    t_lst.append(t)
for i in t_lst:i.join()
print(n) # 7512233


上锁(运行结果正确为0# 线程资源安全,加了互斥锁
from threading import Lock,Thread
n = 10000000
def func(lock):
    global n
    for i in range(1000000):
        # 上锁
        lock.acquire()
        n = n - 1
        # 释放锁
        lock.release()
t_lst = []
lock = Lock()
for i in range(10):
    t = Thread(target=func,args=(lock,))
    t.start()
    t_lst.append(t)
# 调用一个 Thread 的 join() 方法,可以阻塞自身所在的线程。
for i in t_lst:i.join()
print(n) # 0


GIL 锁(有时候,面试官不问,你自己要主动说,增加 b 格,尽量别一问一答的尬聊,不然最后等到的一句话就是:你还有什么想问的么?) 
GIL 锁 全局解释器锁(只在 cpython 里才有) 
作用:限制多线程同时执行,保证同一时间只有一个线程执行,所以 cpython 里的多线程其实是伪多线程!
所以 Python 里常常使用协程技术来代替多线程,协程是一种更轻量级的线程

(自己加的)线程的join() | 守护进程

join所完成的工作就是线程同步,即主线程任务结束之后,进入阻塞状态,一直等待其他的子线程执行结束之后,主线程再终止

当我们使用setDaemon(True)方法,设置子线程为守护线程时,主线程一旦执行结束,全部守护线程全部被杀掉,所以可能出现的情况就是,子线程的任务还没有完全执行结束,因为主线程执行完毕退出,就被迫停止

# https://www.cnblogs.com/cnkai/p/7504980.html
知识点一:
当一个进程启动之后,会默认产生一个主线程,因为线程是程序执行流的最小单元,当设置多线程时,主线程会创建多个子线程,在python中,默认情况下(其实就是setDaemon(False)),主线程执行完自己的任务以后,就退出了,此时子线程会继续执行自己的任务,直到自己的任务结束,例子见下面一。

知识点二:
当我们使用setDaemon(True)方法,设置子线程为守护线程时,主线程一旦执行结束,则全部线程全部被终止执行,可能出现的情况就是,子线程的任务还没有完全执行结束,就被迫停止,例子见下面二。

知识点三:
此时join的作用就凸显出来了,join所完成的工作就是线程同步,即主线程任务结束之后,进入阻塞状态,一直等待其他的子线程执行结束之后,主线程再终止,例子见下面三。

知识点四:
join有一个timeout参数:
当设置守护线程时,含义是主线程对于子线程等待timeout的时间将会杀死该子线程,最后退出程序。所以说,如果有10个子线程,全部的等待时间就是每个timeout的累加和。简单的来说,就是给每个子线程一个timeout的时间,让他去执行,时间一到,不管任务有没有完成,直接杀死。
没有设置守护线程时,主线程将会等待timeout的累加和这样的一段时间,时间一到,主线程结束,但是并没有杀死子线程,子线程依然可以继续执行,直到子线程全部结束,程序退出。

一:Python多线程的默认情况
import threading
import time

def run():
    time.sleep(2)
    print('当前线程的名字是: ', threading.current_thread().name)
    time.sleep(2)

if __name__ == '__main__':
    start_time = time.time()
    print('这是主线程:', threading.current_thread().name)
    thread_list = []
    for i in range(5):
        t = threading.Thread(target=run)
        thread_list.append(t)
    for t in thread_list:
        t.start()
    print('主线程结束!' , threading.current_thread().name)
    print('一共用时:', time.time()-start_time)

关键点:
我们的计时是对主线程计时,主线程结束,计时随之结束,打印出主线程的用时。
主线程的任务完成之后,主线程随之结束,子线程继续执行自己的任务,直到全部的子线程的任务全部结束,程序结束。

在这里插入图片描述

二:设置守护线程
import threading
import time

def run():
    time.sleep(2)
    print('当前线程的名字是: ', threading.current_thread().name)
    time.sleep(2)

if __name__ == '__main__':
    start_time = time.time()
    print('这是主线程:', threading.current_thread().name)
    thread_list = []
    for i in range(5):
        t = threading.Thread(target=run)
        thread_list.append(t)
    for t in thread_list:
        t.setDaemon(True)
        t.start()
    print('主线程结束了!' , threading.current_thread().name)
    print('一共用时:', time.time()-start_time)

其执行结果如下,注意请确保setDaemon()在start()之前
关键点:
非常明显的看到,主线程结束以后,子线程还没有来得及执行,整个程序就退出了。

在这里插入图片描述

三:join的作用
import threading
import time

def run():
    time.sleep(2)
    print('当前线程的名字是: ', threading.current_thread().name)
    time.sleep(2)


if __name__ == '__main__':
    start_time = time.time()
    print('这是主线程:', threading.current_thread().name)
    thread_list = []
    for i in range(5):
        t = threading.Thread(target=run)
        thread_list.append(t)
    for t in thread_list:
        t.setDaemon(True)
        t.start()
    for t in thread_list:
        t.join()
    print('主线程结束了!' , threading.current_thread().name)
    print('一共用时:', time.time()-start_time)

关键点:
可以看到,主线程一直等待全部的子线程结束之后,主线程自身才结束,程序退出。
其执行结果如下:

加粗样式

7. 说说下面几个概念:同步,异步,阻塞,非阻塞?

1.同步与异步同步和异步关注的是消息通信机制。所谓同步,就是在发出一个*调用*时,在没有得到结果之前,该*调用*就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由*调用者*主动等待这个*调用*的结果。同步就是必须一件一件事做,等前一件做完了才能做下一件事。而异步则是相反,*调用*在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在*调用*发出后,*被调用者*通过状态、通知来通知调用者,或通过回调函数处理这个调用。
举个通俗的例子:你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。

2. 阻塞与非阻塞阻塞和非阻塞关注的是程序在等待调用结果时的状态.阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。还是上面的例子,你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。

8. 什么是僵尸进程和孤儿进程?怎么避免僵尸进程?

孤儿进程:父进程退出,子进程还在运行的这些子进程都是孤儿进程,孤儿进程将被systemd 进程(pid为1)所收养,并由systemd(centos7,init是centos6的) 进程对它们完成状态收集工作。 

僵尸进程:子进程退出,而父进程并没有调用 wait 或 waitpid 获取这些子进程的状态信息并回收子进程的资源,那么这些子进程就是僵尸进程会对系统pid进行无效地占用(一个人死了,没有注销身份证号-僵尸)。

避免僵尸进程的方法:
1.手动杀死父进程,让孤儿进程被systemd托管
2.就可以调用 fork() 两次。让子进程再去创建一个孙子进程,然后子进程立即退出,在父进程中等待子进程的退出,由于子进程马上就退出了,所以父进程不会阻塞很长时间就会立即返回,接着指向父进程该执行的任务;接着说孙子进程,由于子进程已经退出,所以孙子进程此时已经变成孤儿进程,会由 init 进程领养,不可能会变成 僵尸进程。
2.用wait()函数清除但父进程会阻塞等待,直到子进程退出; 
3.使用信号量,在signal handler中调用 waitpid,这样父进程不用阻塞(https://blog.csdn.net/ZHYFXY/article/details/64505205----以下是方便理解的-------
问:那为什么子进程要把PCB残留在内核里呢? 

答:因为子进程终止后,它会把终止信号等退出状态(不管正常终止还是异常终止都对应一个信号)保存在内核的PCB里面,只有这个子进程的父亲节调用wait或者waitpid获取这些退出状态,然后才会彻底清除掉这个子进程。如果父进程不调用wait或者waitpid,那么这个子进程就会成为僵尸进程。

问:什么方法可以清除掉一个僵尸进程。(附:kill命令清除不了僵尸进程的,因为kill命令只是用来终止进程的,而僵尸进程已经是终止的了) 

答:kill确实是直接清除不掉僵尸进程,但是我们可以kill掉僵尸进程的父进程,这样僵尸进程的父进程就变为init进程,init进程自然会调用wait或者waitpid清除这个僵尸进程。 

9. Python 中的进程与线程的使用场景?

多进程适合在 CPU 密集型操作(cpu 操作指令比较多,如位数多的浮点运算)。 
多线程适合在 IO 密集型操作(读写数据操作较多的,网络请求,比如爬虫)

10.线程是并发还是并行,进程是并发还是并行?

线程是并发,进程是并行
进程之间相互独立,是系统分配资源的最小单位,同一个线程中的所有线程共享资源。

11.并行(parallel)和并发(concurrency)?

并行:同一时刻多个任务同时在运行。 
并发:在同一时间间隔内多个任务都在运行,但是并不会在同一时刻同时运行,存在交替执行的情况。
实现并行的库有:multiprocessing 
实现并发的库有:threading 
程序需要执行较多的读写、请求和回复任务的需要大量的 IO 操作,IO 密集型操作使用并发更好。 
CPU 运算量大的程序程序,使用并行会更好。

12.IO 密集型和 CPU 密集型区别?

计算密集型(CPU密集型)任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。

计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。

第二种任务的类型是IO密集型,涉及到网络、磁盘读写IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。

IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。

13.开3个线程按照顺序打印ABC 10次。正好是 Condition 的使用场景

# https://python-parallel-programmning-cookbook.readthedocs.io/zh_CN/latest/chapter2/09_Thread_synchronization_with_a_condition.html
# -*- coding: utf-8 -*-

"""
Three threads print A B C in order.
"""


from threading import Thread, Condition

condition = Condition()
current = "A"


class ThreadA(Thread):
    def run(self):
        global current
        for _ in range(10):
            with condition:
                while current != "A":
                    condition.wait()
                print("A")
                current = "B"
                condition.notify_all()


class ThreadB(Thread):
    def run(self):
        global current
        for _ in range(10):
            with condition:
                while current != "B":
                    condition.wait()
                print("B")
                current = "C"
                condition.notify_all()


class ThreadC(Thread):
    def run(self):
        global current
        for _ in range(10):
            with condition:
                while current != "C":
                    condition.wait()
                print("C")
                current = "A"
                condition.notify_all()


a = ThreadA()
b = ThreadB()
c = ThreadC()

a.start()
b.start()
c.start()

a.join()
b.join()
c.join()
# 另一种按顺序执行并同步主线程的方法
import threading
import time
from threading import Thread, Event


class Timer(Thread):  # 继承自Thread类
    """Call a function after a specified number of seconds:

            t = Timer(30.0, f, args=None, kwargs=None)
            t.start()
            t.cancel()     # stop the timer's action if it's still waiting
    """
    def __init__(self, interval, function, args=None, kwargs=None):  # 初始化的时候传参是延迟时间、调用的函数,函数的可变位置参数、函数的可变关键字参数
        Thread.__init__(self)  # 调用Thread类初始化配置实例
        self.interval = interval  # 在使用Thread类初始化配置实例之后再额外的增加interval属性
        self.function = function  # 同理再额外的增加function属性
        self.args = args if args is not None else []   # 如果args不是空的话就使用args,如果是空就给一个空list
        self.kwargs = kwargs if kwargs is not None else {}  # 同理,kwargs不是空的就是kwargs,如果是空就给一个空字典
        self.finished = Event()  # 再添加一个属性finished,是一个Event类的实例,这里知道Event类的实例用法就知道它在这里要怎么用了

    def cancel(self):
        """如果finished属性还没有被set,即函数function还没有被调用的之前阻止,因为下面函数调用之前会判断finished是否被set了,所以这里赶在调用之前注定set就能够阻止后面的调用。"""
        """Stop the timer if it hasn't finished yet."""
        self.finished.set()

    # 继承自Thread类,并且重写了Thread类,我们分析Thread类的源码会发现,start()方法会主动调用self.run()
    # 我们Timer类没有实现start()方法,这样Timer类实例在执行start()的时候会跑到父类Thread上,然后调用父类的start,
    # 在父类的start()方法中会有一句self.run()来调用工作线程中的函数,这里self是Timer的实例,所以,这里可以重写run就可以设定run的时间了。
    def run(self):
        # 这里使用Event类的实例的wait方法,等待了我们设定的self.interval时间,然后关键点是下面一句
        self.finished.wait(self.interval)
        # 这一句是关键点,检查一下是否被set了,如果没有被set了就调用传入的函数,如果被set了有两种情况:
        # 第一种情况是在self.finished.wait(self.interval)的期间,我们调用cancel主动提前set了;
        # 第二种情况是已经start()过一次了,这里就不能再进行start了这样就和父类的保持了一致:即一个线程只能够start一次
        if not self.finished.is_set():
            self.function(*self.args, **self.kwargs)
        # 调用完成后set,即便之前已经被set了,这里还可以被set,因为Event实例可以被set多次。
        self.finished.set()



def DetectThread(Id):
    global timer
    if Id == 0:
        t1 = time.time()
        time_local = time.localtime(t1)
        dt = time.strftime("%Y.%m.%d %H:%M:%S", time_local)
        print('执行线程1,执行时间:', dt)
        timer = threading.Timer(2, DetectThread, (1,))
        timer.start()
    if Id == 1:
        t2 = time.time()
        time_local = time.localtime(t2)
        dt = time.strftime("%Y.%m.%d %H:%M:%S", time_local)
        print('执行线程2,执行时间:', dt)
        timer = threading.Timer(2, DetectThread, (2,))
        timer.start()
    if Id == 2:
        time_local = time.localtime(time.time())
        dt = time.strftime("%Y.%m.%d %H:%M:%S", time_local)
        print('执行线程3,执行时间:', dt)
timer = threading.Timer(2, DetectThread, (0,))
timer.start()

第四章 爬虫

一.常用库与模块

1. 试列出至少三种目前流行的大型数据库的名称:_________

Mysql、redis、MongoDB

2. 列举您使用过的 Python 网络爬虫所用到的网络数据包?

requests、urllib(urllib.parse.quote(q)

3. 列举您使用过的 Python 网络爬虫所用到的解析数据包

parsel(css)、BeautifulSoup

4. 爬取数据后使用哪个数据库存储数据的,为什么?

我们能用的数据库很多,老牌关系型数据库如 MySQL, PostgreSQL 等,新型的NoSQL数据库,还有NewSqL数据库。选择实在太多,但MySQL从易获取性、易使用性、稳定性、社区活跃性方面都有较大优势,所以,我们在够用的情况下都选择MySQL。
Python对MySQL操作的模块最好的两个模块是:
1. MySQLdb
这是一个老牌的MySQL模块,它封装了MySQL client的C语言API,但是它主要支持Python 2.x的版本
2. PyMySQL
这是纯Python实现,它和Python3的异步模块aysncio可以很好的结合起来,形成了aiomysql 模块,可以对异步爬虫时就可以对数据库进行异步操作了。所以选择了PyMySQL来作为我们的数据库客户端模块。后来在github看到一个大神的对pymysql进行的封装叫ezpymysql,基本上是对查询和插入,基本的select, insert这些sql语句的一些操作改变,过度的封装很不适合爬虫应用场景。

5. 你用过的爬虫框架或者模块有哪些?谈谈他们的区别或者优缺点?

Python 自带:urllib、urllib2 
第三方:requests 
框架: Scrapy 


urllib库,是python内置的http标准请求库,但有请求参数类型等处理的问题,使用较不方便,我现在用urllib主要是用来做一些url的解析处理,比如说urlparse做host的提取,urljoin做url的合并
import urllib.parse as urlparse

url = 'https://blog.csdn.net/fengxinlinux/article/details/77281253'
host = urlparse.urlparse(url).netloc
print(host,'-host') # blog.csdn.net -host
urlparse_result = urlparse.urlparse(url)
print(urlparse_result,'-urlparse_result') # ParseResult(scheme='https', netloc='blog.csdn.net', path='/fengxinlinux/article/details/77281253', params='', query='', fragment='') -urlparse_result

url = 'https://blog.csdn.net/fengxinlinux/article/details'
link = '/article/details/77281253'
urljoin = urlparse.urljoin(url, link)
print(urljoin,'urljoin') # https://blog.csdn.net/article/details/77281253 urljoin

request 是基于urllib做的封装,功能更健全,代码量更少,是一个非常强大的网络请求库,使用非常方便,高并发与分布式部署也非常灵活,对于功能可以更好实现。

scrapy 是内部基于twisted的方式实现好异步的,基于多线程(默认10个线程),拥有良好的并发性能,能胜任数据量较大的爬虫任务,强大的统计和 log 系统,而且对千万级URL去重支持很好。
scrapy 缺点:
对于固定单个网站的爬取开发,有优势,但是对于多网站爬取,并发及分布式处理方面,不够灵活,不便调整与括展。因为基于 twisted 框架,运行中出错也不会停掉reactor,数据出错后难以察觉。

6. 写爬虫是用多进程好?还是多线程好? 为什么?

IO 密集型代码(文件处理、网络爬虫等),多线程能够有效提升效率(单线程下有 IO 操作会进行 IO 等待,造成不必要的时间浪费,而开启多线程能在线程 A 等待时,自动切换到线程 B,可以不浪费 CPU的资源,从而能提升程序执行效率)。在实际的数据采集过程中,既考虑网速和响应的问题,也需要考虑自身机器的硬件情况,来设置多进程或多线程。

7. 常见的反爬虫和应对方法?

通过 Headers 反爬虫: 
从用户请求的 Headers 反爬虫是最常见的反爬虫策略。很多网站都会对 Headers 的 User-Agent进行检测,还有一部分网站会对 Referer 进行检测(一些资源网站的防盗链就是检测 Referer),还有像淘宝、美团之类header里面还有加密参数需要校验的,一般对于检测 Headers 的反爬虫,通过fiddler或者charles的重放就能较快对比出什么是关键请求参数,在爬虫中修改或者添加修改关键参数就能很好的绕过。简单的反爬虫机制,可以直接在爬虫中添加 Headers,将浏览器的 User-Agent 复制到爬虫的Headers 中,或者用fake_useragent生成随机UA;或者将 Referer 值修改为目标网站域名。加密的参数则要要代码里分析加密逻辑。

基于用户行为反爬虫。
还有一部分网站是通过检测用户行为,例如同一 IP 短时间内多次访问同一页面,或者同一账户短时间内多次进行相同操作。大多数网站都是前一种情况,对于这种情况,使用 IP 代理就可以解决。但使用代理IP有一个很大的问题就是,费用非常高。现在我在用的是自己搭建的VPS服务器,将爬虫直接部署在服务器上,写程序自动拨号切换IP,平均3分钟切换一个IP来算一天要480个ip,用付费代理的话一天就差不多20块,一个月要600。搭建个VPS一个月才100多一点,节约成本。而且拨号的IP更干净一点,买的代理会比较脏。
对于第二种情况,可以在每次请求后随机间隔几秒再进行下一次请求。有些有逻辑漏洞的网站,可以通过请求几次,退出登录,重新登录,继续请求来绕过同一账号短时间内不能多次进行相同请求的限制。又或者如果有多个账号就切换使用,效果更好。

动态加载的反爬虫: 
需要爬取的数据是通过 ajax 请求得到,或者通过 JavaScript 动态生成的。首先用谷歌开发者或者 Fiddler 抓包,找到目标请求包对里面的参数进行分析构造。通过requests模拟 ajax 请求,对响应的 json 进行分析得到需要的数据。有些网站把 ajax 请求的所有参数全部加密了,就需要通过参数搜索或者一些逆向手段去找到加密参数生成的位置,在本地用代码实现逻辑生成加密参数请求。
在加密参数生成十分复杂,考虑开发成本的情况下,也可以使用selenium控制浏览器进行动态抓取,模拟人为操作以及触发页面中的 js 脚本,从而获取数据

8. 解析网页的解析器使用最多的是哪几个?

import parsel

save_html_css = parsel.Selector(html)
content = save_html_css.css('.reviews-items')

9. 需要登录的网页,如何解决同时限制 ip,cookie,session(其中有一些是动态 生成的)在不使用动态爬取的情况下?

解决限制 IP 可以使用代理IP、搭建VPS服务器拨号
cookie先试试能不能复用,时效性如何,可通过selenium登录账号获取cookies,或js分析他生成方式

10. 验证码的解决?

图形验证码:干扰、杂色不是特别多的图片可以使用开源库pytesseract进行二值化、降噪操作进行识别,或者调用百度的开发者平台。难一点的考虑用超级鹰或者菲菲打码

滑动验证码:简单的通过PIL库的image对比原图和需要滑动的图就能得出需要滑动的距离,用selenium通过匀加速,划过目标位置,再慢慢回到目标位置的拖拽方式通过。不过现在selenium要魔改一下不然容易被识别出来,或者使用pyppeteer,也可以通过js分析滑块的代码。

11. 使用最多的数据库(Mysql,Mongodb,redis 等),对他们的理解?

MySQL 数据库:开源免费的关系型数据库,需要实现创建数据库、数据表和表的字段,表与表之间可以进行关联(一对多、多对多),是持久化存储。 
Mongodb 数据库:是非关系型数据库,数据库的三元素是,数据库、集合、文档,可以进行持久化存储,也可作为内存数据库,存储数据不需要事先设定格式,数据以键值对的形式存储。
redis 数据库:非关系型数据库,使用前可以不用设置格式,以键值对的方式保存,文件格式相对自由,主要用与缓存数据库,也可以进行持久化存储。 

12. 字符集和字符编码

字符是各种文字和符号的总称
字符集容纳不同数量的文字和符号的字符编码方案,常见的字符集有:unicode、GB2312、GB18030、GBK、ASCII等
字符编码是将字符集中的数字转换到程序数据的编码方案,unicode有utf-8,utf-16,utf-32,utf8mb4(兼容一些emoji的编码)。其他的字符集一般只有一种字符编码方式,像ASCII 既是编码字符集,又是字符编码(早期的计算机系统只能处理英文,所以ASCII对汉字支持不友好),然后支持的字符集量是GB18030>GBK>GB2312

13. 写一个邮箱地址的正则表达式 | 网页中html的正则表达式?

import re
email_str = '=465072111@qq.com,'
# \w:表示一个字 [0-9a-zA-Z_]
# \W:表示除[0-9a-zA-Z_]之外的字符
email = re.search('\w+@\w+\.\w+',email_str).group(0)
print(email)

网页中html的正则表达式
html1 = '''
<ul id="main-list" class="main-list fl"><li class="disabled active" data-id="1-0">
      <a href="https://news.qq.com/" target="">要闻</a>

    </li><li class="item " data-id="1-1">
      <a href="https://new.qq.com/ch/antip/" target="">抗肺炎 </a>
      <i></i>
    </li><li class="item " data-id="1-2">
      <a href="https://new.qq.com/d/bj/" target="">北京</a>
      <i></i>
    </li><li class="item " data-id="1-3">
      <a href="https://new.qq.com/ch/ent/" target="">娱乐</a>
      <i></i>
'''

# 常见格式<a href="https://new.qq.com/ch/ent/" target="">娱乐</a>
# 我的 re.I	使匹配对大小写不敏感 re.M 多行匹配 re.S 使.匹配包括换行在内的所有字符
g_pattern_tag_a = re.compile(r'<a href=[\"|\'](.*?)[\"|\']', re.I|re.S|re.M)

# 平哥的
g_pattern_tag_a = re.compile(r'<a[^>]*?href=[\'"]?([^> \'"]+)[^>]*?>(.*?)</a>', re.I|re.S|re.M)
aa = g_pattern_tag_a.findall(html1)
for a in aa:
    print(a)

14. 编写过哪些爬虫中间件?

# https://blog.csdn.net/freeking101/article/details/88040929

下载中间件:
更换代理
class ProxyMiddleware(object):
 
    def __init__(self):
        self.settings = get_project_settings()
 
    def process_request(self, request, spider):
        proxy = random.choice(self.settings['PROXIES'])
        request.meta['proxy'] = proxy

更换UA
class UAMiddleware(object):
 
    def process_request(self, request, spider):
        ua = random.choice(settings['USER_AGENT_LIST'])
        request.headers['User-Agent'] = ua

更换cookies
class LoginMiddleware(object):
    def __init__(self):
        self.client = redis.StrictRedis()
    
    def process_request(self, request, spider):
        if spider.name == 'loginSpider':
            cookies = json.loads(self.client.lpop('cookies').decode())
            request.cookies = cookies
            
捕获异常并处理
# https://blog.csdn.net/sc_lilei/article/details/80702449
scrapy爬取中遇到的错误包括但不限于:download error, http code(403/500),我们可以从from scrapy.downloadermiddlewares.retry import RetryMiddleware,里面process_response里面的retry_http_codes定义了retry的状态码和处理方式。然后对于download error可以同样通过RetryMiddleware的process_exception方法里来进行处理,这个方法里的ALL_EXCEPTIONS包括了一些错误类型和处理方法

15. “极验”滑动验证码如何破解?

# 渣总的讲解 https://virjar-comon.oss-cn-beijing.aliyuncs.com/02_%E6%9E%81%E9%AA%8C%E6%BB%91%E5%9D%97.mp4
# github上的 https://github.com/FanhuaandLuomu/geetest_break
# github上的轨迹库 https://github.com/selfshore/spiders/blob/master/%E6%9E%81%E9%AA%8C%E6%BB%91%E5%8A%A8/track.py
# 简书上的 https://www.jianshu.com/p/3f968958af5a
# CSDN上的下载极验本地demo等操作 https://blog.csdn.net/qq_26877377/article/details/80452086
# 极验破解的PDF file:///E:/1A%E7%88%AC%E8%99%AB%E8%AF%BE%E7%A8%8B%20%E5%B9%B3%E5%93%A5/%E7%88%AC%E8%99%AB%E8%AF%BE%E7%A8%8B%20%E5%B9%B3%E5%93%A5/%E5%8F%B6%E6%A3%AE/%E7%8C%BF%E4%BA%BA%E5%AD%A6-%E7%88%AC%E8%99%AB%E8%BF%9B%E9%98%B6%E8%AF%BE/%E6%BB%91%E5%9D%97%E8%BD%A8%E8%BF%B9%E8%AE%B0%E5%BD%95/%E6%9E%81%E9%AA%8C%E9%AA%8C%E8%AF%81%E7%A0%81%E7%A0%B4%E8%A7%A3.pdf

计算缺口位置,使用 Selenium 自动化测试工具,模拟人类手动拖动滑块的过程。这种方法实现较为简单,但存在着两个缺点:一是模拟滑动,容易被极验检测到我们使用的是自动化软件,从而导致滑动操作失败,二是效率低

还有一种死磕 JavaScript 代码,破解每个请求中的加密参数,之后在程序中发送请求得到正确响应

在这里插入图片描述

16. 爬的那些内容数据量有多大,多久爬一次,爬下来的数据是怎么存储?

爬取的新闻数据大概每天50万,爬下来的数据存入mysql数据库。每隔一段时间就上传到服务器给后端那边。多久爬一次这个问题要根据公司的要求去处理,不一定是每天都爬。但是像新闻这种现在基本上是一直挂着

17. cookie 过期的处理问题?

因为 cookie 存在过期的现象,一个很好的处理方法就是做一个异常类,如果有异常的话 cookie 抛出异常重新获取cookies进行请求。

18. 动态加载又对及时性要求很高怎么处理?

尽量不使用 sleep 而使用 WebDriverWait
禁用图片加载

19. HTTPS 有什么优点和缺点

优点:
1、使用 HTTPS 协议可认证用户和服务器,确保数据发送到正确的客户机和服务器;
2、HTTPS 协议是由 SSL+HTTP 协议构建的进行加密传输、身份认证的网络协议,要比http协议安全,可防止数据在传输过程中被窃取、改变,确保数据的完整性。 
3、HTTPS 是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本

缺点:
1、HTTPS 连接对服务器端的资源占用高很多,握手阶段比较费时对网站的相应速度有负面影响。
2、SSL 证书需要钱。功能越强大的证书费用越高。个人网站、小网站没有必要一般不会用
3、HTTPS 连接缓存不如 HTTP 高效,影响缓存,增加数据开销和功耗,甚至已有安全措施也会受到影响也会因此而受到影响

20. HTTPS 是如何实现安全传输数据的

HTTPS相对于HTTP有哪些不同呢?其实就是在HTTP跟TCP中间加多了一层加密层TLS/SSL。SSL是个加密套件,负责对HTTP的数据进行加密。TLS是SSL的升级版。现在提到HTTPS,加密套件基本指的是TLS。

传输加密的流程
原先是应用层将数据直接给到TCP进行传输,现在改成应用层将数据给到TLS/SSL,将数据加密后,再给到TCP进行传输。

# https://www.cnblogs.com/kerwincui/p/14179509.html
# 证书包括:证书内容,散列算法,加密密文
# CA如何生成证书: 证书内容-->散列算法-->hash值-->CA机构提供的私钥对hash值进行RSA加密-->加密密文

# 客户端如何校验证书(通过对比计算得出的hash值):
# 客户端-->加密密文-->CA公钥-->hash值
# 客户端-->证书内容-->散列算法-->hash值

加密过程(单向认证)
客户端向服务端发送SSL协议版本号、加密算法种类等信息;
服务端给客户端返回返回服务器端公钥证书等信息
客户端使用服务端返回的信息验证服务器的合法性
验证通过后,将继续进行通信,否则,终止通信;
客户端向服务端发送自己所能支持的对称加密方案,供服务器端进行选择;
服务器端在客户端提供的加密方案中选择加密程度最高的加密方式,并将选择好的加密方案通过明文方式返回给客户端;
客户端接收到服务端返回的加密方式后,使用该加密方式生成产生随机码,用作通信过程中对称加密的密钥,使用服务端返回的公钥进行加密,将加密后的随机码发送至服务器;
服务器收到客户端返回的加密信息后,使用自己的私钥进行解密,获取对称加密密钥;
在接下来的会话中,服务器和客户端将会使用该密码进行对称加密,保证通信过程中信息的安全;

在这里插入图片描述

21. TTL,MSL,RTT?

MSL:报文最大生存时间”,他是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。实际应用中常用的是 30 秒,1 分钟和 2 分钟等。

TTL:TTL 是 time to live 的缩写,中文可以译为“生存时间”,这个生存时间是由源主机设置初始值但不是存的具体时间,而是存储了一个 ip 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。

RTT: RTT 是客户到服务器往返所花时间(round-trip time,简称 RTT),TCP 含有动态估算 RTT的算法。

22. 谈一谈你对 Selenium 和 Pyppeteer 了解?

Selenium 是一个 Web 的自动化测试工具,可以根据我们的指令,让浏览器自动加载页面,获取需要的数据,甚至页面截屏,或者判断网站上某些动作是否发生。Selenium 自己不带浏览器,它需要与第三方浏览器结合在一起才能使用。但是我们有时候需要让它内嵌在代码中运行。在使用的时候能通过WebDriverWait、不加载图片,无头运行等方式减少延时,提高抓取效率和稳定性。

pyppeteer是基于谷歌开发的一块自动化测试工具的封装,使用的是chromium浏览器。基于 asyncio 构建,它的所有 属性 和方法 几乎都是 coroutine (协程) 对象,因此在构建异步程序的时候非常方便,天生就支持异步运行。

23.我的新闻爬虫框架

在这里插入图片描述

待分配问题

对于分布式爬虫,有做过吗,一天最大爬取量是多少
做爬虫一般用哪种数据库,可以说说为什么要用这种数据库吗,以及它和其他数据库的区别?
ip 代理池,你是怎么搭建的?
遇到账号封禁,你是怎么解决的?
说说你是怎么解决验证码这一块的逆向,思路,还有具体一点的步骤。

对于 app 抓包你是怎么处理的?
字体反爬你是怎么处理的?
处理爬虫这一块的数据,你是怎么处理的,例如数据的重复性,异常值

增量抓取的策略

对于风控,你是怎么处理的?

平常自己做过算法类的模型吗?

说说你做过项目中你认为最难的地方。

说说你遇到过最有成就感的逆向。

在反爬过程中,一般你遇到解决不了的问题,你会通过什么方式解决?

进程间和线程间的通信方式

一些算法内容:不用sort进行排序

Git工具的使用

mysql leftjoin rightjoin unionjoin的区别

redis的数据结构

asyncio和aiohttp
https://www.jianshu.com/p/87cd240dbb6b(很好的文章,很值得一看)

lzma
lz系列有很多,主要有lz77,lz78,lzma,基本思想是一样的,都是一种字典编码,如,我有一段文本,里面有“abcdefgabcde”,那么后面的abcde并没有必要,可以用前面的替代,所以可以存储为“abcdefg65”,6代表offset,5代表length,即用距离当前位置6个字节长度为5的字节串代表当前字节串,由此就减少了三个字节。
https://wenku.baidu.com/view/1a9acaa472fe910ef12d2af90242a8956aecaa48.html?_wkts_=1674186353456&bdQuery=lzma

nvloop
https://blog.csdn.net/whatday/article/details/106900142

framhash和hash
https://www.biaodianfu.com/string-hash.html(比较详细,fram是2014年的,md5和sha1是基于md4的是上个世纪的产物)
关于hash算法和加密算法,这个很深刻

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值