天天看点

十一、python学习之python高级一(GIL、深/浅拷贝、模块导入、上下文管理器)

type: <class 'ZeroDivisionError'>
value: division by zero
treacBack <traceback object at 0x0000015A6BCEC848>
over           

一、GIL:

1.GIL的概念:

GIL:全局解释器锁。GIL不是python的语法特征,它是实现python解释器时引用的一个应用。GIL只有在CPython解释器中存在。

2.GIL和线程互斥锁的区别:

2.1线程互斥锁:

造成全局变量数据错误的原因是由于多线程同步竞争共享数据资源时,导致问题的产生,可以通过线程互斥锁解决

2.2 GIL:

由图像分析:

十一、python学习之python高级一(GIL、深/浅拷贝、模块导入、上下文管理器)

 程序的中断状态保存在python解释器中

  1. python解释器也是一个应用程序
  2. GIL只在CPython解释器中存在
  3. 线程互斥锁是Python代码层面的锁,解决python程序中多线程共享资源的问题
  4. GIL是python解释器层面的锁,解决解释器中多线程竞争资源的问题。

3.什么是计算密集型程序/IO密集型程序:

计算密集型程序:在多任务系统中,大部分时间用来做计算、逻辑判断等CPU动作程序称为计算密集型程序。

IO密集型程序:一般在达到性能极限时,CPU的占用率仍然比较低。这可能时因为任务本身需要大量的I/O操作。

4.GIL对计算密集型程序的影响:

  1. 在python中同一时刻有且只有一个线程会执行
  2. Python中的多线程由于GIL的存在无法利用多核
  3. python中的多线程不适合计算密集型的程序
  4. 如果程序需要大量的计算,利用多核cpu资源,可以使用多进程来解决

5.GIL对IO密集型程序的影响:

在python解释器执行程序是,由于GIL的存在,导致同一时刻只能有一个线程执行,那么程序执行效率非常低,那么python解释器在程序执行IO等待时,自动释放GIL,让其他线程执行,从而提高py程序的执行效率。

6.如何改善GIL对程序产生的影响:

因为GIL是解释器层面的锁,无法取出GIL在执行程序是带来的问题.只能去改善:

  1. 更换更高版本的解释器,python做了优化,虽然不理想
  2. 更换解释器,但是其他解释器比较小众,支持的模块较少
  3. pyhton为了解决程序使用多核问题,使用多进程替换多线程

7.小结:

GIL ( Global Interpreter Lock ) 全局解释器锁。

GIL 不是 Python 语言的特性,是CPython中的一个概念。

Python 解释器也是一个应用程序

线程互斥锁是 Python 代码层面的锁,解决 Python 程序中多线程共享资源的问题

GIL是Python 解释层面的锁,解决解释器中多个线程的竞争资源问题。

由于 GIL 的存在, Python程序中同一时刻有且只有一个线程会执行。

Python 中的多线程由于 GIL 锁的存在无法利用多核 CPU

Python 中的多线程不适合计算密集型的程序。

GIL 锁在遇到IO等待时,会释放 GIL 锁,可以提高Python中IO密集型程序的效率

如果程序需要大量的计算,利用多核CPU资源,可以使用多进程来解决

自动释放GIL的情况:

    时间片耗尽

    执行一定量的字节码

    遇到IO等待

解决GIL的方法:使用多进程替换多线程

二、深拷贝和浅拷贝:

1.浅拷贝和深拷贝的概述:

在程序开发过程中,经常涉及数据的传递,在数据传递使用过程中,可能会发生数据被修改的问题.为了防止数据被修改,就需要在传递一个副本,即副本被修改,也不会影响原数据的使用.为了生成这个副本就产生了拷贝。

  • 万物皆对象:
  • 不可变对象:在python中,int,str,tuple等数据类型
  • 可变对象:list、set、dict等容器对象
  • 引用:在python程序中,每个对象都会在内存中申请开辟一块空间来保存该对象,该对象在内存中所在位置的地址称为引用。引用别名没有不占存储空间

2.可变类型与不可变类型的的引用赋值:

2.1 不可变对象的引用赋值

        在不可变对象赋值时,不可变对象不会被修改,而是会新开辟一个空间。

2.2 可变对象的引用赋值:

        在可变对象中,保存的并不真正的对象数据,而对象的引用。 当对可变对象进行修改时,只是将可变对象中保存的引用进行更改。

3. 浅拷贝:

拷贝对象需要导入copy模块

import copy           

使用copy模块中的copy方法就可以拷贝对象

3.1 不可变对象的拷贝:

因为不可变对象只有在修改时才会开辟新空间,所以拷贝也相当于让多个引用同时引用了一个数据,所以不可变对象的浅拷贝和赋值没有区别.

3.2 可变对象的拷贝:

简单数据时,对不可变对象进行赋值时,对象引用并没有发生变化。

复杂的对象在进行拷贝时,并没有真正的解决数据传递后,数据被改变的问题。 这是因为,copy() 函数在拷贝对象时,只是将指定对象中的所有引用拷贝了一份,如果这些引用当中包含了一个可变对象的话,那么数据还是会被改变。 这种拷贝方式,称为浅拷贝。

4.深拷贝:

4.1 深拷贝的概念:

相对于浅拷贝只拷贝顶层的引用外,copy模块还提供了另外一个拷贝方法 deepcopy() 函数,这个函数可以逐层进行拷贝引用,直到所有的引用都是不可变引用为止。

5.浅拷贝的几种方式:

  • copy模块中的copy方法
  • 对象本身的copy方法
  • 切片
  • 工厂方法:
a = [1, 2]
l1 = [3, 4, a]
l2 = list(l1)    # 工厂方法:list、set、dict、tuple           

Python中的默认拷贝方式都是使用浅拷贝

6.浅拷贝的优势:

  • 时间角度,浅拷贝花费的时间更少
  •  空间角度,浅拷贝花费的内存更少
  • 效率角度,浅拷贝只拷贝顶层数据,一般情况下比深拷贝效率高

7.小结:

  • 不可变对象在赋值时会开辟新的空间
  • 可变对象在赋值时,修改一个值,另一个也会发生变化
  • 深浅拷贝对不可变对象拷贝时,不开辟新的空间,相当于赋值操作
  • 浅拷贝在拷贝时,只拷贝第一层中的引用,如果元素是可变对象,并且被修改,那么拷贝的对象也会发生变化
  • 深拷贝在拷贝时,会逐层进行拷贝,知道所有的引用都时不可变对象为止
  • python中有多种方式实现浅拷贝:copy模块中的copy函数、对象的copy函数、工厂方法、切片等
  • 大多数情况下,编写程序时,都是使用浅拷贝,除非有特定的要求
  • 浅拷贝的优点:拷贝速度快,占用空间少,拷贝效率高

三、模块导入:

1.模块导入概述:

在 Python 开发过程中,需要使用大量的系统模块,第三方模块,自定义模块。这些模块以 Python 文件的形式进行组织。

当需要使用模块中提供的功能时,只需要将模块导入到当前文件中即可。

如果有多个模块可以将这些模块放在一个文件中,并创建一个 __init__.py 的文件,这个文件夹称为 package

2.模块导入方式:

import module

import package.module

from module import 成员

from package import module

from package.module import 成员           

3. 模块的别名 as,代替较长的模块名:

import long-long-module as AA
           

4.模块的搜索路径:

4.1 查看模块路径:得到的是一个列表。也就是说列表的方法都可以使用(pend、insert)

import sys    # 导入sys模块
path_list = sys.path    # 获取path列表           

4.2 模块的搜索路径:

  1. 当前程序所在目录
  2. 当前程序根目录
  3.  PYTHONPATH系统
  4. 第三方库目录site-packages目录
  5. 如果模块不在path保存的路径中,会抛出导入模块异常

5.重新加载模块:

5.1为什么要重新加载:

模块导入成功后,在使用模块过程中,如果被导入的模块对数据进行了修改,那么正在使用该模块的程序并不会修改。

5.2 重新加载模块的方法:

from importlib import reload    # 引用reload方法,旧版本的reload在imp模块中,之后被废弃,使用importlib
reload(module)    # 重新加载模块module           

6.import 和 from-import 导入的使用区别:

x = 1     # 全局变量
_y = 2    # 文件内私有
__z = 3   # 类私有属性           

6.1 import方式:

import 方式导入时,只是在当前文件中建立了一个模块的引用,通过模块的引用来使用模块内的数据 。

使用这种方式导入时,访问控制权限对文件内级别的数据不起作用,通过模块名都可以进行访问。

相当于将一个模块中所有的内容都导入到当前文件中使用。

6.2 from-import 方式:

from-import 方式在导入数据时,会将导入模块中数据复制一份到当前文件中,所以可以直接使用模块中的变量,函数,类等内容。

在使用 from-import 方式导入时,文件内私有属性 _xxx 形式的数据不会被导入。

在使用 from-import 方式导入时,如果模块内和当前文件中有标识符命名重名,会引用命名冲突,当前文件中的内容会覆盖模块的数据。

6.3 __all__魔法发方法:

from-import 方式可以理解成深拷贝,被导入模块中的数据被拷贝了一份放在当前文件中。

 __all__ 魔法变量 在 Python 中还提供种方式来隐藏或公开数据 ,就是使用 __all__。

 __all__ 本质是一个列表,在列表中以字符串形式加入要公开的数据。

在使用 from-import 导入模块时,如果模块中存在这个变量,那么就按这个变量里的内容进行导入,没有包含的不导入。

7.循环导入的问题:

7.1 什么会循环导入:

在开发过程中,可能会遇到这种情况。两个模块相互之间进行导入。这样的话,会造成程序出现死循环。程序运行时就会报错。

7.2 模块导入时要经过以下几步:

1.在sys.modules(得到一个字典类型的返回值)中去搜索导入的模块对象

2.如果没有找到就创建一个空白的模块并加入到sys.modules中,然后读取模块中的数据对空模块进行初始化

3.如果找到了,就对存在的模块直接建立引用当前文件中使用

7.3 注意:

循环导入不是语法知识,也不止在 Python 中出现。这是在程序设计时的逻辑出现了问题。 不要想出现逻辑错误的时候怎么修改。而是要从根本上去避免不能出现这种设计逻辑。就像函数调用死循环一样。 切记切记!!!

8.小结:

  1. 在Python中,一个文件就是一个模块。
  2. 使用模块时,可以使用 import 或 from-import 来将模块导入。
  3. 导入模块时,程序到 sys.path 路径中去搜索,如果路径中没有指定的模块会报错。
  4. 可以向 sys.path 中去添加搜索路径。
  5. 模块导入后,在执行过程中是不可更新的,如果模块发生了变化,需要使用 imp 模块中的reload 函数重新导入。
  6. import 导入类似浅拷贝,使用模块的引用操作模块中的数据。
  7. from-import 导入类似深拷贝,相当于复制了一份模块中的数据到当前文件中,可能会命名冲突。
  8. 循环导入模块会出错,这不是语法,是思想逻辑错误,不要想着怎么改,要想怎么避免发生。

四、上下文管理器:

语法糖:语法糖指那些没有给计算机语言添加新功能,而只是对人类来说更“甜蜜”的语法。语法糖往往给程序员提供了更实用的编码方式,有益于更好的编码风格,更易读。不过其并没有给语言添加什么新东西。

1.with概述:

with(上下文管理器):属于python语法糖的一种,是python提供的一种简化的语法,是一种与异常处理有关的功能操作。

使用场合:with语法适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的”清理“操作,释放资源。如:文件,数据库,网络等的访问和关闭。

2.with语句的使用:

with open("file.txt", 'rb') as file:    # 打开文件
    file_data = f.read()    # 文件操作的功能代码           

通过 with 语句在编写代码时,会使代码变得更加简洁。

在编写代码时,不用再显示的去关闭文件。

3. with语句的执行过程:

  1. 在执行 with 语句时,首先执行 with 后面的 open 代码
  2. 执行完代码后,会将代码的结果通过 as 保存到 f 中
  3. 然后在下面实现真正要执行的操作
  4. 在操作后面,并不需要写文件的关闭操作,文件会在使用完后自动关闭

4.with语句的执行原理:

实际上,在文件操作时,并不是不需要写文件的关闭,而是文件的关闭操作在 with 的上下文管理器中的协议方法里已经写好了。

当文件操作执行完成后, with语句会自动调用上下文管理器里的关闭语句来关闭文件资源。

5. 上下问管理器:

上下文管理器(ContextManager):在文件操作时,需要打开,关闭文件,而在文件在进行读写操作时,就是处在文件操作的上下文中,也就是文件操作环境中。

with 语句在执行时,需要调用上下文管理器中的 __enter__ 和 __exit__ 两个方法。

__enter__ 方法会在执行 with 后面的语句时执行,一般用来处理操作前的内容。比如一些创建对象,初始化等。

__exit__ 方法会在 with 内的代码执行完毕后执行,一般用来处理一些善后收尾工作,比如文件的关闭,数据库的关闭等。

6. 自定义上文管理器:

在自定义上下文管理器时,只需要在类中实现__enter__和__exit__两个方法:

代码实现:

#!/usr/bin/env python
# coding=utf-8


# 定义一个上下文管理器
class MyOpen(object):
    def __init__(self, file, mode):
        self.__file = file
        self.__mode = mode

    # 实现__enter__方法
    def __enter__(self):
        print("执行__enter__")
        self.__handle = open(self.__file, self.__mode)
        return  self.__handle

    # 实现一个__exit__方法
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("执行__exit__")
        self.__handle.close()


if __name__ == '__main__':
    # 使用自定义的上下文管理器
    with MyOpen("AA.py", 'r') as file:
        file_data = file.read()
        print(file_data)
           

运行结果:

执行__enter__
#!/usr/bin/env python
# coding=utf-8
# import BB
def ashow():
    print('A - show')
import BB
BB.bshow()

执行__exit__
           

分析:从运行结果可以分析出,通过在类中实现__enter__方法和__exit__方法,实现和自定义的上下文管理器。最开始进入上下文时调用了__enter__方法返回了打开文件的字节流文件,通过as关键字传递给了file变量,这样就可以通过file来完成对文件的操作。最后结束的时候调用了__exit__方法,释放了资源。但是,发现了__exit__方法中是有参数的。

7.__exit__方法的参数:

__exit__方法中有三个参数,用来接收处理异常,如果代码在运行时发生了异常,异常会通过着三个参数保存到这里。

exc_type:异常类型

exc_val:异常值 

exc_tb:异常回溯追溯

 代码实现:(做除0的运行)

#!/usr/bin/env python
# coding=utf-8


# 自定义上下文管理器
class ContxtTest(object):
    def __init__(self, x, y):
        self.__x = x
        self.__y = y

    # 实现__enter__方法
    def __enter__(self):
        return self

    # 实现__exit__方法
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("type:", exc_type)
        print("value:", exc_val)
        print("treacBack", exc_tb)

    # 定义功能方法
    def div(self):
        return self.__x / self.__y


if __name__ == '__main__':
    with ContxtTest(1,0) as mc:
        ret = mc.div()
        print(ret)

    print("over")           

运行结果:

type: <class 'ZeroDivisionError'>    # 异常类型
Traceback (most recent call last):    
value: division by zero    # 异常值
  File "G:/2_Mysql/day12/代码/03_上下文的exit参数.py", line 27, in <module>
treacBack <traceback object at 0x000002486CF2A808>    # 异常追踪对象
    ret = mc.div()
  File "G:/2_Mysql/day12/代码/03_上下文的exit参数.py", line 22, in div
    return self.__x / self.__y
ZeroDivisionError: division by zero           

因为程序发生了除零错误,所以出现异常,异常信息被保存到这三个变量中。但是抛出异常后,最后一行的print("over")"并没有执行。

8.异常信息处理:

当with中执行的语句发生异常时,异常信息会被发送到 __exit__ 方法的参数中,这时可以根据情况选择如何处理异常。

还是上面的程序,加入了异常的处理

代码实现:

#!/usr/bin/env python
# coding=utf-8


# 自定义上下文管理器
class ContxtTest(object):
    def __init__(self, x, y):
        self.__x = x
        self.__y = y

    # 实现__enter__方法
    def __enter__(self):
        return self

    # 实现__exit__方法
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type == None:
            print("运行正确...")
        else:
            print("type:", exc_type)
            print("value:", exc_val)
            print("treacBack", exc_tb)
            # 返回值决定了捕获的异常是否继续向外抛出
            # 如果是 False 那么就会继续向外抛出,程序会看到系统提示的异常信息
            # 如果是 True 不会向外抛出,程序看不到系统提示信息,只能看到else中的输出
        return True

    # 定义功能方法
    def div(self):
        return self.__x / self.__y


if __name__ == '__main__':
    with ContxtTest(1,0) as mc:
        ret = mc.div()
        print(ret)

    print("over")           

运行结果:

type: <class 'ZeroDivisionError'>
value: division by zero
treacBack <traceback object at 0x0000015A6BCEC848>
over           

分析:__exit__方法的返回值决定了捕获的异常是否继续向外抛出; 如果是 False 那么就会继续向外抛出,程序会看到系统提示的异常信息; 如果是 True 不会向外抛出,程序看不到系统提示信息,只能看到else中的输出。代码执行到了最后。

8.小结:

  1. with 语句主要是为了简化代码操作。
  2. with 在执行过程中,会自动调用上下文管理器中的 __enter__ 和 __exit__ 方法
  3. __enter__ 方法主要用来做一些准备操作
  4. __exit__ 方法主要用来做一些善后工作