python面试知识点小结

614 阅读37分钟

python部分 总结

大部分文字都是自每个标题的链接摘录,以便个人复习回顾。标准库实例教程

1.python函数传参

a = 1
def fun(a):
    a = 2
fun(a)
print(a)  # 1

a = []
def fun(a):
    a.append(1)
fun(a)
print(a)  # [1]
  • 1.所有的变量都可以理解是内存中一个对象的“引用”,或者,也可以看似c中void*的感觉

  • 2.函数传参分两种:可变对象和不可变对象,当一个引用传递给函数的时候,函数自动复制一份引用,这个函数里的引用和外边的引用没有半毛关系了,函数内的引用指向的是可变对象,对它的操作就和定位了指针地址一样,在内存里进行修改。

  • 衍生问题:

    • 1.python中有哪些可变类型和不可变类型,它们的区别是什么?

      • 可变类型:会在原来的内存地址上修改元素如列表,字典,集合
      • 不可变类型:不会在原来的内存地址上修改元素如strings, tuples, 和numbers,尤其记住string类型
    • 2.深拷贝和浅拷贝的区别?

      • 他们本质的区别是拷贝出来的对象的地址是否和原对象一样,也就是地址的复制还是值的复制 的区别

      • 1.浅拷贝是对一个对象父级(外层)的拷贝,并不会拷贝子级(内部) 使用浅拷贝的时候,分为两种情况:

        • 1.如果最外层的数据类型是可变的,比如说列表,字典等,浅拷贝会开启新的地址空间去存放。
        • 2.如果最外层的数据类型是不可变的,比如元组,字符串等,浅拷贝对象的时候, 还是引用对象的地址空间。
      • 2.深拷贝是对一个对象是所有层次的拷贝(递归),内部和外部都会被拷贝过来。

      深拷贝也分两种情况: - 1.最外层数据类型可变。这个时候,内部和外部的都会拷贝过来。 - 2.外层数据类型不可变,如果里面是可变数据类型,会新开辟地址空间存放。如果内部数据类型不可变, 才会如同浅拷贝一样,是对地址的引用。

2.实例方法、类方法、静态方法

@classmethod@staticmethod
class A(object):
    def foo(self,x):
        print ("executing foo(%s,%s)"%(self,x))

    @classmethod
    def class_foo(cls,x):
        print ("executing class_foo(%s,%s)"%(cls,x))

    @staticmethod
    def static_foo(x):
        print ("executing static_foo(%s)"%x)

a=A()

1.self和cls是对类或者实例的绑定
2.实例方法需要实例调用a.foo(x)
3.类方法一样,只不过它传递的是类而不是实例,A.class_foo(x)
4.静态方法其实和普通的方法一样,不需要对谁进行绑定,唯一的区别是调用的时候需要使用
a.static_foo(x)或者A.static_foo(x)来调用
5.self和cls可以替换别的参数,但是python的约定是这俩,还是不要改的好

3.一切皆是对象 metaclass

在 Python 中,一切皆对象

字符串,列表,字典,函数是对象,类也是一个对象,因此你可以:
1.把类赋值给一个变量。
2.把类作为函数参数进行传递。
3.把类作为函数的返回值。
4.在运行时动态地创建类。

class Foo(object):
    foo = True

class Bar(object):
    bar = True

def echo(cls):
    print (cls)

def select(name):
    if name == 'foo':
        return Foo        # 返回值是一个类
    if name == 'bar':
        return Bar
        
>>> a = select('foo')
>>> a
<class '__main__.Foo'>


type 除了可以返回对象的类型,它还可以被用来动态地创建类(对象)

1.基本类创建
class Foo(object):
    pass  
等同于
Foo = type('Foo', (object, ), {})    # 使用 type 创建了一个类对象


2.创建有方法、参数的类
class Foo(object):
    foo = True
    def greet(self):
        print('hello world')
        print(self.foo)
等同于
def greet(self):
    print('hello world')
    print(self.foo)

Foo = type('Foo', (object, ), {'foo': True, 'greet': greet})

4.什么是metaclass

类是实例对象的模板,元类是类的模板

元类(metaclass)是用来创建类(对象)的可调用对象,这里的可调用对象可以是函数或者类等,
但一般情况下,我们使用类作为元类。

type 就是一个元类,元类的主要目的是为了控制类的创建行为

当我们定义 class Bar(Foo) 时,Python 会首先在当前类,即 Bar 中寻找 __metaclass__,如果没
有找到,就会在父类 Foo 中寻找 __metaclass__,如果找不到,就继续在 Foo 的父类寻找,如此继
续下去,如果在任何父类都找不到 __metaclass__,就会到模块层次中寻找,如果还是找不到,就会
用 type 来创建这个类。(详细看链接)

1.在 Python 中,类也是一个对象。
2.类创建实例,元类创建类。
3.元类主要做了三件事:
	1.拦截类的创建
	2.修改类的定义
	3.返回修改后的类
4.当你创建类时,解释器会调用元类来生成它,定义一个继承自 object 的普通类意味着调用 type 
来创建它。

5._new_ _new_(必考)

1.概念

  • __new__是新式类中出现,是一种静态方法在以下情况下可以使用:

    • 1.需要控制实例的创建的时候,使用__new__;在类创建过程中,最先调用并返回类实例
    • 2.在实例已经被创建出来的时候,当需要实例初始化的时候,使用__init__
    • 3.一般,不建议重写__new__,除非是在构建str、int、unincode或者tuple不可变类型的子类的时候
  • new必须要返回一个实例对象,不过可以是别的类的对象,__new__负责对象的创建,__init__负责对象的初始化,因此__new__的过程在__init__之前。

2.单例模式(重要)

  • 单例模式是一种常用的软件设计模式,在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,new是实现单例模式的方法之一。

1.单例模式优点

  • 1.单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁的创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
  • 2.单例模式可以避免对资源的多重占用,例如一个文件操作,由于只有一个实例存在内存中,避免对同一资源文件的同时操作。
  • 3.单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如,可以设计一个单例类,负责所有数据表的映射处理。

2.单例模式缺点

  • 1.单例模式的扩展是比较困难的
  • 2.赋于了单例以太多的职责,某种程度上违反单一职责原则(六大原则后面会讲到
  • 3.单例模式是并发协作软件模块中需要最先完成的,因而其不利于测试
  • 4.单例模式在某种情况下会导致资源瓶颈

3.单例模式的应用场景

  • 1.生成全局惟一的序列号
  • 2.访问全局复用的惟一资源,如磁盘、总线等
  • 3.单个对象占用的资源过多,如数据库等
  • 4.系统全局统一管理,如Windows下的Task Manager
  • 5.网站计数器
class SingleModel():
    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, '_instance'):
            cls._instance = super().__new__(cls, *args, **kwargs)
        return cls._instance

class MyClass(SingleModel):
	pass

>>> MyClass._instance
Traceback (most recent call last):
  File "<pyshell#20>", line 1, in <module>
    MyClass._instance
AttributeError: type object 'MyClass' has no attribute '_instance'

>>> a = MyClass()
>>> MyClass._instance
<__main__.MyClass object at 0x0000017911FBFA30>

>>> c = MyClass()
>>> c
<__main__.MyClass object at 0x0000017911FBFA30>

# 可以看见现在实例指向的地址都是相同的

4.问题:请手写两个实现单例模式的方法。(详见链接单例模式

  • 1.模块化
  • 2.装饰器
  • 3.new(推荐)
  • 4.使用类实现单例模式
class SingleModel():
    @classmethod
    def instance(cls, *args, **kwargs):
        if not hasattr(SingleModel, "_instance"):
            SingleModel._instance = SingleModel(*args, **kwargs)
        return SingleModel._instance

a1 = SingleModel.instance()
a2 = SingleModel.instance()
    
print(f"a1 id: {id(a1)}")
print(f"a2 id: {id(a2)}")

a1 id: 2752146766384
a2 id: 2752146766384

import threading
class SingleModel():
  lock = threading.Lock()
  @classmethod
  def instance(cls, *args, **kwargs):
      with SingleModel.lock:
        if not hasattr(SingleModel, "_instance"):
            SingleModel._instance = SingleModel(*args, **kwargs)
      return SingleModel._instance   
  • 5.使用metaclass元类(通过指定类的metaclass实现)
衍生问题
  • 多线程如何处理?
# 此时会生成多个实例
import time
import threading
class SingleModel():
    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, "_instance"):
        	time.sleep(1)
            cls._instance = super().__new__(cls, *args, **kwargs)
        return cls._instance

def task(arg):
    obj = SingleModel()
    print(obj)
    
for i in range(20):
    t = threading.Thread(target=task,args=[i,])
    t.start()
    
obj = SingleModel()
print(obj)
  • 解决方法加上线程锁

  • 方法一 定义类变量全局锁

class SingleModel():
	lock = threading.Lock()
    def __new__(cls, *args, **kwargs):
    	with SingleModel.lock:
          if not hasattr(cls, "_instance"):
              time.sleep(1)
              cls._instance = super().__new__(cls, *args, **kwargs)
          return cls._instance
  • 方法二 使用同步锁装饰器
from functools import wraps
import threading
def syncLock(func):
	@wraps(func)
    def decorated(*args, **kwargs):
    	with threading.Lock():
        	return func(*args, **kwargs)
    return decorated

class SingleModel():
	lock = threading.Lock()
    @syncLock
    def __new__(cls, *args, **kwargs):
    	with SingleModel.lock:
          if not hasattr(cls, "_instance"):
              time.sleep(1)
              cls._instance = super().__new__(cls, *args, **kwargs)
          return cls._instance
  • 重载__new__方法后类无法传参

6.类变量和实例化变量

  • 类变量是在类中所有实例可以共享的变量
  • 实力变量是每个类独有的变量
class Test(object):  
    num_of_instance = 0  
    def __init__(self, name):  
        self.name = name  
        Test.num_of_instance += 1  
  
if __name__ == '__main__':  
    print Test.num_of_instance   # 0
    t1 = Test('jack')  
    print Test.num_of_instance   # 1
    t2 = Test('lucy')  
    print(t1.name , t1.num_of_instance)  # jack 2
    print(t2.name , t2.num_of_instance)  # lucy 2
    
用num_of_instance统计有多少个实例对象

7.python自省

运行时能够获得对象的类型

type(),dir(),getattr(),hasattr(),isinstance()

8.字典推导式

python2.7后出现

d = {key: value for (key, value) in iterable}

9.python的单下划线和双下划线

  • 单下划线表示:变量私有,程序员用来指定私有变量的一种方式,不能用from module import * 导入,只有类对象和子类对象自己能访问到这些变量。(类似于cpp的protected,初衷是对象内部调用,但还是可以通过外部呼叫到)
  • 双下划线表示:开始的是私有成员,意思是只有类对象自己能访问,连子类对象也不能访问到这个 数据。(类似于cpp的private)
>>> class MyClass():
...     def __init__(self):
...             self.__superprivate = "Hello"
...             self._semiprivate = ", world!"
...
>>> mc = MyClass()
>>> print(mc.__superprivate)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: myClass instance has no attribute '__superprivate'
>>> print(mc._semiprivate)
, world!
>>> print(mc.__dict__)
{'_MyClass__superprivate': 'Hello', '_semiprivate': ', world!'}

解析器用_classname__foo来代替这个名字,以区别和其他类相同的命名,它无法直接像公有成员一样
随便访问,通过对象名._类名__xxx这样的方式可以访问.(其实子类也可以通过_MyClass__superprivate
访问这个私有属性,这是一种伪造的私有变量)

10.迭代器和生成器

  • 将列表生成式中[]改成()之后数据结构是否改变? 答案:是,从列表变为生成器。
>>> L = [x*x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x*x for x in range(10))
>>> g
<generator object <genexpr> at 0x0000028F8B774200>
python中,任意对象,只要定义了__next__方法,它就是一个迭代器。因此,python中的容器如列
表、元组、字典、集合、字符串都可以被称作可迭代对象。
如果你不想用for循环迭代,这时你可以:
先调用容器iter()(以列表为例)的__iter__()方法
再使用 next() 内置函数来调用 __next__() 方法
当元素用尽时,__next__() 将引发 StopIteration 异常
>>> a = [1, 2, 3]
>>> b = a.__iter__()  # 或者iter(a)
>>> b.__next__()  # 或者next(b)
1
>>> b.__next__()
2
>>> 
>>> b.__next__()
3
>>> b.__next__()
Traceback (most recent call last):
  File "<pyshell#71>", line 1, in <module>
    b.__next__()
StopIteration

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

import sys
def fibonacci(n): # 生成器函数 - 斐波那契
    a, b, counter = 0, 1, 0
    while True:
        if (counter > n): 
            return
        yield a
        a, b = b, a + b
        counter += 1
f = fibonacci(10) # f 是一个迭代器,由生成器返回生成
 
while True:
    try:
        print (next(f), end=" ")
    except StopIteration:
        sys.exit()
  • 生成器的应用场景
  • 内容是(一个保存了400W条分词后的中文文本数据的文件,每条数据大概200-400个词,电脑内存32G,现在需要统计词频,用做后续算法的处理。这个问题该怎么处理呢?直接用生成器,然后喂给 Collection.Counter()就可以统计出词频了,也恰好Collection.Counter()支持生成器输入)

11.*args和**kwrgs 查看

  • args 函数参数前有一个表示可变的位置参数,当你不确定你的函数里将要传递多少参数时,可以传入任意个数的参数。
  • ****kwrgs**两个星号表示是可变的关键字参数。
def foo(*args, **kwarg):
    for item in args:
        print(item)

    for k,v in kwarg.items():
        print(k,v)
    # 我是分割线
    print(30*'=')
    
if __name__ == '__main__':
    foo(1, 2, 3, a=4, b=5)
    foo(2, 3, a=4, b=5, c=1)

无论我们怎么传入参数,*args都是一个tuple类型,不能进行修改。
  • 还可以表示解包,把序列/集合解包(unpack)成位置参数,两个星号***把字典解包成关键字参数
>>> print(*[1, 2, 3])
1 2 3

def foo(*args, **kwarg):
    for item in args:
        print(item)

    for k,v in kwarg.items():
        print(k,v)
    # 我是分割线
    print(30*'=')
    
if __name__ == '__main__':
	v = (1, 2, 4)
    v2 = [11, 15, 23]
    d = {'a':1, 'b':12}
    foo(v, d)
    foo(*v, **d)
    foo(v2, d)
    foo(*v2, **d)

foo(*d, **d)等价于foo(1, 2, 4, a=1, b=12)

12.*****面向切面编程AOP和装饰器(必考)

  • AOP 对原有代码毫无入侵性,把和主业务无关的事情,放到代码外面去做,这就是AOP的好处
  • 装饰器是将函数作为参数传给另一个函数,另一个函数修饰一番(比如打印该函数的log)
  • 作用:是python实现AOP的主要方式,是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的有插入日志、性能测试、事务处理等,装饰器的作用就是为已经存在的对象添加额外的功能。
  • 大佬的经典诠释:每个人都有的内裤主要功能是用来遮羞,但是到了冬天它没法为我们防风御寒,咋办?我们想到的一个办法就是把内裤改造一下,让它变得更厚更长,这样一来,它不仅有遮羞功能,还能提供保暖,不过有个问题,这个内裤被我们改造成了长裤后,虽然还有遮羞功能,但本质上它不再是一条真正的内裤了。于是聪明的人们发明长裤,在不影响内裤的前提下,直接把长裤套在了内裤外面,这样内裤还是内裤,有了长裤后宝宝再也不冷了。装饰器就像我们这里说的长裤,在不影响内裤作用的前提下,给我们的身子提供了保暖的功效。
  • 基础装饰器
def a_new_decorator(a_func):
 
    def wrapTheFunction():
        print("I am doing some boring work before executing a_func()")
 
        a_func()
 
        print("I am doing some boring work after executing a_func()")
 
    return wrapTheFunction
 
 
 
def a_function_requiring_decoration():
    print("I am the function which needs some decoration to remove my foul smell")
    
a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)
a_function_requiring_decoration()

等价于

@a_new_decorator
def a_function_requiring_decoration():
    print("I am the function which needs some decoration to remove my foul smell")
    
  • 简单log装饰器
from functools import wraps
import datetime
import os

def logs(func):
    @wraps(func)
    def decorated(*args, **kwargs):
        now = datetime.datetime.now()
        file_name = os.getcwd() + str(now.date()) + '-log.txt'
        with open(file_name, 'a') as f:
            f.write(str(now) + " run the function: " + func.__name__ +
                     '\n')
        return func(*args, **kwargs)
    return decorated

@logs
def justFunc(a, b):
    return a+b

if __name__ == '__main__':
    print(justFunc(1, 2))

注:这里的wraps是为了让传入函数的__name__等于原本的name,因为返回decorated后,其实是执行 了justFunc = logs(justFunc),这时打印justFunc.__name__会是decorated,wraps的功能就是让 justFunc的name还是justFunc,详细看源码。

  • 简单缓存装饰器

写dfs(深度优先遍历)的时候我们经常用到functools.lru_cache,它会查找我们是否执行过这条路 径,如果执行过直接返回储存的结果,否则继续执行基本函数,这样实现基本的剪枝。

我们这里用简单的斐波那契数列递归版完成显示:

1.没有写缓存装饰器

def fib_with_cache(n):
    if n < 2:
        return n
    return fib_with_cache(n - 2) + fib_with_cache(n - 1)

if __name__ == '__main__':
    now = time.time()
    print(fib_with_cache(35))
    print(time.time()-now)
    
结果
9227465
7.306048393249512
运行了7.3

2.简单缓存装饰器

from functools import wraps
import datetime
import time
import os

def cache(func):
    cache_list = {}
    
    @wraps(func)
    def decorated(*args, **kwrgs):
        if args not in cache_list:
            res = func(*args, **kwrgs)
            cache_list[args] = res
            return res
        else:
            return cache_list[args]
    return decorated
    
@cache
def fib_with_cache(n):
    if n < 2:
        return n
    return fib_with_cache(n - 2) + fib_with_cache(n - 1)


if __name__ == '__main__':
    now = time.time()
    print(fib_with_cache(35))
    print(time.time()-now)

结果
9227465
0.01900339126586914

可以看到时间差距很大 当然也可以自己参照lru_cache写一个属于自己定制的缓存装饰器,这里的lru指的是Least Recently Used最近最少使用的就会被清除缓存,也可以基于时间去做清除,可以参照redis的几种缓存机制(当然redis的lru和python有些许不同,可以参照《LRU原理和redis的lru实现》)。

除此以外,装饰器还可以被用在认证授权,判断用户是否有权限执行函数。

3.类装饰器(把日志装饰器用类来实现)

from functools import wraps
class logs():
    def __init__(self, logfile='out.log'):
        self.logfile = logfile
 
    def __call__(self, func):
        @wraps(func)
        def decorated(*args, **kwargs):
            log_string = func.__name__ + " was called"
            print(log_string)
            with open(self.logfile, 'a') as f:
                f.write(log_string + '\n')
            # 现在,发送一个通知(可以在这里发送邮件给管理员)
            self.notify()
            return func(*args, **kwargs)
        return decorated
    
    def notify(self):
        # logit只打日志,不做别的(这里可以写发送邮件)
        pass

@logs()
def myfunc1():
    pass

if __name__ == '__main__':
    print(myfunc1())

这样用类来完善我们的装饰器,可以实现各种定制的需求(在里面添加方法)

13.鸭子类型

我们并不关心对象是什么类型,到底是不是鸭子,只关心行为。

比如在python中,有很多file-like的东西,比如StringIO,GzipFile,socket。它们有很多相同的方法,我们把它们当作文件使用。

又比如list.extend()方法中,我们并不关心它的参数是不是list,只要它是可迭代的,所以它的参数可以是list/tuple/dict/字符串/生成器等.

14.面向对象的三大特性

  • 封装:根据职责将属性和方法封装到一个抽象的类中定义类的准则
  • 继承:实现代码的重用,相同的代码不需要重复的编写
  • 多态:不同的子类调用相同的父类,产生不同的结果

15.闭包

  • 闭包的本质就是函数的嵌套定义,即在函数内部再定义函数,闭包有两种不同的方式,第一种是在函数内部就“直接调用”;第二种是“返回一个函数名称”。装饰器的本质也是一种闭包。

    • 1.必须有一个内嵌函数
    • 2.内嵌函数必须引用外部函数中的变量
    • 3.外部函数的返回值必须是内嵌函数
  • 闭包的作用:

    • 1.保存函数的状态信息,使函数的局部变量信息依然可以保存下来,保存局部信息不被销毁。(如棋盘类游戏棋子的位置,计数器每执行这个函数一次需要加一)
    def create(pos=[0,0]):
      def move(direction, step):
          new_x = pos[0] + direction[0]*step
          new_y = pos[1] + direction[1]*step
          pos[0] = new_x
          pos[1] = new_y
          return pos
      return move
    if __name__ == '__main__':
      move = create()
      print(move([1,0], 10))
      print(move([0,1], 20))
      print(move([-1,0], 10))
    
    [10, 0]
    [10, 20]
    [0, 20]
    
    def maker(step=1):
        num = 1
        def func():
            nonlocal num
            num = num + step
            print(num)
        return func
    
    if __name__ == '__main__':
        func = maker(3)
        j = 1
        while j < 5:
            func()
            j += 1
    结果
    4
    7
    10
    13
    我们再调用func()
    16
    
    • 2.装饰器通过装饰器间接实现闭包价值。
    • 3.面向对象,用类创建出来的对象都具有相同的属性方法。闭包也是实现面向对象的方法之一。在python当中虽然我们不这样用,在其他编程语言入比如avaScript中,经常用闭包来实现面向对象编程
    • 4.单例模式,也是装饰器的应用。

16.python不需要重载!

函数重载主要是为了解决两个问题。

可变参数类型。 可变参数个数。 另外,一个基本的设计原则是,仅仅当两个函数除了参数类型和参数个数不同以外,其功能是完全相同的,此时才使用函数重载,如果两个函数的功能其实不同,那么不应当使用重载,而应当使用一个名字不同的函数。

那么对于情况 1 ,函数功能相同,但是参数类型不同,python 如何处理?答案是根本不需要处理,因为 python 可以接受任何类型的参数,如果函数的功能相同,那么不同的参数类型在 python 中很可能是相同的代码,没有必要做成两个不同函数。

那么对于情况 2 ,函数功能相同,但参数个数不同,python 如何处理?大家知道,答案就是缺省参数。对那些缺少的参数设定为缺省参数即可解决问题。因为你假设函数功能相同,那么那些缺少的参数终归是需要用的。

17.GIL线程全局锁(重要)

概念

GIL: 全局解释器锁(英语:Global Interpreter Lock,缩写GIL),是计算机程序设计语言解释器用于同步线程的一种机制,它使得任何时刻仅有一个线程在执行。即便在多核心处理器上,使用 GIL 的解释器也只允许同一时间执行一个线程。在 Cpython 解释器(Python语言的主流解释器)中,有一把全局解释锁(Global Interpreter Lock)(像Jpython、Ironpython之类的是没有GIL的,为什么不用?用了Java/C#用于解析器实现,他们也失去了利用社区众多C语言模块有用特性的机会。)

GIL的运行

  • 在解释器解释执行 Python 代码时,先要得到这把锁,意味着,任何时候只可能有一个线程在执行代码,其它线程要想获得 CPU 执行代码指令,就必须先获得这把锁,如果锁被其它线程占用了,那么该线程就只能等待,直到占有该锁的线程释放锁才有执行代码指令的可能。
  • 为了让各个线程能够平均利用CPU时间,python会计算当前已执行的微代码数量,达到一定阈值后就强制释放GIL。而这时也会触发一次操作系统的线程调度(当然是否真正进行上下文切换由操作系统自主决定)。

GIL的问题

  • 1.Python的多线程程序并不能利用多核CPU的优势 (比如一个使用了多个线程的计算密集型程序只会在一个单CPU上面运行)因为GIL保证了同一时间只有一个线程运行。
  • 2.线程之间上下文切换耗费时间(比协程上下文切换耗费更多时间),上下文切换时需要保存上一个线程的运行状态,运行到哪行代码。
  • 3.上下文切换不能控制,由操作系统同意调度,且需要时间分配cpu进行线程同步。
  • 4.当执行cpu密集型任务时,从release GIL到acquire GIL之间几乎是没有间隙的。(所以当其他在其他核心上的线程被唤醒时,大部分情况下主线程已经又再一次获取到GIL了,这个时候被唤醒执行的线程只能白白的浪费CPU时间,看着另一个线程拿着GIL欢快的执行着。)

GIL问题解决

1.解决cpu密集型任务

使用multiprocessing(多进程)替代thread,弥补thread库因为GIL而低效的缺陷,每个进程有自己的独立的GIL,因此不会出现进程之间的GIL争抢。

  • 多进程可以有效的解决cpu密集型任务
import multiprocessing
from multiprocessing import Process
import time

def decrement(n):
    while n > 0:
        n -= 1
    return n
     
if __name__ == '__main__':
    start = time.time()
    pool = multiprocessing.Pool(4)
    l = [25000000]*4
    results = pool.map(decrement, l)
    pool.close()
    pool.join()
    for result in results:
        print(result)

    print(time.time()-start)

# 用1个进程时,需要15秒,4个进程3.7秒

2.多进程的缺陷

它的引入会增加程序实现时线程间数据通讯和同步的困难。就拿计数器来举例子,如果我们要多个线程累加同一个变量,对于thread来说,申明一个global变量,用thread.Lock的context包裹住三行就搞定了。而multiprocessing由于进程之间无法看到对方的数据,只能通过在主线程申明一个Queue,put再get或者用share memory的方法。这个额外的实现成本使得本来就非常痛苦的多线程程序编码,变得更加痛苦了。

3.使用协程替代多线程

协程是在一个线程中创建的,协程本质就是用户态下的线程,进程里的线程的切换调度是由操作系统来负责的。但是线程内的协程的调度执行,是由线程来负责的。 协程可以减小多线程的上下文切换开销以及避免应对多线程复杂的同步问题,可定义切换时机。

4.多线程什么时候释放GIL全局解释锁?

  • 1.时间片耗尽(cpu时间)
  • 2.任务遇到I/O等待时
  • 3.执行任务结束
  • 4.执行到字节码阈值时

18.协程

概念

协程是进程和线程的升级版,协程是用户级的线程,是线程之上的轻量级线程,进程和线程都面临着内核态和用户态的切换问题而耗费许多切换时间,而协程就是用户自己控制切换的时机,不再需要陷入系统的内核态。 操作系统感知不到协程的存在,只调度内核线程。

  • 1.多线程的使用是可以让一个程序获得更多的计算时间,但是协程的使用不会。
  • 2.多线程的使用在多核的情况下,可以达到并行的效果,但是协程的使用不会达到并行的效果。

因为操作系统感知不到协程的存在,只会把时间片和CPU核心分给线程。至于分给线程的时间,线程又会分配给哪个协程来运行,那是线程自己决定的内容。比如分配2ms给一个拥有两个协程的线程A,线程被操作系统调度指派给了CPU核心C1, A会决定在C1运行哪个线程,,可以雨露均沾,让两个协程各自运行1ms, 也可以是把2ms全部分配给一个协程,自始至终,所有的协程都运行在CPU核心C1上,所以无法实现协程并行

详细看每个链接,以下都是总结每个链接的问题点

1.生成器和yield

1.请说出yield和return的区别?

  • 1.return不能写成“temp=return xxxx”的形式,会提示语法错误,但是yield可以写成“temp=yield xxxx”的形式。
  • 2.return后面的语句都是不会再执行的,但是yield语句后面的依然会执行(yield后面的代码并不是在第一次迭代的时候执行的,而是第二次迭代的时候才执行第一次yield后面没有执行的代码,这就和协程的切换类似,也正是这个特性,构成了yield为什么是实现协程的最简单实现。)

2.生成器有哪三个重要方法?分别的作用是?

  • next()

    • 用于迭代生成器
    def my_generator(n):
        for i in range(n):
            temp = yield i
            print(f"it's {temp}")
    
    if __name__ == '__main__':
        g = my_generator(5)
        print(next(g))
        print(next(g))
        g.send(100)
        print(next(g))
        print(next(g))
    
  • send()

    • (1)它的主要作用是,当我需要手动更改生成器里面的某一个值并且使用它,则send发送进去一个数据,然后保存到yield语句的返回值,以提供使用
    • (2)send(arg)的返回值就是那个本来应该被迭代出来的那个值,这样既可以保证我能够传入新的值,原来的值也不会弄丢。
  • throw()

    • 手动抛出一个异常,中断yield的迭代,之后的yield全部失效
    def my_generator():
        yield 'a'
        yield 'b'
        yield 'c'
    g=my_generator()
    print(next(g))
    print(next(g))
    print('-------------------------')
    print(g.throw(StopIteration))
    print(next(g))
    '''运行结果为:
    a
    b
    -------------------------
    StopIteration
    

    因为在迭代完 b 之后,就触发了StopIteration异常,这相当于后面的 ‘c’ 已经没用了,跳过了c ,c再也不会执行,就中断了,所以后面的 'c'再也不会迭代,所以这里不会再返回任何值,返回的是StopIteration。

3.生成器的启动?

可以通过两种方式

  • 1.直接使用next(g),这会直接开始迭代第一个元素(推荐使用这个启动)
  • 2.使用g.send(None)进行启动,注意第一次启动的时候只能传入None,如果传入其他具体的值则会报错哦!

4.生成器的关闭?

  • 1.使用close()方法手动关闭生成器函数,后面的调用会直接返回StopIteration异常
  • 2.如果一个生成器被中途关闭(close())之后,在此调用next()方法,后面的迭代或抛出StopIteration异常。
  • 3.在一个生成器中,如果没有return,则默认执行到函数完毕时返回StopIteration,如果遇到return,如果在执行过程中 return,则直接抛出 StopIteration 终止迭代,如果在return后返回一个值,那么这个值为StopIteration异常的说明,不是程序的返回值。
def g3():
    yield 'a'
    return '这是错误说明'
    yield 'b'   #有一些编辑器会提示错误,此处为unreachable code,即不可到达的代码
g=g3()
next(g)
next(g)
'''运行结果为:
a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration: 这是错误说明

注意:生成器没有办法使用return来返回值。因为return返回的那个值是通过StopIteration的异常信息返回的,所以我没办法直接获取这个return返回的值。

5.如何获取return的返回值?

  • 1.使用后面的yield from来获取
  • 2.因为return返回的值是作为StopIteration的一个value属性存在的,StopIteration本质上是一个类,所以可以通过访问它的value属性获取这个return返回的值。

def g3():
    yield 'a'
    return '这是错误说明'
    yield 'b'
g=g3()
 
try:   
    print(next(g))  #a
    print(next(g))  #触发异常
except StopIteration as exc:
    result=exc.value
    print(result)

2.使用yield实现协程

1.什么是进程、线程和协程?

2.进程之间如何进行上下文切换?(线程后面两步,进程的切换代价比较大)

  • 1.切换页目录以使用新的地址空间
  • 2.切换内核栈
  • 3.切换硬件上下文

3.协程相对线程的优势?

  • 1.协没有线程的上下文切换消耗。协程的调度切换是用户(程序员)手动切换的,因此更加灵活,因此又叫用户空间线程。(因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。)
  • 2.原子操作性,由于协程是用户调度的,所以不会出现执行一半的代码片段被强制中断了,因此无需原子操作锁。(就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。)

4.子程序?

  • 子程序又可以叫做函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。

5.用yield实现一个简单的协程

import time
# 定义一个消费者,他有名字name
# 因为里面有yield,本质上是一个生成器
def consumer(name): 
    print(f'{name}  准备吃包子啦!,呼吁店小二')
    while True:
        baozi=yield  # 接收send传的值,并将值赋值给变量baozi
        print(f'包子 {baozi+1} 来了,被 {name} 吃了!')
 
# 定义一个生产者,生产包子的店家,店家有一个名字name,并且有两个顾客c1 c2
def producer(name, c1, c2):
    next(c1)  # 启动生成器c1
    next(c2)  # 启动生成器c2
    print(f'{name} 开始准备做包子啦!')
    for i in range(5):
        time.sleep(1)
        print(f'做了第{i+1}包子,分成两半,你们一人一半')
        c1.send(i)
        c2.send(i)
        print('------------------------------------')
 
c1=consumer('张三')  # 把函数变成一个生成器
c2=consumer('李四')
producer('店小二', c1, c2)

从某些角度来理解,协程其实就是一个可以暂停执行的函数,并且可以恢复继续执行,实现值的传递。那么yield已经可以暂停执行了,如果在暂停后有办法把一些 value 发回到暂停执行的函数中,那么 Python 就有了『协程』。于是在PEP 342中,添加了 把东西发送到已经暂停的生成器中 的方法,这个方法就是send()。

6.协程的四种状态?

  • 1.GEN_CREATED:等待执行,即还没有进入协程
  • 2.GEN_RUNNING:解释器执行(这个只有在多线程时才能查看到它的状态,而协程是单线程的)
  • 3.GEN_SUSPENDED:在yield表达式处暂停(协程在暂停等待的时候的状态)(挂起)
  • 4.GEN_CLOSED:执行结束(协程执行结束了之后的状态)
  • 通过inspect.getgeneratorstate(generator)查看状态
from inspect import getgeneratorstate  # 一定要导入
from time import sleep
 
def my_generator():
    for i in range(3):
        sleep(0.5)
        x = yield i + 1  
 
g=my_generator()  #创建一个生成器对象
 
def main(generator):
    try:
        print("生成器初始状态为:{0}".format(getgeneratorstate(g)))  
        next(g)  #激活生成器
        print("生成器初始状态为:{0}".format(getgeneratorstate(g)))  
        g.send(100)
        print("生成器初始状态为:{0}".format(getgeneratorstate(g)))  
        next(g)
        print("生成器初始状态为:{0}".format(getgeneratorstate(g)))  
        next(g)
    except StopIteration:
        print('全部迭代完毕了')
        print("生成器初始状态为:{0}".format(getgeneratorstate(g))) 
 
main(g)
运行结果为:
生成器初始状态为:GEN_CREATED
生成器初始状态为:GEN_SUSPENDED
生成器初始状态为:GEN_SUSPENDED
生成器初始状态为:GEN_SUSPENDED
全部迭代完毕了
生成器初始状态为:GEN_CLOSED

7.yield实现协程的不足之处

  • 1.协程函数的返回值不是特别方便获取
  • 2.Python的生成器是协程coroutine /ˌkəuru:'ti:n/ 的一种形式,但它的局限性在于只能向它的直接调用者每次yield一个值
  • 可以通过yield from弥补不足

3.yield from详解

1.什么是yield from?

  • yield from 是yield的升级改进版本,如果将yield理解成“返回”,那么yield from就是“从什么(生成器)里面返回”,这就构成了yield from的一般语法,即yield from generator。
  • yield from返回另外一个“生成器 、元组、 列表、range()函数产生的序列等可迭代对象”,而yield只是返回一个元素。
  • yield from iterable本质上等于 for item in iterable: yield item 。

2.yield from实现代码优化(for item in iterable: yield item )

def generator1():
    yield 1
    yield 2
    yield 3

def generator2():
    yield 'a'
    yield 'b'
    yield 'c'
    yield from generator1()  # 生成器
    yield from [11,22,33,44]  # 数组
    yield from (12,23,34)  # 元组
    yield from range(3)  # range
 
for i in generator2():
    print(i,end=' , ')
结果
a , b , c , 1 , 2 , 3 , 11 , 22 , 33 , 44 , 12 , 23 , 34 , 0 , 1 , 2 , 

3.yield from如何解决yield无法获取生成器return的返回值?

  • result = yield from iterator可以接收return的值(因为可以触发stopIteration的异常)
  • 在使用yield生成器的时候,如果使用for语句去迭代生成器,则不会显式的出发StopIteration异常,而是自动捕获StopIteration异常,所以如果遇到return,只是会终止迭代,而不会触发异常,故而也就没办法获取return的值。
def my_generator():
    for i in range(5):
        if i==2:
            return '我被迫中断了'
        else:
            yield i
 
def main(generator):
    try:
        for i in generator:  #不会显式触发异常,故而无法获取到return的值
            print(i)
    except StopIteration as exc:
        print(exc.value)
 
g=my_generator()  #调用
main(g)

# 结果
0
1

使用yield from

def my_generator():
    for i in range(5):
        if i==2:
            return '我被迫中断了'
        else:
            yield i
 
def wrap_my_generator(generator):  # 定义一个包装“生成器”的生成器,它的本质还是生成器
    result=yield from generator    # 自动触发StopIteration异常,并且将return的返回值赋值# 给yield from表达式的结果,即result
    print(result)
 
def main(generator):
    for j in generator:
        print(j)
 
g=my_generator()
wrap_g=wrap_my_generator(g)
main(wrap_g)  # 调用

# 结果
0
1
我被迫中断了

4.yield from如何实现数据传输通道?

1.yield from可以把客户端的send的值传递给子生成器实现通道作用。

  • yield数据交互模式:yield涉及到“调用方与生成器两者”的交互,生成器通过next()的调用将值返回给调用者,而调用者通过send()方法向生成器发送数据。
  • yield from数据交互模式:客户端(即上面的main生成器函数)通过send()和委派生成器(包含 yield from (iterable) 表达式的生成器函数)与子生成器(yield from 表达式中 (iterable) 部分获取的生成器)交互,send的值可以通过yield from传给它下面的子生成器。
def average():
    total = 0.0  # 数字的总和
    count = 0    # 数字的个数
    avg = None   # 平均值
    while True:
        num = yield avg
        total += num
        count += 1
        avg = total/count
 
def wrap_average(generator):
    yield from generator
 
# 定义一个函数,通过这个函数向average函数发送数值
def main(wrap):
    print(next(wrap))  # 启动生成器
    print(wrap.send(10))  # 10
    print(wrap.send(20))  # 15
    print(wrap.send(30))  # 20
    print(wrap.send(40))  # 25
 
g = average()
wrap=wrap_average(g)
main(wrap)
 
# 结果:
None
10.0
15.0
20.0
25.0

即主函数调用方main把各个value传给grouper ,而这个传入的值最终到达averager函数中,grouper并不知道传入的是什么值,因为从上面的代码看出,wrap_average里面完全没有处理这个值的任何代码

4.协程预备知识

1.进程和线程

前面介绍了进程和线程

  • 1.进程
    • 进程是系统资源分配的最小单位, 系统由一个个进程(程序)组成 一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。
    • 文本区域存储处理器执行的代码。
    • 数据区域存储变量和进程执行期间使用的动态分配的内存。
    • 堆栈区域存储着活动过程调用的指令和本地变量。
    • 通信问题:由于进程间是隔离的,各自拥有自己的内存内存资源, 因此相对于线程比较安全,所以不同进程之间的数据只能通过 IPC(Inter-Process Communication) 进行通信共享。
  • 2.线程
  • 进程(程序)就是由一个个的子程序(线程组成),线程又是由一个一个的协程函数组成,(如果定义了协程的话),线程主要是由CPU寄存器、调用栈和线程本地存储器(Thread Local Storage,TLS)组成的,CPU寄存器主要记录当前所执行线程的状态,调用栈主要用于维护线程所调用到的内存与数据,TLS主要用于存放线程的状态信息。
    • 线程属于进程
    • 线程共享进程的内存地址空间
    • 线程几乎不占有系统资源
    • 通信问题:进程相当于一个容器,而线程而是运行在容器里面的,因此对于容器内的东西,线程是共同享有的,因此线程间的通信可以直接通过全局变量进行通信,但是由此带来的例如多个线程读写同一个地址变量的时候则将带来不可预期的后果,因此这时候引入了各种锁的作用,例如互斥锁等。
  • 3.注意
    • 进程是系统分配资源的最小单位
    • 线程是CPU调度的最小单位
    • 由于默认进程内只有一个线程,所以多核CPU处理多进程就像是一个进程一个核心

2.并发和并行

  • 并发:几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态,这种方式我们称之为并发(Concurrent)

  • 并行:同一时间,同时运行几个进程,当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)

  • 并行和并发的区别在于同一时刻(其实也包括cpu核数区别)

3.阻塞和非阻塞

  • 阻塞:阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。(该线程需要等到结果返回才能继续下一步工作)

  • 非阻塞:非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。(创建多个协程,在该协程IO操作时,进行上下文无缝切换,不会阻塞)

  • 阻塞和非阻塞的区别:仅仅与等待消息通知时的状态有关。

  • 同步执行一般都会有阻塞,但也有可能没阻塞(没有等待时间,运行的快);异步执行也有可能有阻塞,也可能没有阻塞(比如一个线程只有一个协程,在这个协程进行IO操作的时候只能选择等待阻塞)。

4.同步和异步(什么是异步)

  • 同步:就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作, 简单来说,同步就是必须一件一件事做,等前一件做完了才能做下一件事。

  • 异步:当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制(协程中可以通过await控制什么时候切换到别的协程,但await的操作结束即回调到原来的协程这里的回调是无法控制的)。

5.真并发和伪并发

并发和并行都是实现异步编程的思路

  • 伪并发只有一个线程的并发,称之为“伪并发”(python的单线程用协程上下文切换就是伪并发)
  • 真并发有多个线程的并发称之为“真并发”,真并发与并行是很接近的

6.异步操作的优缺点

  • 优点
    • 1.异步操作无须额外的线程负担(线程之间的上下文切换和线程之间的数据同步)(这里指的是单线程交替执行的“伪并发”),
    • 2.并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以不必使用共享变量(即使无法完全不用,最起码可以减少共享变量的数量),减少了死锁的可能。
  • 缺点
    • 1.编写异步操作的复杂程度较高,程序主要使用回调方式进行处理,与普通人的思维方式有些 差异
    • 2.难以调试,因为异步等待回调执行的特点。

7.多线程的优缺点

  • 优点
    • 1.线程中的处理程序依然是顺序执行,符合普通人的思维习惯,所以编程简单
  • 缺点
    • 1.线程的使用(滥用)会给系统带来上下文切换的额外负担
    • 2.线程间的共享变量可能造成死锁的出现

5.python协程asyncio的核心概念与基本架构

1.什么是协程函数的事件循环?

协程函数,不是像普通函数那样直接调用运行的,必须添加到事件循环中,然后由事件循环去运行,单独运行协程函数是不会有结果的,一个线程只能有一个事件循环。

2.有几种方式获取事件循环对象(运行协程的方式)?

  • 方式一 通过创建底层事件循环运行协程函数(new或者get)
    • loop=asyncio.get_running_loop() 返回(获取)在当前线程中正在运行的事件循环,如果没有正在运行的事件循环,则会显示错误;它是python3.7中新添加的
    • loop=asyncio.get_event_loop() 获得一个事件循环,如果当前线程还没有事件循环,则创建一个新的事件循环loop;
    • loop=asyncio.set_event_loop(loop) 设置一个事件循环为当前线程的事件循环;
    • loop=asyncio.new_event_loop() 创建一个新的事件循环
    • loop.run_until_complete(coroutine)运行协程函数
    • loop.close()关闭事件循环
    import time
    import asyncio
    async def say_after_time(delay, what):
            await asyncio.sleep(delay)
            print(what)
    
    async def main():
            print(f"开始时间为: {time.time()}")
            await say_after_time(1,"hello")
            await say_after_time(2,"world")
            print(f"结束时间为: {time.time()}")
    
    loop=asyncio.get_event_loop()  # 创建事件循环对象
    # loop=asyncio.new_event_loop()  # 与上面等价,创建新的事件循环
    loop.run_until_complete(main())  # 通过事件循环对象运行协程函数
    loop.close()  # 关闭事件循环
    
  • 方式二 通过高层封装apiasyncio.run(function_name)运行协程函数
    • 1.run函数是python3.7版本新添加的,前面的版本是没有的。
    • 2.run函数总是会创建一个新的事件循环并在run结束之后关闭事件循环。
    • 3.如果在同一个线程中已经有了一个事件循环,则不能再使用run,因为同一个线程不能有两个事件循环,而且这个run函数不能同时运行两次,因为他已经创建一个了。即同一个线程中是不允许有多个事件循环loop的

3.什么是awaitable对象有哪些?

awaitable对象即是可等待对象,正是应为可等待,在等待的时候进行协程间上下文的切换,以此实现伪并行。

  • 1.coroutine:本质上就是一个函数,以前面的生成器yield和yield from为基础。
async def say_after_time(delay,what):
        await asyncio.sleep(delay)
        print(what)
  • 2.Task: 任务,其实就是对协程函数进一步的封装。
# python3.7以后添加
task = asyncio.create_task(coro())
# 老版本
task = asyncio.ensure_future(coro())
# 获取当前正在运行的任务
task = asyncio.current_task(loop=None)  # 不传loop就是默认当前loop,没有task返回None
# 获取指定loop中所有的任务
asyncio.all_tasks(loop=None)
  • 3.Future:它是一个“更底层”的概念,他代表一个一步操作的最终结果,因为一步操作一般用于耗时操作,结果不会立即得到,会在“将来”得到异步运行的结果,故而命名为Future。

  • 4.三者的关系:coroutine可以自动封装成task,而Task是Future的子类。

4.asyncio的高层API和底层API

asyncio分为高层API和低层API,都可以使用(就像drf的APIView和mixins)

1.高层API
  • 1.运行一个协程

    • asyncio.run(coro, *, debug=False) # 运行一个一步程序
  • 2.创建一个task

    • task=asyncio.create_task(coro) # python3.7以后
    • task = asyncio.ensure_future(coro())
  • 3.睡眠sleep

    • await asyncio.sleep(delay, result=None, *, loop=None) 当前的这个任务(协程函数)进入睡眠时间,而允许其他任务执行。这是它与time.sleep()的区 别,time.sleep()是当前线程休息,注意他们的区别哦。 另外如果提供了参数result,当当前任务(协程)结束的时候,它会返回。
  • 4.并发运行多个任务

    • await asyncio.gather(*coros_or_futures, loop=None, return_exceptions=False)
      • 1.*coros_or_futures *表示一个解包操作,如果是一个协程函数,则会自动转换成 Task。

      • 2.当所有的任务都完成之后,返回的结果是一个列表的形式,列表中值的顺序和*coros_or_futures完成的顺序是一样的。

      • 3.return_exceptions:

        • False,这是他的默认值,第一个触发异常的任务会立即返回,然后其他的任务继续执行。
      • True,对于已经发生异常的任务,也会像成功执行了任务那样,等到所有的任务执行结束一起将错误的结果返回到最终的结果列表里面。

      • 4.如果gather()本身被取消,那么绑定在它里面的任务也就取消了。

    import asyncio
    import time
    
    tmp = []
    async def decrement(n):
        for i in range(n):
            tmp.append(i)
        return 'aaa'
    
    async def main():
        l = []
        for i in range(4):
            task = asyncio.create_task(decrement(25000000))
            l.append(task)
        await asyncio.gather(
            *l
            )
        for i in l:
            print(i.result())
    
    start = time.time()
    asyncio.run(main())
    print(time.time()-start)
    
  • 5.防止任务被取消

    • await asyncio.shield(*arg, *, loop=None)

    它本身也是awaitable的。顾名思义,shield为屏蔽、保护的意思,即保护一个awaitable 对象防止取消,一般情况下不推荐使用,而且在使用的过程中,最好使用try语句块更好。

  • 6.设置timeout

    • 1.await asyncio.wait_for(aw, timeout, *, loop=None)(等待一个task)

    • 如果aw是一个协程函数,会自动包装成一个任务task。

    import asyncio
    
    async def eternity():
        print('我马上开始执行')
        await asyncio.sleep(2)  # 当前任务休眠2秒钟,2<3(模拟耗时操作)
        print('终于轮到我了')
    
    async def main():
        # Wait for at most 1 second
        try:
            print('等你3秒钟哦')
            await asyncio.wait_for(eternity(), timeout=3)  # 给你3秒钟执行你的任务
        except asyncio.TimeoutError:
            print('超时了!')
    
    asyncio.run(main())
    
    结果:
    等你3秒钟哦
    我马上开始执行
    终于轮到我了
    
    • 2.await asyncio.wait(aws, *, loop=None, timeout=None, return_when=ALL_COMPLETED)

    • 等待多个task,参数aws是一个集合,要写成集合set的形式,比如: "{fun(), func(), func3()}"

    • 注意:该函数的返回值是两个Tasks/Futures的集合:(done, pending),其中done是一个集合,表示已经完成的任务tasks,pending也是一个集合,表示还没完成的任务

    • 常见的使用方法为:done, pending = await asyncio.wait(aws)

    • timeout (a float or int), 同上面的含义一样,需要注意的是,这个不会触发 asyncio.TimeoutError异常,如果到了timeout还有任务没有执行完,那些没有执行完的tasks和 futures会被返回到第二个集合pending里面。

    • return_when参数,顾名思义,他表示的是,什么时候wait函数该返回值。只能够去下面的几个值。

      • 1.FIRST_COMPLETED当任何一个task或者是future完成或者是取消,wait函数就返回。
      • 2.FIRST_EXCEPTION当任何一个task或者是future触发了某一个异常,就返回,如果是所有的task和future都没有触发异常,则等价与下面的 ALL_COMPLETED。
      • 3.ALL_COMPLETED当所有的task或者是future都完成或者是都取消的时候,再返回。

5.asyncio异步编程基本模板

python3.7之前四步走(具体看链接)
  • 1.构造事件循环
  • 2.将一个或者是多个协程函数包装成任务Task
  • 3.通过事件循环运行
  • 4.关闭事件循环
import asyncio
import time
 
 
async def hello1(a,b):
    print("Hello world 01 begin")
    await asyncio.sleep(3)  # 模拟耗时任务3秒
    print("Hello again 01 end")
    return a+b
 
async def hello2(a,b):
    print("Hello world 02 begin")
    await asyncio.sleep(2)   # 模拟耗时任务2秒
    print("Hello again 02 end")
    return a-b
 
async def hello3(a,b):
    print("Hello world 03 begin")
    await asyncio.sleep(4)   # 模拟耗时任务4秒
    print("Hello again 03 end")
    return a*b
 
loop = asyncio.get_event_loop()  				# 第一步:创建事件循环
task1=asyncio.ensure_future(hello1(10,5))
task2=asyncio.ensure_future(hello2(10,5))
task3=asyncio.ensure_future(hello3(10,5))
tasks = [task1,task2,task3]                     # 第二步:将多个协程函数包装成任务列表
loop.run_until_complete(asyncio.wait(tasks))    # 第三步:通过事件循环运行
print(task1.result())                           # 并且在所有的任务完成之后,获取异步函数的返回值   
print(task2.result())
print(task3.result())
loop.close()                                    # 第四步:关闭事件循环
python3.7以后两步走(封装了一些更高层的API)
  • 1.构建一个入口函数main
    • 对于有返回值的协程函数,一般就在main里面进行结果的获取。
  • 2.启动主函数main
import asyncio
import time
 
 
async def hello1(a,b):
    print("Hello world 01 begin")
    await asyncio.sleep(3)  # 模拟耗时任务3秒
    print("Hello again 01 end")
    return a+b
 
async def hello2(a,b):
    print("Hello world 02 begin")
    await asyncio.sleep(2)   # 模拟耗时任务2秒
    print("Hello again 02 end")
    return a-b
 
async def hello3(a,b):
    print("Hello world 03 begin")
    await asyncio.sleep(4)   # 模拟耗时任务4秒
    print("Hello again 03 end")
    return a*b
 
async def main():
    results=await asyncio.gather(hello1(10,5),hello2(10,5),hello3(10,5))
    for result in results:
        print(result)
 
asyncio.run(main())
gather和wait的差别

gather和wait可以同时注册多个任务,实现并发,但他们的设计是完全不一样的。

  • 1.参数形式不一样
    • gather的参数为 coroutines_or_futures,即如这种形式 tasks = asyncio.gather([task1,task2,task3])或者 tasks = asyncio.gather(task1,task2,task3) loop.run_until_complete(tasks)
    • wait的参数为列表或者集合的形式 tasks = asyncio.wait([task1,task2,task3]) loop.run_until_complete(tasks)
  • 2.返回的值不一样
    • gather返回的是每一个任务运行的结果: results = await asyncio.gather(*tasks)
    • wait的定义如下,返回dones是已经完成的任务,pending是未完成的任务,都是集合类型 done, pending = yield from asyncio.wait(fs)
协程编程的优点
  • 1.无cpu分时切换线程保存上下文问题
    • 协程上下文怎么保存?
      • 协程在线程内部,因此协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。
      • 线程需要把协程A的执行环境进行保存,在下一次执行A的时候,线程需要恢复执行环境,这样 就可以从A之前的位置继续执行。
      • 协程的切换只是线程栈内的切换操作,不涉及内核操作,其切换速度远快于线程。
  • 2.遇到io阻塞切换(怎么实现的)
    • 通过用户自定义await(yield from)让出执行权,等待io操作结束再次被唤醒切换回来。
  • 3.无需共享数据的保护锁(为什么)
    • 协程是一个线程内的用户级线程,在一个循环事件loop中,从始之中每个时刻只有一个协程在运行,最关键的是协程的切换是用户自定义的,所以在执行全局变量变化的操作时用户不会在这个时候去切换协程导致全局变量(伪)并发失真。
    我们定义全局变量count,把他从0加到10万的操作,新建5个线程,和5个协程,可以看到线程的结果本该是500000,但却只有30多万。协程很稳定是准确的50万。一般在万级运算次数以下,不会出现资源竞争问题,当上了十万级或更高量级时,资源竞争问题会越来越明显。当然,这与电脑的配置也有关。
    import threading
    import asyncio
    
    count = 0
    def count1_100000():
        global count
        for i in range(100000):
            count += 1
    
    def count_by_threads():
        threads = []
        for i in range(5):
            t =threading.Thread(target=count1_100000)
            threads.append(t)
            t.start()
    
        for t in threads:
            t.join()
    
    async def count_100000():
        global count
        for i in range(100000):
            count += 1
    
    async def main():
        await asyncio.gather(count_100000(), count_100000(), count_100000(),
                             count_100000(), count_100000())
    
    count_by_threads()
    print(count)
    
    count = 0
    asyncio.run(main())
    print(count)
    
    结果
    361997
    500000
    
  • 4.有全局锁,为什么python的线程还会造成全局变量的失真?
    • 由于线程的切换是随机的,不能保证涉及全局变量操作的原子性。
      • 1.在num=0时,t1取得num=0,但还没有开始做+1运算。此时系统把t1调度为sleeping状 态,把t2转换为running状态,t2也获得num=0。
      • 2.然后t2对得到的值进行加1并赋给num,使得num=1。
      • 3.然后系统又把t2调度为sleeping,把t1转为running。线程t1把它之前得到的0加1 后赋值给num。
      • 4.这样导致虽然t1和t2都对num加1,但结果仍然是num=1
    • 如何解决线程安全问题?
    • 1.线程安全问题都发生在资源共享的地方。
    • 2.共享资源的读写才需要同步,变量才需要同步,常量不需要同步,当多个线程几乎同时修改 某一个共享数据的时候,需要进行同步控制。
    • 3.如何同步?在对应的地方加上互斥锁保证串行。(还可以通过队列保证串行)
    import threading
    
    Lock = threading.Lock()
    count = 0
    def count1_100000():
        global count
        for i in range(100000):
            Lock.acquire()
            count += 1
            Lock.release()
    
    def count_by_threads():
        threads = []
        for i in range(5):
            t =threading.Thread(target=count1_100000)
            threads.append(t)
            t.start()
    
        for t in threads:
            t.join()
    
    count_by_threads()
    print(count)
    
    • 死锁是什么如何解决死锁?
    from threading import Thread, Lock
    import time
    
    mutex_x = Lock()
    mutex_y = Lock()
    
    def x_func():
        print('X...')
        mutex_x.acquire()
        print('Lock x something')
        time.sleep(1)
        mutex_y.acquire()
        print('Use y something')
        time.sleep(1)
        mutex_y.release()
        print('x...')
        mutex_x.release()
    
    def y_func():
        print('Y...')
        mutex_y.acquire()
        print('Lock y something')
        time.sleep(1)
        mutex_x.acquire()
        print('Use x something')
        time.sleep(1)
        mutex_x.release()
        print('y...')
        mutex_y.release()
    
    if __name__ == '__main__':
        t1 = Thread(target=x_func)
        t2 = Thread(target=y_func)
    
        t1.start()
        t2.start()
    
    两个线程互相等待对方释放锁,造成死锁。解决方法给锁加上超时时间。 mutex_y.acquire(timeout=10)
asyncio的底层api

以EventLoop和Future为主,详情看链接

  • 1.很多个协程一起运行有创建新的线程吗?

    • 协程运行时,都是在一个线程中运行的,没有创建新的线程
  • 2.协程一定效率更高吗?

    • 不是,当只有一个协程时运行,效率其实是一样的。
  • 3.协程会不会有阻塞呢?

    • 肯定有,异步方式依然会有阻塞的,当我们定义的很多个异步方法彼此之间有依赖的时候,比如,我必须要等到函数1执行完毕,函数2需要用到函数1的返回值,这也是异步编程的难点之一,如何合理配置这些资源,尽量减少函数之间的明确依赖,这是很重要的
  • 4.协程的4种状态?

    • 协程函数相比于一般的函数来说,我们可以将协程包装成任务Task,任务Task就在于可以跟踪它的状态,我就知道它具体执行到哪一步了,一般来说,协程函数具有4种状态:创建future的时候,task为pending,事件循环调用执行的时候当然就是running,调用完毕自然就是done,如果需要停止事件循环,中途需要取消,就需要先把task取消,即为cancelled。

    • Pending

    • Running

    • Done

    • Cacelled

  • 5.多任务并发

tasks = asyncio.gather(*[task1,task2,task3])
loop.run_until_complete(tasks)
 
# 或者是
tasks = asyncio.wait([task1,task2,task3])
loop.run_until_complete(tasks)
 
# 甚至可以写在一起,即
loop.run_until_complete(asyncio.gather(*[task1,task2,task3])
# 或者是
asyncio.gather(asyncio.wait([task1,task2,task3])

6.多线程加协程解决阻塞

19.lambda

20.函数式编程

兄弟不想用这个东西,具体问问度娘吧-_-

  • 什么是函数式编程?

  • 在Python语言中,用于函数式编程的主要由3个基本函数和1个算子: 基本函数:map()、reduce()、filter() 算子(operator):lambda 仅仅采用这几个函数和算子就基本上可以实现任意Python程序。

  • 函数式编程的特点?

  • 函数式编程的用途?

  • 函数式编程相比于命令式编程和面向对象编程的优缺点?

21.python垃圾回收机制GC(Garbage Collection)

概念

  • 1.主要使用引用计数(reference counting)来跟踪和回收垃圾。
  • 2.在引用计数的基础上,通过标记-清除(mark and sweep)解决容器对象可能产生的循环引用问题
  • 3.在引用计数的基础上,通过分代回收(generation collection)以空间换时间的方法提高垃圾回收效率

主流语言的内存分区

  • 1.栈区(stack)【程序猿不需要管理栈区变量的内存】

    • 1.自动分配内存,通常由语言或者编译器控制。
    • 2.栈是从高地址向低地址扩展的数据结构,是一块连续的内存的区域。存放程序的局部变量, 先进后出,操作方式类似于数据结构中的栈。
    • 3.在函数被调用时,栈用来传递参数和返回值。由于栈的先进先出特点,所以栈特别方便用来 保存/恢复调用现场。
  • 2.堆区(heap)【动态分配,需要管理】

    • 1.堆内存由开发者负责分配(使用new/malloc)、释放, 若不释放,程序结束时可能由OS回收 。
    • 2.堆是低地址向高地址扩展的数据结构,是不连续的内存区域。注意它与数据结构中的堆是两回事,分配方式类似于链表。
    • 3.系统收到程序的申请时,会遍历记录空闲内存地址的链表,以求寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
    • 4.新申请的内存块的首地址记录本次分配的内存块大小。
  • 3.全局区/静态区/文字常量区 (data和bss两个分区)

    • 1.全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量在一块区域。
    • 2.未初始化的全局变量和静态变量在相邻的另一块区域,BSS 通常是指用来存放程序中未初始化 的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。
  • 4.常量区:存储常量字符串

  • 5.代码区(text)

    • 1.代码段是用来存放可执行文件的操作指令(存放程序的二进制代码),也就是说是它是可执行程序在内存种的镜像。
    • 2.代码段需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入操作——它是不可写的。

1.引用计数

typedef struct_object {
 int ob_refcnt;
 struct_typeobject *ob_type;
} PyObject;

在Python中每一个对象的核心就是一个结构体PyObject,它的内部有一个引用计数器ob_refcnt。程序在运行的过程中会实时的更新ob_refcnt的值,来反映引用当前对象的名称数量。当某对象的引用计数值为0,那么它的内存就会被立即释放掉

  • 1.引用计数加1情况:
    • 对象被创建,例如a=2
    • 对象被引用,b=a
    • 对象被作为参数,传入到一个函数中
    • 对象作为一个元素,存储在容器中
  • 2.引用计数减1情况:
    • 对象别名被显示销毁 del
    • 对象别名被赋予新的对象
    • 一个对象离开他的作用域
    • 对象所在的容器被销毁或者是从容器中删除对象
  • 3.可以通过sys包中的getrefcount()来获取一个名称所引用的对象当前的引用计数:
sys.getrefcount(a)
  • 4.引用计数的优点
    • 1.高效,逻辑简单
    • 2.具备实时性
  • 5.引用计数的缺点
    • 1.逻辑简单,但实现有些麻烦。
    • 2.每个对象需要分配单独的空间来统计引用计数,这无形中加大的空间的负担,并且需要对引用计数进行维护,在维护的时候很容易会出错。
    • 3.当需要释放一个大的对象时,比如字典,需要对引用的所有对象循环嵌套调用,从而可能会花费比较长的时间。
    • 4.循环引用。这将是引用计数的致命伤,引用计数对此是无解的,因此必须要使用其它的垃圾回收算法对其进行补充。 (对象的持有关系只要形成有向环,就会导致循环引用!类似于死锁的互相等待)
b = [1, 2]
a = [1, 2, b]
b.append(a)

2.标记清除(解决循环引用)

解决容器对象可能产生的循环引用问题。

  • 1.像数字,字符串这类简单类型不会出现循环引用,作为一种优化策略,对于只包含简单类型的元组也不在标记清除算法的考虑之列

  • 2.只有容器对象才会产生循环引用的情况,比如列表、字典、用户自定义类的对象、元组等。

标记清除两步走

  • 1.标记阶段:遍历所有的对象,如果是可达的(reachable),也就是还有对象引用它,那么就标记该对象为可达。
  • 2.清除阶段:再次遍历对象,如果发现某个对象没有标记为可达,则就将其回收。

标记清除算法

在标记清除算法中,为了追踪容器对象:

  • 1.每个容器对象维护两个额外的指针,用来将容器对象组成一个双端链表,指针分别指向前后两个容器对象,方便插入和删除操作。
  • 2.python解释器(Cpython)维护了两个这样的双端链表,一个链表存放着需要被扫描的容器对象,另一个链表存放着临时不可达对象。
  • 3.这两个链表分别被命名为”Object to Scan”和”Unreachable”。
  • 4.gc(分代回收)启动的时候,会逐个遍历”Object to Scan”链表中的容器对象,并且将当前对象所引用的所有对象的gc_ref减一。
  • 5.将”Objects to Scan”链表中的所有对象考察一遍之后,接着,gc会再次扫描所有的容器对象,如果对象的gc_ref值为0,那么这个对象就被标记为GC_TENTATIVELY_UNREACHABLE,并且被移至”Unreachable”链表中。
  • 6.如果对象的gc_ref不为0,那么这个对象就会被标记为GC_REACHABLE。同时当gc发现有一个节点是可达的,那么他会递归式的将从该节点出发可以到达的所有节点标记为GC_REACHABLE(gc_ref为0的也会)
  • 7.除了将所有可达节点标记为GC_REACHABLE之外,如果该节点当前在”Unreachable”链表中的话,还需要将其移回到”Object to Scan”
  • 8.第二次遍历的所有对象都遍历完成之后,存在于”Unreachable”链表中的对象就是真正需要被释放的对象

注:描述的垃圾回收的阶段,会暂停整个应用程序,等待标记清除结束后才会恢复应用程序的运行。

分代回收(generation collections)

以空间换时间的方法提高垃圾回收效率。

概念

大概就是:对象存在时间越长,越可能不是垃圾,应该越少去收集。这样在执行标记-清除算法时可以有效减小遍历的对象数,从而提高垃圾回收的速度。

  • 1.python gc给对象定义了三种世代(0,1,2)。
  • 2.每一个新生对象在generation zero中。
  • 3.如果它在一轮gc扫描中活了下来,那么它将被移至generation one,在那里他将较少的被扫描,如果它又活过了一轮gc,它又将被移至generation two,在那里它被扫描的次数将会更少。

GC触发机制

  • gc的扫描在什么时候会被触发呢?
    • 1.当某一世代中被分配的对象与被释放的对象之差达到某一阈值的时候,就会触发gc对某一世代的扫描。
    • 2.当某一世代的扫描被触发的时候,比该世代年轻的世代也会被扫描。
  • gc的阈值查看
import gc
gc.get_threshold()  # (threshold0, threshold1, threshold2)
gc.set_threshold(threshold0[, threshold1[, threshold2]])
  • 1.gc会记录自从上次收集以来新分配的对象数量与释放的对象数量,当两者之差超过threshold0的值时,gc的扫描就会启动,初始的时候只有世代0被检查。
  • 2.如果自从世代1最近一次被检查以来,世代0被检查超过threshold1次,那么对世代1的检查将被触发。
  • 3.相同的,如果自从世代2最近一次被检查以来,世代1被检查超过threshold2次,那么对世代2的检查将被触发。
  • 4.get_threshold()是获取三者的值,默认值为(700,10,10)。

性能优化手段

  • 1.手动垃圾回收
  • 2.避免循环引用(手动解循环引用和使用弱引用)
  • 3.调高垃圾回收阈值

22.python的内存管理机制

具体看标题链接

python的内存管理器大概由python内存池【创建内存】和垃圾回收【销毁】组成。

python的内存池

  • 1.当创建大量消耗小内存的对象时,频繁调用new/malloc会导致大量的内存碎片,致使效率降低。
  • 2.内存池的作用就是预先在内存中申请一定数量的,大小相等的内存块留作备用,当有新的内存需求时,就先从内存池中分配内存给这个需求,不够之后再申请新的内存。
  • 3.这样做最显著的优势就是能够减少内存碎片,提升效率。

内存释放

  • 1.关于释放内存方面,当一个对象的引用计数变为0时,Python就会调用它的析构函数。
  • 2.调用析构函数并不意味着最终一定会调用free来释放内存空间,如果真是这样的话,那频繁地申请、释放内存空间会使Python的执行效率大打折扣。
  • 3.因此在析构时也采用了内存池机制,从内存池申请到的内存会被归还到内存池中,以避免频繁地申请和释放动作。(这好像是python2的做法,会造成系统无法使用该段内存,特别是在处理图片时内存越用越大,使得内存爆掉)

python的内存监控

pip install pypler

import tkinter as tk
import asyncio  
import time
import threading

def memory_summary():
    # Only import Pympler when we need it. We don't want it to
    # affect our process if we never call memory_summary.
    from pympler import summary, muppy
    mem_summary = summary.summarize(muppy.get_objects())
    rows = summary.format_(mem_summary)
    return '\n'.join(rows)


class Form:
    def __init__(self):
        self.root=tk.Tk()
        self.root.geometry('500x300') 
        self.root.title('窗体程序')  # 设置窗口标题
 
        self.button=tk.Button(self.root,text="开始计算",command=self.change_form_state)
        self.label=tk.Label(master=self.root,text="等待计算结果")

        self.button.pack()
        self.label.pack()
        self.root.mainloop()

    async def calculate(self):
        print(threading.currentThread(), len(threading.enumerate()))
        time.sleep(3)  # 模拟耗时计算
        self.label["text"]=300
        print(memory_summary())
        
    def get_loop(self, loop):
        asyncio.set_event_loop(loop)
        loop.run_forever()

    def change_form_state(self):
        coroutine1 = self.calculate()
        new_loop = asyncio.new_event_loop()                       
        self.t = threading.Thread(target=self.get_loop,args=(new_loop,))   
        self.t.start()
        print(threading.currentThread(), len(threading.enumerate()))
        asyncio.run_coroutine_threadsafe(coroutine1,new_loop)
        
if __name__=='__main__':
    form = Form()

# 每次计算打印
                       types |   # objects |   total size
============================ | =========== | ============
                         str |       24374 |      3.92 MB
                        dict |        6291 |      2.69 MB
                        code |        8877 |      1.50 MB
                        type |        1381 |      1.16 MB
                       tuple |        5856 |    332.44 KB
                         set |         181 |    199.68 KB
          wrapper_descriptor |        2278 |    160.17 KB
                     weakref |        1745 |    122.70 KB
  builtin_function_or_method |        1534 |    107.86 KB
                 abc.ABCMeta |         108 |    107.73 KB
           method_descriptor |        1489 |    104.70 KB
                        list |         509 |     99.47 KB
           getset_descriptor |        1368 |     85.50 KB
                         int |        2479 |     75.04 KB
         function (__init__) |         517 |     68.66 KB

在每次调用一个函数结束后分析函数内存

pip install memory_profiler

from memory_profiler import profile

@profile
def fib(n):
    if n <= 2:
        return 1
    return fib(n-1) + fib(n-2)

if __name__ == '__main__':
    fib(5)
    
# 结果*5
Filename: E:/fib2.py

Line #    Mem usage    Increment   Line Contents
================================================
     3     22.1 MiB     22.1 MiB   @profile
     4                             def fib(n):
     5     22.1 MiB      0.0 MiB       if n <= 2:
     6     22.1 MiB      0.0 MiB           return 1
     7     22.1 MiB      0.0 MiB       return fib(n-1) + fib(n-2)

23.Cpython list实现

24. Python的is

is是对比地址,==是对比值

25. read、readline和readlines

  • read 读取整个文件
  • readline 读取下一行,使用生成器方法
  • readlines 读取整个文件到一个迭代器以供我们遍历

26. range and xrange

python3已经没有xrange,range也改成了通过生成器实现

def xrange(n):
	for i in range(8):
		yield i
for i in xrange(8):
	print(i)

27.新式类和旧式类的区别

  • 新式类从python2.7后出现
  • 写法不一样
class A:
    pass

class A(object):
    pass
  • 在多继承中,新式类采用广度优先搜索,而旧式类是采用深度优先搜索
  • 旧式类的基于深度优先的mro算法

旧式类在继承父类的方法时,先采用深度优先这时候继承了A的这个方法

class A():
    def who_am_i(self):
        print("I am A")
        
class B(A):
    pass
        
class C(A):
    def who_am_i(self):
        print("I am C")

class D(B,C):
    pass
    
d = D()
  • 新式类采用了基于广度优先遍历的C3算法,解决原来基于深度优先搜索算法不满足本地优先级,和单调性的问题。【具体看链接】

  • 问题,C3算法的核心是什么?

  • 1.merge算法是C3的核心。

  • 2.merge算法:遍历执行merge操作的序列,如果一个序列的第一个元素,在其他序列中也是第一个元素,或不在其他序列出现,则从所有执行merge操作序列中删除这个元素,合并到当前的mro中。

  • 3.merge操作后的序列,继续执行merge操作,直到merge操作的序列为空。

  • 4.如果merge操作的序列无法为空,则说明不合法。

  • 手写一个merge算法。

def c3_lineration(kls):
    if len(kls.__bases__) == 1:
        return [kls, kls.__base__]
    else:
        l = [c3_lineration(base) for base in kls.__bases__]  # drf
        l.append([base for base in kls.__bases__])
        return [kls] + merge(l)  

def merge(args):
    print(args)
    if args:
        for mro_list in args:
            for class_type in mro_list:
                for comp_list in args:
                    if class_type in comp_list[1:]:
                        break
                else:
                    next_merge_list = []
                    for arg in args:
                        if class_type in arg:
                            arg.remove(class_type)
                            if arg:
                                next_merge_list.append(arg)
                        else:
                            next_merge_list.append(arg)
                    return [class_type] + merge(next_merge_list)
        else:
            raise Exception
    else:
        return []


class A(object):pass
class B(object):pass
class C(object):pass
class E(A,B):pass
class F(B,C):pass
class G(E,F):pass

print(c3_lineration(G))

28.property

29.python2和python3的区别

1.解释器默认编码

  • python2 解释器默认编码:ascii
  • python3 解释器默认编码:utf-8
>>> 中国 = 'china'
>>> 中国
'china'

2.输入和输出

  • python2
    • print a
    • name = raw_input('请输入姓名')
  • python3
    • print(a)
    • name = input('请输入姓名')

3.数字表示

python2 64位机器,范围-263~263-1 超出上述范围,python自动转化为long(长整型) 注:long(长整型)数字末尾有一个L

python3 所有整型都是int,没有long(长整型)

4.整型除法

  • python2:只能保留整数位
  • python3:可以保留所有内容

5.range / xrange

  • python2:
    • xrange:不会在内存中立即创建,而是在循环时,边循环边创建
    • range:在内存立即把所有的值创建
  • python3:
    • 只有range,相当于python2中的xrange
    • range:不会在内存中立即创建,而是在循环时,边循环边创建

6.包的定义

  • python2:文件夹中必须有_ init _.py文件
  • python3:不需要有 _ _init _ _.py文件

7.字典的keys / values / items方法

  • python2:返回列表,通过索引可以取值
  • python3:返回迭代器,只能通过循环取值,不能通过索引取值

8.map / filter map

  • python2:返回列表,直接创建值,可以通过索引取值
  • python3:返回迭代器,不直接创建值,通过循环,边循环边创建

9.str(字符串类型)的区别 最大区别!!!!!!

  • python2:

    • str类型,相当于python3中的字节类型,utf-8/gbk等其他编码

    • unicode类型,相当于python3中的字符串类型,unicode编码

    • python2中没有字节类型

  • python3:

    • str类型,字符串类型,unicode编码

    • python3中没有unicode类型

10.object

  • python3
class Foo:
    pass
class Foo(object):
    pass

在python3中这俩的写法是一样,因为所有的类默认都会继承object类,全部都是新式类。

  • python2

如果在python2中这样定义,则称其为:

  • 经典类
    • py2中不继承object
    • 没有super语法
    • 多继承 深度优先
    • 没有mro方法
class Foo:
    pass 

如果在python2中这样定义,则称其为:

  • 新式类
    • 继承object
    • 支持super
    • 多继承 广度优先C3算法
    • mro方法
class Foo(object):
    pass