分类: Python

  • Python 进程, 线程, 协程

    什么是进程?

    进程是对资源进行分配和调度的最小单位,是操作系统结构的基础,是线程的容器(就像是一幢房子,一个空壳子,并不能运动)。

    • 进程是一个实体,每个进程都有自己的地址空间,一般包括文本区域(text region)、数据区域(data region)和堆栈(stack region)
    • 文本区域存储处理器执行的代码;数据区域存储变量和进程在执行期间所使用的动态分配的内存;堆栈区域存储在活动过程中所调用的指令和本地变量
    • 进程是一个“执行中的程序”。程序是一个没有生命的实体,只有在操作系统调用时,他才会成为一个活动的实体:进程。

    什么是线程

    线程被称为轻量级进程,是操作系统能够运算调度的最小单位,线程被包含在进程中,是进程中实际处理单位(就像是房子里的人,人才能动)

    • 一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组 成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,
    • 线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个 进程的其它线程共享进程所拥有的全部资源。

    线程的三种状态

    一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程 在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。

    • 就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;
    • 运行状态是指线程占有处理机正在运行
    • 阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。
    • 每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

     

    什么是协程

    协程又叫微线程,一个程序可以包含多个协程,就好比一个进程包含多个线程。协程的调度完全由用户控制。

    协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。

    直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

    协程和线程的阻塞是有本质区别的。协程的暂停完全由程序控制,线程的阻塞状态是由操作系统内核来进行切换。因此,协程的开销远远小于线程的开销。

     

    相互比较

     

    进程与线程的区别:

      • 进程有自己独占的地址空间,每启动一个进程,系统就需要为它分配地址空间;
      • 而一个进程下所有线程共享该进程的所有资源,使用相同的地址空间,因此CPU在线程之间切换远远比在进城之间切换花费小,而且创建一个线程的开销也远远比开辟一个进程小得多。
      • 线程之间通信更加方便,同一进程下所有线程共享全局变量、静态变量等数据。
      • 而进程之间通信需要借助第三方。
      • 线程只能归属于一个进程并且它只能访问该进程所拥有的资源。
      • 当操作系统创建一个进程后,该进程会自动申请一个名为主线程或首要线程的线程。
      • 处理IO密集型任务或函数用线程;
      • 处理计算密集型任务或函数用进程。

    线程和协程的区别:

     

    • 一个线程可以多个协程,一个进程也可以单独拥有多个协程,这样python中则能使用多核CPU。
    • 线程进程都是同步机制,而协程则是异步
    • 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态

     

    我们常说python中的多线程都是假的,因为无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行。

    这又是为什么呢?其实这主要是由于GIL的存在而造成的,详情查阅 http://bayestalk.com/592

     

     

    总结

     

    至此, 不难发现, 在某个角度来讲。它们三者体现的是一种颗粒粗细度的关系。就像切菜,

    • 你用青龙偃月刀来切, 肯定是可以的, 但应该切得比较大块。
    • 用菜刀, 这种对生活而言, 颗粒粗细度刚好。
    • 但如果是做出蔬菜工艺品, 那么可能要用到非常小巧, 锋利的雕刻工具了。

    再比如对于PC而言,

    • 一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。
    • 有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。
    • 由于每个进程至少要干一件事,所以,一个进程至少有一个线程。
      • 当然,像Word这种复杂的进程可以有多个线程,多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。当然,真正地同时执行多线程需要多核CPU才可能实现。

    大部分情况下, Python程序,都是执行单任务的进程,也就是只有一个线程。如果我们要同时执行多个任务怎么办?

     

    • 一种是启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务。
    • 还有一种方法是启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务。
    • 当然还有第三种方法,就是启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,当然这种模型更复杂,实际很少采用。

    多进程和多线程的程序涉及到同步、数据共享的问题,编写起来更复杂。

    多线程编程

    多线程编程导致的问题

    import time, threading
    
    # 假定这是你的银行存款:
    balance = 0
    
    def change_it(n):
        # 先存后取,结果应该为0:
        global balance
        balance = balance + n
        balance = balance - n
    
    def run_thread(n):
        for i in range(1000000):  # 循环的次数要设置得足够大。
            change_it(n)
    
    t1 = threading.Thread(target=run_thread, args=(5,))
    t2 = threading.Thread(target=run_thread, args=(8,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    
    
    run_thread(1)
    print(balance)

    以单线程的思路来解释, 就是两个人玩一个非常无聊的游戏, A给B 1块, B又给A 1块, 重复无数次。 那么亿万年后, 他们的财产不会因这个游戏有任何影响。

    • balance 初始值 = 0
    • change_it(n) 函数
      • balance+1
      • balance-1
    • 一加一减, 相抵消
    • 所以最后balance

    但是在多线程中, 而多线程中,所有变量都由所有线程共享, 任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量。

    怎么解决?

    import time, threading
    
    # 假定这是你的银行存款:
    balance = 0
    
    def change_it(n):
        # 先存后取,结果应该为0:
        global balance
        balance = balance + n
        balance = balance - n
    
    def run_thread(n):
        for i in range(10000000): # 循环的次数要设置得足够大。
            change_it(n)
    
    t1 = threading.Thread(target=run_thread, args=(5,))
    t2 = threading.Thread(target=run_thread, args=(8,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    
    
    run_thread(1)
    print(balance)

    结果始终为0, 不同线程间被lock隔绝吗相互独立, 互不干扰。

     

    threading 类

    Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。

    启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行:

     

     

    其他

     

    1. 假设你经营着一家物业管理公司。最初,公司的业务量很小,事事都需要你亲力亲为,给老张家修完暖气管道,立马就去老李家换电灯泡—这叫单线程,所有的工作都得顺序执行。
    2. 后来业务拓展了,你雇用了几个工人,这样你的物业公司就可以同时为多户人家提供服务—这叫多线程,而你是主线程。
      1. 工人们使用的工具是物业管理公司提供的,这些工具由大家共享,并不专属于某一个人—这叫多线程资源共享。
      2. 工人们在工作中都需要管钳,可是管钳只有一把—这叫冲突。解决冲突的办法有很多,如排队等候、等同事用完后微信通知等—这叫线程同步。
      3. 你给工人布置任务—这叫创建线程。布置任务后你还要告诉他,可以开始工作了,不然他会一直停在那儿不动—这叫启动线程(start)。
      4. 如果某个工人(线程)的工作非常重要,你(主线程)也许会亲自监工一段时间;如果不指定时间,则表示你会一直监工到该项工作完成—这叫线程参与(join)。
      5. 业务不忙的时候,你就在办公室喝喝茶。下班时间一到,你群发微信,通知工人该下班了,所有的工人不管手里正在做的工作是否完成,都立刻下班。因此如果有必要,你得避免在工人正忙着的时候发下班通知—这叫线程守护属性设置和管理(daemon)。
      6. 再后来,公司规模扩大了,同时为很多生活社区服务,在每个生活社区都设置了分公司,分公司由分公司经理管理,运营机制和总公司几乎一模一样—这叫多进程,总公司叫主进程,分公司叫子进程。
      7. 总公司和分公司,以及各个分公司之间使用的工具都是独立的,不能借用、混用—这叫进程间不能共享资源。各个分公司之间可以通过专线电话联系—这叫管道。各个分公司之间还可以通过公司公告栏交换信息—这叫进程间共享内存。另外,各个分公司之间还有各种协同手段,以便完成更大规模的作业—这叫进程间同步。
      8. 分公司可以跟着总公司一起下班,也可以把当天的工作全部做完之后再下班—这叫守护进程设置。

     

  • 什么是CPython GIL?

    什么是Python GIL?

    什么是解释器?

    Python作为一门解释性语言,先把源代码编译为字节码,再放进虚拟机中执行,整个过程是由解释器执行并完成的。类似的还有JavaScript和PHP等。

    但是解释器并不是只有一种,官方的解释器是基于C语言开发的CPython。但是除了CPython,还有基于Java实现的Jython、基于R 语言实现的RPython等等。

     

     

     什么是GIL?

    GIl 是一种互斥锁

    什么是互斥锁?

    在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为” 互斥锁” 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

     

    当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。

    线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。

    互斥锁为资源引入一个状态:锁定/非锁定

    某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

     

    为什么会产生GIL?

    GIL的产生是因为CPython的内存管理不安全

    In CPython, the global interpreter lock, or GIL, is a mutex that protects access to Python objects, preventing multiple threads from executing Python bytecodes at once. The GIL prevents race conditions and ensures thread safety. A nice explanation of how the Python GIL helps in these areas can be found here. In short, this mutex is necessary mainly because CPython’s memory management is not thread-safe。

    在CPython中,GIL是一个互斥锁,它在任一时刻只允许一个线程对字节码进行执行。这样避免了竞争危害,从而保证了线程安全。简单来说就是,互斥锁之所以存在是因为CPython的内存管理不是“线程安全的”

    由此Python的官方文档可知, 因为因为CPython的内存管理不是“线程安全的”, 所以需要互斥锁, 那么自然会引出另外两个问题。

    GIL的产生是因为CPython的内存管理不安全

    为什么CPython的内存管理不安全?

    Python 第一次发布是在 1991 年,当时的 CPU 都是单核,单核中,多线程主要为了一边做IO,一边做 CPU 计算而设计的,Python 编译器是由 C 语言编写的,因此也叫 CPython,那时候很多编程语言没有自动内存管理的功能,为了实现自动垃圾回收,Python 为每一个对象进行了引用计数,当引用计数为 0 的时候说明该对象可以回收,从而释放内存了,比如:

    >>> import sys
    >>> a = []
    >>> b = a
    >>> sys.getrefcount(a)
    3

    这里 a对象就有 3 个引用,

    • 一个是本身,
    • 一个是变量 b,
    • 一个是 getrefcount 函数的参数,

    如果此时又有一个线程引用了 a,那么引用计数再增加 1,如果某个线程使用了 a 后运行结束,那么引用计数就减少 1,多线程对同一个变量「引用计数」进行修改,就会遇到 race conditions(竞争)。

     

    怎么解决内存管理不安全的问题?

    为了避免 race conditions,最简单有效的办法就是加一个互斥锁。但如果对每一个对象都加锁,有可能引发另一个问题,就是死锁,而且频繁的获取和释放会导致性能下降。

    所以至此, 最简单有效的方法就是加一个解释器锁,线程在执行任何字节码时都先获取解释器锁,这就避免了死锁,而且不会有太多的性能消耗。当时 CPU 都是单核,而且这种 GIL 设计简单,并不会影响性能,因此一直沿用至今天。GIL 存在最主要的原因,就是因为 Python 的内存管理不是线程安全的,这就是 GIL 产生并存在的主要缘由。

    互斥锁的代码实例

    threading模块中定义了Lock类,可以方便的处理锁定:

     # 创建锁
     mutex = threading.Lock()
     ​
     # 锁定
     mutex.acquire()
     ​
     # 释放
     mutex.release()
    • 如果这个锁之前是没有上锁的,那么acquire不会堵塞
    • 如果在调用acquire对这个锁上锁之前,它已经被 其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止

     

    互斥锁在for循环外面
    import threading
    import time
    
    # 定义一个全局变量
    g_num = 0
    
    
    def test1(num):
     global g_num
     # 上锁,如果之前没有被上锁,那么此时 上锁成功
     # 如果上锁之前 已经被上锁了,那么此时会堵塞在这里,直到 这个锁被解开位置
     mutex.acquire()
     for i in range(num):
         g_num += 1
     mutex.release()   # 解锁
     print("-----in test1 g_num=%d----" % g_num)
    
    
    def test2(num):
     global g_num
     mutex.acquire()   # 上锁
     for i in range(num):
         g_num += 1
     mutex.release()   # 解锁
     print("-----in test2 g_num=%d=----" % g_num)
    
    
    # 创建一个互斥锁,默认是没有上锁的
    mutex = threading.Lock()
    
    
    def main():
     t1 = threading.Thread(target=test1, args=(1000000,))
     t2 = threading.Thread(target=test2, args=(1000000,))
    
     t1.start()
     t2.start()
    
     # 等待上面的2个线程执行完毕....
     time.sleep(2)
    
     print("-----in main Thread g_num = %d---" % g_num)
    
    if __name__ == "__main__":
     main()
    
    #-----in test1 g_num=1000000----
    #-----in test2 g_num=2000000=----
    #-----in main Thread g_num = 2000000---
    互斥锁在for循环里面
    import threading
    import time
    
    # 定义一个全局变量
    g_num = 0
    
    def test1(num):
     global g_num
     for i in range(num):
         mutex.acquire()  # 上锁
         g_num += 1
         mutex.release()  # 解锁
    
     print("---test1---g_num=%d"%g_num)
    
    def test2(num):
     global g_num
     for i in range(num):
         mutex.acquire()  # 上锁
         g_num += 1
         mutex.release()  # 解锁
    
     print("---test2---g_num=%d"%g_num)
    
    # 创建一个互斥锁
    # 默认是未上锁的状态
    mutex = threading.Lock()
    
    # 创建2个线程,让他们各自对g_num加1000000次
    p1 = threading.Thread(target=test1, args=(1000000,))
    p1.start()
    
    p2 = threading.Thread(target=test2, args=(1000000,))
    p2.start()
    
    # 等待计算完成
    while len(threading.enumerate()) != 1:
     time.sleep(1)
    
    print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)
    
    
    # ---test1---g_num=1909909
    # ---test2---g_num=2000000
    # 2个线程对同一个全局变量操作之后的最终结果是:2000000

     

    上锁解锁过程
    • 当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。
    • 每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。
    • 线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
    锁的好处
    • 确保了某段关键代码只能由一个线程从头到尾完整地执行
    锁的坏处
    • 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
    • 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁。

     

    死锁代码实例

     

    import threading
    import time
    
    
    #创建互斥锁
    lock = threading.Lock()
    
    
    #根据下标去取值, 保证同一时刻只能有一个线程去取值
    def get_value(index):
    
        # 上锁
        lock.acquire()
        print(threading.current_thread())
        my_list = [3,6,8,1]
        # 判断下标释放越界
        if index >= len(my_list):
            print("下标越界:", index)
    
            return
        value = my_list[index]
        print(f'值是:{value}')
        time.sleep(0.2)
        # 释放锁
        lock.release()
    
    
    if __name__ == '__main__':
        # 模拟大量线程去执行取值操作
        for i in range(30):
            sub_thread = threading.Thread(target=get_value, args=(i,))
            sub_thread.start()

    出现死锁的情况, 程序无法正常停止, 一直在等待

    <Thread(Thread-1, started 30364)>
    值是:3
    <Thread(Thread-2, started 27120)>
    值是:6
    <Thread(Thread-3, started 29632)>
    值是:8
    <Thread(Thread-4, started 29988)>
    值是:1
    <Thread(Thread-5, started 20984)>
    下标越界: 4
    

    避免死锁的代码示例

    # 在合适的地方释放锁
    import threading
    import time
    
    #创建互斥锁
    lock = threading.Lock()
    
    
    #根据下标去取值, 保证同一时刻只能有一个线程去取值
    def get_value(index):
    
        # 上锁
        lock.acquire()
        print(threading.current_thread())
        my_list = [3,6,8,1]
        if index >= len(my_list):
            print("下标越界:", index)
            # 当下标越界需要释放锁,让后面的线程还可以取值
            lock.release()
            return
        value = my_list[index]
        print(value)
        time.sleep(0.2)
        # 释放锁
        lock.release()
    
    
    if __name__ == '__main__':
        # 模拟大量线程去执行取值操作
        for i in range(10):
            sub_thread = threading.Thread(target=get_value, args=(i,))
            sub_thread.start()

     

    <Thread(Thread-1, started 30336)>
    3
    <Thread(Thread-2, started 5920)>
    6
    <Thread(Thread-3, started 28308)>
    8
    <Thread(Thread-4, started 27324)>
    1
    <Thread(Thread-5, started 26840)>
    下标越界: 4
    <Thread(Thread-6, started 30104)>
    下标越界: 5
    <Thread(Thread-7, started 28900)>
    下标越界: 6
    <Thread(Thread-8, started 2676)>
    下标越界: 7
    <Thread(Thread-9, started 28912)>
    下标越界: 8
    <Thread(Thread-10, started 30068)>
    下标越界: 9
    
    Process finished with exit code 0

     

    最后, GIL导致了什么结果?

    正面

    解决安全问题。

    负面

    单线程CPU消耗

    约16% (i5 11代)

     

    import threading
    
    def dead_loop():
        while True:
            pass
    dead_loop()

     

    双线程CPU消耗

    仍然约16%, 而不是32%。

    import threading
    
    def dead_loop():
        while True:
            pass
        # 新起一个死循环线程
        t = threading.Thread(target=dead_loop)
    
        t.start()
        # 主线程也进入死循环
        dead_loop()
        t.join()
    
    dead_loop()

     

    结论:

    • 当双线程时, Cpython 缩小好的CPU的资源, 和单线程时一致。
    • Cpython 当前只能运行一个GIL线程。

     

     

    如果再更进一步, 尝试十个或N个线程, Python的CPU利用率仍然不变。

    但是用C、C++或Java来改写相同的死循环,直接可以把全部核心跑满,为什么Python不行?正是GIL。

    Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。

    在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。

    不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。

    其它

    • 对于 “Python的GIL” 这种表述是不够严谨, 但也不算错。
    • GIL是相对于Cpython 解释器而言, 而不是Python 语言。
    • Cpython是用来解析Python代码.
    • Cpython是目前最流行的, 主流的解释器.

     

    最后, 只要你愿意, 你可以自己开发一个没有GIL的解释器.

     

     

     

  • Python 进程/线程/协程/异步编程

    前置基础

    什么是GIL?

    进程

     

    线程

     

    协程

    非协程实例

    首先来看非协程的代码实例

    t1 = time.time()
    def func1():
        print("当前执行function 1")
        time.sleep(1)  # 当程序出现了同步操作的时候. 异步就中断了
        print("当前执行function 1")
    
    
    def func2():
        print("当前执行function 2")
        time.sleep(2)
        print("当前执行function 2")
    
    def func3():
        print("当前执行function 3")
        time.sleep(3)
        print("当前执行function 3")
    
    if __name__ == '__main__':
        f1 = func1()
        f2 = func2()
        f3 = func3()
        tasks = [
            f1, f2, f3
        ]
        # 一次性启动多个任务(协程)
        # asyncio.run(asyncio.wait(tasks))
        t2 = time.time()
        print(t2 - t1)
    

     

    结果

     

    当前是function 1
    当前是function 1
    当前是function 2
    当前是function 2
    当前是function 3
    当前是function 3
    6.002589225769043

     

    协程实例

    async def func1():
        print("当前执行function_1")
        await asyncio.sleep(1)
        print("当前执行function_1")
    
    
    async def func2():
        print("当前执行function_2")
        await asyncio.sleep(2)
        print("当前执行function_2")
    
    
    async def func3():
        print("当前执行function_3")
        await asyncio.sleep(3)
        print("当前执行function_3")
    
    
    async def main():
        # 第一种写法
        # f1 = func1()
        # await f1  # 一般await挂起操作放在协程对象前面
        # 第二种写法(推荐)
        tasks = [
            asyncio.create_task(func1()),  # py3.8以后加上asyncio.create_task()
            asyncio.create_task(func2()),
            asyncio.create_task(func3())
        ]
        await asyncio.wait(tasks)
    
    
    if __name__ == '__main__':
        t1 = time.time()
        # 一次性启动多个任务(协程)
        asyncio.run(main())
        t2 = time.time()
        print(t2 - t1)
    

     

    结果

    当前执行function_1
    当前执行function_2
    当前执行function_3
    当前执行function_1
    当前执行function_2
    当前执行function_3
    3.0129427909851074

     

    对比与发现

    1. 一共三个任务
    2. 非协程写法里
      1. 如果使用非协程写法,  time.sleep(1)  ,将会导致IO阻塞。 因此程序会在设定的等待时间结束后, 才会往下执行。
      2. 耗时 6.002589225769043
      3. 打印的顺序自上而下
    3. 协程写法里
      1. 耗时3.0129427909851074
      2. 可以反推: 总耗时 = await 挂起时间最长的那个任务所花的时间(function 3) + 切换协程上下文的所需开销的时长(0.01)
    4. 异步协程的语法结构

     

     

    异步协程最简实例

    import asyncio
    import aiohttp
    import aiofiles
    
    # 构造无数个urls
    urls = [
        "https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png",
        "https://inews.gtimg.com/newsapp_bt/0/12171811596_909/0.png",
        "http://www.soso.com/soso/images/logo_index_sosox2.png"
    ]
    
    async def aiodownload(url):
        # 异步下载功能的函数, 以下三个步骤都是IO操作
            # 1.发送请求 aiohttp
            # 2. 得到图片内容 async
            # 3. 保存到文件 aiofiles
    
        filename = url.rsplit("/", 1)[1]
    
        async with aiohttp.ClientSession() as session:  # 类似同步的 requests
            async with session.get(url) as resp:        # 类似同步的 resp = requests.get()
                # 请求回来. 异步写入文件
                async with aiofiles.open(filename, mode='wb') as f:
                    # await f.write(await resp.content.read())  # 读取内容是异步的. 需要await挂起, resp.text()
                    # Response对象的read()和text()方法会将响应一次性全部读入内存,这会导致内存爆满,导致卡顿,影响效率。 因此采取字节流的形式,每次读取4096个字节并写入文件。
                    while True:
                        pic_stream = await resp.content.read(4096)
                        if not pic_stream:
                            break
                        await f.write(pic_stream)
        print(f'{filename} 已下载')
    
    async def main():
        # tasks = []
        # for url in urls:
        #     # tasks.append(aiodownload(url))
        #     d = asyncio.create_task(aiodownload(url))
        #     tasks.append(d)
    
        # 利用推导式的简写方式
        tasks = [asyncio.create_task(aiodownload(url)) for url in urls]
        await asyncio.wait(tasks)
    
    if __name__ == '__main__':
        asyncio.run(main())
    
    
    

     

    反思

    在爬虫的三个基本操作都是涉及到IO

    •  1.发送请求 aiohttp
    • 2. 得到图片内容 async
    • 3. 保存到文件 aiofiles

    但2和1、3有显著区别。

    • 磁盘io与网络io不同,磁盘顺序读写单个文件最快,并发读写会涉及到多个文件的切换问题,反而花了更多的时间,所以异步编程使用aiofiles要谨慎。
    • 举一个有差异但基本原理类似的例子。在同一台电脑上, 将C盘里的文件复制到D盘去, 这里有个很明显的经验是如果同时只有一个这样的复制操作, 那么“较为高效”。如果“将C盘里的文件复制到D盘去”的同时,  开启多个文件转移复制粘贴进程, 速度极为缓慢甚至进程管理亲卡死。
    • 解决的办法应该有很多种。
      • 可以考虑把文件读写任务抽离出来,放到队列里面,然后用专门的线程或进程去按顺序去处理。
      • 因此采取字节流的形式,每次读取4096个字节并写入文件
  • What does if __name__ == “__main__”: do?

    a = 'a'
    print('我是script a')
    print(a)
    
    
    
    import script_a
    b = 'b'
    print('我是script b')
    print(b)

     

    a = 'a'
    print('我是script a')
    
    if __name__ == "main":
        print(a)
    
    
    import script_a
    b = 'b'
    print('我是script b')
    print(b)

     

     

    1. 现在有A.py和 B.py两个脚本文件
      1.  A.py
      2. B.py
    2. 在B中import A
      1. import A这个动作, 意味着导入并执行A里面的每一行(所有)代码.
    3. 因此会导致一个问题: 有些时候, 我并不希望B在引入A时, 去执行A的所有代码.
    4. 为了解决问题,  python里允许你使用条件检测, 它基于以下机制
      1. __name__ 是语言预置的变量
      2. 如果A.py被导入, 则它的__name__ 的值为文件名A
      3. 如果A.py被直接执行, 则它的__name__ 的值为 “__main__”
    5. 因此, 对于A里面, 被导入到B时, 不希望被执行的代码可以放到 if __name__ == “__main__”:里面去. 因为此时条件不成立, __name__ == “A”, 而不是”__main__”

     

    这其实是个非常自然而然的事情, 但最近有初学的朋友问到这问题. 以及另外相关的奇怪问题:

    1. 为什么 “我并不希望B在引入A时, 去执行A的所有代码.”? 这种逻辑在需求中是常见的.
    2. 为什么会有”__name__” 和 “__main__” 这种看起来很奇怪的东西? 这是语言设计者设计出来, 就像人类设计数字来计数一样. 这是个哲学问题和喜好问题. 不是个编程问题.

     

    print(a) 没有被执行

  • Python 符号用法总结

    下划线

    单下划线 _

    函数名称前单下划线

    def _add():
    	...
        return

    是一种私有函数的命名约定,即提示程序员该函数只能在类或者该文件内部使用,但实际上也可以在外部使用。

     

    _xxx 单下划线

    • protected 类型变量
    • 只允许其本身与子类进行访问
    • 也不能使用from xxx import * 的方式导入

    xxx_ 单下划线

    • 避免名称与关键字冲突

     

    星号*

  • Python环境问题记录

    Django

    • 安装django
      • pip install django
    • 创建django项目
      • django-admin startproject projectname
    • 启动django项目
      • python manage.py startapp app_name
    • 注册app
      • INSTALLED_APPS = {
        'app01.apps.App01Config'
        }
    • 配置静态文件和模板路径
    • 配置数据库
      • 创建数据库
      • 安装数据库连接模块
        • pip install mysqlclient
      • 配置数据库连接
        • DATABASES = {
              'default': {
                  'ENGINE': 'django.db.backends.mysql',
                  'NAME': 'django_test_2',  # 数据库名字
                  'USER': 'root',
                  'PASSWORD': 'zxcvbnm',
                  'HOST': '127.0.0.1',  # 哪台机器安装了MySQL
                  'PORT': 3306,
              }
          }
      • 创建表
        • app-models.py 写类
        • python manage.py makemigrations
        • python manage.py migrate

     

    本地conda虚拟环境中部署Django项目

    利用conda创建虚拟环境

    conda create -n env_name python=version_number

    conda remove -n env_name (删除指定环境)

     

    激活新创建的环境

    conda init

    conda activate env_name

    (ENV_20221122) PS D:\Practice\Python\Django_20221122>

    当前已激活的环境名称会显示在最左侧的小括号内.

     

    在新创建的环境中安装所需包

    conda env list (查看已安装哪些包)

    conda安装

    conda install package_name

    (base) PS D:\Practice\Python\Django_20221122> conda env list
    # conda environments:
    #
    base * C:\ProgramData\Anaconda3
    DjangoProject C:\ProgramData\Anaconda3\envs\DjangoProject
    ENV_20221122 C:\ProgramData\Anaconda3\envs\ENV_20221122
    Python36 C:\ProgramData\Anaconda3\envs\Python36
    test C:\ProgramData\Anaconda3\envs\test
    

     

    pip安装

    尽管在anaconda下我们可以很方便的使用conda install来安装我们需要的依赖,但是anaconda本身只提供部分包,远没有pip提供的包多,有时conda无法安装我们需要的包,我们需要用pip将其装到conda环境里。

    首先,我们需要判断目前我们用的pip指令,会把包装到哪里,通常情况下,pip不像conda一样,他不知道环境,我们首先要确保我们用的是当前环境的pip,这样pip install时,包才会创建到本环境中,不然包会创建到base环境,供各个不同的其他conda环境共享,此时可能会产生版本冲突问题(不同环境中可能对同一个包的版本要求不同)

    用 which -a pip 命令查看我们此时用的pip为哪个环境.

    新创建的虚拟环境应该已经包含pip, 如没有, 可用conda install pip先安装

    退出当前环境

     

    本次使用完成后, 最好使用以下命令退出, 不要直接关闭cmd, 有概率会导致产生潜在问题.

    conda deactivate

    (在当前的conda虚拟环境里,只需要执行conda deactivate 命令即可,无须参数)

     

     

    linux 环境下部署Django项目

     

    当前环境的导出和导入(windows)

    pip

    记录和导出环境信息

    pip freeze > requirements_pip.txt

    生成requirements_pip.txt文件

    asgiref==3.5.2
    Django==4.1.3
    sqlparse==0.4.3
    tzdata==2022.6
    

     

    导入和安装环境配置

    pip install freeze > requirements_pip.txt

    conda

    conda list -e > requirements_conda.txt

  • Django – 前端提交数据, 后端接收并入库简例 (ModelForm)

    models.py

     

    class Boss(models.Model):
        name = models.CharField(verbose_name="姓名", max_length=64)
        age = models.IntegerField(verbose_name="年龄")
        img = models.CharField(verbose_name="头像", max_length=256)

     

    这种写法需要在view_name.py文件中去处处理 待保存文件的路径问题, 并调用create方法.

    media_file_path = os.path.join("media", image_object.name)
    print(media_file_path)
    f = open(media_file_path, mode="wb")
    for chunk in image_object.chunks():
        f.write(chunk)
        f.close()
    
    models.Boss.objects.create(
        name = form.cleaned_data['name'],
        age = form.cleaned_data['age'],
        img = media_file_path,

     

     

    class City(models.Model):
        name = models.CharField(verbose_name="名称", max_length=64)
        count = models.IntegerField(verbose_name="人口")
    
        # 此处写成"FileField", 而不是"IntegerField", 这样FileField会多出upload_to='目录名'属性, 在入库时可快速将图片保存到该目录
        logo = models.FileField(verbose_name="logo", max_length=256, upload_to='city/')

     

    view_name.py

    class UpModelForm(BootStrapModelForm):
        class Meta:
    
            model = models.City
            fields = "__all__"
    
    def upload_model_form(request):
        title = "ModelForm上传"
        if request.method == "GET":
            form = UpModelForm()
            return render(request, 'upload_form.html', {'form': form, "title": title})
    
    
        form = UpModelForm(data=request.POST, files=request.FILES)
        if form.is_valid():
            form.save()
            return HttpResponse('成功')

     

    可以看到这种写法非常简捷,  保存路径已在创建model时设置, 而保存数据只需form.save()即可.

    结果验证

    • 保存路径: logo = models.FileField(verbose_name=”logo”, max_length=256, upload_to=’city/’)
    • 如何保存: form.save()

     

     

    前置条件

    配置media目录. (实际使用中, 由于目录位置等差异, 根据实际情调整.)

    setting.py

    MEDIA_ROOT = os.path.join(BASE_DIR, "media")
    MEDIA_URL = "/media/"

     

    urls.py 路由

     

    from django.views.static import serve
    from django.conf import settings
    re_path(r'^media/(?P<path>.*)$', serve, {"document_root": settings.MEDIA_ROOT}, name='media'),

     

  • Django – 前端提交数据, 后端接收并入库简例 (Form)

    view

    class UpForm(BootStrapForm):  封装了bootstrap的Form组件的类, 让表单快速获得bootstrap的样式
        bootstrap_exclude_fields = ['img']  # 在BootStrapForm中,排除img表单的样式
        name = forms.CharField(label="姓名")
        age = forms.IntegerField(label="年龄")
        img = forms.FileField(label="头像")
    
    
    def upload_form(request):
        title = "表格上传"
    
        if request.method == "GET":
            form = UpForm()
            return render(request, 'upload_form.html', {'form':form, "title":title})
    
        if request.method == "POST":
            # 获取前端用户提交的数据
            form = UpForm(data=request.POST, files = request.FILES)
            if form.is_valid():
                image_object = form.cleaned_data.get("img")
              
                # 对图片数据进行特殊处理
                db_file_path = os.path.join('static', 'img', image_object.name)
                file_path = os.path.join('app01', db_file_path)
                print(file_path)
                f = open(file_path, mode="wb")
                for chunk in image_object.chunks():
                    f.write(chunk)
                    f.close()
                # 写入数据库
                models.Boss.objects.create(
                    name = form.cleaned_data['name'],
                    age = form.cleaned_data['age'],
                    img = db_file_path,
                )
    
        return render(request, 'upload_form.html', {'form': form, "title": title})
    

     

    template

    <form method="post" novalidate enctype="multipart/form-data">
        {% csrf_token %}
    
        {% for field in form %}
            <div class="form-group">
                <label>{{ field.label }}</label>
                {{ field }}
                <span style="color: red">{{ field.errors.0 }}</span>
            </div>
        {% endfor %}
    
        <button type="submit" class="btn btn-primary">提交</button>
    
    </form>

     

    router

    path('upload/form/', upload.upload_form)

     

  • Django – Excel上传数据 (Openpyxl)

    view

    def depart_multi(request):
        from openpyxl import load_workbook
    
        # 1.获取文件对象
        file_object = request.FILES.get("exc")
        print(file_object)
        print(type(file_object))
    
    
        # 2.对象传递给openpyxl, 并由openpyxl读取其中内容
        wb = load_workbook(file_object)
        sheet = wb.worksheets[0]
    
    
        # 3. 循环获取数据
        for row in sheet.iter_rows(min_row=2):
            text = row[0].value
            print(text)
    
            # 4. 写入数据库 (更多的验证规则, 此处省略)
            exists = models.Department.objects.filter(title=text).exists()
            if not exists:
                models.Department.objects.create(title=text)
    
        return redirect("/depart/list")

     

    template

     

    <div class="panel-body">
    
      <form method="post" enctype="multipart/form-data" action="/depart/multi/">
          {% csrf_token %}
          <div class="form-group">
            <input type="file" name="exc">
          </div>
    
          <input type="submit" value="上传" class="btn btn-info btn-sm">
    
      </form>
    </div>

     

  • Django – Form和ModelForm组件

    表单的很多字段信息, 和models.py文件里的模型是一致的,为了避免重复代码,以及提高效率, 可以使用ModelForm,将模型和表单进行绑定。

    Form

    views.py

    //创建业务类MyForm, 继承django中Form类
    class MyForm(Form):  
        // Form类会在html中渲染出原始表单
        user = forms.CharField(widget=forms.Input)
        pwd = form.CharFiled(widget=forms.Input)
        email = form.CharFiled(widget=forms.Input)
        account = form.CharFiled(widget=forms.Input)
        create_time = form.CharFiled(widget=forms.Input)
        depart = form.CharFiled(widget=forms.Input)
        gender = form.CharFiled(widget=forms.Input)
    
    
    def user_add(request):
        if request.method == "GET":
            form = MyForm()
            return render(request, 'user_add.html',{"form":form})

     

    user_add.html (Form)

    <form method="post">
        {% for field in form%}
        	{{ field }}
        {% endfor %}
        <!-- <input type="text"  placeholder="姓名" name="user" /> -->
    </form>

    或者

    <form method="post">
        {{ form.user }}
        {{ form.pwd }}
        {{ form.email }}
        <!-- <input type="text"  placeholder="姓名" name="user" /> -->
    </form>

     

     

    ModelForm

     

    models.py 

    class UserInfo(models.Model):
        """ 员工表 """
        name = models.CharField(verbose_name="姓名", max_length=16)
        password = models.CharField(verbose_name="密码", max_length=64)
        age = models.IntegerField(verbose_name="年龄")
        account = models.DecimalField(verbose_name="账户余额", max_digits=10, decimal_places=2, default=0)
        create_time = models.DateTimeField(verbose_name="入职时间")
        depart = models.ForeignKey(to="Department", to_field="id", on_delete=models.CASCADE)
        gender_choices = (
            (1, "男"),
            (2, "女"),
        )
        gender = models.SmallIntegerField(verbose_name="性别", choices=gender_choices)

     

     

    views.py 快速建立表单

    class UserModelForm(forms.ModelForm):
        name = forms.CharField(min_length=2, label="用户名")
        password = forms.CharField(label="密码")
        class Meta:
            model = UserInfo
            fields = ["name","password","age","xx"]
    
    
    def user_add(request):
        if request.method == "GET":
            form = MyForm()
            return render(request, 'user_add.html',{"form":form})

    views.py 给表单添加样式

     

    class UserModelForm(forms.ModelForm):
        name = forms.CharField(min_length=2, label="用户名")
        password = forms.CharField(label="密码")
        # create_time = forms.DateTimeField()
        class Meta:
            model = models.UserInfo
            fields = ['name', 'password', 'age', 'account', 'create_time', 'gender', 'depart']
            
            # 方法1: 为前端元素添加类属性, 使其获得样式. 
    
            # widgets = {
            #     "name":forms.TextInput(attrs={"class":"form-control"})
            # }
    
            # 方法2: 为前端元素添加类属性, 使其获得样式. 
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)  #super() 函数是用于调用父类(超类)的一个方法。
            for name, field in self.fields.items():
                field.widget.attrs = {"class":"form-control", "placeholder":field.label}
    

     

     

    user_add.html (ModelForm)

    <form method="post" novalidate>
        {% csrf_token %}
    
        {% for field in form %}
            <div class="form-group">
                <label>{{ field.label }}</label>
                {{ field }}
                <span style="color: red">{{ field.errors.0 }}</span>
            </div>
        {% endfor %}
    
        <button type="submit" class="btn btn-primary">提交</button>
    
    </form>