Python 技术要点汇总

1,113 阅读13分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。


with 语句

with 语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的清理操作,释放资源。底层通过 __enter____exit__,来实现上下文管理,释放资源。


file = './a.txt'

with open(file) as f:
    file_data = file.read()
    print(file_data)
    

详情请看:Python with关键字原理详解 https://juejin.cn/post/6959886107496415246


闭包、装饰器

闭包

闭包概念:在一个内部函数中,对外部作用域的变量进行了引用, (并且一般外部函数的返回值为内部函数),那么将这个内部函数以及用到的一些变量称之为闭包 (colsure)


def func(number):

    # 在函数内部再定义一个函数,并且这个函数用到了外部函数的变量,
    # 那么将这个函数以及用到的一些变量称之为闭包
    def func_in(number_in):
        print("in func_in 函数, number_in is %d" % number_in)
        return number + number_in

    # 这里返回的就是闭包
    return func_in


利用闭包的特性和函数引用的传递,就可以实现 装饰器

更多详情请看:深入浅出Python闭包 https://juejin.cn/post/6960487978703519775


装饰器

装饰器就是 python 中用于扩展原来函数功能的函数。通过 @函数名 它可以将被装饰的函数当做参数传递给装饰器函数。@函数名Python 的一种语法糖。装饰器的有很多应用场景,例如插入日志,计算程序运行时间,登录认证,事务处理等。

很好的提升了代码的复用性,并且不会破坏函数内部结构。非常适合 面向切面编程 AOP

import time


def calc_time(func):
    """
    计算函数运行时间
    """
    
    def wapper():
        start_time = time.time()
        func() # 调用传递过来的函数
        use_time = start_time - time.time()
        print(use_time)
        
    return wapper
        

@calc_time    
def demo():
    
    for i in range(100000):
        print(i)
   

@calc_time 的作用就是会让 Python解释器 执行 demo = calc_time(demo)


面试题:编写一个带参数的装饰器

带参数的装饰器,可能函数的嵌套有点多,只要理解它,其实和普通的装饰器没什么差别。

大概的思路就是:通过带参函数调用然后返回内部的装饰器,就实现了带参数的装饰器。

# 1、定义一个带参数的函数
def router(name):


    # 2、函数内部定义装饰器
    def _router(func):

        def wapper():
            pass

        return wapper

    # 3、返回装饰器
    return _router 

# 1、定义一个带参数的函数
def router(path):

    # 2、函数内部定义装饰器
    def _router(func):

        def wapper():
            print('path:', path)    # 使用函数传递过来的参数
            func()

        return wapper

    # 3、返回装饰器
    return _router


# 使用router装饰器
@router('/index')
def index():
    print('首页')


index()

# 结果如下
path: /index
首页


上面程序的执行流程如下

  1. 首先执行 router('/index')的函数调用,返回了 _router 装饰器
  2. 然后就是执行Python的语法糖 @_router,把 index的函数引用传递给 _router(func)
    • index = _router(index)
  3. 最后就是调用 index() 函数

Python 装饰器使用详解 https://juejin.cn/post/6961360227690086437


迭代器、生成器

迭代器

迭代是访问集合元素的一种方式。迭代器是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。

通过 __iter__() 来实现获取一个迭代器对象,然后利用 __next__() 来迭代元素。__iter__(), __next__() 都是具体的实现细节,通常都是用 iter() 函数来获取可迭代对象的迭代器,然后 next() 函数用于遍历迭代。


来看看构造一个斐波那契数列迭代器

class FibIterator(object):
    """斐波那契数列迭代器"""
    
    def __init__(self, n):
        """
        :param n: int, 指明生成数列的前n个数
        """
        self.n = n
        
        # current用来保存当前生成到数列中的第几个数了
        self.current = 0
        
        # num1用来保存前前一个数,初始值为数列中的第一个数0
        self.num1 = 0
        
        # num2用来保存前一个数,初始值为数列中的第二个数1
        self.num2 = 1

    def __next__(self):
        """被next()函数调用来获取下一个数"""
        if self.current < self.n:
            num = self.num1
            self.num1, self.num2 = self.num2, self.num1+self.num2
            self.current += 1
            return num
        else:
            raise StopIteration

    def __iter__(self):
        """迭代器的__iter__返回自身即可"""
        return self


if __name__ == '__main__':
    fib = FibIterator(10)
    for num in fib:
        print(num, end=" ")

# 结果如下
0 1 1 2 3 5 8 13 21 34

更多细节请移步到:Python 迭代器 https://juejin.cn/post/6948437239286202375


生成器

利用迭代器,我们可以在每次迭代获取数据(通过 next() 方法)时按照特定的规律进行生成。但是我们在实现一个迭代器时,关于当前迭代到的状态需要我们自己记录,进而才能根据当前状态生成下一个数据。为了达到记录当前状态(保存上下文环境),并配合 next() 函数进行迭代使用,我们可以采用更简便的语法,即 生成器(generator)。

Python 生成器 https://juejin.cn/post/6948437559533895693


生成器的创建方式

把列表推导式 [] 换成 () 就是生成器。

In [21]: L = [i for i in range(10)]

In [22]: G = (i for i in range(10))

In [23]: type(L)
Out[23]: list

In [24]: type(G)
Out[24]: generator
    

另一种方式就是利用 yiled 关键字

用生成器来实现斐波那契数列

def fib(n):

    cur = 0
    num1 = 0
    num2 = 1

    while cur < n:

        yield num1

        num1, num2 = num2, num1 + num2

        cur += 1
        
        
In [11]: f = fib(5)

In [12]: type(f)
Out[12]: generator

In [13]: next(f)
Out[13]: 0

In [14]: next(f)
Out[14]: 1

In [15]: next(f)
Out[15]: 1

In [16]: next(f)
Out[16]: 2

In [17]: next(f)
Out[17]: 3

In [18]: next(f)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-18-aff1dd02a623> in <module>
----> 1 next(f)

StopIteration:

In [19]: for i in fib(10):
    ...:     print(i, end=' ')
    ...:
0 1 1 2 3 5 8 13 21 34
In [20]:
    

通过列表推导式,可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含百万元素的列表,不仅是占用很大的内存空间,如:我们只需要访问前面的几个元素,后面大部分元素所占的空间都是浪费的。因此,没有必要创建完整的列表。在Python中,我们可以采用生成器:边循环,边计算的机制—> generator。可以节省大量内存空间。


import sys
import time


def calc_time(func):
    """
    计算函数运行时间装饰器
    """

    def wapper(*args, **kwargs):
        start_time = time.time()
        f = func(*args, **kwargs)
        use_time = time.time() - start_time
        print(use_time, 's')
        return f
    return wapper


@calc_time
def get_list(n):
    """列表推导式生成列表数据"""
    return [i for i in range(n)]


@calc_time
def generate_list(n):
    """通过生成器生成列表数据"""
    return (i for i in range(n))


n = 1_0000_0000
li = get_list(n)
g_li = generate_list(n)

print('li size:', sys.getsizeof(li))

print('g_li size', sys.getsizeof(g_li))

for i in range(5):
    print('li', li[i], 'g_li', next(g_li))


运行结果

4.615425109863281s	# 列表推导式所花费的时间
0.0s				# 生成器所花费的时间

li size: 859724472	# 列表推导式产生对象大小
g_li size 120		# 生成器产生对象的大小

li 0 g_li 0
li 1 g_li 1
li 2 g_li 2
li 3 g_li 3


可以看出列表推导式在生成很多数据耗时久且占用内存大,但是通过 生成器 几乎不需要花费时间生成,因为他是运行时动态生成数据,因此生成器也不用保存所有数据,只需保存一些上下文环境所用的变量,但生成器不方便操作,不支持切片,也没有列表这么丰富的方法。各有优缺点。


面试题:文件中有 1000w 条数据,内存无法一次全部存储,该如何读取?

def read_file(file):
    with open(file, mode='r') as f:

        # f.read() 加载所有数据到内存中
        # f.readline() 每次读取文件中的一行
        # f.readlines() 返回的是每行组成的列表

        for row in f.readlines():
            yield row   # 通过 yield 返回每行数据


file = 'aaa.txt'

for row in read_file(file):
    print(row)           
            

赋值与深浅拷贝

  • 直接赋值: 其实就是指向对象的引用(别名)。
  • 浅拷贝(copy): 拷贝父对象,不会拷贝对象的内部的子对象。但对于不可变数据类型,不会拷贝,仅仅是指向
  • 深拷贝(deepcopy): copy 模块的 deepcopy 方法,完全(递归)拷贝了父对象及其子对象。

内建模块 copy 可以帮我们实现 浅拷贝 (copy)深拷贝 (deepcopy)


来看个例子拷贝 c = [ [1, 2], [3, 4] ]

# 直接赋值
In [54]: import copy

In [55]: c = [ [1, 2], [3, 4] ]

In [56]: d = c

In [57]: id(d), id(c)
Out[57]: (2365035580680, 2365035580680)

d = c 赋值引用,cd 都指向同一个对象,地址相同。


In [58]: # 浅拷贝

In [59]: e = copy.copy(c)

In [60]: id(e), id(c)
Out[60]: (2365034915848, 2365035580680)

In [61]: e
Out[61]: [[1, 2], [3, 4]]

In [62]: c
Out[62]: [[1, 2], [3, 4]]

In [63]: e[0].append(5)

In [64]: e
Out[64]: [[1, 2, 5], [3, 4]]

In [65]: c
Out[65]: [[1, 2, 5], [3, 4]]

e = copy.copy(c) 浅拷贝,ce 是一个 独立的对象(地址不同),但他们的 子对象还是指向统一对象即引用。因此往e[0] 添加数据 5,会影响到 c


In [66]: # 深拷贝

In [67]: c=[ [1, 2], [3, 4] ]

In [68]: f = copy.deepcopy(c)

In [69]: id(c), id(f)
Out[69]: (2365035892552, 2365035662792)

In [70]: c
Out[70]: [[1, 2], [3, 4]]

In [71]: f
Out[71]: [[1, 2], [3, 4]]

In [72]: f.append(5)

In [73]: f[0].append(6)

In [74]: f[1].append(7)

In [75]: c
Out[75]: [[1, 2], [3, 4]]

In [76]: f
Out[76]: [[1, 2, 6], [3, 4, 7], 5]
    

f = copy.deepcopy(c) 深度拷贝, f 完全拷贝了 c 的父对象及其子对象,两者是完全独立的f 做任何的修改都不会影响到 c

深浅拷贝理解图1


注意事项

  • copy.copy() 对于可变类型,会进行浅拷贝。
  • copy.copy() 对于不可变类型,不会拷贝,仅仅是指向。
  • copy.deepcopy() 深拷贝对可变、不可变类型都一样递归拷贝所有,对象完全独立。

对于 可变数据类型、不可变数据类型的详情和拷贝的细节请查看:

深度解析Python的赋值、浅拷贝、深拷贝 https://juejin.cn/post/6955354098988220423/


GIL锁

GIL 的全称为 Global Interpreter Lock(全局解释器锁),由于这个锁的存在,CPython 在多线程并发的环境下 同一时刻 只有一个线程在运行,无法充分利用多核 CPU 的性能。虽然很鸡肋,但是遇到 IO 堵塞的情况下,全局解释器锁会释放,让其他线程工作。例如:在文件读取,网络请求(IO密集的场景下),还是能有效的提升性能。


解决 GIL 锁的问题

1、使用多进程

原理: 每个进程分配不同的解释器,有单独的 GIL,内建模块 multiprocessing 可以开启多进程。

缺点: 资源消耗大,额外产生数据序列化与通信的开销。


2、使用C语言扩展模块

原理: C语言扩展程序的执行保持与Python解释器隔离,在C代码中释放GIL,例如 numpy, pandas 等库。

缺点: 调用C函数时GIL会被锁定,若阻塞,解释器无法释放GIL。

使用方法: 在C代码中插入特殊的宏或是使用其他工具来访问C代码,如 ctypes 库或者 Cython。(ctypes默认会在调用C代码时自动释放GIL)


3、选用其他没有 GIL 的解释器代替 CPython

原理: 使用没有GIL的解释器实现。

缺点: 不完全兼容。

使用方法: 目前 JythonIronPython 没有GIL。


进程,线程,协程

  • 进程 Process 是操作系统资源分配的基本单位
    • 在 Python 中使用 multiprocessing 内建模块可以创建进程。
  • 线程 Thread 是CPU调度的最小执行单元。一个进程内可以有多个子线程。
    • 在 Python 中使用 threading 内建模块可以创建线程。
  • 协程 CoRoutine 是一种轻量级的用户态 微线程,实现了上下文环境的保存与切换。
    • 简单来说,线程的调度是由操作系统负责,线程的睡眠、等待、唤醒的时机是由操作系统控制,开发者无法决定。使用协程,开发者可以自行控制程序切换的时机,可以在一个函数执行到一半的时候中断执行,让出 CPU,在需要的时候再回到中断点继续执行。切换的时机是由开发者来决定。
    • 在 Python 中可以使用 yield 关键字来实现 协程,或者使用第三方库 greenlet, gevent
    • Python 3.5版本之后新增了 async/await 关键字配合 asyncio 内建模块可以实现协程和异步编程。

Python 中进程、线程、协程的使用请查阅如下链接:


asyncio 异步编程

asyncioPython 3.4版本引入到标准库,Python3.5 又加入了 async/await 特性,能很方便的创建协程以及控制其进行上下文切换。


在定义函数前面添加 aysnc 关键字,调用函数时返回的是 <class 'coroutine'> ,Python 内部封装好的协程类的实例对象。

In [93]: async def task():
    ...:
    ...:     i = 0
    ...:     while i < 5:
    ...:         print(i)
    ...:

In [94]: c = task()

In [95]: c
Out[95]: <coroutine object task at 0x00000226A71505C8>

In [96]: type(c)
Out[96]: coroutine

import asyncio


async def task1():
    i = 0
    while i < 5:
        print('task1', i)
        i += 1
        await asyncio.sleep(0.1)    # await 让耗时任务自动挂起


async def task2():
    i = 0
    while i < 5:
        print('task2', i)
        i += 1
        await asyncio.sleep(0.1)


def main():
    loop = asyncio.get_event_loop()

    tasks = [
        task1(),
        task2()
    ]

    # 等待所有任务执行完成
    loop.run_until_complete(asyncio.wait(tasks))
    print('end')


if __name__ == '__main__':
    main()


结果如下:

task2 0
task1 0
task2 1
task1 1
task2 2
task1 2
task2 3
task1 3
task2 4
task1 4
end

单例模式的实现

1、重写类的 __new__() 方法

class Singleton(object):

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

    def __new__(cls, *args, **kwargs):

        if not hasattr(cls, '_instance'):
            cls._instance = super().__new__(cls)

        return cls._instance


s1 = Singleton('hui')
s2 = Singleton('jun')

print(id(s1), id(s2))
print(s1.name, s2.name)

# 结果如下
3097281233736 3097281233736		# 地址一样
jun jun		# 因此操作的是同一个对象

2、使用装饰器

def singleton(cls):

    _instance = {}

    def _singleton(*args, **kwargs):

        if cls not in _instance:

            _instance[cls] = cls(*args, **kwargs)

        return _instance[cls]

    return _singleton


@singleton
class Demo(object):

    def __init__(self, name, age=18):
        self.name = name
        self.age = age


d1 = Demo('hui', age=18)
d2 = Demo('jun', age=21)

print(id(d1), id(d2))
print(d1.name, d1.age)
print(d2.name, d2.age)


# 结果如下
2107174554696 2107174554696
hui 18
hui 18

分析如下


def singleton(cls):

    print(type(cls))

    _instance = {}

    def _singleton(*args, **kwargs):

        print(args)
        print(kwargs)

        if cls not in _instance:

            print(cls.__dict__)
            instance = cls(*args, **kwargs)
            print(type(instance))

            _instance[cls] = instance

        return _instance[cls]

    return _singleton


@singleton
class Demo(object):

    def __init__(self, name, age=18):
        self.name = name
        self.age = age


d1 = Demo('hui', age=18)
d2 = Demo('jun', age=21)

print(id(d1), id(d2))
print(d1.name, d1.age)
print(d2.name, d2.age)

运行结果与分析:

<class 'type'> 	# Demo类对象(cls)的类型
('hui',)		# Demo 初始化时的位置参数
{'age': 18}		# Demo 初始化时的关键字参数

# Demo类对象的__dict__属性
{'__module__': '__main__',
 '__init__': <function Demo.__init__ at 0x00000239C8784798>, 
 '__dict__': <attribute '__dict__' of 'Demo' objects>,
 '__weakref__': <attribute '__weakref__' of 'Demo' objects>,
 '__doc__': None
}

# 通过类对象构造出的实例对象
<class '__main__.Demo'>
('jun',)
{'age': 21}
2447199767752 2447199767752 # 地址相同

# 因为第一次构建好了实例对象,下次就不会创建,直接返回第一次
# 所以后面创建对象所传递的初始化参数没有效果
hui 18
hui 18

更多创建单例的单例方法请查看:www.cnblogs.com/huchong/p/8…


元类

追溯Python类的鼻祖——元类 https://juejin.cn/post/6957631734343008269


CGI,WSGI

CGI 全称为 Common Gateway Interface (通用网关接口),是一种定义程序和服务器交互方式的标准协议。

WSGI的全称为 Python Web Server Gateway Interface (Web 服务网关接口),它是Web服务器和Web应用程序通信的接口规范,用来支持Web服务器和Web应用程序交互。


Python Web通信示意图


Python 内存管理与垃圾回收机制

Python中的内存管理由Python私有堆空间管理。所有Python对象和数据结构都位于私有堆中。程序员无权访问此私有堆。Python解释器负责处理这个问题。

Python 内存管理机制:引用计数、垃圾回收、内存池。

垃圾回收机制:主要以引用计数器为主,标记清除和分代回收为辅进行垃圾回收。


大家可以参考:www.pythonav.com/wiki/detail…