python高级


多任务编程


多任务是指在同一时间内执行多个任务,例如: 现在电脑安装的操作系统都是多任务操作系统,可以同时运行着多个软件。

多任务的执行方式:并发和并行。

并发:在一段时间内交替去执行任务。

并行:对于多核cpu处理多任务,操作系统会给cpu的每个内核安排一个执行的软件,多个内核是真正的一起执行软件。这里需要注意多核cpu是并行的执行多任务,始终有多个软件一起执行。

一个程序运行后至少有一个进程,一个进程默认有一个线程,进程里面可以创建多个线程,线程是依附在进程里面的,没有进程就没有线程。


多进程


进程使用:

#导入进程包
import multiprocessing
#创建子进程并指定执行的任务
sub_process = multiprocessing.Process (target=任务名)
#启动进程执行任务
sub_process.start()

Process进程类的说明:

group:指定进程组,目前只能使用None
target:执行的目标任务名
name:进程名字
args:以元组方式给执行任务传参
kwargs:以字典方式给执行任务传参

Process创建的实例对象的常用方法:

start():启动子进程实例(创建子进程)
join():等待子进程执行结束
terminate():不管任务是否完成,立即终止子进程

Process创建的实例对象的常用属性:

name:当前进程的别名,默认为Process-N,N为从1开始递增的整数

代码:

import multiprocessing
import time

# 跳舞任务
def dance():
    for i in range(5):
        print("跳舞中...")
        time.sleep(0.2)
# 唱歌任务
def sing():
    for i in range(5):
        print("唱歌中...")
        time.sleep(0.2)

if __name__ == '__main__':
    # 创建跳舞的子进程
    # group: 表示进程组,目前只能使用None
    # target: 表示执行的目标任务名(函数名、方法名)
    # name: 进程名称, 默认是Process-1, .....
    dance_process = multiprocessing.Process(target=dance, name="myprocess1")
    sing_process = multiprocessing.Process(target=sing)
    # 启动子进程执行对应的任务
    dance_process.start()
    sing_process.start()

进程编号


获取进程编号的目的是验证主进程和子进程的关系,可以得知子进程是由那个主进程创建出来的。

获取进程编号的两种操作:获取当前进程编号和获取当前父进程编号。


获取当前进程编号

os.getpid() 

示例代码:

import multiprocessing
import time
import os

# 跳舞任务
def dance():
    # 获取当前进程的编号
    print("dance:", os.getpid())
    # 获取当前进程
    print("dance:", multiprocessing.current_process())
    for i in range(5):
        print("跳舞中...")
        time.sleep(0.2)
        # 扩展:根据进程编号杀死指定进程
        os.kill(os.getpid(), 9)

# 唱歌任务
def sing():
    # 获取当前进程的编号
    print("sing:", os.getpid())
    # 获取当前进程
    print("sing:", multiprocessing.current_process())
    for i in range(5):
        print("唱歌中...")
        time.sleep(0.2)

if __name__ == '__main__':
    # 获取当前进程的编号
    print("main:", os.getpid())
    # 获取当前进程
    print("main:", multiprocessing.current_process())
    # 创建跳舞的子进程
    # group: 表示进程组,目前只能使用None
    # target: 表示执行的目标任务名(函数名、方法名)
    # name: 进程名称, 默认是Process-1, .....
    dance_process = multiprocessing.Process(target=dance, name="myprocess1")
    sing_process = multiprocessing.Process(target=sing)
    # 启动子进程执行对应的任务
    dance_process.start()
    sing_process.start()

运行结果:

main: 10796
main: <_MainProcess name='MainProcess' parent=None started>
dance: 2388
dance: <Process name='myprocess1' parent=10796 started>
跳舞中...
sing: 2220
sing: <Process name='Process-2' parent=10796 started>
唱歌中...
唱歌中...
唱歌中...
唱歌中...
唱歌中...

获取当前父进程编号

os.getppid()

示例代码:

import multiprocessing
import time
import os

# 跳舞任务
def dance():
    # 获取当前进程的编号
    print("dance:", os.getpid())
    # 获取当前进程
    print("dance:", multiprocessing.current_process())
    # 获取父进程的编号
    print("dance的父进程编号:", os.getppid())
    for i in range(5):
        print("跳舞中...")
        time.sleep(0.2)
        # 扩展:根据进程编号杀死指定进程
        os.kill(os.getpid(), 9)

# 唱歌任务
def sing():
    # 获取当前进程的编号
    print("sing:", os.getpid())
    # 获取当前进程
    print("sing:", multiprocessing.current_process())
    # 获取父进程的编号
    print("sing的父进程编号:", os.getppid())
    for i in range(5):
        print("唱歌中...")
        time.sleep(0.2)

if __name__ == '__main__':
    # 获取当前进程的编号
    print("main:", os.getpid())
    # 获取当前进程
    print("main:", multiprocessing.current_process())
    # 创建跳舞的子进程
    # group: 表示进程组,目前只能使用None
    # target: 表示执行的目标任务名(函数名、方法名)
    # name: 进程名称, 默认是Process-1, .....
    dance_process = multiprocessing.Process(target=dance, name="myprocess1")
    sing_process = multiprocessing.Process(target=sing)
    # 启动子进程执行对应的任务
    dance_process.start()
    sing_process.start()

运行结果:

main: 4832
main: <_MainProcess name='MainProcess' parent=None started>
dance: 14372
dance: <Process name='myprocess1' parent=4832 started>
sing: 8948
sing: <Process name='Process-2' parent=4832 started>
dance的父进程编号: 4832
跳舞中...
sing的父进程编号: 4832
唱歌中...
唱歌中...
唱歌中...
唱歌中...
唱歌中...

进程执行带有参数的任务


Process类执行任务并给任务传参数有两种方式:args 表示以元组的方式给执行任务传参;kwargs 表示以字典方式给执行任务传参。


args参数的使用,示例代码:

import multiprocessing
import time

# 带有参数的任务
def task(count):
    for i in range(count):
        print("任务执行中..")
        time.sleep(0.2)
    else:
        print("任务执行完成")

if __name__ == '__main__':
    # 创建子进程
    # args: 以元组的方式给任务传入参数
    sub_process = multiprocessing.Process(target=task, args=(5,))
    sub_process.start()

元组方式传参一定要和参数的顺序保持一致。


kwargs参数的使用,示例代码:

import multiprocessing
import time

# 带有参数的任务
def task(count):
    for i in range(count):
        print("任务执行中..")
        time.sleep(0.2)
    else:
        print("任务执行完成")

if __name__ == '__main__':
    # 创建子进程
    # kwargs: 表示以字典方式传入参数
    sub_process = multiprocessing.Process(target=task, kwargs={"count": 3})
    sub_process.start()

字典方式传参字典中的key一定要和参数名保持一致。


进程的注意点


进程之间不共享全局变量:子线程和主线程之间,以及子线程和子线程之间都不共享。创建子进程会对主进程资源进行拷贝,也就是说子进程是主进程的一个副本,好比是一对双胞胎,之所以进程之间不共享全局变量,是因为操作的不是同一个进程里面的全局变量,只不过不同进程里面的全局变量名字相同而已。

主进程会等待所有的子进程执行结束再结束。

守护主进程就是主进程退出子进程销毁不再执行。

子进程对象.daemon = True

子进程销毁:子进程执行结束。

# 让子进程销毁
子进程对象.terminate()

保证主进程正常退出的示例代码:

import multiprocessing
import time

# 定义进程所需要执行的任务
def task():
    for i in range(10):
        print("任务执行中...")
        time.sleep(0.2)

if __name__ == '__main__':
    # 创建子进程
    sub_process = multiprocessing.Process(target=task)
    # 设置守护主进程,主进程退出子进程直接销毁,子进程的生命周期依赖与主进程
    # sub_process.daemon = True
    sub_process.start()
    time.sleep(0.5)
    print("over")
    # 让子进程销毁
    sub_process.terminate()
    exit()
    # 总结: 主进程会等待所有的子进程执行完成以后程序再退出
    # 如果想要主进程退出子进程销毁,可以设置守护主进程或者在主进程退出之前让子进程销毁

多线程的使用


线程是cpu调度的基本单位,每个进程至少都有一个线程,而这个线程就是我们通常说的主线程。

多线程的使用

#导入线程模块
import threading
#创建子线程并指定执行的任务
sub_thread = threading.Thread(target=任务名)
#启动线程执行任务
sub_thread.start()

线程类Thread参数说明

group: 线程组,目前只能使用None
target: 执行的目标任务名
args: 以元组的方式给执行任务传参
kwargs: 以字典方式给执行任务传参
name: 线程名,一般不用设置

代码

import threading
import time

# 唱歌任务
def sing():
    # 扩展:获取当前线程
    # print("sing当前执行的线程为:", threading.current_thread())
    for i in range(3):
        print("正在唱歌...%d" % i)
        time.sleep(1)

# 跳舞任务
def dance():
    # 扩展:获取当前线程
    # print("dance当前执行的线程为:", threading.current_thread())
    for i in range(3):
        print("正在跳舞...%d" % i)
        time.sleep(1)

if __name__ == '__main__':
    # 扩展: 获取当前线程
    # print("当前执行的线程为:", threading.current_thread())
    # 创建唱歌的线程
    # target: 线程执行的函数名
    sing_thread = threading.Thread(target=sing)
    # 创建跳舞的线程
    dance_thread = threading.Thread(target=dance)
    # 开启线程
    sing_thread.start()
    dance_thread.start()

线程执行带有参数的任务


Thread类执行任务并给任务传参数有两种方式:args表示以元组的方式给执行任务传参;kwargs表示以字典方式给执行任务传参。

元组方式传参(args),元组方式传参一定要和参数的顺序保持一致:

import threading
import time

# 带有参数的任务
def task(count):
    for i in range(count):
        print("任务执行中..")
        time.sleep(0.2)
    else:
        print("任务执行完成")

if __name__ == '__main__':
    # 创建子线程
    # args: 以元组的方式给任务传入参数
    sub_thread = threading.Thread(target=task, args=(5,))
    sub_thread.start()

字典方式传参(kwargs),字典方式传参字典中的key一定要和参数名保持一致:

import threading
import time

# 带有参数的任务
def task(count):
    for i in range(count):
        print("任务执行中..")
        time.sleep(0.2)
    else:
        print("任务执行完成")

if __name__ == '__main__':
    # 创建子线程
    # kwargs: 表示以字典方式传入参数
    sub_thread = threading.Thread(target=task, kwargs={"count": 3})
    sub_thread.start()

线程的注意点


线程之间执行是无序的,它是由cpu调度决定的,cpu调度哪个线程,哪个线程就先执行,没有调度的线程不能执行。

进程之间执行也是无序的,它是由操作系统调度决定的,操作系统调度哪个进程,哪个进程就先执行,没有调度的进程不能执行。

主线程会等待所有的子线程执行结束再结束。

守护主线程:主线程退出子线程销毁不再执行。

#设置守护主线程有两种方式:
threading.Thread(target=show_info, daemon=True)
线程对象.setDaemon(True)

线程之间共享全局变量,多线程同时对全局变量操作数据会发生错误。

全局变量数据错误的解决办法:线程同步: 保证同一时刻只能有一个线程去操作全局变量。

线程同步的方式:线程等待(join);互斥锁。

线程等待的示例代码:

# 启动线程
first_thread.start()
# 主线程等待第一个线程执行完成以后代码再继续执行,让其执行第二个线程
# 线程同步: 一个任务执行完成以后另外一个任务才能执行,同一个时刻只有一个任务在执行
first_thread.join()
# 启动线程
second_thread.start()

互斥锁


互斥锁: 对共享数据进行锁定,保证同一时刻只能有一个线程去操作。互斥锁是多个线程一起去抢,抢到锁的线程先执行,没有抢到锁的线程需要等待,等互斥锁使用完释放后,其它等待的线程再去抢这个锁。

互斥锁使用步骤:

# 创建锁
mutex = threading.Lock()
# 上锁
mutex.acquire()
...这里编写代码能保证同一时刻只能有一个线程去操作, 对共享数据进行锁定...
# 释放锁
mutex.release()

acquire和release方法之间的代码同一时刻只能有一个线程去操作。如果在调用acquire方法的时候,其他线程已经使用了这个互斥锁,那么此时acquire方法会堵塞,直到这个互斥锁释放后才能再次上锁。

示例代码:

import threading

# 定义全局变量
g_num = 0
# 创建全局互斥锁
lock = threading.Lock()

# 循环一次给全局变量加1
def sum_num1():
    # 上锁
    lock.acquire()
    for i in range(1000000):
        global g_num
        g_num += 1
    print("sum1:", g_num)
    # 释放锁
    lock.release()

# 循环一次给全局变量加1
def sum_num2():
    # 上锁
    lock.acquire()
    for i in range(1000000):
        global g_num
        g_num += 1
    print("sum2:", g_num)
    # 释放锁
    lock.release()

if __name__ == '__main__':
    # 创建两个线程
    first_thread = threading.Thread(target=sum_num1)
    second_thread = threading.Thread(target=sum_num2)
    # 启动线程
    first_thread.start()
    second_thread.start()
    # 提示:加上互斥锁,那个线程抢到这个锁我们决定不了,那线程抢到锁那个线程先执行,没有抢到的线程需要等待
    # 加上互斥锁多任务瞬间变成单任务,性能会下降,也就是说同一时刻只能有一个线程去执行

互斥锁的作用就是保证同一时刻只能有一个线程去操作共享数据,保证共享数据不会出现错误问题。使用互斥锁会影响代码的执行效率,多任务改成了单任务执行。互斥锁如果没有使用好容易出现死锁的情况。


死锁


死锁: 一直等待对方释放锁的情景就是死锁。死锁的结果会造成应用程序的停止响应,不能再处理其它任务了。

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(30):
        sub_thread = threading.Thread(target=get_value, args=(i,))
        sub_thread.start()

进程和线程的对比


线程是依附在进程里面的,没有进程就没有线程。一个进程默认提供一条线程,进程可以创建多个线程。

多进程要比多线程消耗的资源多,但是多进程开发比单进程多线程开发稳定性要强,某个进程挂掉不会影响其它进程。

多进程可以使用cpu的多核运行,多线程可以共享全局变量。

线程不能单独执行必须依附在进程里面。

进程之间不共享全局变量。

线程之间共享全局变量,但是要注意资源竞争的问题,解决办法: 互斥锁或者线程同步。


网络编程

TCP客户端程序开发


TCP客户端程序开发步骤:创建客户端套接字对象、和服务端套接字建立连接、发送数据、接收数据、关闭客户端套接字。

socket类:

#导入socket模块 
import socket

#创建客户端socket对象 
socket.socket(AddressFamily, Type)

参数说明:AddressFamily表示IP地址类型, 分为TPv4和IPv6。Type 表示传输协议类型。

方法说明:

connect((host, port)) 表示和服务端套接字建立连接, host是服务器ip地址,port是应用程序的端口号
send(data) 表示发送数据,data是二进制数据
recv(buffersize) 表示接收数据, buffersize是每次接收数据的长度

示例代码:

import socket

if __name__ == '__main__':
    # 创建tcp客户端套接字
    # 1. AF_INET:表示ipv4
    # 2. SOCK_STREAM: tcp传输协议
    tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 和服务端应用程序建立连接
    tcp_client_socket.connect(("192.168.52.1", 8080))
    # 代码执行到此,说明连接建立成功
    # 准备发送的数据
    send_data = "你好服务端,我是客户端!".encode("gbk")
    # 发送数据
    tcp_client_socket.send(send_data)
    # 接收数据, 这次接收的数据最大字节数是1024
    recv_data = tcp_client_socket.recv(1024)
    # 返回的直接是服务端程序发送的二进制数据
    print(recv_data)
    # 对数据进行解码
    recv_content = recv_data.decode("gbk")
    print("接收服务端的数据为:", recv_content)
    # 关闭套接字
    tcp_client_socket.close()
    #str.encode(编码格式) 表示把字符串编码成为二进制
    #data.decode(编码格式) 表示把二进制解码成为字符串

TCP服务端程序开发


TCP服务端程序开发步骤回顾:创建服务端端套接字对象、绑定端口号、设置监听、等待接受客户端的连接请求、接收数据、发送数据、关闭套接字。

socket类:

#导入 socket 模块
import socket
#创建服务端 socket 对象
socket.socket(AddressFamily, Type)

参数说明:AddressFamily表示IP地址类型, 分为TPv4和IPv6。Type表示传输协议类型。

方法说明:

bind((host, port)) 表示绑定端口号, host 是 ip 地址,port 是端口号,ip 地址一般不指定,表示本机的任何一个ip地址都可以。
listen (backlog) 表示设置监听,backlog参数表示最大等待建立连接的个数。
accept() 表示等待接受客户端的连接请求
send(data) 表示发送数据,data 是二进制数据
recv(buffersize) 表示接收数据, buffersize 是每次接收数据的长度

示例代码:

import socket

if __name__ == '__main__':
    # 创建tcp服务端套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 设置端口号复用,让程序退出端口号立即释放
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
    # 给程序绑定端口号
    tcp_server_socket.bind(("", 8989))
    # 设置监听
    # 128:最大等待建立连接的个数, 提示: 目前是单任务的服务端,同一时刻只能服务与一个客户端,后续使用多任务能够让服务端同时服务与多个客户端,
    # 不需要让客户端进行等待建立连接
    # listen后的这个套接字只负责接收客户端连接请求,不能收发消息,收发消息使用返回的这个新套接字来完成
    tcp_server_socket.listen(128)
    # 等待客户端建立连接的请求, 只有客户端和服务端建立连接成功代码才会解阻塞,代码才能继续往下执行
    # 1. 专门和客户端通信的套接字: service_client_socket
    # 2. 客户端的ip地址和端口号: ip_port
    service_client_socket, ip_port = tcp_server_socket.accept()
    # 代码执行到此说明连接建立成功
    print("客户端的ip地址和端口号:", ip_port)
    # 接收客户端发送的数据, 这次接收数据的最大字节数是1024
    recv_data = service_client_socket.recv(1024)
    # 获取数据的长度
    recv_data_length = len(recv_data)
    print("接收数据的长度为:", recv_data_length)
    # 对二进制数据进行解码
    recv_content = recv_data.decode("gbk")
    print("接收客户端的数据为:", recv_content)
    # 准备发送的数据
    send_data = "ok, 问题正在处理中...".encode("gbk")
    # 发送数据给客户端
    service_client_socket.send(send_data)
    # 关闭服务与客户端的套接字, 终止和客户端通信的服务
    service_client_socket.close()
    # 关闭服务端的套接字, 终止和客户端提供建立连接请求的服务
    tcp_server_socket.close()

说明:当客户端和服务端建立连接后,服务端程序退出后端口号不会立即释放,需要等待大概1-2分钟。

解决办法有两种:更换服务端端口号;设置端口号复用(推荐使用),也就是说让服务端程序退出后端口号立即释放。

设置端口号复用的代码如下:

# 参数1: 表示当前套接字
# 参数2: 设置端口号复用选项
# 参数3: 设置端口号复用选项对应的值
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)

TCP网络应用程序的注意点


当 TCP 客户端程序想要和 TCP 服务端程序进行通信的时候必须要先建立连接。

TCP 客户端程序一般不需要绑定端口号,因为客户端是主动发起建立连接的。

TCP 服务端程序必须绑定端口号,否则客户端找不到这个 TCP 服务端程序。

listen 后的套接字是被动套接字,只负责接收新的客户端的连接请求,不能收发消息。

当 TCP 客户端程序和 TCP 服务端程序连接成功后, TCP 服务器端程序会产生一个新的套接字,收发客户端消息使用该套接字。

关闭 accept 返回的套接字意味着和这个客户端已经通信完毕。

关闭 listen 后的套接字意味着服务端的套接字关闭了,会导致新的客户端不能连接服务端,但是之前已经接成功的客户端还能正常通信。

当客户端的套接字调用 close 后,服务器端的 recv 会解阻塞,返回的数据长度为0,服务端可以通过返回数据的长度来判断客户端是否已经下线,反之服务端关闭套接字,客户端的 recv 也会解阻塞,返回的数据长度也为0。


多任务版TCP服务端程序开发


完成多任务,可以使用线程,比进程更加节省内存资源。

实现步骤:

  1. 编写一个TCP服务端程序,循环等待接受客户端的连接请求。
  2. 当客户端和服务端建立连接成功,创建子线程,使用子线程专门处理客户端的请求,防止主线程阻塞。
  3. 把创建的子线程设置成为守护主线程,防止主线程无法退出。

示例代码:

import socket
import threading

# 处理客户端的请求操作
def handle_client_request(service_client_socket, ip_port):
    # 循环接收客户端发送的数据
    while True:
        # 接收客户端发送的数据
        recv_data = service_client_socket.recv(1024)
        # 容器类型判断是否有数据可以直接使用if语句进行判断,如果容器类型里面有数据表示条件成立,否则条件失败
        # 容器类型: 列表、字典、元组、字符串、set、range、二进制数据
        if recv_data:
            print(recv_data.decode("gbk"), ip_port)
            # 回复
            service_client_socket.send("ok,问题正在处理中...".encode("gbk"))
        else:
            print("客户端下线了:", ip_port)
            break
    # 终止和客户端进行通信
    service_client_socket.close()

if __name__ == '__main__':
    # 创建tcp服务端套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 设置端口号复用,让程序退出端口号立即释放
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
    # 绑定端口号
    tcp_server_socket.bind(("", 9090))
    # 设置监听, listen后的套接字是被动套接字,只负责接收客户端的连接请求
    tcp_server_socket.listen(128)
    # 循环等待接收客户端的连接请求
    while True:
        # 等待接收客户端的连接请求
        service_client_socket, ip_port = tcp_server_socket.accept()
        print("客户端连接成功:", ip_port)
        # 当客户端和服务端建立连接成功以后,需要创建一个子线程,不同子线程负责接收不同客户端的消息
        sub_thread = threading.Thread(target=handle_client_request, args=(service_client_socket, ip_port))
        # 设置守护主线程
        sub_thread.setDaemon(True)
        # 启动子线程
        sub_thread.start()
    # tcp服务端套接字可以不需要关闭,因为服务端程序需要一直运行
    # tcp_server_socket.close()

socket之send和recv原理剖析


当创建一个TCP socket对象的时候会有一个发送缓冲区和一个接收缓冲区,这个发送和接收缓冲区指的就是内存中的一片空间。

send原理剖析:send是不是直接把数据发给服务端?不是,要想发数据,必须得通过网卡发送数据,应用程序是无法直接通过网卡发送数据的,它需要调用操作系统接口,也就是说,应用程序把发送的数据先写入到发送缓冲区(内存中的一片空间),再由操作系统控制网卡把发送缓冲区的数据发送给服务端网卡。

recv原理剖析:recv是不是直接从客户端接收数据?不是,应用软件是无法直接通过网卡接收数据的,它需要调用操作系统接口,由操作系统通过网卡接收数据,把接收的数据写入到接收缓冲区(内存中的一片空间),应用程序再从接收缓存区获取客户端发送的数据。

不管是recv还是send都不是直接接收到对方的数据和发送数据到对方,发送数据会写入到发送缓冲区,接收数据是从接收缓冲区来读取,发送数据和接收数据最终是由操作系统控制网卡来完成。


静态Web服务器

启动python自带静态Web服务


静态Web服务器:可以为发出请求的浏览器提供静态文档的程序。

搭建Python自带的静态Web服务器

python -m http.server 端口号

-m表示运行包里面的模块,执行这个命令的时候,需要进入你自己指定静态文件的目录,然后通过浏览器就能访问对应的html文件了,这样一个静态的web服务器就搭建好了。


开发自己的静态Web服务器


实现步骤:编写一个TCP服务端程序、获取浏览器发送的http请求报文数据、读取固定页面数据,把页面数据组装成HTTP响应报文数据发送给浏览器。HTTP响应报文数据发送完成以后,关闭服务于客户端的套接字。

示例代码:

import socket

if __name__ == '__main__':
    # 创建tcp服务端套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 设置端口号复用, 程序退出端口立即释放
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
    # 绑定端口号
    tcp_server_socket.bind(("", 9000))
    # 设置监听
    tcp_server_socket.listen(128)
    while True:
        # 等待接受客户端的连接请求
        new_socket, ip_port = tcp_server_socket.accept()
        # 代码执行到此,说明连接建立成功
        recv_client_data = new_socket.recv(4096)
        # 对二进制数据进行解码
        recv_client_content = recv_client_data.decode("utf-8")
        print(recv_client_content)
        with open("static/index.html", "rb") as file:
            # 读取文件数据
            file_data = file.read()
        # 响应行
        response_line = "HTTP/1.1 200 OK\r\n"
        # 响应头
        response_header = "Server: PWS1.0\r\n"
        # 响应体
        response_body = file_data
        # 拼接响应报文
        response_data = (response_line + response_header + "\r\n").encode("utf-8") + response_body
        # 发送数据
        new_socket.send(response_data)
        # 关闭服务与客户端的套接字
        new_socket.close()

开发自己的静态Web服务器返回指定页面数据


返回指定页面数据的实现步骤:获取用户请求资源的路径;根据请求资源的路径,读取指定文件的数据;组装指定文件数据的响应报文,发送给浏览器;判断请求的文件在服务端不存在,组装404状态的响应报文,发送给浏览器。

import socket

def main():
    # 创建tcp服务端套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 设置端口号复用, 程序退出端口立即释放
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
    # 绑定端口号
    tcp_server_socket.bind(("", 9000))
    # 设置监听
    tcp_server_socket.listen(128)
    while True:
        # 等待接受客户端的连接请求
        new_socket, ip_port = tcp_server_socket.accept()
        # 代码执行到此,说明连接建立成功
        recv_client_data = new_socket.recv(4096)
        if len(recv_client_data) == 0:
            print("关闭浏览器了")
            new_socket.close()
            return
        # 对二进制数据进行解码
        recv_client_content = recv_client_data.decode("utf-8")
        print(recv_client_content)
        # 根据指定字符串进行分割, 最大分割次数指定2
        request_list = recv_client_content.split(" ", maxsplit=2)
        # 获取请求资源路径
        request_path = request_list[1]
        print(request_path)
        # 判断请求的是否是根目录,如果条件成立,指定首页数据返回
        if request_path == "/":
            request_path = "/index.html"
        try:
            # 动态打开指定文件
            with open("static" + request_path, "rb") as file:
                # 读取文件数据
                file_data = file.read()
        except Exception as e:
            # 请求资源不存在,返回404数据
            # 响应行
            response_line = "HTTP/1.1 404 Not Found\r\n"
            # 响应头
            response_header = "Server: PWS1.0\r\n"
            with open("static/error.html", "rb") as file:
                file_data = file.read()
            # 响应体
            response_body = file_data
            # 拼接响应报文
            response_data = (response_line + response_header + "\r\n").encode("utf-8") + response_body
            # 发送数据
            new_socket.send(response_data)
        else:
            # 响应行
            response_line = "HTTP/1.1 200 OK\r\n"
            # 响应头
            response_header = "Server: PWS1.0\r\n"
            # 响应体
            response_body = file_data
            # 拼接响应报文
            response_data = (response_line + response_header + "\r\n").encode("utf-8") + response_body
            # 发送数据
            new_socket.send(response_data)
        finally:
            # 关闭服务与客户端的套接字
            new_socket.close()

if __name__ == '__main__':
    main()

静态Web服务器-多任务版


多任务版web服务器程序的实现步骤:当客户端和服务端建立连接成功,创建子线程,使用子线程专门处理客户端的请求,防止主线程阻塞。把创建的子线程设置成为守护主线程,防止主线程无法退出。

import socket
import threading

# 处理客户端的请求
def handle_client_request(new_socket):
    # 代码执行到此,说明连接建立成功
    recv_client_data = new_socket.recv(4096)
    if len(recv_client_data) == 0:
        print("关闭浏览器了")
        new_socket.close()
        return
    # 对二进制数据进行解码
    recv_client_content = recv_client_data.decode("utf-8")
    print(recv_client_content)
    # 根据指定字符串进行分割, 最大分割次数指定2
    request_list = recv_client_content.split(" ", maxsplit=2)
    # 获取请求资源路径
    request_path = request_list[1]
    print(request_path)
    # 判断请求的是否是根目录,如果条件成立,指定首页数据返回
    if request_path == "/":
        request_path = "/index.html"
    try:
        # 动态打开指定文件
        with open("static" + request_path, "rb") as file:
            # 读取文件数据
            file_data = file.read()
    except Exception as e:
        # 请求资源不存在,返回404数据
        # 响应行
        response_line = "HTTP/1.1 404 Not Found\r\n"
        # 响应头
        response_header = "Server: PWS1.0\r\n"
        with open("static/error.html", "rb") as file:
            file_data = file.read()
        # 响应体
        response_body = file_data
        # 拼接响应报文
        response_data = (response_line + response_header + "\r\n").encode("utf-8") + response_body
        # 发送数据
        new_socket.send(response_data)
    else:
        # 响应行
        response_line = "HTTP/1.1 200 OK\r\n"
        # 响应头
        response_header = "Server: PWS1.0\r\n"
        # 响应体
        response_body = file_data
        # 拼接响应报文
        response_data = (response_line + response_header + "\r\n").encode("utf-8") + response_body
        # 发送数据
        new_socket.send(response_data)
    finally:
        # 关闭服务与客户端的套接字
        new_socket.close()

# 程序入口函数
def main():
    # 创建tcp服务端套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 设置端口号复用, 程序退出端口立即释放
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
    # 绑定端口号
    tcp_server_socket.bind(("", 9000))
    # 设置监听
    tcp_server_socket.listen(128)
    while True:
        # 等待接受客户端的连接请求
        new_socket, ip_port = tcp_server_socket.accept()
        print(ip_port)
        # 当客户端和服务器建立连接程,创建子线程
        sub_thread = threading.Thread(target=handle_client_request, args=(new_socket,))
        # 设置守护主线程
        sub_thread.setDaemon(True)
        # 启动子线程执行对应的任务
        sub_thread.start()

if __name__ == '__main__':
    main()

静态Web服务器-面向对象开发


实现步骤:把提供服务的Web服务器抽象成一个类(HTTPWebServer);提供Web服务器的初始化方法,在初始化方法里面创建socket对象;提供一个开启Web服务器的方法,让Web服务器处理客户端请求操作。

import socket
import threading

# 定义web服务器类
class HttpWebServer(object):
    def __init__(self):
        # 创建tcp服务端套接字
        tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 设置端口号复用, 程序退出端口立即释放
        tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
        # 绑定端口号
        tcp_server_socket.bind(("", 9000))
        # 设置监听
        tcp_server_socket.listen(128)
        # 保存创建成功的服务器套接字
        self.tcp_server_socket = tcp_server_socket

    # 处理客户端的请求
    @staticmethod
    def handle_client_request(new_socket):
        # 代码执行到此,说明连接建立成功
        recv_client_data = new_socket.recv(4096)
        if len(recv_client_data) == 0:
            print("关闭浏览器了")
            new_socket.close()
            return
        # 对二进制数据进行解码
        recv_client_content = recv_client_data.decode("utf-8")
        print(recv_client_content)
        # 根据指定字符串进行分割, 最大分割次数指定2
        request_list = recv_client_content.split(" ", maxsplit=2)
        # 获取请求资源路径
        request_path = request_list[1]
        print(request_path)
        # 判断请求的是否是根目录,如果条件成立,指定首页数据返回
        if request_path == "/":
            request_path = "/index.html"
        try:
            # 动态打开指定文件
            with open("static" + request_path, "rb") as file:
                # 读取文件数据
                file_data = file.read()
        except Exception as e:
            # 请求资源不存在,返回404数据
            # 响应行
            response_line = "HTTP/1.1 404 Not Found\r\n"
            # 响应头
            response_header = "Server: PWS1.0\r\n"
            with open("static/error.html", "rb") as file:
                file_data = file.read()
            # 响应体
            response_body = file_data
            # 拼接响应报文
            response_data = (response_line + response_header + "\r\n").encode("utf-8") + response_body
            # 发送数据
            new_socket.send(response_data)
        else:
            # 响应行
            response_line = "HTTP/1.1 200 OK\r\n"
            # 响应头
            response_header = "Server: PWS1.0\r\n"
            # 响应体
            response_body = file_data
            # 拼接响应报文
            response_data = (response_line + response_header + "\r\n").encode("utf-8") + response_body
            # 发送数据
            new_socket.send(response_data)
        finally:
            # 关闭服务与客户端的套接字
            new_socket.close()

    # 启动web服务器进行工作
    def start(self):
        while True:
            # 等待接受客户端的连接请求
            new_socket, ip_port = self.tcp_server_socket.accept()
            # 当客户端和服务器建立连接程,创建子线程
            sub_thread = threading.Thread(target=self.handle_client_request, args=(new_socket,))
            # 设置守护主线程
            sub_thread.setDaemon(True)
            # 启动子线程执行对应的任务
            sub_thread.start()

# 程序入口函数
def main():
    # 创建web服务器对象
    web_server = HttpWebServer()
    # 启动web服务器进行工作
    web_server.start()

if __name__ == '__main__':
    main()

命令行启动动态绑定端口号


实现步骤:获取执行python程序的终端命令行参数;判断参数的类型,设置端口号必须是整型;给Web服务器类的初始化方法添加一个端口号参数,用于绑定端口号。

import socket
import threading
import sys

# 定义web服务器类
class HttpWebServer(object):
    def __init__(self, port):
        # 创建tcp服务端套接字
        tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 设置端口号复用, 程序退出端口立即释放
        tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
        # 绑定端口号
        tcp_server_socket.bind(("", port))
        # 设置监听
        tcp_server_socket.listen(128)
        # 保存创建成功的服务器套接字
        self.tcp_server_socket = tcp_server_socket

    # 处理客户端的请求
    @staticmethod
    def handle_client_request(new_socket):
        # 代码执行到此,说明连接建立成功
        recv_client_data = new_socket.recv(4096)
        if len(recv_client_data) == 0:
            print("关闭浏览器了")
            new_socket.close()
            return
        # 对二进制数据进行解码
        recv_client_content = recv_client_data.decode("utf-8")
        print(recv_client_content)
        # 根据指定字符串进行分割, 最大分割次数指定2
        request_list = recv_client_content.split(" ", maxsplit=2)
        # 获取请求资源路径
        request_path = request_list[1]
        print(request_path)
        # 判断请求的是否是根目录,如果条件成立,指定首页数据返回
        if request_path == "/":
            request_path = "/index.html"
        try:
            # 动态打开指定文件
            with open("static" + request_path, "rb") as file:
                # 读取文件数据
                file_data = file.read()
        except Exception as e:
            # 请求资源不存在,返回404数据
            # 响应行
            response_line = "HTTP/1.1 404 Not Found\r\n"
            # 响应头
            response_header = "Server: PWS1.0\r\n"
            with open("static/error.html", "rb") as file:
                file_data = file.read()
            # 响应体
            response_body = file_data
            # 拼接响应报文
            response_data = (response_line + response_header + "\r\n").encode("utf-8") + response_body
            # 发送数据
            new_socket.send(response_data)
        else:
            # 响应行
            response_line = "HTTP/1.1 200 OK\r\n"
            # 响应头
            response_header = "Server: PWS1.0\r\n"
            # 响应体
            response_body = file_data
            # 拼接响应报文
            response_data = (response_line + response_header + "\r\n").encode("utf-8") + response_body
            # 发送数据
            new_socket.send(response_data)
        finally:
            # 关闭服务与客户端的套接字
            new_socket.close()

    # 启动web服务器进行工作
    def start(self):
        while True:
            # 等待接受客户端的连接请求
            new_socket, ip_port = self.tcp_server_socket.accept()
            # 当客户端和服务器建立连接程,创建子线程
            sub_thread = threading.Thread(target=self.handle_client_request, args=(new_socket,))
            # 设置守护主线程
            sub_thread.setDaemon(True)
            # 启动子线程执行对应的任务
            sub_thread.start()

# 程序入口函数
def main():
    print(sys.argv)
    # 判断命令行参数是否等于2,
    if len(sys.argv) != 2:
        print("执行命令如下: python3 xxx.py 8000")
        return
    # 判断字符串是否都是数字组成
    if not sys.argv[1].isdigit():
        print("执行命令如下: python3 xxx.py 8000")
        return
    # 获取终端命令行参数
    port = int(sys.argv[1])
    # 创建web服务器对象
    web_server = HttpWebServer(port)
    # 启动web服务器进行工作
    web_server.start()

if __name__ == '__main__':
    main()

数据库

介绍


数据库就是存储和管理数据的仓库,数据按照一定的格式进行存储,用户可以对数据库中的数据进行增加、修改、删除、查询等操作。

数据库的分类:关系型数据库和非关系型数据库。

关系型数据库:是指采用了关系模型来组织数据的数据库,简单来说,关系模型指的就是二维表格模型。

关系型数据库中核心元素:数据行、数据列、数据表、数据库(数据表的集合)。

常用的关系型数据库:Oracle、Microsoft SQL Server、MySQL、SQLite。

非关系型数据库,又被称为NoSQL(Not Only SQL ),意为不仅仅是SQL,对NoSQL最普遍的定义是“非关联型的”,强调 Key-Value 的方式存储数据。

常用的非关系型数据库:MongoDB、Redis。

数据库管理系统(英语全拼:Relational Database Management System,简称RDBMS)是为管理关系型数据库而设计的软件系统,如果大家想要使用关系型数据库就需要安装数据库管理系统,其实就是一个应用软件。

SQL(Structured Query Language)是结构化查询语言,是一种用来操作RDBMS的数据库的语言。也就是说通过 SQL 可以操作 oracle,sql server,mysql,sqlite 等关系型的数据库。

SQL的作用是实现数据库客户端和数据库服务端之间的通信,SQL就是通信的桥梁。

SQL语言主要分为:

DQL:数据查询语言,用于对数据进行查询,如select
DML:数据操作语言,对数据进行增加、修改、删除,如insert、update、delete
TPL:事务处理语言,对事务进行处理,包括begin transaction、commit、rollback
DCL:数据控制语言,进行授权与权限回收,如grant、revoke
DDL:数据定义语言,进行数据库、表的管理等,如create、drop

对于程序员来讲,重点是数据的增、删、改、查,必须熟练编写DQL、DML,能够编写DDL完成数据库、表的操作,其它操作如TPL、DCL了解即可。SQL语言不区分大小写。


数据类型和约束


数据库中的数据保存在数据表中,在表中为了更加准确的存储数据,保证数据的正确有效,可以在创建表的时候,为表添加一些强制性的验证,比如:数据类型和约束。

数据类型是指在创建表的时候为表中字段指定数据类型,只有数据符合类型要求才能存储起来,使用数据类型的原则是:够用就行,尽量使用取值范围小的,而不用大的,这样可以更多的节省存储空间。

常用数据类型如下:

整数:int,bit
小数:decimal
字符串:varchar,char
日期时间: date, time, datetime
枚举类型(enum)
  1. decimal表示浮点数,如 decimal(5, 2) 表示共存5位数,小数占 2 位.
  2. char表示固定长度的字符串,如char(3),如果填充’ab’时会补一个空格为’ab ‘,3表示字符数
  3. varchar表示可变长度的字符串,如varchar(3),填充’ab’时就会存储’ab’,3表示字符数
  4. 对于图片、音频、视频等文件,不存储在数据库中,而是上传到某个服务器上,然后在表中存储这个文件的保存路径.
  5. 字符串 text 表示存储大文本,当字符大于 4000 时推荐使用, 比如技术博客

数据约束
约束是指数据在数据类型限定的基础上额外增加的要求.

常见的约束如下:

主键 primary key: 物理上存储的顺序. MySQL 建议所有表的主键字段都叫 id, 类型为 int unsigned.
非空 not null: 此字段不允许填写空值.
惟一 unique: 此字段的值不允许重复.
默认 default: 当不填写字段对应的值会使用默认值,如果填写时以填写为准.
外键 foreign key: 对关系字段进行约束, 当为关系字段填写值时, 会到关联的表中查询此值是否存在, 如果存在则填写成功, 如果不存在则填写失败并抛出异常.

整数类型:

类型 字节大小 有符号范围(Signed) 无符号范围(Unsigned)
TINYINT 1 -128 ~ 127 0 ~ 255
SMALLINT 2 -32768 ~ 32767 0 ~ 65535
MEDIUMINT 3 -8388608 ~ 8388607 0 ~ 16777215
INT/INTEGER 4 -2147483648 ~2147483647 0 ~ 4294967295
BIGINT 8 -9223372036854775808 ~ 9223372036854775807 0 ~ 18446744073709551615

字符串:

类型 说明 使用场景
CHAR 固定长度,小型数据 身份证号、手机号、电话、密码
VARCHAR 可变长度,小型数据 姓名、地址、品牌、型号
TEXT 可变长度,字符个数大于 4000 存储小型文章或者新闻
LONGTEXT 可变长度, 极大型文本数据 存储极大型文本数据

时间类型:

类型 字节大小 示例
DATE 4 ‘2020-01-01’
TIME 3 ‘12:29:59’
DATETIME 8 ‘2020-01-01 12:29:59’
YEAR 1 ‘2017’
TIMESTAMP 4 ‘1970-01-01 00:00:01’ UTC ~ ‘2038-01-01 00:00:01’ UTC


Navicat 是一个数据库图形化客户端软件, 可以以安全并且简单的方式对数据库进行操作。

新建数据库:字符集: 就是编码格式,选择utf8的国际通用编码格式, 支持中文。排序规则: utf8_general_ci 表示大小写不敏感,不区分大小写字母,a 和 A 在字符判断中会被当做一样的处理,区分大小写可以选择utf8_bin


命令行客户端MySQL的使用


登录数据库:

mysql -uroot -p

显示当前时间:

select now();

登出(退出)数据库:

quit 或 exit 或 ctrl + d

数据库操作

查看所有数据库:

show databases;

创建数据库:

create database 数据库名 charset=utf8;
例:
create database python charset=utf8;

使用数据库:

use 数据库名;

查看当前使用的数据库:

select database();

删除数据库-慎重:

drop database 数据库名;
例:
drop database python;

表结构操作

查看当前数据库中所有表:

show tables;

创建表:

create table students(
 id int unsigned primary key auto_increment not null,
 name varchar(20) not null,
 age tinyint unsigned default 0,
 height decimal(5,2),
 gender enum('男','女','人妖','保密')
);

说明:

create table 表名(
字段名称 数据类型  可选的约束条件,
column1 datatype contrai,
...
);

修改表-添加字段:

alter table 表名 add 列名 类型 约束;
例:
alter table students add birthday datetime;

修改表-修改字段类型:

alter table 表名 modify 列名 类型 约束;
例:
alter table students modify birthday date not null;

说明:

modify: 只能修改字段类型或者约束,不能修改字段名

修改表-修改字段名和字段类型:

alter table 表名 change 原名 新名 类型及约束;
例:
alter table students change birthday birth datetime not null;

说明:

change: 既能对字段重命名又能修改字段类型还能修改约束

修改表-删除字段:

alter table 表名 drop 列名;
例:
alter table students drop birthday;

查看创表SQL语句:

show create table 表名;
例:
show create table students;

查看创库SQL语句:

show create database 数据库名;
例:
show create database mytest;

删除表:

drop table 表名;
例:
drop table students;

表数据操作

查询数据:

1. 查询所有列
select * from 表名;
例:
select * from students;
2. 查询指定列
select 列1,列2,... from 表名;
例:
select id,name from students;

添加数据:

1. 全列插入:值的顺序与表结构字段的顺序完全一一对应
insert into 表名 values (...)
例:
insert into students values(0, 'xx', default, default, '男');
2. 部分列插入:值的顺序与给出的列顺序对应
insert into 表名 (列1,...) values(值1,...)
例:
insert into students(name, age) values('王二小', 15);
3. 全列多行插入
insert into 表名 values(...),(...)...;
例:
insert into students values(0, '张飞', 55, 1.75, '男'),(0, '关羽', 58, 1.85, '男');
4. 部分列多行插入
insert into 表名(列1,...) values(值1,...),(值1,...)...;
例:
insert into students(name, height) values('刘备', 1.75),('曹操', 1.6);

说明:

主键列是自动增长,但是在全列插入时需要占位,通常使用空值(0或者null或者default)。在全列插入时,如果字段列有默认值可以使用 default 来占位,插入后的数据就是之前设置的默认值

修改数据:

update 表名 set 列1=值1,列2=值2... where 条件
例:
update students set age = 18, gender = '女' where id = 6;

删除数据:

delete from 表名 where 条件
例:
delete from students where id=5;

问题:上面的操作称之为物理删除,一旦删除就不容易恢复,我们可以使用逻辑删除的方式来解决这个问题。

添加删除表示字段,0表示未删除 1表示删除
alter table students add isdelete bit default 0;
逻辑删除数据
update students set isdelete = 1 where id = 8;

说明:逻辑删除,本质就是修改操作。


as和distinct关键字


as关键字:在使用SQL语句显示结果的时候,往往在屏幕显示的字段名并不具备良好的可读性,此时可以使用 as 给字段起一个别名。

使用 as 给字段起别名:

select id as 序号, name as 名字, gender as 性别 from students;

可以通过 as 给表起别名

-- 如果是单表查询 可以省略表名
select id, name, gender from students;

-- 表名.字段名
select students.id,students.name,students.gender from students;

-- 可以通过 as 给表起别名 
select s.id,s.name,s.gender from students as s;

distinct关键字:distinct可以去除重复数据行。

select distinct 列1,... from 表名;

例: 查询班级中学生的性别
select name, gender from students;

-- 看到了很多重复数据 想要对其中重复数据行进行去重操作可以使用 distinct
select distinct name, gender from students;

where条件查询


使用where条件查询可以对表中的数据进行筛选,条件成立的记录会出现在结果集中。

格式如下:

select * from 表名 where 条件;
例:
select * from students where id = 1;

比较运算符查询(=、>、>=、<、<=、!= 或 <>):

例1:查询编号大于3的学生:
select * from students where id > 3;

例2:查询编号不大于4的学生:
select * from students where id <= 4;

例3:查询姓名不是“黄蓉”的学生:
select * from students where name != '黄蓉';

例4:查询没被删除的学生:
select * from students where is_delete=0;

逻辑运算符查询(and、or、not)

例1:查询编号大于3的女同学:
select * from students where id > 3 and gender=0;

例2:查询编号小于4或没被删除的学生:
select * from students where id < 4 or is_delete=0;

例3:查询年龄不在10岁到15岁之间的学生:
select * from students where not (age >= 10 and age <= 15);

说明:多个条件判断想要作为一个整体,可以结合()

模糊查询(like是模糊查询关键字、%表示任意多个任意字符、_表示一个任意字符):

例1:查询姓黄的学生:
select * from students where name like '黄%';

例2:查询姓黄并且“名”是一个字的学生:
select * from students where name like '黄_';

例3:查询姓黄或叫靖的学生:
select * from students where name like '黄%' or name like '%靖';

范围查询(between .. and .. 表示在一个连续的范围内查询、in 表示在一个非连续的范围内查询):

例1:查询编号为3至8的学生:
select * from students where id between 3 and 8;

例2:查询编号不是3至8的男生:
select * from students where (not id between 3 and 8) and gender='男';

空判断查询(判断为空使用: is null、判断非空使用: is not null):

例1:查询没有填写身高的学生:
select * from students where height is null;

排序


排序查询语法:

select * from 表名 order by 列1 asc|desc [,列2 asc|desc,...]

语法说明:先按照列1进行排序,如果列1的值相同时,则按照 列2 排序,以此类推。asc从小到大排列,即升序。desc从大到小排序,即降序。默认按照列值从小到大排列(即asc)。

例1:查询未删除男生信息,按学号降序:
select * from students where gender=1 and is_delete=0 order by id desc;

例2:显示所有的学生信息,先按照年龄从大-->小排序,当年龄相同时 按照身高从高-->矮排序:
select * from students  order by age desc,height desc;

分页查询


分页查询的语法

select * from 表名 limit start,count

说明:limit是分页查询关键字;start表示开始行索引,默认是0;count表示查询条数。

查询前3行男生信息:
select * from students where gender=1 limit 0,3;
简写:
select * from students where gender=1 limit 3;

已知每页显示m条数据,求第n页显示的数据

查询学生表,获取第n页数据的SQL语句:
select * from students limit (n-1)*m,m

聚合函数


聚合函数又叫组函数,通常是对表中的数据进行统计和计算,一般结合分组(group by)来使用,用于统计和计算分组数据。

常用的聚合函数:

count(col): 表示求指定列的总行数
max(col): 表示求指定列的最大值
min(col): 表示求指定列的最小值
sum(col): 表示求指定列的和
avg(col): 表示求指定列的平均值

求总行数

-- 返回非NULL数据的总行数.
select count(height) from students; 
-- 返回总行数,包含null值记录;
select count(*) from students;

求最大值

-- 查询女生的编号最大值
select max(id) from students where gender = 2;

求最小值

-- 查询未删除的学生最小编号
select min(id) from students where is_delete = 0;

求和

-- 查询男生的总身高
select sum(height) from students where gender = 1;
-- 平均身高
select sum(height) / count(*) from students where gender = 1;

求平均值

-- 求男生的平均身高, 聚合函数不统计null值,平均身高有误
select avg(height) from students where gender = 1;
-- 求男生的平均身高, 包含身高是null的
select avg(ifnull(height,0)) from students where gender = 1;

说明:ifnull函数: 表示判断指定字段的值是否为null,如果为空使用自己提供的值。

聚合函数的特点:聚合函数默认忽略字段为null的记录,要想列值为null的记录也参与计算,必须使用ifnull函数对null值做替换。


分组查询


分组查询介绍:将查询结果按照指定字段进行分组,字段中数据相等的分为一组。

语法格式如下:

GROUP BY 列名 [HAVING 条件表达式] [WITH ROLLUP]

说明:列名: 是指按照指定字段的值进行分组。HAVING 条件表达式: 用来过滤分组后的数据。WITH ROLLUP:在所有记录的最后加上一条记录,显示select查询时聚合函数的统计和计算结果。

group by可用于单个字段分组,也可用于多个字段分组:

-- 根据gender字段来分组
select gender from students group by gender;
-- 根据name和gender字段进行分组
select name, gender from students group by name, gender;

group_concat(字段名): 统计每个分组指定字段的信息集合,每个信息之间使用逗号进行分割:

-- 根据gender字段进行分组, 查询gender字段和分组的name字段信息
select gender,group_concat(name) from students group by gender;

group by + 聚合函数的使用:

-- 统计不同性别的人的平均年龄
select gender,avg(age) from students group by gender;
-- 统计不同性别的人的个数
select gender,count(*) from students group by gender;

group by + having的使用:

-- 根据gender字段进行分组,统计分组条数大于2的
select gender,count(*) from students group by gender having count(*)>2;

group by + with rollup的使用:

-- 根据gender字段进行分组,汇总总人数
select gender,count(*) from students group by gender with rollup;
-- 根据gender字段进行分组,汇总所有人的年龄
select gender,group_concat(age) from students group by gender with rollup;

内连接查询


连接查询可以实现多个表的查询,当查询的字段数据来自不同的表就可以使用连接查询来完成。

连接查询可以分为:内连接查询、左连接查询、右连接查询、自连接查询。

内连接查询:查询两个表中符合条件的共有记录

语法格式:

select 字段 from 表1 inner join 表2 on 表1.字段1 = 表2.字段2

说明:inner join 就是内连接查询关键字。on 就是连接查询条件

例1:使用内连接查询学生表与班级表:
select * from students as s inner join classes as c on s.cls_id = c.id;

左连接查询


左连接查询:以左表为主根据条件查询右表数据,如果根据条件查询右表数据不存在使用null值填充。

语法格式:

select 字段 from 表1 left join 表2 on 表1.字段1 = 表2.字段2

说明:left join 就是左连接查询关键字;on 就是连接查询条件;表1是左表;表2是右表。

例1:使用左连接查询学生表与班级表:
select * from students as s left join classes as c on s.cls_id = c.id;

右连接查询


以右表为主根据条件查询左表数据,如果根据条件查询左表数据不存在使用null值填充。

右连接查询语法格式:

select 字段 from 表1 right join 表2 on 表1.字段1 = 表2.字段2

说明:right join 就是右连接查询关键字;on 就是连接查询条件;表1是左表;表2是右表。

使用右连接查询学生表与班级表:
select * from students as s right join classes as c on s.cls_id = c.id;

自连接查询


左表和右表是同一个表,根据连接查询条件查询两个表中的数据。

自连接查询的用法:

select c.id, c.title, c.pid, p.title from areas as c inner join areas as p on c.pid = p.id where p.title = '山西省';

子查询


在一个 select 语句中,嵌入了另外一个 select 语句, 那么被嵌入的 select 语句称之为子查询语句,外部那个select语句则称为主查询.

主查询和子查询的关系:子查询是嵌入到主查询中;子查询是辅助主查询的,要么充当条件,要么充当数据源;子查询是可以独立存在的语句,是一条完整的 select 语句。

例1. 查询大于平均年龄的学生:
select * from students where age > (select avg(age) from students);

例2. 查询学生在班的所有班级名字:
select name from classes where id in (select cls_id from students where cls_id is not null);

例3. 查找年龄最大,身高最高的学生:
select * from students where (age, height) =  (select max(age), max(height) from students);

数据库设计之三范式


第一范式(1NF): 强调的是列的原子性,即列不能够再分成其他几列。

第二范式(2NF): 满足 1NF,另外包含两部分内容,一是表必须有一个主键;二是非主键字段必须完全依赖于主键,而不能只依赖于主键的一部分。

第三范式(3NF): 满足 2NF,另外非主键列必须直接依赖于主键,不能存在传递依赖。即不能存在:非主键列 A 依赖于非主键列 B,非主键列 B 依赖于主键的情况。

E-R模型即实体-关系模型,E-R模型就是描述数据库存储数据的结构模型。E-R模型由实体、属性、实体之间的关系构成,主要用来描述数据库中表结构。


外键SQL语句的编写


外键约束:对外键字段的值进行更新和插入时会和引用表中字段的数据进行验证,数据如果不合法则更新和插入会失败,保证数据的有效性。

添加外键约束:

alter table 从表 add foreign key(外键字段) references 主表(主键字段);

删除外键约束:

alter table 表名 drop foreign key 外键名;

对于已经存在的字段添加外键约束:

-- 为cls_id字段添加外键约束
alter table students add foreign key(cls_id) references classes(id);

在创建数据表时设置外键约束:

-- 创建学校表
create table school(
    id int not null primary key auto_increment, 
    name varchar(10)
);

-- 创建老师表
create table teacher(
    id int not null primary key auto_increment, 
    name varchar(10), 
    s_id int not null, 
    foreign key(s_id) references school(id)
);

删除外键约束

-- 需要先获取外键约束名称,该名称系统会自动生成,可以通过查看表创建语句来获取名称
show create table teacher;

-- 获取名称之后就可以根据名称来删除外键约束
alter table teacher drop foreign key 外键名;

将查询结果插入到其它表中


把查询结果插入到指定表中,也就是表复制:

insert into .. select .. SQL语句

把goods表中的cate_name商品分类添加到good_cates商品分类表:

-- 查询goods表中商品的分类信息
select cate_name from goods group by cate_name;

-- 将查询结果插入到good_cates表中
insert into good_cates(name) select cate_name from goods group by cate_name;

-- 添加移动设备分类信息
insert into good_cates(name) values('移动设备');

使用连接更新表中某个字段数据


连接更新表中数据使用:

update .. join .. 

将goods表中的分类名称更改成商品分类表中对应的分类id

-- 查看goods表中的商品分类名称对应的商品分类id
select * from goods inner join good_cates on goods.cate_name = good_cates.name;

-- 把该语句中from 后的语句理解为一张虚表  
update goods g inner join good_cates gc on g.cate_name=gc.name set g.cate_name=gc.id;

创建表并给某个字段添加数据


创建表并给字段插入数据使用:

create table .. select 语句

创建品牌表

-- 查询品牌信息 
select brand_name from goods group by brand_name;

-- 通过create table ...select来创建数据表并且同时插入数据
-- 创建商品分类表,注意: 需要对brand_name 用as起别名,否则name字段就没有值
create table good_brands (     
id int unsigned primary key auto_increment,     
name varchar(40) not null) select brand_name as name from goods group by brand_name;

说明:create table .. select 列名 .. 表示创建表并插入数据

更新goods表中的品牌信息

-- 将goods表中的品牌名称更改成品牌表中对应的品牌id
update goods as g inner join good_brands gb on g.brand_name = gb.name set g.brand_name = gb.id;

修改表结构


alter table 语句

cate_namebrand_name 字段分别改成 cate_idbrand_id 字段,类型都改成int类型:

-- 查看表结构
desc goods;
-- 通过alter table语句修改表结构
alter table goods change cate_name cate_id int not null, change brand_name brand_id int not null;

事务


事务就是用户定义的一系列执行SQL语句的操作, 这些操作要么完全地执行,要么完全地都不执行, 它是一个不可分割的工作执行单元。事务能够保证数据的完整性和一致性,让用户的操作更加安全。

事务的特性:

原子性: 强调事务中的多个操作时一个整体
一致性: 强调数据库中不会保存不一致状态
隔离性: 强调数据库中事务之间相互不可见
持久性: 强调数据库能永久保存数据,一旦提交就不可撤销

MySQL数据库默认采用自动提交(autocommit)模式, 也就是说修改数据(insert、update、delete)的操作会自动的触发事务,完成事务的提交或者回滚。开启事务使用 begin 或者 start transaction;回滚事务使用 rollback。

常用的表的存储引擎是 InnoDB 和 MyISAM。InnoDB 是支持事务的;MyISAM 不支持事务,优势是访问速度快,对事务没有要求或者以select、insert为主的都可以使用该存储引擎来创建表。

事务演练的SQL语句:

begin;
insert into students(name) values('李白');
-- 查询数据,此时有新增的数据, 注意: 如果这里后续没有执行提交事务操作,那么数据是没有真正的更新到物理表中
select * from students;
-- 只有这里提交事务,才把数据真正插入到物理表中
commit;

-- 新打开一个终端,重新连接MySQL数据库,查询students表,这时没有显示新增的数据,说明之前的事务没有提交,这就是事务的隔离性
-- 一个事务所做的修改操作在提交事务之前,对于其他事务来说是不可见的
select * from students;

索引


索引在MySQL中也叫做“键”,它是一个特殊的文件,它保存着数据表里所有记录的位置信息,更通俗的来说,数据库索引好比是一本书前面的目录,能加快数据库的查询速度。

查看表中已有索引:

show index from 表名;

说明:主键列会自动创建索引

索引的创建:

-- alter table 表名 add index 索引名[可选](列名, ..)
-- 给name字段添加索引
alter table classes add index my_name (name);

说明:索引名不指定,默认使用字段名。

索引的删除:

-- alter table 表名 drop index 索引名
-- 如果不知道索引名,可以查看创表sql语句
show create table classes;
alter table classes drop index my_name;

联合索引又叫复合索引,即一个索引覆盖表中两个或者多个字段,一般用在多个字段一起查询的时候。

-- 创建联合索引
alter table teacher add index (name,age);

联合索引的好处:减少磁盘空间开销,因为每创建一个索引,其实就是创建了一个索引文件,那么会增加磁盘空间的开销。

联合索引的最左原则:在使用联合索引的时候,我们要遵守一个最左原则,即index(name,age)支持name、name和age组合查询,而不支持单独age 查询,因为没有用到创建的联合索引。

最左原则示例:

-- 下面的查询使用到了联合索引
select * from stu where name='张三' -- 这里使用了联合索引的name部分
select * from stu where name='李四' and age=10 -- 这里完整的使用联合索引,包括 name 和 age 部分 
-- 下面的查询没有使用到联合索引
select * from stu where age=10 -- 因为联合索引里面没有这个组合,只有 name | name age 这两种组合

PyMySQL的使用


使用代码的方式操作数据库就称为数据库编程。

win电脑cd到python的安装目录Scripts文件夹下

安装:

pip install pymysql

pymysql的使用:

#导包
import pymysql
#创建连接对象
pymysql.connect(参数列表)
#获取游标对象
cursor =conn.cursor()
#执行SQL语句
row_count = cursor.execute(sql)
#获取查询结果集
result = cursor.fetchall()
#将修改操作提交到数据库
conn.commit()
#回滚数据
conn.rollback()
#关闭游标
cursor.close()
#关闭连接
conn.close()

查询操作:

import pymysql

# 创建连接对象
conn = pymysql.connect(host='localhost', port=3306, user='root', password='*****', database='python', charset='utf8')
# 获取游标对象
cursor = conn.cursor()
# 查询 SQL 语句
sql = "select * from test;"
# 执行 SQL 语句 返回值就是 SQL 语句在执行过程中影响的行数
row_count = cursor.execute(sql)
print("SQL 语句执行影响的行数%d" % row_count)

# 取出结果集中一行数据, 例如:(1, '张三')
#print(cursor.fetchone())
# 取出结果集中的所有数据, 例如:((1, '张三'), (2, '李四'), (3, '王五'))
for line in cursor.fetchall():
    print(line)
# 关闭游标
cursor.close()
# 关闭连接
conn.close()

增删改操作:

import pymysql

# 创建连接对象
conn = pymysql.connect(host='localhost', port=3306, user='root', password='824699',database='python', charset='utf8')
# 获取游标对象
cursor = conn.cursor()
try:
    # 添加 SQL 语句
    # sql = "insert into test(name) values('刘璐'), ('王美丽');"
    # 删除 SQ L语句
    # sql = "delete from test where id = 2;"
    # 修改 SQL 语句
    sql = "update test set name = '王铁蛋' where id = 1;"
    # 执行 SQL 语句
    row_count = cursor.execute(sql)
    print("SQL 语句执行影响的行数%d" % row_count)
    # 提交数据到数据库
    conn.commit()
except Exception as e:
    # 回滚数据, 即撤销刚刚的SQL语句操作
    conn.rollback()
# 关闭游标
cursor.close()
# 关闭连接
conn.close()

什么是SQL注入?用户提交带有恶意的数据与SQL语句进行字符串方式的拼接,从而影响了SQL语句的语义,最终产生数据泄露的现象。

如何防止SQL注入?SQL语句参数化

SQL语言中的参数使用%s来占位,此处不是python中的字符串格式化操作。将SQL语句中%s占位所需要的参数存在一个列表中,把参数列表传递给。execute方法中第二个参数。

防止SQL注入的示例代码:

from pymysql import connect

def main():
    find_name = input("请输入物品名称:")
    # 创建Connection连接
    conn = connect(host='localhost',port=3306,user='root',password='mysql',database='jing_dong',charset='utf8')
    # 获得Cursor对象
    cs1 = conn.cursor()
    # 非安全的方式
    # 输入 ' or 1 = 1 or '   (单引号也要输入)
    # sql = "select * from goods where name='%s'" % find_name
    # print("""sql===>%s<====""" % sql)
    # # 执行select语句,并返回受影响的行数:查询所有数据
    # count = cs1.execute(sql)
    # 安全的方式
    # 构造参数列表
    params = [find_name]
    # 执行select语句,并返回受影响的行数:查询所有数据
    count = cs1.execute("select * from goods where name=%s", params)
    # 注意:
    # 如果要是有多个参数,需要进行参数化
    # 那么params = [数值1, 数值2....],此时sql语句中有多个%s即可
    # %s 不需要带引号
    # 打印受影响的行数
    print(count)
    # 获取查询的结果
    # result = cs1.fetchone()
    result = cs1.fetchall()
    # 打印查询的结果
    print(result)
    # 关闭Cursor对象
    cs1.close()
    # 关闭Connection对象
    conn.close()

if __name__ == '__main__':
    main()

闭包


闭包的定义:在函数嵌套的前提下,内部函数使用了外部函数的变量,并且外部函数返回了内部函数,我们把这个使用外部函数变量的内部函数称为闭包。

闭包的形成条件:在函数嵌套(函数里面再定义函数)的前提下;内部函数使用了外部函数的变量(还包括外部函数的参数);外部函数返回了内部函数。

示例代码

# 定义一个外部函数
def func_out(num1):
    # 定义一个内部函数
    def func_inner(num2):
        # 内部函数使用了外部函数的变量(num1)
        result = num1 + num2
        print("结果是:", result)
    # 外部函数返回了内部函数,这里返回的内部函数就是闭包
    return func_inner
# 创建闭包实例    
f = func_out(1)
# 执行闭包
f(2) #结果是: 3
f(3) #结果是: 4

闭包不仅可以保存外部函数的变量还可以提高代码的可重用行。


修改闭包内使用的外部函数变量使用 nonlocal 关键字来完成。

# 定义一个外部函数
def func_out(num1):
    # 定义一个内部函数
    def func_inner(num2):
        # 这里本意想要修改外部num1的值,实际上是在内部函数定义了一个局部变量num1
        nonlocal num1  # 告诉解释器,此处使用的是 外部变量a
        # 修改外部变量num1
        num1 = 10
        # 内部函数使用了外部函数的变量(num1)
        result = num1 + num2
        print("结果是:", result)
    #print(num1)
    # func_inner(1)
    # print(num1)
    # 外部函数返回了内部函数,这里返回的内部函数就是闭包
    return func_inner

# 创建闭包实例
f = func_out(1)
# 执行闭包
f(2) #结果是: 12

装饰器


就是给已有函数增加额外功能的函数,它本质上就是一个闭包函数。

装饰器的功能特点:不修改已有函数的源代码;不修改已有函数的调用方式;给已有函数增加额外的功能。

装饰器的语法格式:

# 装饰器
# def decorator(fn): # fn:被装饰的目标函数.
#     def inner():
#         '''执行函数之前'''
#         fn() # 执行被装饰的目标函数
#         '''执行函数之后'''
#     return inner

示例代码:

# 添加一个登录验证的功能
def check(fn):
    def inner():
        print("请先登录....")
        fn()
    return inner

def comment():
    print("发表评论")

# 使用装饰器来装饰函数
comment = check(comment)
comment()

闭包函数有且只有一个参数,必须是函数类型,这样定义的函数才是装饰器。写代码要遵循开放封闭原则,它规定已经实现的功能代码不允许被修改,但可以被扩展。


语法糖的书写格式是: @装饰器名字,通过语法糖的方式也可以完成对已有函数的装饰:

# 添加一个登录验证的功能
def check(fn):
    print("装饰器函数执行了")
    def inner():
        print("请先登录....")
        fn()
    return inner
# 使用语法糖方式来装饰函数
@check
def comment():
    print("发表评论")
comment()

装饰器实现已有函数执行时间的统计:

import time

# 装饰器函数
def get_time(func):
    def inner():
        begin = time.time()
        func()
        end = time.time()
        print("函数执行花费%f" % (end-begin))
    return inner

@get_time
def func1():
    for i in range(100000):
        print(i)

func1()

装饰带有参数的函数:

# 添加输出日志的功能
def logging(fn):
    def inner(num1, num2):
        print("--正在努力计算--")
        fn(num1, num2)
    return inner

# 使用装饰器装饰函数
@logging
def sum_num(a, b):
    result = a + b
    print(result)

sum_num(1, 2)

装饰带有返回值的函数:

# 添加输出日志的功能
def logging(fn):
    def inner(num1, num2):
        print("--正在努力计算--")
        result = fn(num1, num2)
        return result
    return inner

# 使用装饰器装饰函数
@logging
def sum_num(a, b):
    result = a + b
    return result

result = sum_num(1, 2)
print(result)

装饰带有不定长参数的函数:

# 添加输出日志的功能
def logging(fn):
    def inner(*args, **kwargs):
        print("--正在努力计算--")
        fn(*args, **kwargs)
    return inner

# 使用语法糖装饰函数
@logging
def sum_num(*args, **kwargs):
    result = 0
    for value in args:
        result += value
    for value in kwargs.values():
        result += value
    print(result)

sum_num(1, 2, a=10)

通用装饰器:

# 添加输出日志的功能
def logging(fn):
    def inner(*args, **kwargs):
        print("--正在努力计算--")
        result = fn(*args, **kwargs)
        return result
    return inner

# 使用语法糖装饰函数
@logging
def sum_num(*args, **kwargs):
    result = 0
    for value in args:
        result += value
    for value in kwargs.values():
        result += value
    return result

@logging
def subtraction(a, b):
    result = a - b
    print(result)

result = sum_num(1, 2, a=10)
print(result)
subtraction(4, 2)

多个装饰器的使用示例代码:

def make_div(func):
    """对被装饰的函数的返回值 div标签"""
    def inner(*args, **kwargs):
        return "<div>" + func() + "</div>"
    return inner

def make_p(func):
    """对被装饰的函数的返回值 p标签"""
    def inner(*args, **kwargs):
        return "<p>" + func() + "</p>"
    return inner

# 装饰过程: 1 content = make_p(content) 2 content = make_div(content)
# content = make_div(make_p(content))
@make_div
@make_p
def content():
    return "人生苦短"

result = content()
print(result)

多个装饰器的装饰过程是: 离函数最近的装饰器先装饰,然后外面的装饰器再进行装饰,由内到外的装饰过程


带有参数的装饰器:在装饰器外面再包裹上一个函数,让最外面的函数接收参数,返回的是装饰器,因为@符号后面必须是装饰器实例。

# 添加输出日志的功能
def logging(flag):
    def decorator(fn):
        def inner(num1, num2):
            if flag == "+":
                print("--正在努力加法计算--")
            elif flag == "-":
                print("--正在努力减法计算--")
            result = fn(num1, num2)
            return result
        return inner
    # 返回装饰器
    return decorator

# 使用装饰器装饰函数
@logging("+")
def add(a, b):
    result = a + b
    return result

@logging("-")
def sub(a, b):
    result = a - b
    return result

result = add(1, 2)
print(result)
result = sub(1, 2)
print(result)

类装饰器:

class Check(object):
    def __init__(self, fn):
        # 初始化操作在此完成
        self.__fn = fn
    # 实现__call__方法,表示对象是一个可调用对象,可以像调用函数一样进行调用。
    def __call__(self, *args, **kwargs):
        # 添加装饰功能
        print("请先登陆...")
        self.__fn()
@Check
def comment():
    print("发表评论")
comment()

@Check 等价于 comment = Check(comment), 所以需要提供一个init方法,并多增加一个fn参数。

要想类的实例对象能够像函数一样调用,需要在类里面使用call方法,把类的实例变成可调用对象(callable),也就是说可以像调用函数一样进行调用。

在call方法里进行对fn函数的装饰,可以添加额外的功能。


mini-web框架

概述


使用web框架专门负责处理用户的动态资源请求,这个web框架其实就是一个为web服务器提供服务的应用程序,简称web框架。

和静态资源相反, 动态资源会经常变化,比如: 我们在京东浏览商品时经常会根据条件进行筛选,选择不同条件, 浏览的商品就不同,这种资源web服务器无法提前准备好,需要web框架来帮web服务器进行准备,在这里web服务器可以把.html的资源请求认为是动态资源请求交由web框架进行处理。

WSGI协议:它是web服务器和web框架之间进行协同工作的一个规则,WSGI协议规定web服务器把动态资源的请求信息传给web框架处理,web框架把处理好的结果返回给web服务器。


正则表达式

property属性


property属性就是负责把一个方法当做属性进行使用,这样做可以简化代码使用。定义property属性有两种方式:装饰器方式和类属性方式。


装饰器方式:

class Person(object):
    def __init__(self):
        self.__age = 0
    # 装饰器方式的property, 把age方法当做属性使用, 表示当获取属性时会执行下面修饰的方法
    @property
    def age(self):
        return self.__age
    # 把age方法当做属性使用, 表示当设置属性时会执行下面修饰的方法
    @age.setter
    def age(self, new_age):
        if new_age >= 150:
            print("成精了")
        else:
            self.__age = new_age
# 创建person
p = Person()
print(p.age)
p.age = 100
print(p.age)
p.age = 1000

@property表示把方法当做属性使用, 表示当获取属性时会执行下面修饰的方法。@方法名.setter 表示把方法当做属性使用,表示当设置属性时会执行下面修饰的方法。装饰器方式的property属性修饰的方法名一定要一样。


类属性方式:

class Person(object):
    def __init__(self):
        self.__age = 0
    def get_age(self):
        """当获取age属性的时候会执行该方法"""
        return self.__age
    def set_age(self, new_age):
        """当设置age属性的时候会执行该方法"""
        if new_age >= 150:
            print("成精了")
        else:
            self.__age = new_age
    # 类属性方式的property属性
    age = property(get_age, set_age)

# 创建person
p = Person()
print(p.age)
p.age = 100
print(p.age)
p.age = 1000

property的参数说明:第一个参数是获取属性时要执行的方法;第二个参数是设置属性时要执行的方法。


with语句和上下文管理器


向文件中写入数据的示例代码:

try:
    # 1、以读的方式打开文件
    f = open("1.txt", "r")
    # 2、读取文件内容
    f.write("xxxxx")
except IOError as e:
    print("文件操作出错", e)
finally:
    # 3、关闭文件
    f.close()

这种方法代码过于冗长,Python提供了 with 语句的这种写法,既简单又安全,并且 with 语句执行完成以后自动调用关闭文件操作,即使出现异常也会自动调用关闭文件操作。

with 语句的示例代码:

# 1、以写的方式打开文件
with open("1.txt", "w") as f:
# 2、读取文件内容
f.write("hello world")

一个类只要实现了__enter__()__exit__()这个两个方法,通过该类创建的对象我们就称之为上下文管理器。上下文管理器可以使用 with 语句,with语句之所以这么强大,背后是由上下文管理器做支撑的。

自定义上下文管理器类,模拟文件操作:

class File(object):
    # 初始化方法
    def __init__(self, file_name, file_model):
        # 定义变量保存文件名和打开模式
        self.file_name = file_name
        self.file_model = file_model

    # 上文方法
    def __enter__(self):
        print("进入上文方法")
        # 返回文件资源
        self.file = open(self.file_name,self.file_model)
        return self.file

    # 下文方法
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("进入下文方法")
        self.file.close()

if __name__ == '__main__':
    # 使用with管理文件
    with File("1.txt", "r") as file:
        file_data = file.read()
        print(file_data)

__enter__表示上文方法,需要返回一个操作文件对象。__exit__表示下文方法,with语句执行完成会自动执行,即使出现异常也会执行该方法。


假如想要让一个函数成为上下文管理器,Python 还提供了一个@contextmanager的装饰器,更进一步简化了上下文管理器的实现方式。通过 yield 将函数分割成两部分,yield 上面的语句在__enter__方法中执行,yield 下面的语句在__exit__方法中执行,紧跟在 yield 后面的参数是函数的返回值。

# 导入装饰器
from contextlib import contextmanager
# 装饰器装饰函数,让其称为一个上下文管理器对象
@contextmanager
def my_open(path, mode):
    try:
        # 打开文件
        file = open(file_name, file_mode)
        # yield之前的代码好比是上文方法
        yield file
    except Exception as e:
        print(e)
    finally:
        print("over")
        # yield下面的代码好比是下文方法
        file.close()

# 使用with语句
with my_open('out.txt', 'w') as f:
    f.write("hello , the simplest context manager")

生成器的创建方式


根据程序员制定的规则循环生成数据,当条件不成立时则生成数据结束。数据不是一次性全部生成处理,而是使用一个,再生成一个,可以节约大量的内存。创建生成器的方式:生成器推导式和 yield 关键字。


生成器推导式:

# 创建生成器
my_generator = (i * 2 for i in range(5))
print(my_generator)
# next获取生成器下一个值
# value = next(my_generator)
# print(value)
# 遍历生成器
for value in my_generator:
    print(value)

代码说明:next 函数获取生成器中的下一个值;for 循环遍历生成器中的每一个值。


yield 关键字:

只要在def函数里面看到有 yield 关键字那么就是生成器。

def mygenerater(n):
    for i in range(n):
        print('开始生成...')
        yield i
        print('完成一次...')
if __name__ == '__main__':
    g = mygenerater(2)
    # 获取生成器中下一个值
    # result = next(g)
    # print(result)

    # while True:
    #     try:
    #         result = next(g)
    #         print(result)
    #     except StopIteration as e:
    #         break

    # # for遍历生成器, for 循环内部自动处理了停止迭代异常,使用起来更加方便
    for i in g:
        print(i)

代码说明:代码执行到 yield 会暂停,然后把结果返回出去,下次启动生成器会在暂停的位置继续往下执行。生成器如果把数据生成完成,再次获取生成器中的下一个数据会抛出一个StopIteration 异常,表示停止迭代异常。while 循环内部没有处理异常操作,需要手动添加处理异常操作。for 循环内部自动处理了停止迭代异常,使用起来更加方便,推荐大家使用。


深拷贝和浅拷贝




文章作者:
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 !
  目录