Python 面试问题总结

一. 进程和线程

1. 简述线程,进程,协程的区别?

线程:

操作系统能够进行运算调度的最小单位。 它包含在进程之中,是进程的实际运作单位。 一条线程指的是进程中一个单一顺序的控制流, 一个进程中可以并发多个线程,每一条线程并行执行不同的任务。

进程:
对一堆资源的整合。 比如说QQ就是一个进程。
目的:最大限度的利用CPU,节省时间。

从操作系统角度来讲,进程是资源分配单元,线程是执行单元,多个线程可以共享所在进程的资源。

协程:
从程序运行角度出发,是由用户(程序)控制和调度的一个过程。

多线程并不会充分调用两个CPU,而是会在一个CPU上充分运转;
而多进程则是会完全调用两个CPU,同时执行;

并行:多个处理器同时处理多个任务

并发:一个处理器同时交替处理多个任务

2. 简述线程同步和异步的区别?

同步:指一个线程需要主动等待上一个线程执行完之后才开始执行。

异步:指一个线程不需要主动等待上一个线程执行完之后就开始执行。

区别关键是 需不需要主动等待

3. python 如何开启一个进程?

知识点一:进程的理论

什么是进程?

进程是指一个程序在一个数据集上的动态执行过程(程序执行过程的抽象)

进程包含

  • 程序:我们通过程序来描述一个进程所要执行的内容以及如何执行

  • 数据集:数据集代表程序在执行过程中所需要的资源

  • 进程控制块:用于描述进程的外部特征,记录进程的执行过程,系统可以用来控制/管理进程,也是操作系统感知进程存在的唯一标识

进程运行的的三种状态:

在这里插入图片描述

  1. 进程执行过程中出现IO,进入阻塞状态(操作系统强行剥夺CPU)
  2. 进程占用CPU时间过长/出现一个优先级更高的进程,进入就绪状态(CPU被剥夺)
  3. 就绪态的进程被分配到CPU,进入运行状态
  4. 阻塞态的进程出现有效输入,进入就绪态,等待操作系统分配CPU

此外,我们能直接控制的只有阻塞状态,减少阻塞,使进程尽可能的保持在就绪状态,提高效率

知识点二:开启进程的两种方式

开启进程:开启进程就是将父进程里面串行执行的程序放到子进程里,实现并发执行,这个过程中,会将父进程数据拷贝一份到子进程。
运行角度:是2个进程
注意:子进程内的初始数据与父进程的一样,如果子进程被创建或者被运行了,那么
   子进程里面数据更改对父进程无影响,2个进程是存在运行的

方式一:通过调用multiprocessing模块下面的Process类方法

from multiprocessing import Process
 
def func(name):
    print('%s is running...' % name)
    print('%s is ending...' % name)
 
 
if __name__ == '__main__':
    p = Process(target=func, args=('子进程',))  
	# 如果只有一个参数,args括号内一定要加逗号,确保以元组的形式传入
	# 这一步:只是在向操作系统发我要开启一个子进程的信号(具体开子进程的操作是由操作系统来完成的)
    p.start() 
	# 只是主进程给操作系统发送建立子进程的请求,并非立刻建立子进程
    print('主进程')

运行结果:主进程
		子进程 is running...
		子进程 is ending...

方式二:借助process类,自定义一个类(继承Process),从而创造一个对象

定义process类的子类,并重写该类的run()方法,run()方法的中写进程需要完成的任务。

from multiprocessing import Process

class MyProcess(Process):     # 继承Process类
    def run(self):  # run名字是固定的,不能更改
        print("%s is running" % self.name)  # 默认函数对象有name方法 ,结果为:Myprocess-1
        print('%s is done' % self.name)

if __name__ == '__main__':
    obj = MyProcess()
    obj.start()  # 本质上是在调用父类的start方法,而start方法下会触发run方法
    print('主进程')
运行结果:主进程
		MyProcess-1 is running
		MyProcess-1 is done

为什么开启进程要在main内执行?

由于在windows系统下,子进程是通过导入模块的方式拿到父进程的代码,如果没有main会一直开启子进程,

而子进程的申请是需要开辟内存以及申请pid等的。

4. python 如何开启一个线程?

方式一:通过thread类直接创建

import threading

def foo(n):
    print('%s'%n)

def bar(n):
    print('%s'%n)

print('33')   # 主线程

t1 = threading.Thread(target=foo, args=(1,))    # 线程一
t2 = threading.Thread(target=bar, args=(2,))    # 线程二
t1.start()
t2.start()
# 创建线程,第一个参数是函数名字(不加括号,加括号就执行函数了)。
# 第二个参数是要传给函数的参数,以元组的形式。
# 创建线程之后, obj.start()启动线程。
执行结果:33
		1
		2		  

方式二:继承 Thread 类:

定义Thread类的子类,并重写该类的run()方法,run()方法的中写线程需要完成的任务。

import threading

# 定义MyThread类,其继承自threading.Thread这个父类
class MyThread(threading.Thread):
   def __init__(self):
     threading.Thread.__init__(self)
   def run(self):
     print("start t1")
     print("end t1")

t1=MyThread()  # 对类进行实例化
t1.start()   # 启动线程
print("主线程")
运行结果:start t1
		end t1
		主线程

补充:Thread 类的一些常用方法

  • join(): 在子线程完成之前,主线程将一直被阻塞
  • setDaemon(True) 方法: 将进程声明为守护线程,必须在start()方法调用之前

5. 线程之间如何通信?

方式一:threading.Event 事件

Python提供了非常简单的通信机制 Threading.Event,通用的条件变量(event.isSet==False/True)。多个线程可以等待某个事件的发生,在事件发生后,所有的线程都会被激活

关于Event的使用,就四个函数:

event = threading.Event()

  • event.isSet() # 返回event的状态值。
  • event.wait() # 等待接收event的状态值,决定是否阻塞程序执行。如果event.isSet==False,将阻塞线程。
  • event.set() # 设置event的状态值为True,使所有设置该event事件的线程执行。
  • event.clear() # 重置event的状态值为False,使得所有该event事件都处于待命状态。

举个例子:

import time
import threading

class MyThread(threading.Thread):
    def __init__(self, name, event):
        super().__init__()
        self.name = name
        self.event = event

    def run(self):
        print('Thread: {} start at {}'.format(self.name, time.ctime(time.time())))
        # 等待event.set()后,才能往下执行
        self.event.wait()
        print('Thread: {} finish at {}'.format(self.name, time.ctime(time.time())))

threads = []
event = threading.Event()

# 定义三个线程,使用event事件
[threads.append(MyThread(str(i), event)) for i in range(1, 4)]

# 重置event,使得event.wait()起到阻塞作用
event.clear()

# 启动所有线程
[t.start() for t in threads]

print('等待5s...')
time.sleep(5)

print('唤醒所有线程...')
event.set()     
执行结果:Thread: 1 start at Fri Jul 12 15:05:50 2019
		Thread: 2 start at Fri Jul 12 15:05:50 2019
		Thread: 3 start at Fri Jul 12 15:05:50 2019
		等待5s...
		唤醒所有线程...
		Thread: 3 finish at Fri Jul 12 15:05:55 2019
		Thread: 2 finish at Fri Jul 12 15:05:55 2019
		Thread: 1 finish at Fri Jul 12 15:05:55 2019

可见在所有线程都启动 start() 后,并不会执行完,而是都在self.event.wait()阻塞了,需要通过event.set()来给所有线程发送执行指令才能往下执行。

方式二:threading.Condition条件

Condition和Event 是类似的,并没有多大区别。

Condition需要掌握几个函数:

cond = threading.Condition() # 创建一个cond条件 , 默认锁为 Rlock.

  • cond.acquire() # 类似lock.acquire()

  • cond.release() # 类似lock.release()

  • cond.wait() # 等待指定触发,同时会释放锁,直到被notify才重新占有琐。

  • cond.notify() # 发送指定,触发执行一个线程

  • cond.notifyAll() # 发送指定,触发执行所有线程

举个生产消费的例子:

import threading
import time
from random import randint

class Producer(threading.Thread):
    def run(self):
        global L
        while True:
            val=randint(0,100)
            print('生产者',self.name,":Append"+str(val), L)
            if lock_con.acquire():
                L.append(val)
                lock_con.notify()
                lock_con.release()
            time.sleep(3)
            
class Consumer(threading.Thread):
    def run(self):
        global L
        while True:
                lock_con.acquire()
                if len(L)==0:
                    lock_con.wait()
                print('消费者',self.name,":Delete"+str(L[0]),L)
                del L[0]
                lock_con.release()
                time.sleep(0.25)

if __name__=="__main__":

    L=[]
    lock_con=threading.Condition()
    threads=[]
    for i in range(5):
        threads.append(Producer())
    threads.append(Consumer())
    for t in threads:
        t.start()
    for t in threads:
        t.join()
执行结果:生产者 Thread-1 :Append48 []
		生产者 Thread-2 :Append9 [48]
		生产者 Thread-3 :Append73 [48, 9]
		生产者 Thread-4 :Append11 [48, 9, 73]
		生产者 Thread-5 :Append94 [48, 9, 73, 11]
		消费者 Thread-6 :Delete48 [48, 9, 73, 11, 94]
		消费者 Thread-6 :Delete9 [9, 73, 11, 94]
		消费者 Thread-6 :Delete73 [73, 11, 94]
		消费者 Thread-6 :Delete11 [11, 94]
		消费者 Thread-6 :Delete94 [94]

可见通过cond来通信,阻塞自己,并使对方执行。

方式三:queue.Queue队列

从一个线程向另一个线程发送数据最安全的方式可能就是使用 queue 库中的队列了。

创建一个被多个线程共享的 Queue 对象,这些线程通过使用put()get() 操作来向队列中添加或者删除元素。

队列需要掌握的函数:

q = Queue(maxsize=0) # maxsize默认为0,不受限。

  • q.get() # 阻塞程序,等待队列消息。

  • q.get(timeout=5.0) # 获取消息,设置超时时间。

  • q.put() # 发送消息。

  • q.join() # 等待所有的消息都被消费完。

举一个老师点名的例子:

from threading import Thread
from queue import Queue
import time

class Student(Thread):
    def __init__(self, name, queue):
        super().__init__()
        self.name = name
        self.queue = queue

    def run(self):
        while True:
            # 阻塞程序,时刻监听老师,接收消息
            msg = self.queue.get()
            # 一旦发现点到自己名字,就赶紧答到
            if msg == self.name:
                print("{}:到!".format(self.name))

class Teacher:
    def __init__(self, queue):
        self.queue = queue

    def call(self, student_name):
        print("老师:{}来了没?".format(student_name))
        # 发送消息,要点谁的名
        self.queue.put(student_name)

queue = Queue()
teacher = Teacher(queue=queue)
s1 = Student(name="小明", queue=queue)
s2 = Student(name="小亮", queue=queue)
s1.start()
s2.start()

print('开始点名~')
teacher.call('小明')
time.sleep(1)
teacher.call('小亮')

总结:

学习了以上三种通信方法,我们很容易就能发现EventCondition 是threading模块原生提供的模块,原理简单,功能单一,它能发送 TrueFalse 的指令,所以只能适用于某些简单的场景中。

Queue则是比较高级的模块,它可能发送任何类型的消息,包括字符串、字典等。其内部实现其实也引用了Condition模块(譬如putget函数的阻塞),正是其对Condition进行了功能扩展,所以功能更加丰富,更能满足实际应用。

6. 进程之间如何通信?

方式一:Queue队列,用于多个进程间实现通信

一个进程向 Queue 中放入数据,另一个进程从 Queue 中读取数据。

multiprocessing 模块下的 Queue 和 queue 模块下的 Queue 基本类似,它们都提供了 qsize()、empty()、full()、put()、put_nowait()、get()、get_nowait() 等方法。

区别只是 multiprocessing 模块下的 Queue 为进程提供服务,而 queue 模块下的 Queue 为线程提供服务。

from queue import Queue    #  为线程提供服务
from multiprocessing import Queue   # 为进程提供服务

举个例子:

import multiprocessing

def fun(que):
    print('(%s) 进程开始放入数据...' % multiprocessing.current_process().pid)
    que.put('Python')    # 向 Queue 中放入数据

if __name__ == '__main__':

    que = multiprocessing.Queue()    # 创建进程通信的Queue
    p = multiprocessing.Process(target=fun, args=(que,))  # 创建子进程
    p.start()  # 启动子进程
    print('(%s) 进程开始取出数据...' % multiprocessing.current_process().pid)   # 先走主进程
    print(que.get())  # 从 Queue 中读取数据
执行结果:(6364) 进程开始取出数据...
		(12716) 进程开始放入数据...
		Python

方式二:Pipe管道,用于两个进程的通信

使用 Pipe 实现进程通信,程序会调用 multiprocessing.Pipe() 函数来创建一个管道,该函数会返回两个 PipeConnection 对象,代表管道的两个连接端,用于连接通信的两个进程。

PipeConnection 对象包含如下常用方法:

  • send(obj):发送一个 obj 给管道的另一端,另一端使用 recv() 方法接收。
  • recv():接收另一端通过 send() 方法发送过来的数据。
  • fileno():关于连接所使用的文件描述器。
  • close():关闭连接。
  • poll([timeout]):返回连接中是否还有数据可以读取。
  • send_bytes(buffer[, offset[, size]]):发送字节数据。
  • recv_bytes([maxlength]):接收通过 send_bytes() 方法发迭的数据,maxlength 指定最多接收的字节数。
  • recv_bytes_into(buffer[, offset]):功能与 recv_bytes() 方法类似,只是该方法将接收到的数据放在 buffer 中。

举一个例子:

import multiprocessing

def f(conn):
    print('(%s) 进程开始发送数据...' % multiprocessing.current_process().pid)
    # 使用conn发送数据
    conn.send('Python')

if __name__ == '__main__':

    parent_conn, child_conn = multiprocessing.Pipe()    # 创建Pipe,该函数返回两个PipeConnection对象
    p = multiprocessing.Process(target=f, args=(child_conn, ))    # 创建子进程
    p.start()
    print('(%s) 进程开始接收数据...' % multiprocessing.current_process().pid)
    # 通过conn读取数据
    print(parent_conn.recv())  # Python
    p.join()
执行结果:(13796) 进程开始接收数据...
		(14792) 进程开始发送数据...
		Python

7. 什么时候用多线程?什么时候用多进程?

  • 多线程使用场景:IO 密集型
  • 多进程使用场景:CPU 密集型

8. python 的 GIL

全局解释器锁,Guido van Rossum(吉多·范罗苏姆)创建python时就只考虑到单核cpu,解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁, 于是有了GIL这把超级大锁。因为cpython解析只允许拥有GIL全局解析器锁才能运行程序,这样就保证了保证同一个时刻只允许一个线程可以使用cpu。也就是说多线程并不是真正意义上的同时执行。

如何解决 GIL 锁的问题呢?

1.更换 cpython 为 jpython(不建议)

2.使用多进程完成多线程的任务

3.在使用多线程可以使用c语言去实现

9. socked 通信

socket是在应用层和传输层之间的一个抽象层,socket本质是编程接口(API),它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用以实现进程在网络中通信。TCP/IP只是一个协议栈,必须要具体实现,同时还要提供对外的操作接口(API),这就是Socket接口。通过Socket,我们才能使用TCP/IP协议。

socket通信流程如图:
在这里插入图片描述

根据上图顺序写出代码:
说明:建立2个py文件,原则上应该是在2台电脑上各自建立1个py文件,分别是 server.py 和 client.py 。由于现在只有一台电脑,所以建立2个py文件。

server.py:

import socket

sk = socket.socket()
sk.bind(('127.0.0.1',8000))   # 通过sk中的bind()方法,绑定地址和端口,参数必须是元组
sk.listen(3)   # 设置访问程序的最大排队人数
conn, address = sk.accept()
inp = input('请输入需要传输的内容:')
conn.send(bytes(inp,'utf8'))   # 传输的内容一定是byte类型

client.py

import socket

sk = socket.socket()
sk.connect(('127.0.0.1',8000))
data = sk.recv(1024)
print(str(data,'utf8'))  # str()转换

二. 类

1. 普通方法,类方法,静态方法区别

  • 普通方法,保存在类中,执行时通过对象调用
  • 静态方法,保存在类中,需要加装饰器(@staticmethod),执行通过类直接调用。不需要创建对象,可直接通过类调用,相当于函数
  • 类方法,保存在类中,需要加装饰器(@classmethod),执行通过类直接调用。不需要创建对象,可直接通过类调用,相当于函数
class Dog:
    
    dogbook = {'黄色': 30, '黑色': 20, '白色': 0}   # 类变量

    def __init__(self, name, color, weight):
        self.name = name       # 实例变量
        self.color = color     # 实例变量
        self.weight = weight   # 实例变量

    '''
        实例方法
            必须将self作为第一个参数
            可以访问实例变量,不能访问类变量
            只能通过实例名访问
    '''
    def bark(self):
        print('{}叫了一声'.format(self.name))   # 大黄叫了一声
        # print(dogbook)   # name 'dogbook' is not defined

    '''
        类方法
            必须将cls作为第一个参数
            可以访问类变量,不能访问实例变量
            可以通过实例名或者类名访问
    '''
    @classmethod
    def dog_num(cls):
        num = 0
        # print(self.name)  # name 'self' is not defined
        for v in cls.dogbook.values():
            num += v
        print(num)   # 50

    '''
        静态方法:
            不强制传入self,cls
            不能访问类变量,也不能访问实例变量
            可以通过实例名或类名访问
        静态方法和普通函数没有区别,可以放在类外面。
        之所以放在这里,是因为此方法和这个类有关联
    '''
    @staticmethod
    def total_weights(dogs_weight_list):
        total = 0
        for i in dogs_weight_list:
            total += i
        print(total)   # 30


Dog('大黄', '黄色', 10).bark()
Dog('大黄', '黄色', 10).dog_num()
Dog.total_weights([10,20])

静态方法是类中的函数,不需要实例。当某个方法不需要用到对象中的任何资源,将这个方法改为一个静态方法, 加一个@staticmethod。加上之后, 这个方法就和普通的函数没有什么区别了, 只不过写在了一个类中, 可以使用这个类的对象调用,也可以使用类直接调用。

类方法是将类本身作为对象进行操作的方法。它和静态方法的区别在于:不管这个方式是从实例调用还是从类调用,它都用第一个参数把类传递过来。

2. python 中的@property,@setter 实现成员变量的get, set属性?

@property装饰器: 将一个方法的调用方式变成“属性调用”。

通常用在属性的get方法,set方法,通过设置@property可以实现实例成员变量的直接访问,又保留了参数的检查。

class Student(object):
 
    @property
    def score(self):
        return self.__score   
    
    @score.setter
    def score(self,value):
        if value>=0 and value <= 100:
            self.__score = value #还记得__score吗?前面加一个双下划线,表示private私有属性
        else:
            raise ValueError('score must between 0 ~ 100!')
            
    
s = Student()
s.score = 90   # 设置分数
print(s.score)

@property 修饰函数score(getter),将score函数变成score属性输出,此外,@property 本身又创建了另一个装饰器@score.setter,负责把一个 setter 函数变成属性赋值,于是,我们虽然看到了类Student内部定义了两个函数score,对,没错,都是score,但是,他们却被不同的装饰器装饰,getter函数被@property修饰,setter函数被@property创建的函数的setter装饰器@score.setter修饰,因此,我们可以直接用s.score=90来代替s.set_socre(90),达到给score属性赋值的效果,简单粗暴,精益求精,

我们上面创建了@property另一个装饰器函数@xxx.setter,对私有属性__score进行输入值的判断,如果,我们不创建装饰器@xxx.setter可以吗?如果不创建的话,属性xxx就是一个只读属性,意味着不能被修改,

3. python 是如何实现多态的?

多态:一类事物有多种形态,(一个抽象类有多个子类,因而多态的概念依赖于继承)

class Animal():
    def __init__(self, name):
        self.name = name

    def talk(self):  # 抽象方法,仅由约定定义
        print(self.name, '叫')  # 当子类没有重写talk方法的时候调用

    def animal_talk(obj):  # 多态
        obj.talk()

class Cat(Animal):
    def talk(self):
        print('%s: 喵喵喵!' % self.name)  # 重写talk方法

class Dog(Animal):
    def talk(self):
        print('%s: 汪汪汪!' % self.name)


a = Dog('小狗')
Animal.animal_talk(a)  # 多态调用
b = Cat('小猫')
Animal.animal_talk(b)
c = Animal('111')
Animal.animal_talk(c)
执行结果:小狗: 汪汪汪!
		小猫: 喵喵喵!
		111 叫

4. 单元测试,单例模式

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。在python中指一个类。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。

单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的一个类只有一个对象实例。(详见面试总结一)

5. python 如何实现单例模式?

什么是单例模式(Singleton)?

单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。

单例模式优点:
1. 由于单例模式要求全局内只有一个实例,所以可以节省很多内存空间
2. 全局只有一个接入点,可以更好的进行数据同步,避免多重占用
3. 单例可以常驻内存,减少系统开销

单例模式应用:
1. 生成唯一序列号
2. 访问全局复用的唯一资源,如磁盘,总线等
3. 数据库连接池
4. 网站计数器

实现单例的方式?

  • 全局变量:不直接调用 Config() ,而使用同一个全局变量
  • 重写 __new__ 方法:重写 __new__来保证每次调用 Config() 都会返回同一个对象, __new__ 为对象分配空间,返回对象的引用
  • 使用metaclass:metaclass重写__call__ 方法来保证每次调用 Config() 都会返回同一个对象
  • 使用装饰器:使用装饰器来保证每次调用 Config() 都会返回同一个对象

原理:在真正调用 Config() 之前进行一些拦截操作,来保证返回的对象都是同一个:

具体实现:

  1. 全局变量

    # config.py
    from dataclasses import dataclass
    
    @dataclass
    class Config:
         SQLALCHEMY_DB_URI  = SQLALCHEMY_DB_URI
    config  = Config(SQLALCHEMY_DB_URI = "mysql://")
    

    通过使用全局变量,我们在所有需要引用配置的地方,都使用 from config import config 来导入,这样就达到了全局唯一的目的。

  2. 重写__new__方法

    class Singleton(object):
        _instance = None   # _instance 作为类属性,保证了所有对象的实例都是同一个
    
        def __new__(cls, *args, **kwargs):  
            if cls._instance is None:
                cls._instance = super().__new__(cls)  # 为对象分配空间
            return cls._instance   # 返回对象的引用
    
    a = Singleton()    # 创建对象时,new方法会被自动调用
    b = Singleton()
    print(a)
    print(b)
    
    执行结果:<__main__.Singleton object at 0x0000023A88E0F2B0>
    		<__main__.Singleton object at 0x0000023A88E0F2B0>
    

    每次实例化一个对象时,都会先调用 __new__() 创建一个对象,再调用 __init__() 函数初始化数据。因而,在 new 函数中判断 Singleton类 是否已经实例化过,如果不是,调用父类的 new 函数创建实例;否则返回之前创建的实例。

  3. 使用metaclass元类

    class SigletonMetaClass(type):
        _instance = None
    
        def __new__(cls, *args, **kwargs):
            return super().__new__(cls, *args, **kwargs)
    
        def __call__(self, *args, **kwargs):
            if self._instance is None:
                self._instance = super().__call__(*args, **kwargs)
            return self._instance
    
    class Singleton(metaclass=SigletonMetaClass):
        def __new__(cls, *args, **kwargs):
            return super().__new__(cls)
        
    a = Singleton()
    b = Singleton()
    print(a)
    print(b)
    
    执行结果:<__main__.Singleton object at 0x000001A11E4AC908>
    		<__main__.Singleton object at 0x000001A11E4AC908>
    

    因此,用元类实现单例时仍需按照三步骤:1. 拦截 2. 判断是否已经创建过对象 3. 返回对象。与上个方法相比,区别在于拦截的地点不同。

  4. 装饰器

    4.1 函数装饰器

    def SingletonDecorator(cls):
        _instance = None
    
        def get_instance(*args, **kwargs):
            nonlocal _instance
            if _instance is None:
                _instance = cls(*args, **kwargs)
            return _instance
        return get_instance
    
    @SingletonDecorator
    class Singleton(object):
        pass
    
    a = Singleton()
    b = Singleton()
    print(a)
    print(b)
    
    执行结果:<__main__.Singleton object at 0x0000022194E6C438>
    		<__main__.Singleton object at 0x0000022194E6C438>
    

    4.2 类装饰器

    class SingletonDecorator(object):
        _instance = None
    
        def __init__(self, cls):
            self._cls = cls
    
        def __call__(self, *args, **kwargs):
            if self._instance is None:
                self._instance = self._cls(*args, **kwargs)
            return self._instance
    
    @SingletonDecorator
    class Singleton(object):
        pass
    
    a = Singleton()
    b = Singleton()
    print(a)
    print(b)
    
    执行结果:<__main__.Singleton object at 0x0000028F42CFC8D0>
    		<__main__.Singleton object at 0x0000028F42CFC8D0>
    

6. 装饰器有什么作用

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

7. 打印时间的装饰器

import time

# 装饰函数
def timer(func):
    def wrapper(*args, **kw):
        start =time.time()
        # 这是函数真正执行的地方
        func(*args, **kw)
        end =time.time()
        cost_time = end - start
        print("花费时间:{}秒".format(cost_time))
    return wrapper

@timer    
def want_sleep(sleep_time):
    time.sleep(sleep_time)

want_sleep(5)

8. fun(*args,**kwargs)中的 *args,**kwargs什么意思?

*args: 用来发送一个非键值对的可变数量的参数列表给一个函数。

**kwargs: 用来发送一个可变的键值对的参数给一个函数。

9. python 中的闭包?

闭包函数 指定义在一个函数内部的函数,被外层函数包裹着,其特点是可以访问外层函数的变量。

def outer():
    num = 1
    def inner():
        print num   # 内层函数可以访问外层函数中的 num
        
    return inner    # 外层函数的返回值必须是内层函数

fun = outer()
num = 1
fun()     # 1

10. 简述 Python 的作用域以及 Python 搜索变量的顺序

Python作用域简单说就是一个变量的命名空间。代码中变量被赋值的位置,就决定了哪些范围的对象可以访问这个变量,这个范围就是变量的作用域。

在Python中,只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域。

Python的变量名解析机制也称为 LEGB 法则:本地作用域Local→当前作用域被嵌入的本地作用域Enclosing locals→全局/模块作用域Global→内置作用域Built-in

11. 简述__new____init__的区别

创建一个新实例时调用 __new__,初始化一个实例时用__init__,这是它们最本质的区别。

__new__方法会返回所构造的对象,__init__则不会.

__new__函数必须以 cls 作为第一个参数,而__init__则以 self 作为其第一个参数.

例如,构造单例模式:

cls sington():
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

12. 双下划线和单下划线的区别?

单下划线”开始的成员变量叫做保护变量,类对象和子类对象都能访问。

双下划线”开始的成员变量叫做私有成员,只有类对象自己能访问,子类对象不能访问。

13. 列出几种魔法方法并简要介绍用途

__init__: 对象初始化方法

__new__:创建对象时候执行的方法,单列模式会用到

__str__ : 当使用print输出对象的时候,只要自己定义了__str__(self)方法,那么就会打印从在这个方法中return的数据

__del__: 删除对象执行的方法

14. 什么是 python 元类?

在 Python 当中万物皆对象,我们用 class 关键字定义的类本身也是一个对象,负责产生该对象的类称之为元类,元类可以简称为类的类,

元类的主要目的是为了控制类的创建行为.

type是 Python 的一个内建元类,用来直接控制生成类,在 python 当中任何 class 定义的类其实都是type 类实例化的结果。

只有继承了 type 类才能称之为一个元类,否则就是一个普通的自定义类,自定义元类可以控制类的产生过程,类的产生过程其实就是元类的调用过程.

三. 函数

1. 高阶函数:map() , reduce() , filter() ,lambda()

1.map():遍历序列,根据提供的函数对指定序列做映射,对序列中每个元素进行操作,最终获取新的序列

例1:

print(list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9])))

# 输出结果:['1', '2', '3', '4', '5', '6', '7', '8', '9']

例2:

def square(x):
     return x**2
result = list(map(square,[1,2,3,4,5]))
print(result)

# 输出结果:[1, 4, 9, 16, 25]

备注:map() : Python 2.x 返回列表;Python 3.x 返回迭代器

2.reduce():对于序列内所有元素进行累计操作,即是序列中后面的元素与前面的元素做累积计算

(结果是所有元素共同作用的结果)

from functools import reduce

def addl(x,y):
    return x + y

print(reduce(addl,range(1,5)))   

# 输出结果:10

reduce()的作用是接收一个列表,[1,2,3,4]
首先,将1,2传给addl(),计算结果为3
接着,将3,3传给addl(),计算结果为6
接着,将6,4传给addl(),计算结果为10

3.filter():过滤序列,过滤掉不符合条件的元素,返回一个迭代器对象,如果要转换为列表,可以使用 list() 来转换

def func(x):
    return x%2==0
print(list(filter(func,range(1,6))))

# 输出结果:[2, 4]

4.lambda(),匿名函数,不需要命名的函数def f ( x ,y):

def add(x,y):
    return x + y
print(add(1,2))

# 等效于:
g = lambda x, y : x + y
print(g(1,2))

lambda 表达式的作用:

  1. python 写一些执行脚本时,使用 lambda 就可以省下定义函数过程,比如说我们只是需要写个简单的脚本来管理服务器时间,我们就不需要专门定义一个函数然后再写调用,使用 lambda 就可以使得代码更加精简。
  2. 对于一些比较抽象并且整个程序执行下来只需要调用一两次的函数,有时候给函数起个名字也是比较头疼的问题,使用 lambda 就不需要考虑命名的问题了。
  3. 简化代码的可读性,由于普通的函数阅读经常要跳到开头 def 定义部分,使用 lambda 函数可以省去这样的步骤。

四. 底层问题

1. 垃圾回收机制

python 垃圾回收主要以引用计数为主,标记-清除和分代清除为辅的机制,其中标记-清除和分代回收主要是为了处理循环引用的难题。

引用计数

引用计数算法

当有1个变量保存了对象的引用时,此对象的引用计数就会加1。
当使用del删除变量指向的对象时,如果对象的引用计数不为1,比如3,那么此时只会让这个引用计数减1,即变为2,当再次调用del时,变为1,如果再调用1次del,此时会真的把对象进行删除。

引用计数+1的四种情况:

  1. 对象被创建  a=14
  2. 对象被引用  b=a
  3. 对象被作为参数,传到函数中   func(a)
  4. 对象作为一个元素,存储在容器中   List={a,”a”,”b”,2}

对应的引用计数-1的四种情况:

  1. 当该对象的别名被显式销毁时  del a
  2. 当该对象的引别名被赋予新的对象,   a=26
  3. 一个对象离开它的作用域,例如 func函数执行完毕时,函数里面的局部变量的引用计数器就会减一(但是全局变量不会)
  4. 将该元素从容器中删除时,或者容器被销毁时。

优点: 简单实时,一旦没有引用,内存就直接释放了。处理回收内存的时间分摊到了平时。
缺点: 维护引用计数消耗资源,会造成循环引用导致无法回收,造成内存泄露,比如:

list1 = []
list2 = []
list1.append(list2)
list2.append(list1)
'''
	list1与list2相互引用,即使不存在其他对象对它们的引用,list1与list2的引用计数也
	仍然为1,所占用的内存永远无法被回收,这将是致命的。
'''
标记清除(Mark—Sweep)

算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。
它分为两个阶段:

  • 第一阶段是标记阶段,GC会把所有的『活动对象』打上标记,
  • 第二阶段是把那些没有标记的对象『非活动对象』进行回收。

那么GC又是如何判断哪些是活动对象哪些是非活动对象的呢?

  1. 对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。
  2. 从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。

根对象就是全局变量、调用栈、寄存器。
标记清除算法作为Python的辅助垃圾收集技术主要处理的是一些容器对象,比如list、dict、tuple,instance等,
因为对于字符串、数值对象是不可能造成循环引用问题。
缺点: 清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。

分代回收

分代回收是一种以空间换时间的操作方式,Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代。

  • Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),
  • 他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。
  • 新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,
  • 依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。
  • 同时,分代回收是建立在标记清除技术基础之上。
  • 说明对象存在时间越长,越可能不是垃圾,应该越少去收集
  • 分代回收同样作为Python的辅助垃圾收集技术处理那些容器对象

2. 赋值,深拷贝,浅拷贝

浅拷贝: 拷贝对象的第一层。

深拷贝: 拷贝对象的所有层。

说大白话:
B 拷贝于 A
浅拷贝虽然说是拷贝,但也是 “ 身不由己 ” ,当修改除了第一层之外的值时,都会改动(内嵌列表表面上也拷贝过来了,但实际还是不是自己说了算的,只要改了内嵌列表的值,拷贝的也要改,这就是只拷贝一层,内嵌的就无能为力了),当A的第二层改变时,B的第二层也会随之改变。

深拷贝就是彻底的拷贝,两者就再毫无关系,虽然拷贝完不改的话长的一样,但是不管对谁改动,另一个也是毫不受影响。当A的第二层改变时,B的第二层不受影响。

3. 可变对象和不可变对象

什么是可变/不可变对象?

  • 不可变对象,该对象所指向的内存中的值不能被改变,当改变某个变量时候,由于其所指的值不能被改变,相当于把原来的值复制一份后再改变,这会开辟一个新的地址,变量再指向这个新的地址。
  • 可变对象,该对象所指向的内存中的值可以被改变,变量(准确的说是引用)改变后,实际上是其所指的值直接发生改变,并没有发生复制行为,也没有开辟新的出地址,通俗点说就是原地改变。

Python中
可变对象:dict、list、set
不可变对象:str、int、tuple、float

附: python函数调用时,参数传递方式是值传递还是引用传递?

不可变参数: 值传递
可变参数: 引用传递

4. python 传参数是传值还是传址?

Python中函数参数是引用传递(注意不是值传递)。

  • 对于不可变类型(数值型、字符串、元组),因变量不能修改,所以运算不会影响到变量自身;

  • 对于可变类型(列表、字典)来说,函数体运算可能会更改传入的参数变量。

5. def fun(a, b=[]): 这种写法有什么坑?

def func(a,b=[]):
    b.append(a)
    print(b)
    
func(1)    # [1]
func(1)    # [1, 1]
func(1)    # [1, 1, 1]
func(1)    # [1, 1, 1, 1]

函数的第二个默认参数是一个list,当第一次执行的时候实例化了一个list,第二次执行还是用第一次执行的时候实例化的地址存储,所以三次执行的结果就是 [1, 1, 1] 。

想每次执行只输出[1] ,b = [] 应该放在函数里面。

6. is 和 == 的区别?

  • is 就是判断两个对象的id是否相同
  • == 判断的则是内容是否相同

7. 连接字符串用 join 还是 +

字符串是不可变对象。

  • 当用操作符+连接字符串的时候,每执行一次+都会申请一块新的内存,然后复制上一个+操作的结果和本次操作的右操作符到这块内存空间,因此用+连接字符串的时候会涉及好几次内存申请和复制。
  • join在连接字符串的时候,会先计算需要多大的内存存放结果,然后一次性申请所需内存并将字符串复制过去,这是为什么join的性能优于+的原因。
  • 所以在连接字符串数组的时候,我们应考虑优先使用join。

8. Python 中的复制(赋值),底层是怎么实现的?

Python中,对象的赋值实际上是简单的对象引用。也就是说,当你创建一个对象,然后把它复制给另一个变量的时候,Python并没有拷贝这个对象,而是拷贝了这个对象的引用(指针)。

9. python with 用法+原理剖析

用法:with语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会释放资源,比如文件使用后自动关闭线程中锁的自动获取和释放等。

with open("xxx") as f:
    f.write() #文件操作

原理: 基本思想是with所求值的对象必须有一个__enter__()方法,一个__exit__()方法。

紧跟with后面的语句被求值后,__enter__()方法的返回值将被赋值给as后面的变量。当with下面的代码块全部被执行完之后,将调用前面返回对象的__exit__()方法,对象的__exit__要接受三个参数:异常类型、异常对象和异常跟踪。

10. 如何捕获异常,常用的异常机制有哪些?

捕获异常需调用python默认的异常处理器,并在终端输出异常信息。

  1. try...except...finally语句:当try语句执行时发生异常,回到try语句层,寻找后面是否有except语句。找到except语句后,会调用这个自定义的异常处理器。except将异常处理完毕后,程序继续往下执行。finally语句表示,无论异常发生与否,finally中的语句都要执行。

  2. assert语句:判断assert后面紧跟的语句是True还是False,如果是True则继续执行print,如果是False则中断程序,调用默认的异常处理器,同时输出assert语句逗号后面的提示信息。

  3. with语句:如果with语句或语句块中发生异常,会调用默认的异常处理器处理,但文件还是会正常关闭。

11. python 中断言方法举例

assert()方法,断言成功,则程序继续执行,断言失败,则程序报错

a = 3
assert (a > 1)
print("断言成功,程序继续往下执行")
b = 4
assert (b > 7)
print("断言失败,程序报错")

12. 如何提高 python 的运行效率?

  1. 关键代码使用外部功能包(Cython,pylnlne,pypy,pyrex)
  2. 针对循环的优化–尽量避免在循环中访问变量的属性
  3. 使用较新版本的python
  4. 尝试多种编码方法
  5. 交叉编译应用程序

五. 列表

1. 迭代器和生成器

列表生成式:(还没有放到内存中)
a1 = [x for x in range(1,10)]
a2 = [x**2 for x in range(1,10)]

也可以放一个函数:

def f(n):
    return n**3

a3 = [f(x) for x in range(1,10)]
生成器:

背景:通过列表生成式,我们可以直接创建一个列表,但是,受到内存限制,列表容量肯定是有限的,而且创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
  所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间,在Python中,这种一边循环一边计算的机制,称为生成器:generator
  
定义: 在Python中,这种一边循环一边计算的机制,称为生成器:generator 。

说明:生成器是一个特殊的程序,可以被用作控制循环的迭代行为,python中生成器是迭代器的一种,使用yield返回值函数,每次调用yield会暂停,而可以使用next()函数和send()函数恢复生成器。

两种创建方式

  1. (x*2 for x in range(5,100)) # 此处注意区分列表生成式
  2. yield

其中:

  • 生成器表达式:返回一个对象,这个对象只有在需要的时候才产生结果
  • 生成器函数:也是用def定义的,利用关键字yield(相当于return)一次性返回一个结果,阻塞,重新开始

为了节省空间,生成器生成的数据并没有放到内存中,只有在调取的时候才往内存中放。
只能从前往后逐一调取。

next() 返回迭代器的下一个项目

a = (x*2 for x in range(5,100))   #元组
print(next(a))  #将5返回给x*2    10

用生成器来实现斐波那契数列的例子是:

def fib(max):
    a, b = 0, 1
    while max > 0:
        a, b = b, a + b
        yield a
        max -= 1

print([i for i in fib(10)])

f.send() 和 next() 的区别:

  • f.send(),将参数传给yield前一个变量,也就是说send()可以强行修改上一个yield的值;
  • next()参数是迭代器函数名字;
  • 第一个send()不可传值,写作f.send(None),相当于next(b)。
迭代器

定义:任何实现了__iter__和__next__()方法的对象都是迭代器(这是迭代器协议),__iter__返回迭代器自身,__next__返回容器中的下一个值,直到容器中没有更多元素时抛出StopIteration异常并停止迭代。

迭代就是循环,它是一个带状态的对象,能在你调用next()方法的时候返回容器中的下一个值。
迭代器就像一个懒加载的工厂,等到有人需要的时候,它才会生成值并返回,没调用的时候就处于休眠状态等待下一次调用。

生成器与迭代器的区别
  • 迭代器(iterator)是一个实现了迭代器协议(iter__和__next()方法)的对象。
  • 生成器(generator)是通过yield语句或生成器函数快速生成迭代器,可以不用iter和next方法 。
  • 生成器本质上也是一个迭代器,自己实现了可迭代协议,与迭代器不同的是生成器的实现方式不同,可以通过生成器表达式和生成器函数两种方式实现,代码更简洁。
  • 生成器和迭代器都是惰性可迭代对象,只能遍历一次,数据取完抛出Stopiteration异常
什么是可迭代对象?

我们已经知道,可以直接作用于for循环的数据类型有以下几种:

  • 一类是集合数据类型,如list,tuple,dict,set,str等
  • 一类是generator,包括生成器表达式和生成器函数

定义:实现了__iter__方法的对象就叫做可迭代对象。说白了就是可以直接作用于for 循环的对象统称为可迭代对象:Iterable

区别:一个可迭代的对象必须是定义了__iter__()方法的对象;而一个迭代器必须是同时定义了__iter__()方法和next()方法的对象。

for函数的本质:在python3中,就是不断调用next() 方法实现for循环。

x = [1,2,3]
for i in x:
    print(i)

实际执行情况:
在这里插入图片描述
等价于:

# 首先获得Iterator对象:
iterator = iter([1, 2, 3])
# 循环:
while True:
    try:
        # 获得下一个值:
        x = next(iterator)
        print(x)
    except StopIteration:
        # 遇到StopIteration就退出循环
        break

总结:

  • 凡是可作用于for循环的对象都是可迭代对象;
  • 凡是可作用于next()函数的对象都是迭代器,它们表示一个惰性计算的序列;
  • 集合数据类型如list、dict、str等是可迭代对象,但不是迭代器,不过可以通过iter()方法获得一个Iterator对象;
  • 文件是可迭代对象,也是迭代器。

你可能会问,为什么list、dict、str等数据类型不是迭代器?

这是因为Python的迭代器对象表示的是一个数据流,可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。
迭代器甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。
list,str,dict…这些可迭代的对象你可以随意的读取所以非常方便易用,但是你必须把它们的值放到内存里,当它们有很多值时就会消耗太多的内存。

如何判断是可迭代对象还是迭代器?如何把可迭代对象转换为迭代器?

from collections import Iterator  # 迭代器
from collections import Iterable  # 可迭代对象

s = 'hello'

print(isinstance(s, Iterator))  # 判断是不是迭代器
print(isinstance(s, Iterable))  # 判断是不是可迭代对象

print(isinstance(iter(s), Iterator))  # 把可迭代对象转换为迭代器 iter(s)

2. sorted() 函数

'''
sorted(iterable, cmp=None, key=None, reverse=False)
    iterable -- 可迭代对象。
    cmp -- 比较的函数,这个具有两个参数,参数的值都是从可迭代对象中取出,此函数必须遵守的规则为,大于则返回1,小于则返回-1,等于则返回0。
    key -- 主要是用来进行比较的元素,只有一个参数,具体的函数的参数就是取自于可迭代对象中,指定可迭代对象中的一个元素来进行排序。
    reverse -- 排序规则,reverse = True 降序 , reverse = False 升序(默认)。
'''

3. 现有字典 dict = {‘a’ : 24, ‘d’ : 55, ‘kj’ : 86},请按照字段中的的value值进行排序

dict = {'a': 24, 'd': 55, 'kj': 30}

# 按照 value 进行排序
new_dict1 = sorted(dict.items(), key=lambda c: c[1])
print new_dict1

# 按照 key 进行排序
new_dict2 = sorted(dict.items(), key=lambda c: c[0])
print new_dict2

4. 现有列表 list=[{‘name’: ‘a’, ‘age’: 24}, {‘name’: ‘d’, ‘age’: 55}, {‘name’: ‘c’, ‘age’: 67}],请按照 age 进行从大到小排序

list = [{'name': 'a', 'age': 24}, {'name': 'd', 'age': 55}, {'name': 'c', 'age': 32}]

new_list1 = sorted(list, key=lambda c: c['age'], reverse=True)
print new_list1

5. 下面代码输出结果是?

list = [1, 2, 3, 4, 5]

print list[10:]
'''
代码输出空列表[], 并不会出现 IndexError
'''

print list[10]
'''
IndexError: list index out of range
'''

6. 修改列表时,如何保证地址不变?

若要维持地址不变,需要加上[:]

l = [1, 2, 3]
print id(l)    # 52733960
l = [2, 3]
print id(l)    # 52661384
l[:] = [3]
print id(l)    # 52661384

7.列表和元组的区别

区别:

  • 语法差异:使用方括号 [ ] 创建列表,而使用括号 ( ) 创建元组
  • 是否可变:列表是可变的,而元组是不可变的。可以修改列表的值,但是不修改元组的值。
  • 重用与拷贝:元组无法复制。 原因是元组是不可变的。 如果运行tuple(tuple_name)将返回自己。

相同点:

  • 都是序列
  • 都可以存储任何数据类型
  • 可以通过索引访问

什么时候用元组什么时候用列表?

  • 不确定元素可以用列表
  • 确定元素的用元组。

六. 常用模块

time 模块:从1970年开始

time.time()  #记录当前时间戳 1556723770.0009158
time.sleep(3)  #延迟3秒钟
time.localtime() #结构化时间(本地)
time.strftime('%Y-%m-%d %H:%M:%S')  #自定义时间格式2019-05-01 23:05:45
time.mktime(time.localtime())  #将本地时间转化为时间戳

datetime 模块

datetime.datetime.now()  #2019-05-01 23:18:38.512192

random 模块

import random

random.random()  # 生成一个0到1的随机数  0.758108354250769
random.randint(1, 8)  # 生成一个0到8的随机整数  4
random.choice({}/[]/()/'')  # 从一个对象中随机选择一个元素
random.randrang(1, 3)   # 从1到3中随机取一个整数(不包括3)

os 模块:与操作系统进行交互

import os

os.getcwd()  # 获取当前工作目录
os.chdir()   # 更改当前工作目录 change
os.makedirs('dirname1/dirname2')  # 可生成多层递归目录
os.removedirs()  #(递归)删除空文件夹
os.mkdir('dirname')  # 生成一个文件夹
os.rmdir('dirname')  # 删除一个空文件夹,若文件不为空,报错
os.remove()   #只能删除文件,不能删除文件夹
os.listdir("G:\python")  # 显示出目录下的所有文件
os.rename('oldname','newname')  # 改变文件夹名称
os.path.dirname('./abc')  # 返回abc所在的文件的路径
os.path.exists(path)  # 如果path存在,返回Ture
os.path.join(path1,path2,) # 将多个路径合并并返回

sys 模块:与 python 解释器进行交互

sys.argv

sys.argv[]说白了就是一个从程序外部获取参数的桥梁。因为我们从外部取得的参数可以是多个,所以获得的是一个列表(list),也就是说sys.argv其实可以看作是一个列表,所以才能用[]提取其中的元素。其第一个元素是程序本身,随后才依次是外部给予的参数。

Sys.argv[ ]其实就是一个列表,里边的项为用户输入的参数,关键就是要明白这参数是从程序外部输入的,而非代码本身的什么地方,要想看到它的效果就应该将程序保存了,从外部来运行程序并给出参数。

hashlib 模块:提供了很多加密的算法

这里介绍一下hashlib的简单使用事例,用hashlib的md5算法加密数据
加密算法的转化结果是唯一的,不会改变。(唯一性)

import hashlib
def md5(arg):  # 这是加密函数,将传进来的函数加密
    md5_pwd = hashlib.md5(bytes('abd', encoding='utf-8'))
    md5_pwd.update(bytes(arg, encoding='utf-8'))
    return md5_pwd.hexdigest()  # 返回加密后的数据(十六进制)

def log(user, pwd):  # 登陆时候的函数,由于md5不能反解,因此登陆的时候用正解
    with open('db', 'r', encoding='utf-8') as f:
        for line in f:
            u, p = line.strip().split('|')
            if u == user and p == md5(pwd):  # 登陆的时候验证用户名以及加密的密码跟之前保存的是否一样
                return True

def register(user, pwd):  # 注册的时候把用户名和加密的密码写进文件,保存起来
    with open('db', 'a', encoding='utf-8') as f:
        temp = user + '|' + md5(pwd)
        f.write(temp)

i = input('1表示登陆,2表示注册:')
if i == '2':
    user = input('用户名:')
    pwd = input('密码:')
    register(user, pwd)   
elif i == '1':
    user = user = input('用户名:')
    pwd = input('密码:')
    r = log(user, pwd)  # 验证用户名和密码
    if r == True:
        print('登陆成功')
    else:
        print('登陆失败')
else:
    print('账号不存在')

logging 模块:输出日志

在这里插入图片描述

import logging

# 1.创建一个logger
logger = logging.getLogger()  # get一个Logger对象,日志对象
logger.setLevel(logging.INFO)  # Log等级总开关

# 2.创建一个handler,用于写入日志文件
fh = logging.FileHandler('txt')  # 用于写入文件,文件对象
fh.setLevel(logging.DEBUG)  # 输出到file的log等级的开关

# 3.创建一个handler,用于输出控制台
ch = logging.StreamHandler()  # 输出控制台,屏幕对象
ch.setLevel(logging.WARNING)  # 输出到console的log等级的开关

# 4.定义handler的输出格式
formatter = logging.Formatter("%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s")

fh.setFormatter(formatter)  # fh创建格式,和formatter一样
ch.setFormatter(formatter)  # ch创建格式,和formatter一样

# 5.控制输出
logger.addHandler(fh)  # 输出文件
logger.addHandler(ch)  # 输出控制台

# 6.日志
logger.debug('this is a logger debug message')
logger.info('this is a logger info message')
logger.warning('this is a logger warning message')
logger.error('this is a logger error message')
logger.critical('this is a logger critical message')

json 模块

json.dumps()   # 字典转json字符串

json.loads()   # json字符串转字典

re 模块:正则表达式

1. 限定符
字符描述
*匹配前面的子表达式零次或多次。* 等价于{0,}。
+匹配前面的子表达式一次或多次。+ 等价于 {1,}。
?匹配前面的子表达式零次或一次。? 等价于 {0,1}。 例如,do(es)? 可以匹配 dodoes 中的 doesdoxy 中的 do
^1.匹配输入字符串的开始位置 2.在[ ]中使用时,表示不匹配[ ]中的字符集合。
$匹配输入字符串的结尾位置。
{n}n 是一个非负整数。匹配确定的 n 次。 例如,o{2} 不能匹配 Bob 中的 o,但是能匹配 food中的oo
{n,}n 是一个非负整数。至少匹配n 次。 例如,o{2,} 不能匹配 Bob 中的 o,但能匹配 foooood 中的所有 oooooo。‘o{1,}’ 等价于 ‘o+’。‘o{0,}’ 则等价于 ‘o*’。
{n,m}m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。 例如,o{1,3}将匹配 fooooood 中的前三个 ooo。‘o{0,1}’ 等价于 ‘o?’。请注意在逗号和两个数之间不能有空格。
( )标记一个子表达式的开始和结束位置。即分组匹配。
[ ]符合 [ ]中的任一字符就匹配。例如,a[bcd]f可以匹配acf, 但不能匹配agf
|指明两项之间的一个选择。例如,`s
2. 预定义字符
字符描述规则结果
\d\d匹配任何十进制数,它相当于类[0-9] \d+如果需要匹配一位或者多位数的数字时用a\dca1c
\D\D匹配任何非数字字符,它相当于类[ ^0-9 ]a\Dcabc
\s\s匹配任何空白字符,它相当于类[\t\n\r\f\v]a\sca c
\S\S匹配任何非空白字符,它相当于类[ ^\t\n\r\f\v ]a\Scabc
\w\w匹配包括下划线在内任何字母数字字符,它相当于类[a-zA-Z0-9_]a\wcabc
\W\W匹配非任何字母数字字符包括下划线在内,它相当于类[ ^a-zA-Z0-9_ ]a\Wca c
3. 常用函数

re.findall() 匹配所有符合规则的字符串

语法:

findall(pattern, string, flags=0)
# pattern: 正则模型
# string : 要匹配的字符串
# falgs : 匹配模式

re.match() 从起始位置开始匹配,匹配成功返回一个对象,未匹配成功返回None

语法:

match(pattern, string, flags=0)
# pattern: 正则模型
# string : 要匹配的字符串
# falgs : 匹配模式

re.search() 浏览全部字符串,直到匹配出第一个符合规则的字符串

语法:

search(pattern, string, flags=0)
# pattern: 正则模型
# string : 要匹配的字符串
# falgs : 匹配模式

注意:search()和match()的区别

match() 从第一个字符开始匹配, 如果第一个字符不匹配就返回None, 不再继续匹配。

search() 在整个字符串查找,直到找到一个匹配的字符。

re.split() 根据正则匹配分割字符串,返回分割后的一个列表

语法:

split(pattern, string, maxsplit=0, flags=0)

# pattern: 正则模型
# string : 要匹配的字符串
# maxsplit:指定分割个数
# flags  : 匹配模式

re.sub() 替换匹配成功的指定位置字符串

语法:

sub(pattern, repl, string, count=0, flags=0)

# pattern: 正则模型
# repl   : 要替换的字符串
# string : 要匹配的字符串
# count  : 指定匹配个数
# flags  : 匹配模式
问题:正则表达式匹配中,(.*)和(.*?)匹配区别?

(.*)是贪婪匹配,会把满足正则的尽可能多的往后匹配

(.*?)是非贪婪匹配,会把满足正则的尽可能少匹配

import re

s = "<a>哈哈</a><a>呵呵呵</a>"
res1 = re.findall("<a>(.*)</a>",s)
print("贪婪匹配", res1)
res2 = re.findall("<a>(.*?)</a>",s)
print("非贪婪匹配", res2)
运行结果:贪婪匹配 ['哈哈</a><a>呵呵呵']
		非贪婪匹配 ['哈哈', '呵呵呵']

七. 常见问题

1. 时间复杂度

如何推导出时间复杂度呢?有如下几个原则:

  1. 如果运行时间是常数量级,用常数1表示;

  2. 只保留时间函数中的最高阶项;

  3. 如果最高阶项存在,则省去最高阶项前面的系数。

场景1:T(n) = 3n,执行次数是线性的,T(n) = O(n)

void eat1(int n){for(int i=0; i<n; i++){;System.out.println("等待一天");System.out.println("等待一天");System.out.println("吃一寸面包");}
}

场景2:T(n) = 5logn,执行次数是对数的,T(n) = O(log n)

void eat2(int n)
   for(int i=1; i<n; i*=2){System.out.println("等待一天");System.out.println("等待一天");System.out.println("等待一天");System.out.println("等待一天");System.out.println("吃一半面包");

   }

}

场景3:T(n) = 2,执行次数是常量的, T(n) = O(1)

void eat3(int n){
    
   System.out.println("等待一天");
   System.out.println("吃一个鸡腿");
}

场景4:T(n) = 0.5n^2 + 0.5n,执行次数是一个多项式,T(n) = O(n^2)

void eat4(int n){
   for(int i=0; i<n; i++){
       for(int j=0; j<i; j++){
           System.out.println("等待一天");
       }
       System.out.println("吃一寸面包");
   }

}

这四种时间复杂度究竟谁用时更长,谁节省时间呢?

O(1)< O(logn)< O(n)< O(n^2)

2. python2 和 python3 的 range(100)的区别

python2返回列表,python3返回迭代器,节约内存

3. 文件操作时:xreadlines 和 readlines 的区别?

readlines() 是把文件全部读到内存,并解析为一个list, 当文件体积比较大时,会占用很多内存。

xreadlines() 直接返回一个 iter(file) 迭代器,在python2.3之后已经不推荐这种方法了,直接用for循环迭代文件对象。

4. 列举布尔值为 False 的常见值?

0, [], (), {}, '', False, None

5. python 递归的最大层数

python最大递归层数为:1000

如何修改最大递归层数?
在python内置的sys模块中,可以获取和调整最大递归深度

import sys

print sys.getrecursionlimit()           # 1000
sys.setrecursionlimit(3000)
print sys.getrecursionlimit()           # 3000

6. mro 是什么?

Method Resolution Order, 方法解析顺序

方法调用时,对当前类以及基类进行搜索以确定方法所在的位置。搜索顺序即为方法解析顺序

7. 生产者和消费者模型

生产者和消费者模型 是通过一个容器来解决生产者和消费者的强耦合关系。
生产者和消费者不直接通信,而是利用阻塞队列通信,生产者生成数据后直接存入队列中,消费者需要数据时,从队列中获取。
主要解决生产者和消费者速率不一致的问题,平衡生产者和消费者的处理能力.

8. 什么是反射?

'''
通过“字符串”的形式操作(获取、判断、设置、删除)对象中的成员

getattr()  返回一个对象属性值。
hasattr()  判断对象中是否含有某属性
setattr(obj,‘sex’, ‘men’)   给obj对象添加属性sex,值为men
delattr(obj,‘name’)   删除obj对象中的name属性
'''

class Foo:
    def __init__(self,name,age):
        self.name = name
        self.age = age

obj = Foo('Alex',18)
print(getattr(obj,'name'))  # Alex
print(hasattr(obj,'name'))  # True
setattr(obj,'sex','men')
print(obj.sex)              # men
delattr(obj,'name')

9. Python 进制转换

将字符串先转换为十进制,再使用相应的函数进行转换。

2进制8进制10进制16进制
2进制-bin(int(n,8))bin(int(n,10))bin(int(n,16))
8进制oct(int(n,2))-oct(int(n,10))oct(int(n,16))
10进制int(n,2)int(n,8)-int(n,16)
16进制hex(int(n,2))hex(int(n,8))hex(int(n,10))-
  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值