python面试知识点

103 阅读14分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

1. 语法/常见函数

1.1 基础函数的使用

  1. 求1--100之和

    sum(range(1, 101))
    
  2. 字典的删除和更新

    data = {"a": 1, "b": 2}
    del data["a"]  # 若key不存在会报错
    data.update({"a": 10, "b": 20})
    
  3. 列表去重

    data = [1, 1, 2, 3, 2, 5]
    res = list(set(data))
    

1.2 global, nonlocal

  1. 在函数中声明全局变量global

    a = 1
    c = 3
    
    def test():
        # 即使还未定义的变量也可直接声明
        # 但不建议直接声明未定义变量, 容易导致代码可读性降低
        global a, b
        a, b = 10, 20
        # 不使用global声明则c是局部变量
        c = 30
        print(c)
    
    if __name__ == "__main__":
        test()
        print(a, b, c)
    
  2. 嵌套函数声明上层局部变量nonlocal

    def test():
        a = 1
        def deep1():
            # 声明的变量必须在上层已经定义, 否则报错
            nonlocal a
            a += 10
        deep1()
        print(a)
    
    if __name__ == "__main__":
        test()
    

1.3 *args, **kwargs

  • 函数定义 def fun(*args, **kwargs)
    • *args 表示可以接收任意长度的位置参数
    • **kwargs 表示可以接收任意关键字参数
  • 函数调用 fun(*args, **kwargs)
    • *args 表示将迭代器解析成函数的前n个位置参数
    • **kwargs 表示将字典解析成关键字参数
  • 列表, 元组, 集合, 字典
    • [1, 2, *range(3)] 将迭代器解析成列表的元素
    • (1, 2, *range(3))
    • {1, 2, *range(3)}
    • {1:1, 2:2, **{3:3, 2:20}} 将一个字典的元素更新到另一个字典中

1.4 强制位置/关键字参数

*def fun(a, b, /, c, , d)

  • / 前面的参数 a, b 在使用时只能通过位置参数的形式使用
  • * 后面的参数 d 只能通过关键字参数的形式使用
  • 参数 c 既可以通过位置参数形式使用也可通过关键字参数形式使用

2. 进程, 线程, 协程

此部分引用文章

Python进程、线程、协程概念 - 知乎 (zhihu.com)

Python协程详解 - 知乎 (zhihu.com)

2.1 进程

  1. 进程是一个实体。每个进程都有自己的地址空间(CPU分配)。实体空间包括三部分

    • 文本区域:存储处理器执行的代码。
    • 数据区域:存储变量或进程执行期间使用的动态分配的内存。
    • 堆栈:进程执行时调用的指令和本地变量。
  2. 进程是一个“执行中的程序”

    程序是指令与数据的有序集合,程序本身是没有生命的,只有CPU赋予程序生命时(CPU执行程序),它才能成为一个活动的实体,称为“进程”。

    概括来说,进程就是一个具有独立功能的程序在某个数据集上的一次运行活动

  3. 进程的特点

    • 动态性:进程是程序的一次执行过程,动态产生,动态消亡。
    • 独立性:进程是一个能独立运行的基本单元。是系统分配资源与调度的基本单元。
    • 并发性:任何进程都可以与其他进程并发执行。

2.2 并发与并行

  1. 并发:在操作系统中,某一时间段,几个程序在同一个CPU上运行,但在任意一个时间点上,只有一个程序在CPU上运行。

    当有多个线程时,如果系统只有一个CPU,那么CPU不可能真正同时进行多个线程,CPU的运行时间会被划分成若干个时间段,每个时间段分配给各个线程去执行,一个时间段里某个线程运行时,其他线程处于挂起状态,这就是并发。并发解决了程序排队等待的问题,如果一个程序发生阻塞,其他程序仍然可以正常执行。

  2. 并行:当操作系统有多个CPU时,一个CPU处理A线程,另一个CPU处理B线程,两个线程互相不抢占CPU资源,可以同时进行,这种方式成为并行。

  3. 区别

    并发只是在宏观上给人感觉有多个程序在同时运行,但在实际的单CPU系统中,每一时刻只有一个程序在运行,微观上这些程序是分时交替执行。 在多CPU系统中,将这些并发执行的程序分配到不同的CPU上处理,每个CPU用来处理一个程序,这样多个程序便可以实现同时执行。

2.3 线程

  1. 线程的引入

    60年代,操作系统中拥有资源并独立运行的基本单位是进程,进程是资源的拥有者,进程的创建、撤销、切换花销太大。多CPU处理出现,可以满足多个单位同时运行,但是多个进程并行花销太大。80年代,出现了轻量级的,能够独立运行的基本单位,线程。

  2. 线程的概念

    • 线程是进程中的一个实体,是被系统独立调度和分派的基本单位。 线程的实体包括程序,数据,TCB。TCB包括:
      • 线程状态
      • 线程不运行时, 被保存的现场资源
      • 一组执行堆栈
      • 每个线程的局部变量
      • 访问同一进程中的资源
    • 线程自己不拥有系统资源, 只拥有一点运行中必不可少的资源
    • 同一进程中的多个线程并发执行, 这些线程共享进程所拥有的资源
  3. 进程与线程的区别

    • 进程是CPU资源分配的基本单位,线程是独立运行和独立调度的基本单位(CPU上真正运行的是线程)。
    • 进程拥有自己的资源空间,一个进程包含若干个线程,线程与CPU资源分配无关,多个线程共享同一进程内的资源。
    • 线程的调度与切换比进程快很多。
  4. python的GIL锁

    为了解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁, 于是有了GIL这把超级大锁. 由于GIL是一把全局排他锁导致python几乎等同于是个单线程程序, 即使在多核CPU中同一时间也只会有一个线程在有效执行.

    • 在CPU密集型运算中, 多线程不仅无法提高程序效率反而会因进程间的切换导致运行效率大幅降低
    • 可使用多线程提高IO密集型程序的执行效率

2.4 协程

  1. 协程的特点

    • 协程是一种比线程更加轻量级的存在,最重要的是,协程不被操作系统内核管理,协程是完全由程序控制的。

    • 运行效率极高,协程的切换完全由程序控制,不像线程切换需要花费操作系统的开销, 线程数量越多,协程的优势就越明显。

    • 协程不需要多线程的锁机制,因为只有一个线程,不存在变量冲突。

    • 对于多核CPU,利用多进程+协程的方式,能充分利用CPU,获得极高的性能。

  2. python 生成器

    • yield关键字相当于是暂停功能,程序运行到yield停止,send函数可以传参给生成器函数,参数赋值给yield。

      def customer():
          while True:
              number = yield
              print('开始消费:',number)
              
      custom = customer()
      next(custom)
      # 或者 send None 来启动生成器
      # custom.send(None)
      for i in range(5):
          print('开始生产:',i)
          custom.send(i)
      
      开始生产: 0
      开始消费: 0
      开始生产: 1
      开始消费: 1
      开始生产: 2
      开始消费: 2
      开始生产: 3
      开始消费: 3
      开始生产: 4
      开始消费: 4
      
    • send也可获取yield后的变量

      def customer():
          j = 0
          while True:
              j -= 1
              number = yield j
              print('开始消费:',number)
      
      custom = customer()
      # 启动生成器时会返回第一个yield后的值, 并等待参数赋值给yield
      print(next(custom))
      for i in range(5):
          print('开始生产:',i)
          jj = custom.send(i)
          print('customer return', jj)
      
      -1
      开始生产: 0
      开始消费: 0
      customer return -2
      开始生产: 1
      开始消费: 1
      customer return -3
      开始生产: 2
      开始消费: 2
      customer return -4
      开始生产: 3
      开始消费: 3
      customer return -5
      开始生产: 4
      开始消费: 4
      customer return -6
      
  3. asyncio + yield from

    asyncio是Python3.4版本引入的标准库,直接内置了对异步IO的支持。asyncio的异步操作,需要在coroutine中通过yield from完成。

    # Python3.8弃用下述用法
    import asyncio
    
    @asyncio.coroutine
    def test(i):
        print('test_1', i)
        r = yield from asyncio.sleep(1)
        print('test_2', i)
    
    if __name__ == '__main__':
        loop = asyncio.get_event_loop()
        tasks = [test(i) for i in range(3)]
        loop.run_until_complete(asyncio.wait(tasks))
        loop.close()
    
    test_1 1
    test_1 2
    test_1 0
    test_2 1
    test_2 2
    test_2 0
    
  4. asyncio + async/await

    为了简化并更好地标识异步IO,从Python3.5开始引入了新的语法async和await,可以让coroutine的代码更简洁易读。请注意,async和await是coroutine的新语法,使用新语法只需要做两步简单的替换:

    • 把@asyncio.coroutine替换为async
    • 把yield from替换为await
    import asyncio
    
    async def test(i):
        print('test_1', i)
        await asyncio.sleep(1)
        print('test_2', i)
    
    if __name__ == '__main__':
        loop = asyncio.get_event_loop()
        tasks = [test(i) for i in range(3)]
        loop.run_until_complete(asyncio.wait(tasks))
        loop.close()
    
    test_1 1
    test_1 0
    test_1 2
    test_2 1
    test_2 0
    test_2 2
    
  5. Gevent

    Gevent是一个基于Greenlet实现的网络库,通过greenlet实现协程。基本思想是一个greenlet就认为是一个协程,当一个greenlet遇到IO操作的时候,比如访问网络,就会自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO操作。

    import gevent
    
    def test(n):
        for i in range(n):
            print(gevent.getcurrent(), i)
    
    if __name__ == '__main__':
        g1 = gevent.spawn(test, 3)
        g2 = gevent.spawn(test, 3)
        g3 = gevent.spawn(test, 3)
    
        g1.join()
        g2.join()
        g3.join()
    

    上述程序无IO操作, 程序依次运行

    <Greenlet at 0x1f11c538040: test(3)> 0
    <Greenlet at 0x1f11c538040: test(3)> 1
    <Greenlet at 0x1f11c538040: test(3)> 2
    <Greenlet at 0x1f11e32e040: test(3)> 0
    <Greenlet at 0x1f11e32e040: test(3)> 1
    <Greenlet at 0x1f11e32e040: test(3)> 2
    <Greenlet at 0x1f11e32e150: test(3)> 0
    <Greenlet at 0x1f11e32e150: test(3)> 1
    <Greenlet at 0x1f11e32e150: test(3)> 2
    

    使用gevent.sleep()交出控制权

    import gevent
    
    def test(n):
        for i in range(n):
            print(gevent.getcurrent(), i)
            gevent.sleep(1)
    
    if __name__ == '__main__':
        g1 = gevent.spawn(test, 3)
        g2 = gevent.spawn(test, 3)
        g3 = gevent.spawn(test, 3)
    
        g1.join()
        g2.join()
        g3.join()
    
    <Greenlet at 0x1daaf588040: test(3)> 0
    <Greenlet at 0x1dab1252040: test(3)> 0
    <Greenlet at 0x1dab1252150: test(3)> 0
    <Greenlet at 0x1daaf588040: test(3)> 1
    <Greenlet at 0x1dab1252040: test(3)> 1
    <Greenlet at 0x1dab1252150: test(3)> 1
    <Greenlet at 0x1daaf588040: test(3)> 2
    <Greenlet at 0x1dab1252040: test(3)> 2
    <Greenlet at 0x1dab1252150: test(3)> 2
    

    在实际的代码里,我们不会用gevent.sleep()去切换协程,而是在执行到IO操作时gevent会自动完成,所以gevent需要将Python自带的一些标准库的运行方式由阻塞式调用变为协作式运行。这一过程在启动时通过monkey patch完成:

    from gevent import monkey; monkey.patch_all()
    from urllib import request
    import gevent
    
    def test(url):
        print('Get: %s' % url)
        response = request.urlopen(url)
        content = response.read().decode('utf8')
        print('%d bytes received from %s.' % (len(content), url))
    
    if __name__ == '__main__':
        gevent.joinall([
            gevent.spawn(test, 'http://httpbin.org/ip'),
            gevent.spawn(test, 'http://httpbin.org/uuid'),
            gevent.spawn(test, 'http://httpbin.org/user-agent')
        ])
    
    Get: http://httpbin.org/ip
    Get: http://httpbin.org/uuid
    Get: http://httpbin.org/user-agent
    53 bytes received from http://httpbin.org/uuid.
    40 bytes received from http://httpbin.org/user-agent.
    33 bytes received from http://httpbin.org/ip.
    

3. 修饰器

3.1 一般修饰器

def decorator(func):
    def wrapper(*args, **kwargs):
        # Todo 被修饰函数执行前做一些事情
        print('被修饰函数执行前')
        res = func(*args, **kwargs)
        # Todo 被修饰函数执行后做一些事情
        print('被修饰函数执行后')
        return res

    return wrapper

@decorator
def test():
    print('被修饰函数')

say_hello()

3.2 带参数的修饰器

def print_info(value):
    def decorator(func):
        def wrapper(*args, **kwargs):
            # Todo 被修饰函数执行前做一些事情
            print(value)
            res = func(*args, **kwargs)
            # Todo 被修饰函数执行后做一些事情
            return res

        return wrapper

    return decorator

@print_info("传入的内容")
def test():
    print('被修饰函数')

say_hello()

3.3 wraps修饰器

一般修饰器会导致被修饰函数的一些基本信息无法正常获取, 被修饰的函数获取函数信息其实是修饰器内部函数的信息

def decorator(func):
    def wrapper(*args, **kwargs):
        """修饰器文档"""
        # Todo 被修饰函数执行前做一些事情
        res = func(*args, **kwargs)
        # Todo 被修饰函数执行后做一些事情
        return res

    return wrapper

@decorator
def test():
    """函数文档"""
    print('被修饰函数')

print(test.__name__)
print(test.__doc__)
wrapper
修饰器文档

使用wraps修饰器来保留被修饰函数的基本信息

from functools import wraps

def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """修饰器文档"""
        # Todo 被修饰函数执行前做一些事情
        res = func(*args, **kwargs)
        # Todo 被修饰函数执行后做一些事情
        return res

    return wrapper

@decorator
def test():
    """函数文档"""
    print('被修饰函数')

print(test.__name__)
print(test.__doc__)
test
函数文档

3.4 类装饰器

class Decorator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print('被修饰函数执行前')
        return self.func(*args, **kwargs)

@Decorator
def test():
    print('被修饰函数')

test()

4. 类class

_init_

_new_

3.4 一些常用的内置修饰器

3.4.1 property 如属性般调用函数

property 装饰器用于类中的函数,使得我们可以像访问属性一样来获取一个函数的返回值。

class C(object):
    def __init__(self):
        self._x = None

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

    @x.deleter
    def x(self):
        del self._x

c = C()
c.x = 10
print(c.x)
del c.x

3.4.2 staticmethod 静态方法

3.4.3 classmethod 类方法

5. 垃圾回收

5.1 引用计数

Python垃圾回收主要以引用计数为主,分代回收为辅。引用计数法的原理是每个对象维护一个ob_ref,用来记录当前对象被引用的次数,也就是来追踪到底有多少引用指向了这个对象,当发生以下四种情况的时候,该对象的引用计数器 +1

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

与上述情况相对应,当发生以下四种情况时,该对象的引用计数器 -1

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

a = []   # 引用+1
b = a    # 引用+1
c = b    # 引用+1
d = [a, a]  # 引用+2
e = d    # d和e内存共享引用不变
print(sys.getrefcount(a))  # getrefcount函数引用+1
# getrefcount函数结束引用 -1
del b    # 引用-1
c = 2    # 引用-1
d.pop()  # 引用-1
print(sys.getrefcount(a))

5.2 循环引用

循环引用是引用计数的致命伤,引用计数对此是无解的,因此必须要使用其它的垃圾回收算法对其进行补充。

import sys

v1 = 1111111111111      # v1 引用+1
print(sys.getrefcount(v1))   # getrefcount会使常量引用+3 (运行环境windows python3.9)
a, b = [v1], []    # v1 引用+1
print(sys.getrefcount(v1))

a.append(b)
b.append(a)
print(sys.getrefcount(v1))
del a, b  # 循环引用导致v1引用数未减小
# 我们查看的是v1的计数, 但实际是因为a和b循环引用导致a,b未正确清除而导致v1的引用无法清除
print(sys.getrefcount(v1))

5.3 标记清除解决循环引用

只有容器对象才会产生循环引用的情况, 所以标记清除只针对容器对象

标记清除阶段会暂停程序已保证清除正常

该算法在进行垃圾回收时分成了两步,分别是:

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

Python垃圾回收机制!非常实用 - 知乎 (zhihu.com)

# 强制进行垃圾回收
gc.collect()
# 强制回收后循环引用导致的计数错误恢复正常
print(sys.getrefcount(v1))

5.4 分代回收

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

6. 内存泄漏

对于一个用 python 实现的,长期运行的后台服务进程来说,如果内存持续增长,那么很可能是有了“内存泄露”。

6.1 内存泄漏可能产生的原因

  1. 所用到的用 C 语言开发的底层模块中出现了内存泄露。
  2. 代码中用到了全局的 list、 dict 或其它容器,不停的往这些容器中插入对象,而忘记了在使用完之后进行删除回收
  3. 代码中有“引用循环”,并且被循环引用的对象定义了__del__方法,就会发生内存泄露。

Python内存泄漏和内存溢出的解决方案_python_脚本之家 (jb51.net)

6.2 排查工具

pyrasite, objgraph