Python进阶语法笔记(2)

python变量和静态语言变量的区别

对于静态语言(如C、Java语言),使用变量前必须先定义类型,此时会根据类型在内存中开辟相对应的空间大小,然后将变量名指向该内存地址

python定义变量时,首先在内存创建好对象(值),然后将变量名指向该对象的地址,但是变量名是没有类型的,它的类型由指向的对象决定

python对小整数和字符串优化

>>> a = 256
>>> b = 256
>>> id(a)
140707735073792
>>> id(b)
140707735073792
>>> a = 257
>>> b = 257
>>> id(a)
3102019559184
>>> id(b)
3102019559152

>>> a = 's5d9fa5l8j'
>>> b = 's5d9fa5l8j'
>>> id(a)
3102021280368
>>> id(b)
3102021280368
>>> a = 's5d?fa5l8j'  # 含有?号字符
>>> b = 's5d?fa5l8j'
>>> id(a)
3102021280432
>>> id(b)
3102021280496

对于-5 ~ 256的整数,python对它们进行优化,它们是唯一的,创建一个值之后再遇到相同的值时不会再创建了,所以相同小整数的变量的地址都指向同一个地址。

对于只有字母、数字或字母数字混合的字符串(任意长度),它们也都是指向同一个对象,但是如果字符串中包含其它的字符就不行了

不同函数的参数使用到同一个列表时共享该列表

>>> a = [1,2]
>>> def b(num1):
...     num1.append(6)
...
>>> def c(num2):
...     num2.append(7)
...
>>> b(a)  # 把列表a传递
>>> a
[1, 2, 6]
>>> c(a)  # 又把列表a传递
>>> a  # 两个函数都修改了该列表
[1, 2, 6, 7]

GIL锁

GIL(global interpreter lock 全局解释器锁),Cpython中的一个线程相当于C语言的一个线程,gil使得同一时刻只有一个线程在一个CPU上执行,无法将多个线程映射到多个CPU上执行。gil会根据执行的字节码行数以及时间片释放gil,在遇到 I/O 操作时也会主动释放。gil是线程级的,对多进程无影响,并且只在Cpython解释器中有,如 Jpython等解释器不存在。

创建多线程

对于I/O密集或爬虫等比较耗时的操作,相对CPU来说太慢了,所以操作系统就会阻塞发出I/O请求的线程,所以就可以让更多的其它线程执行,提高CPU的工作效率。

创建线程方法一,普通函数

对于简单逻辑的业务可以使用该方法写多线程

import threading
def Linux(name, version):
    print(name, version, '是Linux操作系统')

threading_1 = threading.Thread(target=Linux, args=('Ubuntu', 18))
threading_1.start()  # 启动线程
threading_1.join()  # 阻塞线程,运行完该线程之后才会继续执行后面的语句
创建线程方法二,类继承

类继承threading.Thread之后,要重写run方法,run方法中写业务逻辑。也可以重写__init__()方法,非必须,但是要注意重写的写法,不能覆盖掉父类threading.Thread的__init__()方法

import threading
class get_time(threading.Thread):
    def __init__(self, name):
        super().__init__(name=name)  # 修改线程的名称
    
    def run(self):
        print('I love you')

thread_1 = get_time()  # 创建线程实例

线程之间的通信

  1. 共享全局变量(global)

  2. 参数传递同一个列表,等于共享该列表

上面两种方法都是有安全问题的(脏读等等),要加锁才行,所以不推荐使用该方法

  1. queue消息队列(queue对线程是安全的)
from queue import Queue
numbers = Queue(maxsize=100)  # 设置最大只能存放100个

# 将put和get放入线程中就行,可达到安全共享该消息队列
numbers.put('hello')  # 放入一个值
numbers.put('world')  # 继续放入一个值。当里面值已满时该方法会一值等待,可put(timeout=3)添加timeout参数解决,当时间超过参数定义的时间,就不会继续等待
num_1 = numbers.get()  # 弹出并获取一个值。当里面没有值时该方法会一值等待,可get(timeout=3)添加timeout参数解决,当时间超过参数定义的时间,就不会继续等待

# 有时希望子线程没有运行完之前主线程不能继续向下执行,可以使用join()在消息队列的层面阻塞,但是要配合task_done()使用,子线程的尾部使用task_done()方法,子线程结束后就向join()发送消息,join()才会解除阻塞,两个方法成对出现。俩方法和线程的join()方法的效果是一致的,任选其一。
numbers.task_done()  # 放到子线程的结尾处
numbers.join()  # 放到主程序中
  1. threading.Condition

    Condition( )使用到notify( )和wait( )实现在两个线程之间通信,但同样也需要使用到锁。首先要使用到内部的acquire( )方法加锁,并结束通信时使用release( )方法解锁,这两个方法其实本质是调用了RLock( )里面的两个方法,但Condition( )类中有__enter__( )和__exit__( )两个魔法方法,所以支持上下文管理协议,并且里面实现了加锁和解锁的两个方法,可以使用with语句达到以上效果。

    threading.Condition.notify( ),用于通知另一个线程。threading.Condition.wait( ),用于阻塞该线程,直至另一个线程的notify( )方法发来信号才能继续运行。两个方法分别用于不同的线程,但它们又互相联系。

    import threading
    
    class Xiaoai(threading.Thread):
        def __init__(self, con):
            super().__init__(name='我是小爱同学')
            self.con = con
    
        def run(self):
            with self.con:
                # self.con.acquire()  # 如果不使用with就使用该语句加锁
                self.con.wait()  # 3.收到Tianmao()的通知,停止阻塞,继续运行
                print('在')
                self.con.notify()  # 4.通知Tianmao()的wait()
                self.con.wait()  # 5.阻塞线程。   8.收到Tianmao()的通知,停止阻塞,继续运行
                print('好呀!')
                # self.con.release()  # 如果不使用with就使用该语句解锁
    
    class Tianmao(threading.Thread):
        def __init__(self, con):
            super().__init__(name='我是天猫精灵')
            self.con = con
    
        def run(self):
            with self.con:
                # self.con.acquire()  # 如果不使用with就使用该语句加锁
                print('小爱同学在吗')
                self.con.notify()  # 1.执行到该语句之后就通知Xiaoai()的wait()
                self.con.wait()  # 2.阻塞线程。  6.收到Xiaomi()的通知,停止阻塞,继续运行
                print('我们一起拉屎吧')
                self.con.notify()  # 7.通知Xiaomi()的wait()
                # self.con.release()  # 如果不使用with就使用该语句解锁
    
    if __name__ == '__main__':
        con = threading.Condition()
        xiaoai = Xiaoai(con)
        tianmao = Tianmao(con)
        xiaoai.start()  # 必须先开启该线程,如果不是,那么遇到notify()方法的信号时没有wait()接收
        tianmao.start()
    

    首先进入Xiaomi( ) 线程,执行到with self.con语句就调用con的__enter__( )方法,__enter__( )内部又调用了RLock( ).acquire( )方法对线程加锁,此时Tianmao( )线程启动后执行到with语句时,由于Condition( )的锁已经被用了就不能运行,然后运行Xiaomi( )线程到wait( )方法,wait( )方法就会阻塞该线程,然后释放锁,此时Tianmao( )线程就能获得锁继续执行,遇到notify( )之后就向Xiaomi( )一直发送信号,然而Xiaomi( )线程没有获得锁就算的wait( )收到信号也不能运行,直到Tianmao( )执行到wait( )方法之后,阻塞了自己的线程并释放了锁…………

    如果先执行tianmao.start( ),Tianmao( )获得了锁之后Xiaomi( )不能运行,notify( )发送的信号没有wait( )方法接收,就会发生超时异常。

线程同步——普通锁(Lock)、可重入锁(RLock)

锁用于锁住一段代码段,该代码段能运行,剩余的其它代码段不能运行,只有释放锁之后剩余的才能运行。用锁会影响多线程的性能。

import threading
from threading import Lock
# from threading import Rock
total = 1
lock = Lock()  # 实例化普通锁
# lock = RLock()
def add():  # 做加法
    global lock  # 声明全局变量
    global total
    for i in range(1000):
        lock.acquire()  # 加锁
        total += 1  # 该段代码被加锁之后,其它代码段不能运行,等该代码段运行完之后才能继续运行 
        lock.release()  # 解锁

def desc():  # 做减法
    global lock
    global total
    for i in range(1000):
        lock.acquire()  # 加锁
        total -= 1
        lock.release()  # 解锁

thread_1 = threading.Thread(target=add)  #创建线程
thread_2 = threading.Thread(target=desc)
thread_1.start()  # 开启线程
thread_2.start()
thread_1.join()
thread_2.join()

普通锁Lock.acquire( )一个线程中只能出现一次,不能出现多次

可重入锁RLock.acquire( )一个线程中可以出现多次,但是必须和release()成对出现

加锁如果不小心容易引发死锁,两种常见死锁情况

  1. acquire()和release()本应该成对出现,当acquire()出现两次,release()出现一次,死锁
    • acquire() 和 release() 应该成对出现
  2. A、B两个线程都使用到两个变量 ij 时,当A先对 i 加锁,然后B对 j 加锁,此时,A想要得到 j ,而B想要得到 i ,但是它们都已经被锁住了,此时发生死锁
    • A和B都先对 i 或都先对 j 加锁

信号量

在写爬虫时,想根据数据库或文件中提供的url去爬取网页,但是爬取太频繁就会被反爬机制kill掉,所以要根据实际情况控制爬虫线程的数量。通过threading.Semaphore(value=1)的value参数来控制线程数量。Semaphore( )内部包装了Condition( ),且Condition(Lock( ))传递了Lock( ),而不是RLock( )

import threading
import time

class Puthtml(threading.Thread):
    def __init__(self, oneurl, sem):
        super().__init__()
        self.oneurl = oneurl
        self.sem = sem

    def run(self):
        time.sleep(2)
        print('下载页面完成', self.oneurl)
        self.sem.release()  # 下载html页面完成之后就释放锁

class Geturl(threading.Thread):
    def __init__(self, sem):
        super().__init__()
        self.sem = sem

    def run(self):
        for i in range(10):
            self.sem.acquire()  # 加锁,由于设置最大为3,所以最多有3个线程,只有释放掉锁之后才能继续运行创建线程
            print('获取url成功')
            puthtml = Puthtml(i, self.sem)  # 创建下载html页面的线程
            puthtml.start()  # 运行下载html页面的线程

if __name__ == '__main__':
    sem = threading.Semaphore(3)  # 最大为3个线程
    geturl = Geturl(sem)  # 创建获取url的线程,并将信号量对象传递过去
    geturl.start()

线程池

线程池能很好的创建多个线程并管理它们,concurrent 模块的 futures 包实现了线程池的功能。虽然信号量也能控制创建线程的数量,但不能很好的管理它们,该包有以下优点

  1. 主线程中可以获取某一个线程的状态或者某一个任务的状态,以及返回值
  2. 当一个线程完成的时候我们主线程能立即知道
  3. futures可以让多线程和多进程编码接口一致,降低学习使用两个知识的难度
from concurrent.futures import ThreadPoolExecutor
import time

def say(name):
    time.sleep(2)
    print('我是你的', name)
    return name

executor = ThreadPoolExecutor(max_workers=2)  # 设置线程池的最大数量
# 一个一个的创建是初级写法
task_1 = executor.submit(say, ('爸爸'))  # 添加函数到线程中运行
task_2 = executor.submit(say, ('爷爷'))  # 创建线程的有返回值,返回Future对象
print(task_1.done())  # done()为判断执行成功与否,True和False。非阻塞型,不等执行完毕就判断
print(task_2.cancel())  # cancel()取消未开始的线程任务并返回True或False。当线程池数量小,执行到该位置时,该线程还没有开始
print(task_1.result())  # result()返回线程返回的结果,阻塞型

我们希望根据列表自动创建,并接收显示线程返回的结果

from concurrent.futures import ThreadPoolExecutor
import time

def say(name):
    time.sleep(2)
    print('我是你的', name)
    return name

executor = ThreadPoolExecutor(max_workers=2)  # 设置线程池的最大数量,也可使用with关键字创建
names = ['自己', '爸爸', '爷爷']

all_task = [executor.submit(say, (name)) for name in names]  # 迭代成器动态创建线程池
for future in all_task:
    retu_data = future.result()
    print('执行成功,返回:', retu_data)

# 线程池提供的map()方法也能实现以上的效果,内部是生成器,yield fs.pop().result()
# for data in executor.map(say, names):
#     print(data)

控制线程阻塞,wait( )方法用于阻塞主线程,等待某子线程完成之后主线程再接着运行,return_when参数用于指定某个子线程,默认是‘ALL_COMPLETED’即所有,可选‘FIRST_COMPLETED’即第一个之后

from concurrent.futures import ThreadPoolExecutor, wait
import time

def say(name):
    time.sleep(1)
    print('我是你的', name)
    return name

executor = ThreadPoolExecutor(max_workers=2)  # 设置线程池的最大数量
names = ['自己', '爸爸', '爷爷']
all_task = [executor.submit(say, (name)) for name in names]
wait(all_task, return_when='FIRST_COMPLETED')
print('hello')

多进程

多进程一般用于计算密集的方面,如图像处理、科学计算和人工智能算法等,这样能充分利用多核CPU的优势,对于计算密集的操作,多进程比多线程快的多。

  1. 普通方式创建进程,注意:multiprocessing.Process( )要使用在if __name__ == '__main__'
import multiprocessing

def gethtml(name):
    print('你不是抓到了我吗')

if __name__ == '__main__':
    process = multiprocessing.Process(target=gethtml, args=('牛逼',))
    process.start()
    print(process.pid)  # 获取进程号
    process.join()
  1. 类继承方式创建
import multiprocessing

class Geturl(multiprocessing.Process):
    def __init__(self):
        super().__init__()

    def run(self):
        print('你又抓到我了')

if __name__ == '__main__':
    process = Geturl()
    process.start()
    process.join()
  1. futures包的方式创建

在concurrent.futures中的ProcessPoolExecutor:该方法和ThreadPoolExecutor的使用方法一致,但有一个注意点,创建进程语句在Windows中该方法要使用在if __name__ == '__main__'中,而Linux没有要求。ProcessPoolExecutor中实际上是对multiprocessing的包装。

from concurrent.futures import ProcessPoolExecutor, wait
import time


def say(name):
    time.sleep(1)
    print('我是你的', name)
    return name

if __name__ == '__main__':
	names = ['自己', '爸爸', '爷爷']
	# executor = ProcessPoolExecutor(max_workers=2)  # 方式一,普通方式
    # all_task = [executor.submit(say, (name)) for name in names]
	with ProcessPoolExecutor(2) as executor:  # 方式二,支持上下文管理协议
        all_task = [executor.submit(say, (name)) for name in names]

进程间通信

共享全局变量不能实现和queue.Queue( )的方法已经不适用于多进程中。

这里是引用

  1. multiprocessing.Queue( ) 方法能实现多个进程之间的通信,用法和queue.Queue( )的用法一致

  2. 管道(Pipe)方式通信,但只能在两个进程之间通信,但运行性能高

from multiprocessing import Process, Pipe

def fa(send_pipe):
    send_pipe.send('hello')

def shou(recv_pipe):
    print(recv_pipe.recv())

if __name__ == '__main__':
    recv_pipe, send_pipe = Pipe()  # 类似scoket有收和发的函数
    my_fa = Process(target=fa, args=(send_pipe,))  # 该进程用于发送信息
    my_shou = Process(target=shou, args=(recv_pipe,))  # 该进程用于接收信息
    my_fa.start()
    my_shou.start()
    my_fa.join()
    my_shou.join()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值