Python 提高 - 1

1 GIL(全局解释器锁)

GIL面试题如下:

描述Python GIL的概念, 以及它对python多线程的影响?编写一个多线程抓取网页的程序,并阐明多线程抓取程序是否可比单线程性能有提升,并解释原因。

Guido的声明:http://www.artima.com/forums/flat.jsp?forum=106&thread=214235

he language doesn't require the GIL -- it's only the CPython virtual machine that has historically been unable to shed it.

参考答案:

  1. Python语言和GIL没有半毛钱关系。仅仅是由于历史原因在Cpython虚拟机(解释器),难以移除GIL。
  2. GIL:全局解释器锁。每个线程在执行的过程都需要先获取GIL,保证同一时刻只有一个线程可以执行代码。
  3. 线程释放GIL锁的情况: 在IO操作等可能会引起阻塞的system call之前,可以暂时释放GIL,但在执行完毕后,必须重新获取GIL Python 3.x使用计时器(执行时间达到阈值后,当前线程释放GIL)或Python 2.x,tickets计数达到100
  4. Python使用多进程是可以利用多核的CPU资源的。
  5. 多线程爬取比单线程性能有提升,因为遇到IO阻塞会自动释放GIL锁

1.1 详细解析 :

首先让我们了解一下并发和并行的概念:什么是并发?什么是并行?他们的区别是什么?

举个简单的例子:

你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行.

你吃饭吃到一半,电话来了,你停了下来接了电话,接完后电话以后继续吃饭,这说明你支持并发。

你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。

并行与并发的理解

扫描二维码关注公众号,回复: 2699789 查看本文章

并发交替处理多个任务的能力;

并行同时处理多个任务的能力;

所以它们最大的区别就是:是否是『同时』处理任务。

对于一个多核cpu来说并行显然要比并发快的多,由此我们可以知道一个多核cpu在处理多个任务的时候如果想要发挥最大功效就要实现并行。那我们在使用多线程和多进程来写程序的时候就是为了让多核cup发挥他最大的功效实现并行,也就是我们面试题参考答案的结果。

1.2 代码验证多进程,多线程对cpu的使用情况

1.2.1 实验代码文件

01-单线程死循环.py:

# 主线程死循环,占满cpu
while True:
    pass

02-2个线程死循环.py:

import threading


# 子线程死循环
def test():
    while True:
        pass


t1 = threading.Thread(target=test)
t1.start()

# 主线程死循环
while True:
    pass

03-2个进程死循环.py:

import multiprocessing


def deadLoop():
    while True:
        pass


# 子进程死循环
p1 = multiprocessing.Process(target=deadLoop)
p1.start()

# 主进程死循环
deadLoop()

 1.2.2 实验过程

1.实验开始之前CPU状态:

 2.运行01-单线程死循环.py 程序之后CPU的状态:

3.同时运行两次01-单线程死循环.py 程序之后CPU的状态:

 4.运行02-2个线程死循环.py 程序之后CPU的状态 :

5.运行03-2个进程死循环.py之后CPU的状态:

1.2.3 实验结果:

通过上边实验可以发现多进程可以充分使用cpu的两个内核多线程却不能充分使用cpu的两个内核

问题:通过验证我们发现多线程并不能真正的让多核cpu实现并行

原因:cpython解释器中存在一个GIL(全局解释器锁),它的作用就是保证同一时刻只有一个线程可以执行代码,因此造成了我们使用多线程的时候无法实现并行。

解决方案法:

(1)更换解释器比如使用jpython(java实现的python解释器)(jython xxx.py)

(2)使用其他语言(C语言)编写程序

(3)使用多进程完成多任务的处理

1.3 使用C语言编写程序

用C语言编写一个死循环程序:

void DeadLoop()
{
    while(1)
    {
        ;
    }
} 

把一个c语言文件编译成一个动态库的命令(linux平台下):gcc xxx.c -shared -o libxxxx.so

gcc loop.c -shared -o libdead_loop.so

用python 语言调用 C语言 程序:

from ctypes import *
from threading import Thread

#加载动态库
lib = cdll.LoadLibrary("./libdead_loop.so")

#创建一个子线程,让其执行c语言编写的函数,此函数是一个死循环
t = Thread(target=lib.DeadLoop)
t.start()

#主线程
while True:
    pass

2 深拷贝、浅拷贝

2.1 浅拷贝

浅拷贝是对于一个对象的顶层拷贝,通俗的理解是:拷贝了引用,并没有拷贝内容

 2.2 深拷贝

深拷贝是对于一个对象所有层次的拷贝(递归

进一步理解深拷贝:

说明:

列表c 中的数据仅仅是列表a 和列表b 的引用,而列表d 中的数据是真正的拷贝了一份数据(递归copy),拷贝结束之后与列表 a、b和c 再也没有关系

2.3 拷贝的其他方式

2.3.1 分片表达式可以赋值一个序列

2.3.2 字典的copy方法可以拷贝一个字典

说明: 字典的value 值其实是一个引用(指向)值。

注意: 列表切片、字典的copy方法均属于浅拷贝

2.4 注意点

浅拷贝对不可变类型和可变类型的copy不同

  1. copy.copy对于可变类型,会进行浅拷贝
  2. copy.copy对于不可变类型,不会拷贝,仅仅是指向

2.5 copy.copy和copy.deepcopy的区别:

2.5.1 copy.copy

2.5.2 copy.deepcopy

2.5.3 小结

浅拷贝是对一个对象的顶层(外层)拷贝,只是拷贝了引用,并没有拷贝内容。变量的赋值是地址的引用,也算是一种浅拷贝。

深拷贝则是对一个对象深层(递归)的拷贝,保证了数据的独立性。

可变类型:列表、字典

不可变类型:数字类型、字符串型、元组

如果是可变类型,浅拷贝只拷贝外层,而深拷贝是完全拷贝

如果是纯的不可变类型,那么无论是浅拷贝还是深拷贝,都只是指向同一个地址。如果不可变类型里面还存在可变类型,则浅拷贝是指向,而深拷贝则为完全拷贝。

3 私有化

  • xx:公有变量
  • _x:单前置下划线,私有化属性或方法,from somemodule import *禁止导入,类对象和子类可以访问
  • __xx:双前置下划线,避免与子类中的属性命名冲突,无法在外部直接访问(名字重整所以访问不到)
  • __xx__:双前后下划线,用户名字空间的魔法对象或属性。例如:__init__ , __ 不要自己发明这样的名字
  • xx_:单后置下划线,用于避免与Python关键词的冲突

通过name mangling(名字重整(目的就是以防子类意外重写基类的方法或者属性)如:_Class__object)机制就可以访问private了。

# coding=utf-8

class Person(object):
    def __init__(self, name, age, taste):
        self.name = name
        self._age = age
        self.__taste = taste

    def show_person(self):
        print(self.name)
        print(self._age)
        print(self.__taste)

    def do_work(self):
        self._work()
        self.__away()

    def _work(self):
        print("my _work")

    def __away(self):
        print("my __away")


class Student(Person):
    def construction(self, name, age, taste):
        self.name = name
        self._age = age
        self.__taste = taste

    def show_student(self):
        print(self.name)
        print(self._age)
        print(self.__taste)

    @staticmethod
    def test_bug():
        _Bug.show_bug()


# 模块内可以访问,当from  cur_module import *时,不导入
class _Bug(object):
    @staticmethod
    def show_bug():
        print("show_bug")


s1 = Student('jack', 25, 'football')
s1.show_person()
print('*' * 20)

# 无法访问__taste(私有属性不能被子类继承),导致报错
# s1.show_student()

s1.construction('rose', 30, 'basketball')
s1.show_person()
print('*' * 20)

s1.show_student()
print('*' * 20)

Student.test_bug()

运行结果: 

jack
25
football
********************
rose
30
football
********************
rose
30
basketball
********************
show_bug

总结:

  • 父类中属性名为__名字的,子类不继承,子类不能访问
  • 如果在子类中向__名字赋值,那么会在子类中定义的一个与父类相同名字的属性
  • _名的变量、函数、类在使用from xxx import *时都不会被导入,但用其他导入方法(import XXX)时,可以被导入 

4 import导入模块

4.1 import 搜索路径

4.1.1 路径搜索

  • 从上面列出的目录里依次查找要导入的模块文件
  • '' 表示当前路径
  • 列表中的路径的先后顺序代表了python解释器在搜索模块时的先后顺序

4.1.2 程序执行时添加新的模块路径

sys.path.append("/home/itcast/xxx")
sys.path.insert(0, "/home/itcast/xxx")  # 可以确保先搜索这个路径
In [37]: sys.path.insert(0,"/home/python/xxxx")
In [38]: sys.path
Out[38]: 
['/home/python/xxxx',
 '',
 '/usr/bin',
 '/usr/lib/python35.zip',
 '/usr/lib/python3.5',
 '/usr/lib/python3.5/plat-x86_64-linux-gnu',
 '/usr/lib/python3.5/lib-dynload',
 '/usr/local/lib/python3.5/dist-packages',
 '/usr/lib/python3/dist-packages',
 '/usr/lib/python3/dist-packages/IPython/extensions',
 '/home/python/.ipython']

4.1.3 小结

import sys

sys.path  # 返回查找模块的列表目录,列表中的路径的先后顺序代表了python解释器在搜索模块时的先后顺序。第一个元素返回的是一个空字符串表示当前目录。

sys.path.append('/home/itcast/xxx')  # 在列表最后追加搜索目录

sys.path.insert(0, '/home/itcast/xxx')  # 可以确保先搜索这个路径

4.2 重新导入模块

模块被导入后,import module不能重新导入模块,重新导入需用reload

注意: 必须得先 import 模块 ,重新导入时才能使用 reaload (模块名)

4.3 多模块开发时的注意点

4.3.1 recv_msg.py模块

from common import RECV_DATA_LIST
# from common import HANDLE_FLAG
import common


def recv_msg():
    """模拟接收到数据,然后添加到common模块中的列表中"""
    print("--->recv_msg")
    for i in range(5):
        RECV_DATA_LIST.append(i)


def test_recv_data():
    """测试接收到的数据"""
    print("--->test_recv_data")
    print(RECV_DATA_LIST)


def recv_msg_next():
    """已经处理完成后,再接收另外的其他数据"""
    print("--->recv_msg_next")
    # if HANDLE_FLAG:
    if common.HANDLE_FLAG:
        print("------发现之前的数据已经处理完成,这里进行接收其他的数据(模拟过程...)----")
    else:
        print("------发现之前的数据未处理完,等待中....------")

4.3.2 handle_msg.py模块

from common import RECV_DATA_LIST
# from common import HANDLE_FLAG
import common


def handle_data():
    """模拟处理recv_msg模块接收的数据"""
    print("--->handle_data")
    for i in RECV_DATA_LIST:
        print(i)

    # 既然处理完成了,那么将变量HANDLE_FLAG设置为True,意味着处理完成
    # global HANDLE_FLAG
    # HANDLE_FLAG = True
    common.HANDLE_FLAG = True


def test_handle_data():
    """测试处理是否完成,变量是否设置为True"""
    print("--->test_handle_data")
    # if HANDLE_FLAG:
    if common.HANDLE_FLAG:
        print("=====已经处理完成====")
    else:
        print("=====未处理完成====")

4.3.3 common.py模块

RECV_DATA_LIST = list()
HANDLE_FLAG = False

4.3.4 main.py模块

from recv_msg import *
from handle_msg import *


def main():
    # 1. 接收数据
    recv_msg()
    # 2. 测试是否接收完毕
    test_recv_data()
    # 3. 判断如果处理完成,则接收其它数据
    recv_msg_next()
    # 4. 处理数据
    handle_data()
    # 5. 测试是否处理完毕
    test_handle_data()
    # 6. 判断如果处理完成,则接收其它数据
    recv_msg_next()


if __name__ == "__main__":
    main()

4.3.5 运行main程序

4.3.6 注意

说明:通过from 模块 import 变量,此时相当于给一个变量赋值,如果在程序中修改了变量的值就导致这个变量成了局部变量,跟其他模块就不共享了。如果多模块开发时想导入变量,建议使用import 模块名的方式,然后通过模块名.变量的方式去调用。

 5 再议 封装、继承、多态

封装、继承、多态 是面向对象的3大特性

5.1 封装

好处:

  1. 在使用面向过程编程时,当需要对数据处理时,需要考虑用哪个模板中哪个函数来进行操作,但是当用面向对象编程时,因为已经将数据存储到了这个独立的空间中,这个独立的空间(即对象)中通过一个特殊的变量(__class__)能够获取到类(模板),而且这个类中的方法是有一定数量的,与此类无关的将不会出现在本类中,因此需要对数据处理时,可以很快速的定位到需要的方法是谁,这样更方便。
  2. 全局变量是只能有1份的,当很多个函数需要多个备份时,往往需要利用其它的变量来进行储存;而通过封装会将用来存储数据的这个变量变为对象中的一个“全局”变量,只要对象不一样那么这个变量就可以再有1份,所以这样更方便
  3. 代码划分更清晰

5.2 继承

说明:

  1. 能够提升代码的重用率,即开发一个类,可以在多个子功能中直接使用
  2. 继承能够有效的进行代码的管理,当某个类有问题只要修改这个类就行,而其继承这个类的子类往往不需要就修改

5.3 多态

class MiniOS(object):
    """MiniOS 操作系统类"""

    def __init__(self, name):
        self.name = name
        self.apps = []  # 安装的应用程序名称列表

    def __str__(self):
        return "%s 安装的软件列表为 %s" % (self.name, str(self.apps))

    def install_app(self, app):
        # 判断是否已经安装了软件
        if app.name in self.apps:
            print("已经安装了 %s,无需再次安装" % app.name)
        else:
            app.install()
            self.apps.append(app.name)


class App(object):
    def __init__(self, name, version, desc):
        self.name = name
        self.version = version
        self.desc = desc

    def __str__(self):
        return "%s 的当前版本是 %s - %s" % (self.name, self.version, self.desc)

    def install(self):
        print("将 %s [%s] 的执行程序复制到程序目录..." % (self.name, self.version))


class PyCharm(App):
    pass


class Chrome(App):
    def install(self):
        print("正在解压缩安装程序...")
        super().install()


linux = MiniOS("Linux")
print(linux)

pycharm = PyCharm("PyCharm", "1.0", "python 开发的 IDE 环境")
chrome = Chrome("Chrome", "2.0", "谷歌浏览器")

linux.install_app(pycharm)
linux.install_app(chrome)
linux.install_app(chrome)

print(linux)

运行结果:

Linux 安装的软件列表为 []
将 PyCharm [1.0] 的执行程序复制到程序目录...
正在解压缩安装程序...
将 Chrome [2.0] 的执行程序复制到程序目录...
已经安装了 Chrome,无需再次安装
Linux 安装的软件列表为 ['PyCharm', 'Chrome']

5.4 面向对象的三大特性

封装就是把方法和属性封装到类(类是抽象的,不能直接使用)的内部,只需要在类的外部,通过对象即可调用。继承实现了代码的重用。子类可以继承父类,并且可以继承多个父类即多继承,子类可以使用父类所拥有的属性和方法(除了私有属性和方法)。多态是以继承和重写父类方法为前提,增加了代码的灵活度,只是一种调用技巧。

猜你喜欢

转载自blog.csdn.net/apollo_miracle/article/details/81503703