0%

day24 阶段总结

课程目标:对第三模块 阶段的知识点进行总结和考试,让学员更好的掌握此模块的相关知识。

课程概要:

  • 知识补充
  • 阶段总结(思维导图)
  • 考试题

1. 知识点补充

我们来分析如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import socket
import threading
import select


def process(request, client_address):
print(request,client_address)
conn = request
while True:
data = conn.recv(1024)
print(data)

sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sk.bind(('127.0.0.1',8002))
sk.listen(5)

while True:
r, w, e = select.select([sk,],[],[],1)
if sk in r:
request, client_address = sk.accept()
t = threading.Thread(target=process, args=(request, client_address))
t.daemon = False
t.start()

sk.close()

上述代码集成了:线程、网络编程、IO多路复用,实现了一个支持并发的服务端(可以同时接受多个客户端的连接)。

socketserver,是Python中的内置模块。该模块内部基于线程、进程 + socket + IO多路复用,帮我们实现了上述的功能(写的更加严谨)。

  • 服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import socketserver
    import threading


    class MyRequestHandler(socketserver.BaseRequestHandler):

    def handle(self):
    conn = self.request
    while True:
    data = conn.recv(1024)
    if not data:
    break
    print("收到消息:", data.decode('utf-8'), threading.current_thread())
    conn.close()


    obj = socketserver.ThreadingTCPServer(('127.0.0.1', 9000), MyRequestHandler)
    obj.serve_forever()

  • 客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import socket

    client = socket.socket()
    client.connect(('127.0.0.1', 9000))

    while True:
    content = input(">>>")
    if content.upper() == 'Q':
    break
    client.sendall(content.encode('utf-8'))

    client.close()

在很多的框架和模块中已集成了并发相关的内容,让程序员可以只关注业务功能的开发,避免重复造轮子了(Stop Trying to Reinvent the Wheel)。

2. 阶段总结

思维导图

3. 考试题

考试题的目的是让大家对自己近期知识点学习练习 以及 自测,请大家务必【独立】完成(切勿翻看笔记 & 切勿网上搜索 )。

  • 第一步:自己独立完成(编程题目可以在pycharm中编写)

  • 第二步:做完之后,翻看自己笔记去修改和更正。

  • 第三步:觉自己做的没问题了,最后再去看考试题的参考答案和讲解。

详情见附件《第三阶段考试题.md》文件。

day23 并发编程(下)

image-20210226164423992

课程目标:掌握多进程开发的相关知识点并初步认识协程。

今日概要:

  • 多进程开发
  • 进程之间数据共享
  • 进程锁
  • 进程池
  • 协程

day23 并发编程(下)

image-20210226164423992

课程目标:掌握多进程开发的相关知识点并初步认识协程。

今日概要:

  • 多进程开发
  • 进程之间数据共享
  • 进程锁
  • 进程池
  • 协程

1. 多进程开发

1
2
3
4
5
6
7
8
9
10
11
12
from multiprocessing import Process


def task(arg):
print("执行中...")


if __name__ == '__main__':
p = Process(target=task, args=('xxx',))
p.start()

print("继续执行...")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from multiprocessing import Process


def task(arg):
print("执行中...")


def run():
p = Process(target=task, args=('xxx',))
p.start()
print("继续执行...")


if __name__ == '__main__':
run()

进程的常见方法:

  • p.start(),当前进程准备就绪,等待被CPU调度(工作单元其实是进程中的线程)。

  • p.join(),等待当前进程的任务执行完毕后再向下继续执行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import time
    from multiprocessing import Process


    def task(arg):
    time.sleep(2)
    print("执行中...")


    if __name__ == '__main__':
    p = Process(target=task, args=('xxx',))
    p.start()
    p.join()

    print("继续执行...")
  • p.daemon = 布尔值,守护进程(必须放在start之前)

    • p.daemon =True,设置为守护进程,主进程执行完毕后,子进程也自动关闭。
    • p.daemon =False,设置为非守护进程,主进程等待子进程,子进程执行完毕后,主进程才结束。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import time
    from multiprocessing import Process


    def task(arg):
    time.sleep(2)
    print("执行中...")


    if __name__ == '__main__':
    p = Process(target=task, args=('xxx',))
    p.daemon = True
    p.start()

    print("继续执行...")

  • 进程的名称的设置和获取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import time
    import multiprocessing


    def task(arg):
    time.sleep(2)
    print("当前进程的名称:", multiprocessing.current_process().name)


    if __name__ == '__main__':
    p = multiprocessing.Process(target=task, args=('xxx',))
    p.name = "哈哈哈哈"
    p.start()

    print("继续执行...")

  • 自定义进程类,直接将线程需要做的事写到run方法中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import multiprocessing


    class MyProcess(multiprocessing.Process):
    def run(self):
    print('执行此进程', self._args)


    if __name__ == '__main__':
    p = MyProcess(args=('xxx',))
    p.start()
    print("继续执行...")

2. 进程数据共享

进程是资源分配的最小单元,每个进程中都维护自己独立的数据,不共享。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import multiprocessing


def task(data):
for i in range(10):
data.append(i)
print("子进程中处理后的结果:", data)


if __name__ == '__main__':
data_list = []
p = multiprocessing.Process(target=task, args=(data_list,))
p.start()
p.join()

print("主进程:", data_list)

如果想让进程间做数据共享,可以借助一些特殊的数据类型,例如:

  • Value

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    from multiprocessing import Process, Value


    def f(n):
    n.value = 3.1415927


    if __name__ == '__main__':
    num = Value('d', 0.0)

    p = Process(target=f, args=(num,))
    p.start()
    p.join()

    print(num.value)

    1
    2
    3
    4
    5
    6
    'c': ctypes.c_char,  'u': ctypes.c_wchar,
    'b': ctypes.c_byte, 'B': ctypes.c_ubyte,
    'h': ctypes.c_short, 'H': ctypes.c_ushort,
    'i': ctypes.c_int, 'I': ctypes.c_uint,
    'l': ctypes.c_long, 'L': ctypes.c_ulong,
    'f': ctypes.c_float, 'd': ctypes.c_double
  • Manager

    1
    2
    3
    A manager object returned by Manager() controls a server process which holds Python objects and allows other processes to manipulate them using proxies.

    A manager returned by Manager() will support types list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Barrier, Queue, Value and Array. For example,
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import multiprocessing


    def task(data):
    for i in range(10):
    data.append(i)
    print("子进程中处理后的结果:", data)


    if __name__ == '__main__':
    manager = multiprocessing.Manager()
    data_list = manager.list()

    p = multiprocessing.Process(target=task, args=(data_list,))
    p.start()
    p.join()

    print("主进程:", data_list)

  • multiprocessing.Queue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import multiprocessing


    def task(q):
    for i in range(10):
    q.put(i)


    if __name__ == '__main__':
    queue = multiprocessing.Queue()
    p = multiprocessing.Process(target=task, args=(queue,))
    p.start()
    p.join()

    print("主进程")
    print(queue.get())
    print(queue.get())
    print(queue.get())
    print(queue.get())
    print(queue.get())
  • multiprocessing.Pipe

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import time
    import multiprocessing


    def task(conn):
    time.sleep(1)
    conn.send([111, 22, 33, 44])
    data = conn.recv() # 阻塞
    print("子进程接收:", data)
    time.sleep(2)


    if __name__ == '__main__':
    parent_conn, child_conn = multiprocessing.Pipe()

    p = multiprocessing.Process(target=task, args=(child_conn,))
    p.start()

    info = parent_conn.recv() # 阻塞
    print("主进程接收:", info)
    parent_conn.send(666)

上述都是Python内部提供的进程之间数据共享的机制,作为了解即刻。一般在项目开发中很少使用,后期项目中一般会借助第三方的来做资源的共享,例如:MySQL、redis等。

image-20210220051730124

3. 进程锁

如果多个进程抢占式去做某些操作时候,为了防止操作出问题,可以通过进程锁来避免。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import time
import multiprocessing


def task():
# 假设文件中保存的内容就是一个值:10
with open('f1.txt', mode='r', encoding='utf-8') as f:
current_num = int(f.read())

print("排队抢票了")
time.sleep(1)

current_num -= 1
with open('f1.txt', mode='w', encoding='utf-8') as f:
f.write(str(current_num))


if __name__ == '__main__':
for i in range(20):
p = multiprocessing.Process(target=task)
p.start()

很显然,多进程在操作时就会出问题,此时就需要锁来介入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import time
import multiprocessing


def task(lock):
print("开始")
lock.acquire()
# 假设文件中保存的内容就是一个值:10
with open('f1.txt', mode='r', encoding='utf-8') as f:
current_num = int(f.read())

print("排队抢票了")
time.sleep(1)
current_num -= 1

with open('f1.txt', mode='w', encoding='utf-8') as f:
f.write(str(current_num))
lock.release()


if __name__ == '__main__':
lock = multiprocessing.RLock()
for i in range(10):
p = multiprocessing.Process(target=task, args=(lock,))
p.start()

time.sleep(10) # windows系统是spawn模式创建的进程,需要特殊处理。(window系统)- 【也可以通过join等待】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import time
import multiprocessing

def task(lock):
print("开始")
lock.acquire()
# 假设文件中保存的内容就是一个值:10
with open('f1.txt', mode='r', encoding='utf-8') as f:
current_num = int(f.read())

print("排队抢票了")
time.sleep(1)
current_num -= 1

with open('f1.txt', mode='w', encoding='utf-8') as f:
f.write(str(current_num))
lock.release()


if __name__ == '__main__':
multiprocessing.set_start_method('fork') # Linux系统fork;mac支持:fork和spawn(python3.8默认设置spawn)。
lock = multiprocessing.RLock()
for i in range(10):
p = multiprocessing.Process(target=task, args=(lock,))
p.start()

4. 进程池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import time
from concurrent.futures.process import ProcessPoolExecutor


def task(num):
print("执行", num)
time.sleep(2)


if __name__ == '__main__':

pool = ProcessPoolExecutor(4)
for i in range(10):
pool.submit(task, i)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import time
from concurrent.futures.process import ProcessPoolExecutor


def task(num):
print("执行", num)
time.sleep(2)


if __name__ == '__main__':

pool = ProcessPoolExecutor(4)
for i in range(10):
pool.submit(task, i)
pool.shutdown(True)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import time
from concurrent.futures.process import ProcessPoolExecutor
import multiprocessing

def task(num):
print("执行", num)
time.sleep(2)
return num


def done(res):
print(multiprocessing.current_process())
time.sleep(1)
print(res.result())
time.sleep(1)


if __name__ == '__main__':

pool = ProcessPoolExecutor(4)
for i in range(50):
fur = pool.submit(task, i)
fur.add_done_callback(done) # done的调用由主进程处理(与线程池不同)

print(multiprocessing.current_process())
pool.shutdown(True)

注意:如果在线程池中要使用线程锁,则需要结束Manager中的Lock和RLock。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import time
import multiprocessing
from concurrent.futures.process import ProcessPoolExecutor


def task(lock):
print("开始")
with lock:
# 假设文件中保存的内容就是一个值:10
with open('f1.txt', mode='r', encoding='utf-8') as f:
current_num = int(f.read())

print("排队抢票了")
time.sleep(1)
current_num -= 1

with open('f1.txt', mode='w', encoding='utf-8') as f:
f.write(str(current_num))


if __name__ == '__main__':
pool = ProcessPoolExecutor()
# lock_object = multiprocessing.RLock() # 不能使用
m = multiprocessing.Manager()
lock_object = m.RLock()
for i in range(10):
pool.submit(task, lock_object)

5. 协程

计算机中提供了:线程、进程 用于实现并发编程(真实存在)。

协程(Coroutine),是程序员通过代码搞出来的一个东西(非真实存在)。

1
2
协程也可以被称为微线程,是一种用户态内的上下文切换技术。
简而言之,其实就是通过一个线程实现代码块相互切换执行。

例如:

1
2
3
4
5
6
7
8
9
10
def func1():
print(1)
...
print(2)
def func2():
print(3)
...
print(4)
func1()
func2()

上述代码是普通的函数定义和执行,按流程分别执行两个函数中的代码,并先后会输出:1、2、3、4

但如果介入协程技术那么就可以实现函数见代码切换执行,最终输入:1、3、2、4

在Python中有多种方式可以实现协程,例如:

  • greenlet

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    from greenlet import greenlet
    def func1():
    print(1) # 第1步:输出 1
    gr2.switch() # 第3步:切换到 func2 函数
    print(2) # 第6步:输出 2
    gr2.switch() # 第7步:切换到 func2 函数,从上一次执行的位置继续向后执行
    def func2():
    print(3) # 第4步:输出 3
    gr1.switch() # 第5步:切换到 func1 函数,从上一次执行的位置继续向后执行
    print(4) # 第8步:输出 4
    gr1 = greenlet(func1)
    gr2 = greenlet(func2)
    gr1.switch() # 第1步:去执行 func1 函数
  • yield

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    def func1():
    yield 1
    yield from func2()
    yield 2
    def func2():
    yield 3
    yield 4

    f1 = func1()
    for item in f1:
    print(item)

虽然上述两种都实现了协程,但这种编写代码的方式没啥意义。

这种来回切换执行,可能反倒让程序的执行速度更慢了(相比较于串行)。

协程如何才能更有意义呢?

不要让用户手动去切换,而是遇到IO操作时能自动切换。

Python在3.4之后推出了asyncio模块 + Python3.5推出async、async语法 ,内部基于协程并且遇到IO请求自动化切换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import asyncio

async def func1():
print(1)
await asyncio.sleep(2)
print(2)

async def func2():
print(3)
await asyncio.sleep(2)
print(4)

tasks = [
asyncio.ensure_future(func1()),
asyncio.ensure_future(func2())
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"""
需要先安装:pip3 install aiohttp
"""
import aiohttp
import asyncio
async def fetch(session, url):
print("发送请求:", url)
async with session.get(url, verify_ssl=False) as response:
content = await response.content.read()
file_name = url.rsplit('_')[-1]
with open(file_name, mode='wb') as file_object:
file_object.write(content)

async def main():
async with aiohttp.ClientSession() as session:
url_list = [
'https://www3.autoimg.cn/newsdfs/g26/M02/35/A9/120x90_0_autohomecar__ChsEe12AXQ6AOOH_AAFocMs8nzU621.jpg',
'https://www2.autoimg.cn/newsdfs/g30/M01/3C/E2/120x90_0_autohomecar__ChcCSV2BBICAUntfAADjJFd6800429.jpg',
'https://www3.autoimg.cn/newsdfs/g26/M0B/3C/65/120x90_0_autohomecar__ChcCP12BFCmAIO83AAGq7vK0sGY193.jpg'
]
tasks = [asyncio.create_task(fetch(session, url)) for url in url_list]
await asyncio.wait(tasks)
if __name__ == '__main__':
asyncio.run(main())

通过上述内容发现,在处理IO请求时,协程通过一个线程就可以实现并发的操作。

协程、线程、进程的区别?

1
2
3
4
5
6
7
8
9
10
11
线程,是计算机中可以被cpu调度的最小单元。
进程,是计算机资源分配的最小单元(进程为线程提供资源)。

一个进程中可以有多个线程,同一个进程中的线程可以共享此进程中的资源。

由于CPython中GIL的存在:
- 线程,适用于IO密集型操作。
- 进程,适用于计算密集型操作。

协程,协程也可以被称为微线程,是一种用户态内的上下文切换技术,在开发中结合遇到IO自动切换,就可以通过一个线程实现并发操作。
所以,在处理IO操作时,协程比线程更加节省开销(协程的开发难度大一些)。

现在很多Python中的框架都在支持协程,比如:FastAPI、Tornado、Sanic、Django 3、aiohttp等,企业开发使用的也越来越多(目前不是特别多)。

关于协程,目前同学们先了解这些概念即可,更深入的开发、应用 暂时不必过多了解,等大家学了Web框架和爬虫相关知识之后,再来学习和补充效果更佳。有兴趣想要研究的同学可以参考我写的文章和专题视频:

  • 文章

    1
    2
    https://pythonav.com/wiki/detail/6/91/
    https://zhuanlan.zhihu.com/p/137057192
  • 视频

    1
    https://www.bilibili.com/video/BV1NA411g7yf

总结

  1. 掌握进程和进程池的操作
  2. 进程之间数据共享
  3. 进程锁
  4. 协程、进程、线程的区别

day22 并发编程(上)

image-20210224144810148

网络编程,了解网络相关的知识点并且要知道几乎所有网络的通信本质上都是通过socket模块实现。例如:网站、网络爬虫。

并发编程,提升代码执行的效率。原来代码执行需要20分钟,学习并发编程后可以加快到1分钟执行完毕。

今日课程目标:初步了解进程和线程并可以基于线程实现并发编程。

今日概要:

  • 初识进程和线程
  • 多线程开发
  • 线程安全
  • 线程锁
  • 死锁
  • 线程池

1. 进程和线程

先来了解下进程和线程。

类比:

  • 一个工厂,至少有一个车间,一个车间中至少有一个工人,最终是工人在工作。

  • 一个程序,至少有一个进程,一个进程中至少有一个线程,最终是线程在工作。

    1
    上述串行的代码示例就是一个程序,在使用python xx.py 运行时,内部就创建一个进程(主进程),在进程中创建了一个线程(主线程),由线程逐行运行代码。

进程和线程:

1
2
3
4
线程,是计算机中可以被cpu调度的最小单元(真正在工作)。
进程,是计算机资源分配的最小单元(进程为线程提供资源)。

一个进程中可以有多个线程,同一个进程中的线程可以共享此进程中的资源。

以前我们开发的程序中所有的行为都只能通过串行的形式运行,排队逐一执行,前面未完成,后面也无法继续。例如:

1
2
3
4
5
import time
result = 0
for i in range(100000000):
result += i
print(result)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import time
import requests

url_list = [
("东北F4模仿秀.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300f570000bvbmace0gvch7lo53oog"),
("卡特扣篮.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f3e0000bv52fpn5t6p007e34q1g"),
("罗斯mvp.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg")
]

print(time.time())
for file_name, url in url_list:
res = requests.get(url)
with open(file_name, mode='wb') as f:
f.write(res.content)
print(file_name, time.time())

通过 进程线程 都可以将 串行 的程序变为并发,对于上述示例来说就是同时下载三个视频,这样很短的时间内就可以下载完成。

1.1 多线程

基于多线程对上述串行示例进行优化:

  • 一个工厂,创建一个车间,这个车间中创建 3个工人,并行处理任务。
  • 一个程序,创建一个进程,这个进程中创建 3个线程,并行处理任务。
1
2
3
4
5
6
7
8
9
10
11
12
13
import time
import requests

url_list = [
("东北F4模仿秀.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300f570000bvbmace0gvch7lo53oog"),
("卡特扣篮.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f3e0000bv52fpn5t6p007e34q1g"),
("罗斯mvp.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg")
]

for file_name, url in url_list:
res = requests.get(url)
with open(file_name, mode='wb') as f:
f.write(res.content)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import time
import requests
import threading
"""
def func(a1,a2,a3):
pass

t = threaing.Thread(target=func,args=(11,22,33))
t.start()
"""

url_list = [
("东北F4模仿秀.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300f570000bvbmace0gvch7lo53oog"),
("卡特扣篮.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f3e0000bv52fpn5t6p007e34q1g"),
("罗斯mvp.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg")
]


def task(file_name, video_url):
res = requests.get(video_url)
with open(file_name, mode='wb') as f:
f.write(res.content)
print(time.time())


for name, url in url_list:
# 创建线程,让每个线程都去执行task函数(参数不同)
t = threading.Thread(target=task, args=(name, url))
t.start()

1.2 多进程

基于多进程对上述串行示例进行优化:

  • 一个工厂,创建 三个车间,每个车间 一个工人(共3人),并行处理任务。
  • 一个程序,创建 三个进程,每个进程 一个线程(共3人),并行处理任务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import time
import requests
import multiprocessing

# 进程创建之后,在进程中还会创建一个线程。
# t = multiprocessing.Process(target=函数名, args=(name, url))
# t.start()



url_list = [
("东北F4模仿秀.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300f570000bvbmace0gvch7lo53oog"),
("卡特扣篮.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f3e0000bv52fpn5t6p007e34q1g"),
("罗斯mvp.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg")
]


def task(file_name, video_url):
res = requests.get(video_url)
with open(file_name, mode='wb') as f:
f.write(res.content)
print(time.time())


if __name__ == '__main__':
print(time.time())
for name, url in url_list:
t = multiprocessing.Process(target=task, args=(name, url))
t.start()

综上所述,大家会发现 多进程 的开销比 多线程 的开销大。哪是不是使用多线程要比多进程更好呀?

接下来,给大家再来介绍一个Python内置的GIL锁的知识,然后再根据 进程 和 线程 各自的特点总结各自适合应用场景。

1.3 GIL锁

GIL, 全局解释器锁(Global Interpreter Lock),是CPython解释器特有一个玩意,让一个进程中同一个时刻只能有一个线程可以被CPU调用。

image-20210218184651385

如果程序想利用 计算机的多核优势,让CPU同时处理一些任务,适合用多进程开发(即使资源开销大)。

image-20210218185849637

如果程序不利用 计算机的多核优势,适合用多线程开发。

image-20210218185953326

常见的程序开发中,计算操作需要使用CPU多核优势,IO操作不需要利用CPU的多核优势,所以,就有这一句话:

  • 计算密集型,用多进程,例如:大量的数据计算【累加计算示例】。
  • IO密集型,用多线程,例如:文件读写、网络数据传输【下载抖音视频示例】。

累加计算示例(计算密集型):

  • 串行处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import time

    start = time.time()

    result = 0
    for i in range(100000000):
    result += i
    print(result)

    end = time.time()

    print("耗时:", end - start) # 耗时: 9.522780179977417
  • 多进程处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    import time
    import multiprocessing


    def task(start, end, queue):
    result = 0
    for i in range(start, end):
    result += i
    queue.put(result)


    if __name__ == '__main__':
    queue = multiprocessing.Queue()

    start_time = time.time()

    p1 = multiprocessing.Process(target=task, args=(0, 50000000, queue))
    p1.start()

    p2 = multiprocessing.Process(target=task, args=(50000000, 100000000, queue))
    p2.start()

    v1 = queue.get(block=True)
    v2 = queue.get(block=True)
    print(v1 + v2)

    end_time = time.time()

    print("耗时:", end_time - start_time) # 耗时: 2.6232550144195557

当然,在程序开发中 多线程 和 多进程 是可以结合使用,例如:创建2个进程(建议与CPU个数相同),每个进程中创建3个线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import multiprocessing
import threading


def thread_task():
pass


def task(start, end):
t1 = threading.Thread(target=thread_task)
t1.start()

t2 = threading.Thread(target=thread_task)
t2.start()

t3 = threading.Thread(target=thread_task)
t3.start()


if __name__ == '__main__':
p1 = multiprocessing.Process(target=task, args=(0, 50000000))
p1.start()

p2 = multiprocessing.Process(target=task, args=(50000000, 100000000))
p2.start()

2. 多线程开发

1
2
3
4
5
6
7
8
9
10
11
12
import threading

def task(arg):
pass


# 创建一个Thread对象(线程),并封装线程被CPU调度时应该执行的任务和相关参数。
t = threading.Thread(target=task,args=('xxx',))
# 线程准备就绪(等待CPU调度),代码继续向下执行。
t.start()

print("继续执行...") # 主线程执行完所有代码,不结束(等待子线程)

线程的常见方法:

  • t.start(),当前线程准备就绪(等待CPU调度,具体时间是由CPU来决定)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import threading

    loop = 10000000
    number = 0

    def _add(count):
    global number
    for i in range(count):
    number += 1

    t = threading.Thread(target=_add,args=(loop,))
    t.start()

    print(number)
  • t.join(),等待当前线程的任务执行完毕后再向下继续执行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import threading

    number = 0

    def _add():
    global number
    for i in range(10000000):
    number += 1

    t = threading.Thread(target=_add)
    t.start()

    t.join() # 主线程等待中...

    print(number)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    import threading

    number = 0


    def _add():
    global number
    for i in range(10000000):
    number += 1


    def _sub():
    global number
    for i in range(10000000):
    number -= 1


    t1 = threading.Thread(target=_add)
    t2 = threading.Thread(target=_sub)
    t1.start()
    t1.join() # t1线程执行完毕,才继续往后走
    t2.start()
    t2.join() # t2线程执行完毕,才继续往后走

    print(number)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    import threading

    loop = 10000000
    number = 0


    def _add(count):
    global number
    for i in range(count):
    number += 1


    def _sub(count):
    global number
    for i in range(count):
    number -= 1


    t1 = threading.Thread(target=_add, args=(loop,))
    t2 = threading.Thread(target=_sub, args=(loop,))
    t1.start()
    t2.start()

    t1.join() # t1线程执行完毕,才继续往后走
    t2.join() # t2线程执行完毕,才继续往后走

    print(number)
  • t.setDaemon(布尔值) ,守护线程(必须放在start之前)

    • t.setDaemon(True),设置为守护线程,主线程执行完毕后,子线程也自动关闭。
    • t.setDaemon(False),设置为非守护线程,主线程等待子线程,子线程执行完毕后,主线程才结束。(默认)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import threading
    import time

    def task(arg):
    time.sleep(5)
    print('任务')

    t = threading.Thread(target=task, args=(11,))
    t.setDaemon(True) # True/False
    t.start()

    print('END')
  • 线程名称的设置和获取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import threading


    def task(arg):
    # 获取当前执行此代码的线程
    name = threading.current_thread().getName()
    print(name)


    for i in range(10):
    t = threading.Thread(target=task, args=(11,))
    t.setName('日魔-{}'.format(i))
    t.start()
  • 自定义线程类,直接将线程需要做的事写到run方法中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import threading


    class MyThread(threading.Thread):
    def run(self):
    print('执行此线程', self._args)


    t = MyThread(args=(100,))
    t.start()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import requests
    import threading


    class DouYinThread(threading.Thread):
    def run(self):
    file_name, video_url = self._args
    res = requests.get(video_url)
    with open(file_name, mode='wb') as f:
    f.write(res.content)


    url_list = [
    ("东北F4模仿秀.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300f570000bvbmace0gvch7lo53oog"),
    ("卡特扣篮.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f3e0000bv52fpn5t6p007e34q1g"),
    ("罗斯mvp.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg")
    ]
    for item in url_list:
    t = DouYinThread(args=(item[0], item[1]))
    t.start()

3. 线程安全

一个进程中可以有多个线程,且线程共享所有进程中的资源。

多个线程同时去操作一个”东西”,可能会存在数据混乱的情况,例如:

  • 示例1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    import threading

    loop = 10000000
    number = 0


    def _add(count):
    global number
    for i in range(count):
    number += 1


    def _sub(count):
    global number
    for i in range(count):
    number -= 1


    t1 = threading.Thread(target=_add, args=(loop,))
    t2 = threading.Thread(target=_sub, args=(loop,))
    t1.start()
    t2.start()

    t1.join() # t1线程执行完毕,才继续往后走
    t2.join() # t2线程执行完毕,才继续往后走

    print(number)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    import threading

    lock_object = threading.RLock()

    loop = 10000000
    number = 0


    def _add(count):
    lock_object.acquire() # 加锁
    global number
    for i in range(count):
    number += 1
    lock_object.release() # 释放锁


    def _sub(count):
    lock_object.acquire() # 申请锁(等待)
    global number
    for i in range(count):
    number -= 1
    lock_object.release() # 释放锁


    t1 = threading.Thread(target=_add, args=(loop,))
    t2 = threading.Thread(target=_sub, args=(loop,))
    t1.start()
    t2.start()

    t1.join() # t1线程执行完毕,才继续往后走
    t2.join() # t2线程执行完毕,才继续往后走

    print(number)

  • 示例2:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import threading

    num = 0

    def task():
    global num
    for i in range(1000000):
    num += 1
    print(num)


    for i in range(2):
    t = threading.Thread(target=task)
    t.start()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import threading

    num = 0
    lock_object = threading.RLock()


    def task():
    print("开始")
    lock_object.acquire() # 第1个抵达的线程进入并上锁,其他线程就需要再此等待。
    global num
    for i in range(1000000):
    num += 1
    lock_object.release() # 线程出去,并解开锁,其他线程就可以进入并执行了
    print(num)


    for i in range(2):
    t = threading.Thread(target=task)
    t.start()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import threading

    num = 0
    lock_object = threading.RLock()


    def task():
    print("开始")
    with lock_object: # 基于上下文管理,内部自动执行 acquire 和 release
    global num
    for i in range(1000000):
    num += 1
    print(num)


    for i in range(2):
    t = threading.Thread(target=task)
    t.start()

在开发的过程中要注意有些操作默认都是 线程安全的(内部集成了锁的机制),我们在使用的时无需再通过锁再处理,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import threading

data_list = []

lock_object = threading.RLock()


def task():
print("开始")
for i in range(1000000):
data_list.append(i)
print(len(data_list))


for i in range(2):
t = threading.Thread(target=task)
t.start()

image-20210225102151570

所以,要多注意看一些开发文档中是否标明线程安全。

4. 线程锁

在程序中如果想要自己手动加锁,一般有两种:Lock 和 RLock。

  • Lock,同步锁。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import threading

    num = 0
    lock_object = threading.Lock()


    def task():
    print("开始")
    lock_object.acquire() # 第1个抵达的线程进入并上锁,其他线程就需要再此等待。
    global num
    for i in range(1000000):
    num += 1
    lock_object.release() # 线程出去,并解开锁,其他线程就可以进入并执行了

    print(num)


    for i in range(2):
    t = threading.Thread(target=task)
    t.start()

  • RLock,递归锁。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import threading

    num = 0
    lock_object = threading.RLock()


    def task():
    print("开始")
    lock_object.acquire() # 第1个抵达的线程进入并上锁,其他线程就需要再此等待。
    global num
    for i in range(1000000):
    num += 1
    lock_object.release() # 线程出去,并解开锁,其他线程就可以进入并执行了
    print(num)


    for i in range(2):
    t = threading.Thread(target=task)
    t.start()

RLock支持多次申请锁和多次释放;Lock不支持。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import threading
import time

lock_object = threading.RLock()


def task():
print("开始")
lock_object.acquire()
lock_object.acquire()
print(123)
lock_object.release()
lock_object.release()


for i in range(3):
t = threading.Thread(target=task)
t.start()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import threading
lock = threading.RLock()

# 程序员A开发了一个函数,函数可以被其他开发者调用,内部需要基于锁保证数据安全。
def func():
with lock:
pass

# 程序员B开发了一个函数,可以直接调用这个函数。
def run():
print("其他功能")
func() # 调用程序员A写的func函数,内部用到了锁。
print("其他功能")

# 程序员C开发了一个函数,自己需要加锁,同时也需要调用func函数。
def process():
with lock:
print("其他功能")
func() # ----------------> 此时就会出现多次锁的情况,只有RLock支持(Lock不支持)。
print("其他功能")

5.死锁

死锁,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import threading

num = 0
lock_object = threading.Lock()


def task():
print("开始")
lock_object.acquire() # 第1个抵达的线程进入并上锁,其他线程就需要再此等待。
lock_object.acquire() # 第1个抵达的线程进入并上锁,其他线程就需要再此等待。
global num
for i in range(1000000):
num += 1
lock_object.release() # 线程出去,并解开锁,其他线程就可以进入并执行了
lock_object.release() # 线程出去,并解开锁,其他线程就可以进入并执行了

print(num)


for i in range(2):
t = threading.Thread(target=task)
t.start()


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import threading
import time

lock_1 = threading.Lock()
lock_2 = threading.Lock()


def task1():
lock_1.acquire()
time.sleep(1)
lock_2.acquire()
print(11)
lock_2.release()
print(111)
lock_1.release()
print(1111)


def task2():
lock_2.acquire()
time.sleep(1)
lock_1.acquire()
print(22)
lock_1.release()
print(222)
lock_2.release()
print(2222)


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

t2 = threading.Thread(target=task2)
t2.start()

6.线程池

Python3中官方才正式提供线程池。

线程不是开的越多越好,开的多了可能会导致系统的性能更低了,例如:如下的代码是不推荐在项目开发中编写。

不建议:无限制的创建线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
import threading


def task(video_url):
pass

url_list = ["www.xxxx-{}.com".format(i) for i in range(30000)]

for url in url_list:
t = threading.Thread(target=task, args=(url,))
t.start()

# 这种每次都创建一个线程去操作,创建任务的太多,线程就会特别多,可能效率反倒降低了。

建议:使用线程池

示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import time
from concurrent.futures import ThreadPoolExecutor

# pool = ThreadPoolExecutor(100)
# pool.submit(函数名,参数1,参数2,参数...)


def task(video_url,num):
print("开始执行任务", video_url)
time.sleep(5)

# 创建线程池,最多维护10个线程。
pool = ThreadPoolExecutor(10)

url_list = ["www.xxxx-{}.com".format(i) for i in range(300)]

for url in url_list:
# 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待。
pool.submit(task, url,2)

print("END")

示例2:等待线程池的任务执行完毕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import time
from concurrent.futures import ThreadPoolExecutor


def task(video_url):
print("开始执行任务", video_url)
time.sleep(5)


# 创建线程池,最多维护10个线程。
pool = ThreadPoolExecutor(10)

url_list = ["www.xxxx-{}.com".format(i) for i in range(300)]
for url in url_list:
# 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待。
pool.submit(task, url)

print("执行中...")
pool.shutdown(True) # 等待线程池中的任务执行完毕后,在继续执行
print('继续往下走')

示例3:任务执行完任务,再干点其他事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import time
import random
from concurrent.futures import ThreadPoolExecutor, Future


def task(video_url):
print("开始执行任务", video_url)
time.sleep(2)
return random.randint(0, 10)


def done(response):
print("任务执行后的返回值", response.result())


# 创建线程池,最多维护10个线程。
pool = ThreadPoolExecutor(10)

url_list = ["www.xxxx-{}.com".format(i) for i in range(15)]

for url in url_list:
# 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待。
future = pool.submit(task, url)
future.add_done_callback(done) # 是子主线程执行

# 可以做分工,例如:task专门下载,done专门将下载的数据写入本地文件。

示例4:最终统一获取结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import time
import random
from concurrent.futures import ThreadPoolExecutor,Future


def task(video_url):
print("开始执行任务", video_url)
time.sleep(2)
return random.randint(0, 10)


# 创建线程池,最多维护10个线程。
pool = ThreadPoolExecutor(10)

future_list = []

url_list = ["www.xxxx-{}.com".format(i) for i in range(15)]
for url in url_list:
# 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待。
future = pool.submit(task, url)
future_list.append(future)

pool.shutdown(True)
for fu in future_list:
print(fu.result())

案例:基于线程池下载豆瓣图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
26044585,Hush,https://hbimg.huabanimg.com/51d46dc32abe7ac7f83b94c67bb88cacc46869954f478-aP4Q3V
19318369,柒十一,https://hbimg.huabanimg.com/703fdb063bdc37b11033ef794f9b3a7adfa01fd21a6d1-wTFbnO
15529690,Law344,https://hbimg.huabanimg.com/b438d8c61ed2abf50ca94e00f257ca7a223e3b364b471-xrzoQd
18311394,Jennah·,https://hbimg.huabanimg.com/4edba1ed6a71797f52355aa1de5af961b85bf824cb71-px1nZz
18009711,可洛爱画画,https://hbimg.huabanimg.com/03331ef39b5c7687f5cc47dbcbafd974403c962ae88ce-Co8AUI
30574436,花姑凉~,https://hbimg.huabanimg.com/2f5b657edb9497ff8c41132e18000edb082d158c2404-8rYHbw
17740339,小巫師,https://hbimg.huabanimg.com/dbc6fd49f1915545cc42c1a1492a418dbaebd2c21bb9-9aDqgl
18741964,桐末tonmo,https://hbimg.huabanimg.com/b60cee303f62aaa592292f45a1ed8d5be9873b2ed5c-gAJehO
30535005,TANGZHIQI,https://hbimg.huabanimg.com/bbd08ee168d54665bf9b07899a5c4a4d6bc1eb8af77a4-8Gz3K1
31078743,你的老杨,https://hbimg.huabanimg.com/c46fbc3c9a01db37b8e786cbd7174bbd475e4cda220f4-F1u7MX
25519376,尺尺寸,https://hbimg.huabanimg.com/ee29ee198efb98f970e3dc2b24c40d89bfb6f911126b6-KGvKes
21113978,C-CLong,https://hbimg.huabanimg.com/7fa6b2a0d570e67246b34840a87d57c16a875dba9100-SXsSeY
24674102,szaa,https://hbimg.huabanimg.com/0716687b0df93e8c3a8e0925b6d2e4135449cd27597c4-gWdv24
30508507,爱起床的小灰灰,https://hbimg.huabanimg.com/4eafdbfa21b2f300a7becd8863f948e5e92ef789b5a5-1ozTKq
12593664,yokozen,https://hbimg.huabanimg.com/cd07bbaf052b752ed5c287602404ea719d7dd8161321b-cJtHss
16899164,一阵疯,https://hbimg.huabanimg.com/0940b557b28892658c3bcaf52f5ba8dc8402100e130b2-G966Uz
847937,卩丬My㊊伴er彎,https://hbimg.huabanimg.com/e2d6bb5bc8498c6f607492a8f96164aa2366b104e7a-kWaH68
31010628,慢慢即漫漫,https://hbimg.huabanimg.com/c4fb6718907a22f202e8dd14d52f0c369685e59cfea7-82FdsK
13438168,海贼玩跑跑,https://hbimg.huabanimg.com/1edae3ce6fe0f6e95b67b4f8b57c4cebf19c501b397e-BXwiW6
28593155,源稚生,https://hbimg.huabanimg.com/626cfd89ca4c10e6f875f3dfe1005331e4c0fd7fd429-9SeJeQ
28201821,合伙哼哼,https://hbimg.huabanimg.com/f59d4780531aa1892b80e0ec94d4ec78dcba08ff18c416-769X6a
28255146,漫步AAA,https://hbimg.huabanimg.com/3c034c520594e38353a039d7e7a5fd5e74fb53eb1086-KnpLaL
30537613,配䦹,https://hbimg.huabanimg.com/efd81d22c1b1a2de77a0e0d8e853282b83b6bbc590fd-y3d4GJ
22665880,日后必火,https://hbimg.huabanimg.com/69f0f959979a4fada9e9e55f565989544be88164d2b-INWbaF
16748980,keer521521,https://hbimg.huabanimg.com/654953460733026a7ef6e101404055627ad51784a95c-B6OFs4
30536510,“西辞”,https://hbimg.huabanimg.com/61cfffca6b2507bf51a507e8319d68a8b8c3a96968f-6IvMSk
30986577,艺成背锅王,https://hbimg.huabanimg.com/c381ecc43d6c69758a86a30ebf72976906ae6c53291f9-9zroHF
26409800,CsysADk7,https://hbimg.huabanimg.com/bf1d22092c2070d68ade012c588f2e410caaab1f58051-ahlgLm
30469116,18啊全阿,https://hbimg.huabanimg.com/654953460733026a7ef6e101404055627ad51784a95c-B6OFs4
17473505,椿の花,https://hbimg.huabanimg.com/0e38d810e5a24f91ebb251fd3aaaed8bb37655b14844c-pgNJBP
19165177,っ思忆゜♪,https://hbimg.huabanimg.com/4815ea0e4905d0f3bb82a654b481811dadbfe5ce2673-vMVr0B
16059616,格林熊丶,https://hbimg.huabanimg.com/8760a2b08d87e6ed4b7a9715b1a668176dbf84fec5b-jx14tZ
30734152,sCWVkJDG,https://hbimg.huabanimg.com/f31a5305d1b8717bbfb897723f267d316e58e7b7dc40-GD3e22
24019677,虚无本心,https://hbimg.huabanimg.com/6fdfa9834abe362e978b517275b06e7f0d5926aa650-N1xCXE
16670283,Y-雨后天空,https://hbimg.huabanimg.com/a3bbb0045b536fc27a6d2effa64a0d43f9f5193c177f-I2vHaI
21512483,汤姆2,https://hbimg.huabanimg.com/98cc50a61a7cc9b49a8af754ffb26bd15764a82f1133-AkiU7D
16441049,笑潇啸逍小鱼,https://hbimg.huabanimg.com/ae8a70cd85aff3a8587ff6578d5cf7620f3691df13e46-lmrIi9
24795603,⁢⁢⁢⁢⁢v,https://hbimg.huabanimg.com/a7183cc3a933aa129d7b3230bf1378fd8f5857846cc5-3tDtx3
29819152,妮玛士珍多,https://hbimg.huabanimg.com/ca4ecb573bf1ff0415c7a873d64470dedc465ea1213c6-RAkArS
19101282,陈勇敢❤,https://hbimg.huabanimg.com/ab6d04ebaff3176e3570139a65155856871241b58bc6-Qklj2E
28337572,爱意随风散,https://hbimg.huabanimg.com/117ad8b6eeda57a562ac6ab2861111a793ca3d1d5543-SjWlk2
17342758,幸运instant,https://hbimg.huabanimg.com/72b5f9042ec297ae57b83431123bc1c066cca90fa23-3MoJNj
18483372,Beau染,https://hbimg.huabanimg.com/077115cb622b1ff3907ec6932e1b575393d5aae720487-d1cdT9
22127102,栽花的小蜻蜓,https://hbimg.huabanimg.com/6c3cbf9f27e17898083186fc51985e43269018cc1e1df-QfOIBG
13802024,LoveHsu,https://hbimg.huabanimg.com/f720a15f8b49b86a7c1ee4951263a8dbecfe3e43d2d-GPEauV
22558931,白驹过隙丶梨花泪う,https://hbimg.huabanimg.com/e49e1341dfe5144da5c71bd15f1052ef07ba7a0e1296b-jfyfDJ
11762339,cojoy,https://hbimg.huabanimg.com/5b27f876d5d391e7c4889bc5e8ba214419eb72b56822-83gYmB
30711623,雪碧学长呀,https://hbimg.huabanimg.com/2c288a1535048b05537ba523b3fc9eacc1e81273212d1-nr8M4t
18906718,西霸王,https://hbimg.huabanimg.com/7b02ad5e01bd8c0a29817e362814666a7800831c154a6-AvBDaG
31037856,邵阳的小哥哥,https://hbimg.huabanimg.com/654953460733026a7ef6e101404055627ad51784a95c-B6OFs4
26830711,稳健谭,https://hbimg.huabanimg.com/51547ade3f0aef134e8d268cfd4ad61110925aefec8a-NKPEYX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import os
import requests
from concurrent.futures import ThreadPoolExecutor


def download(file_name, image_url):
res = requests.get(
url=image_url,
headers={
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
}
)
# 检查images目录是否存在?不存在,则创建images目录
if not os.path.exists("images"):
# 创建images目录
os.makedirs("images")
file_path = os.path.join("images", file_name)
# 2.将图片的内容写入到文件
with open(file_path, mode='wb') as img_object:
img_object.write(res.content)


# 创建线程池,最多维护10个线程。
pool = ThreadPoolExecutor(10)

with open("mv.csv", mode='r', encoding='utf-8') as file_object:
for line in file_object:
nid, name, url = line.split(",")
file_name = "{}.png".format(name)
pool.submit(download, file_name, url)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import os
import requests
from concurrent.futures import ThreadPoolExecutor


def download(image_url):
res = requests.get(
url=image_url,
headers={
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
}
)
return res


def outer(file_name):
def save(response):
res = response.result()
# 写入本地
# # 检查images目录是否存在?不存在,则创建images目录
if not os.path.exists("images"):
# 创建images目录
os.makedirs("images")

file_path = os.path.join("images", file_name)

# # 2.将图片的内容写入到文件
with open(file_path, mode='wb') as img_object:
img_object.write(res.content)

return save


# 创建线程池,最多维护10个线程。
pool = ThreadPoolExecutor(10)

with open("mv.csv", mode='r', encoding='utf-8') as file_object:
for line in file_object:
nid, name, url = line.split(",")
file_name = "{}.png".format(name)
fur = pool.submit(download, url)
fur.add_done_callback(outer(file_name))

7.单例模式(扩展)

面向对象 + 多线程相关的一个面试题(以后项目和源码中会用到)。

之前写一个类,每次执行 类() 都会实例化一个类的对象。

1
2
3
4
5
6
7
class Foo:
pass

obj1 = Foo()

obj2 = Foo()
print(obj1,obj2)

以后开发会遇到单例模式,每次实例化类的对象时,都是最开始创建的那个对象,不再重复创建对象。

  • 简单的实现单例模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Singleton:
    instance = None

    def __init__(self, name):
    self.name = name

    def __new__(cls, *args, **kwargs):
    # 返回空对象
    if cls.instance:
    return cls.instance
    cls.instance = object.__new__(cls)
    return cls.instance

    obj1 = Singleton('alex')
    obj2 = Singleton('SB')

    print(obj1,obj2)
  • 多线程执行单例模式,有BUG

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    import threading
    import time


    class Singleton:
    instance = None

    def __init__(self, name):
    self.name = name

    def __new__(cls, *args, **kwargs):
    if cls.instance:
    return cls.instance
    time.sleep(0.1)
    cls.instance = object.__new__(cls)
    return cls.instance


    def task():
    obj = Singleton('x')
    print(obj)


    for i in range(10):
    t = threading.Thread(target=task)
    t.start()

  • 加锁解决BUG

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import threading
    import time
    class Singleton:
    instance = None
    lock = threading.RLock()

    def __init__(self, name):
    self.name = name

    def __new__(cls, *args, **kwargs):
    with cls.lock:
    if cls.instance:
    return cls.instance
    time.sleep(0.1)
    cls.instance = object.__new__(cls)
    return cls.instance

    def task():
    obj = Singleton('x')
    print(obj)

    for i in range(10):
    t = threading.Thread(target=task)
    t.start()
  • 加判断,提升性能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    import threading
    import time
    class Singleton:
    instance = None
    lock = threading.RLock()

    def __init__(self, name):
    self.name = name

    def __new__(cls, *args, **kwargs):

    if cls.instance:
    return cls.instance
    with cls.lock:
    if cls.instance:
    return cls.instance
    time.sleep(0.1)
    cls.instance = object.__new__(cls)
    return cls.instance

    def task():
    obj = Singleton('x')
    print(obj)

    for i in range(10):
    t = threading.Thread(target=task)
    t.start()

    # 执行1000行代码

    data = Singleton('asdfasdf')
    print(data)

总结

  1. 进程和线程的区别和应用场景。
  2. 什么是GIL锁
  3. 多线程和线程池的使用。
  4. 线程安全 & 线程锁 & 死锁
  5. 单例模式

作业

  1. 简述进程和线程的区别以及应用场景。

  2. 什么是GIL锁

  3. 手写单例模式

  4. 程序从flag a执行到falg b的时间大致是多少秒?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import threading
    import time
    def _wait():
    time.sleep(60)
    # flag a
    t = threading.Thread(target=_wait)
    t.setDaemon(False)
    t.start()
    # flag b
  5. 程序从flag a执行到falg b的时间大致是多少秒?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import threading
    import time
    def _wait():
    time.sleep(60)
    # flag a
    t = threading.Thread(target=_wait)
    t.setDaemon(True)
    t.start()
    # flag b
  6. 程序从flag a执行到falg b的时间大致是多少秒?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import threading
    import time
    def _wait():
    time.sleep(60)
    # flag a
    t = threading.Thread(target=_wait)
    t.start()
    t.join()
    # flag b
  7. 读程序,请确认执行到最后number是否一定为0

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import threading
    loop = int(1E7)
    def _add(loop = 1):
    global number
    for _ in range(loop):
    number += 1
    def _sub(loop = 1):
    global number
    for _ in range(loop):
    number -= 1
    number = 0
    ta = threading.Thread(target=_add,args=(loop,))
    ts = threading.Thread(target=_sub,args=(loop,))
    ta.start()
    ta.join()
    ts.start()
    ts.join()
  8. 读程序,请确认执行到最后number是否一定为0

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import threading
    loop = int(1E7)
    number = 0

    def _add(loop = 1):
    global number
    for _ in range(loop):
    number += 1
    def _sub(loop = 1):
    global number
    for _ in range(loop):
    number -= 1

    ta = threading.Thread(target=_add,args=(loop,))
    ts = threading.Thread(target=_sub,args=(loop,))
    ta.start()
    ts.start()
    ta.join()
    ts.join()
  9. data.txt 文件中共有 10000 条数据,请为每 100行 数据创建一个线程,并在线程中把当前100条数据的num列相加并打印。

    1
    2
    3
    4
    5
    6
    7
    subscription_id,erotic,num
    ASDFOKASDJF423KASDFJASDF,5,1
    FSD23R23SFSDF4DFGDFGDFGDF,5,99
    ASDDSFGWERTCERT44GFGDSFG,5,2
    FFFFFFSDSVFG5RTFGDDFFFFA,5,11
    ASDFASDF3234XCVWEGDFGSAF,5,1
    ...

day21 网络编程(下)

image-20210222072456318

课程目标:学会网络编程开发的必备知识点。

今日概要:

  • OSI7 层模型
  • TCP和UDP
  • 粘包
  • 阻塞和非阻塞
  • IO多路复用

1. OSI 7层模型

image-20210222072652918

image-20210209113923860

OSI的7层模型对于大家来说可能不太好理解,所以我们通过一个案例来讲解:

image-20210209101325856

假设,你在浏览器上输入了一些关键字,内部通过DNS找到对应的IP后,再发送数据时内部会做如下的事:

  • 应用层:规定数据的格式。

    1
    "GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n"
  • 表示层:对应用层数据的编码、压缩(解压缩)、分块、加密(解密)等任务。

    1
    "GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8')
  • 会话层:负责与目标建立、中断连接。

    1
    在发送数据之前,需要会先发送 “连接” 的请求,与远程建立连接后,再发送数据。当然,发送完毕之后,也涉及中断连接的操作。
  • 传输层:建立端口到端口的通信,其实就确定双方的端口信息。

    1
    2
    3
    4
    数据:"GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8')
    端口:
    - 目标:80
    - 本地:6784
  • 网络层:标记目标IP信息(IP协议层)

    1
    2
    3
    4
    5
    6
    7
    数据:"GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8')
    端口:
    - 目标:80
    - 本地:6784
    IP:
    - 目标IP:110.242.68.3(百度)
    - 本地IP:192.168.10.1
  • 数据链路层:对数据进行分组并设置源和目标mac地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    数据:"POST /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8')
    端口:
    - 目标:80
    - 本地:6784
    IP:
    - 目标IP:110.242.68.3(百度)
    - 本地IP:192.168.10.1
    MAC:
    - 目标MAC:FF-FF-FF-FF-FF-FF
    - 本机MAC:11-9d-d8-1a-dd-cd
  • 物理层:将二进制数据在物理媒体上传输。

    1
    通过网线将二进制数据发送出去

image-20210209113923860

每一层各司其职,最终保证数据呈现在到用户手中。

简单的可以理解为发快递:将数据外面套了7个箱子,最终用户收到箱子时需要打开7个箱子才能拿到数据。而在运输的过程中有些箱子是会被拆开并替换的,例如:

1
2
3
最终运送目标:上海 ~ 北京(中途可能需要中转站),在中转站会会打开箱子查看信息,在进行转发。
- 对于二级中转站(二层交换机):拆开数据链路层的箱子,查看mac地址信息。
- 对于三级中转站(路由器或三层交换机):拆开网络层的箱子,查看IP信息。

在开发过程中其实只能体现:应用层、表示层、会话层、传输层,其他层的处理都是在网络设备中自动完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('110.242.68.3', 80)) # 向服务端发送了数据包


key = "你好"
# 应用层
content = "GET /s?wd={} http1.1\r\nHost:www.baidu.com\r\n\r\n".format(key)
# 表示层
content = content.encode("utf-8")

client.sendall(content)
result = client.recv(8196)
print(result.decode('utf-8'))

# 会话层 & 传输层
client.close()

2. UDP和TCP协议

协议,其实就是规定 连接、收发数据的一些规定。

在OSI的 传输层 除了定义端口信息以外,常见的还可以指定UDP或TCP的协议,协议不同连接和传输数据的细节也会不同。

  • UDP(User Data Protocol)用户数据报协议, 是⼀个⽆连接的简单的⾯向数据报的传输层协议。 UDP不提供可靠性, 它只是把应⽤程序传给IP层的数据报发送出去, 但是并不能保证它们能到达⽬的地。 由于UDP在传输数据报前不⽤在客户和服务器之间建⽴⼀个连接, 且没有超时重发等机制, 故⽽传输速度很快。

    1
    常见的有:语音通话、视频通话、实时游戏画面 等。
  • TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接,然后再进行收发数据。

    1
    常见有:网站、手机APP数据获取等。

2.1 UDP和TCP 示例代码

UDP示例如下:

  • 服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import socket

    server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    server.bind(('127.0.0.1', 8002))

    while True:
    data, (host, port) = server.recvfrom(1024) # 阻塞
    print(data, host, port)
    server.sendto("好的".encode('utf-8'), (host, port))
  • 客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import socket

    client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    while True:
    text = input("请输入要发送的内容:")
    if text.upper() == 'Q':
    break
    client.sendto(text.encode('utf-8'), ('127.0.0.1', 8002))
    data, (host, port) = client.recvfrom(1024)
    print(data.decode('utf-8'))

    client.close()

TCP示例如下:

  • 服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import socket

    # 1.监听本机的IP和端口
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1', 8001))
    sock.listen(5)

    while True:
    # 2.等待,有人来连接(阻塞)
    conn, addr = sock.accept()

    # 3.等待,连接者发送消息(阻塞)
    client_data = conn.recv(1024)
    print(client_data)

    # 4.给连接者回复消息
    conn.sendall(b"hello world")

    # 5.关闭连接
    conn.close()

    # 6.停止服务端程序
    sock.close()
  • 客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import socket

    # 1. 向指定IP发送连接请求
    client = socket.socket()
    client.connect(('127.0.0.1', 8001))

    # 2. 连接成功之后,发送消息
    client.sendall(b'hello')

    # 3. 等待,消息的回复(阻塞)
    reply = client.recv(1024)
    print(reply)

    # 4. 关闭连接
    client.close()

2.2 TCP三次握手和四次挥手

这是一个常见的面试题。

image-20210222072652918

image-20210209113923860

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

网络中的双方想要基于TCP连接进行通信,必须要经过:

  • 创建连接,客户端和服务端要进行三次握手。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 服务端
    import socket

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1', 8001))
    sock.listen(5)

    while True:
    conn, addr = sock.accept() # 等待客户端连接
    ...
    1
    2
    3
    4
    # 客户端
    import socket
    client = socket.socket()
    client.connect(('127.0.0.1', 8001)) # 发起连接
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
          客户端                                                服务端

    1. SYN-SENT --> <seq=100><CTL=SYN> --> SYN-RECEIVED

    2. ESTABLISHED <-- <seq=300><ack=101><CTL=SYN,ACK> <-- SYN-RECEIVED

    3. ESTABLISHED --> <seq=101><ack=301><CTL=ACK> --> ESTABLISHED


    At this point, both the client and server have received an acknowledgment of the connection. The steps 1, 2 establish the connection parameter (sequence number) for one direction and it is acknowledged. The steps 2, 3 establish the connection parameter (sequence number) for the other direction and it is acknowledged. With these, a full-duplex communication is established.
  • 传输数据

    1
    在收发数据的过程中,只有有数据的传送就会有应答(ack),如果没有ack,那么内部会尝试重复发送。
  • 关闭连接,客户端和服务端要进行4次挥手。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import socket

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1', 8001))
    sock.listen(5)
    while True:
    conn, addr = sock.accept()
    ...
    conn.close() # 关闭连接
    sock.close()
    1
    2
    3
    4
    5
    6
    import socket

    client = socket.socket()
    client.connect(('127.0.0.1', 8001))
    ...
    client.close() # 关闭连接
    1
    2
    3
    4
    5
    6
    7
    8
    9
         TCP A                                                TCP B

    1. FIN-WAIT-1 --> <seq=100><ack=300><CTL=FIN,ACK> --> CLOSE-WAIT

    2. FIN-WAIT-2 <-- <seq=300><ack=101><CTL=ACK> <-- CLOSE-WAIT

    3. TIME-WAIT <-- <seq=300><ack=101><CTL=FIN,ACK> <-- LAST-ACK

    4. TIME-WAIT --> <seq=101><ack=301><CTL=ACK> --> CLOSED

3. 粘包

image-20210222072652918

image-20210215074546610

两台电脑在进行收发数据时,其实不是直接将数据传输给对方。

  • 对于发送者,执行 sendall/send 发送消息时,是将数据先发送至自己网卡的 写缓冲区 ,再由缓冲区将数据发送给到对方网卡的读缓冲区。
  • 对于接受者,执行 recv 接收消息时,是从自己网卡的读缓冲区获取数据。

所以,如果发送者连续快速的发送了2条信息,接收者在读取时会认为这是1条信息,即:2个数据包粘在了一起。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# socket客户端(发送者)
import socket

client = socket.socket()
client.connect(('127.0.0.1', 8001))

client.sendall('alex正在吃'.encode('utf-8'))
client.sendall('翔'.encode('utf-8'))

client.close()


# socket服务端(接收者)
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 8001))
sock.listen(5)
conn, addr = sock.accept()

client_data = conn.recv(1024)
print(client_data.decode('utf-8'))

conn.close()
sock.close()

如何解决粘包的问题?

每次发送的消息时,都将消息划分为 头部(固定字节长度) 和 数据 两部分。例如:头部,用4个字节表示后面数据的长度。

  • 发送数据,先发送数据的长度,再发送数据(或拼接起来再发送)。
  • 接收数据,先读4个字节就可以知道自己这个数据包中的数据长度,再根据长度读取到数据。

对于头部需要一个数字并固定为4个字节,这个功能可以借助python的struct包来实现:

1
2
3
4
5
6
7
8
9
10
11
12
import struct

# ########### 数值转换为固定4个字节,四个字节的范围 -2147483648 <= number <= 2147483647 ###########
v1 = struct.pack('i', 199)
print(v1) # b'\xc7\x00\x00\x00'

for item in v1:
print(item, bin(item))

# ########### 4个字节转换为数字 ###########
v2 = struct.unpack('i', v1) # v1= b'\xc7\x00\x00\x00'
print(v2) # (199,)

image-20210215090446549

示例代码:

  • 服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    import socket
    import struct

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1', 8001))
    sock.listen(5)
    conn, addr = sock.accept()

    # 固定读取4字节
    header1 = conn.recv(4)
    data_length1 = struct.unpack('i', header1)[0] # 数据字节长度 21
    has_recv_len = 0
    data1 = b""
    while True:
    length = data_length1 - has_recv_len
    if length > 1024:
    lth = 1024
    else:
    lth = length
    chunk = conn.recv(lth) # 可能一次收不完,自己可以计算长度再次使用recv收取,指导收完为止。 1024*8 = 8196
    data1 += chunk
    has_recv_len += len(chunk)
    if has_recv_len == data_length1:
    break
    print(data1.decode('utf-8'))

    # 固定读取4字节
    header2 = conn.recv(4)
    data_length2 = struct.unpack('i', header2)[0] # 数据字节长度
    data2 = conn.recv(data_length2) # 长度
    print(data2.decode('utf-8'))

    conn.close()
    sock.close()
  • 客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import socket
    import struct

    client = socket.socket()
    client.connect(('127.0.0.1', 8001))

    # 第一条数据
    data1 = 'alex正在吃'.encode('utf-8')

    header1 = struct.pack('i', len(data1))

    client.sendall(header1)
    client.sendall(data1)

    # 第二条数据
    data2 = '翔'.encode('utf-8')
    header2 = struct.pack('i', len(data2))
    client.sendall(header2)
    client.sendall(data2)

    client.close()

案例:消息 & 文件上传

  • 服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    import os
    import json
    import socket
    import struct


    def recv_data(conn, chunk_size=1024):
    # 获取头部信息:数据长度
    has_read_size = 0
    bytes_list = []
    while has_read_size < 4:
    chunk = conn.recv(4 - has_read_size)
    has_read_size += len(chunk)
    bytes_list.append(chunk)
    header = b"".join(bytes_list)
    data_length = struct.unpack('i', header)[0]

    # 获取数据
    data_list = []
    has_read_data_size = 0
    while has_read_data_size < data_length:
    size = chunk_size if (data_length - has_read_data_size) > chunk_size else data_length - has_read_data_size
    chunk = conn.recv(size)
    data_list.append(chunk)
    has_read_data_size += len(chunk)

    data = b"".join(data_list)

    return data


    def recv_file(conn, save_file_name, chunk_size=1024):
    save_file_path = os.path.join('files', save_file_name)
    # 获取头部信息:数据长度
    has_read_size = 0
    bytes_list = []
    while has_read_size < 4:
    chunk = conn.recv(4 - has_read_size)
    bytes_list.append(chunk)
    has_read_size += len(chunk)
    header = b"".join(bytes_list)
    data_length = struct.unpack('i', header)[0]

    # 获取数据
    file_object = open(save_file_path, mode='wb')
    has_read_data_size = 0
    while has_read_data_size < data_length:
    size = chunk_size if (data_length - has_read_data_size) > chunk_size else data_length - has_read_data_size
    chunk = conn.recv(size)
    file_object.write(chunk)
    file_object.flush()
    has_read_data_size += len(chunk)
    file_object.close()


    def run():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # IP可复用
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    sock.bind(('127.0.0.1', 8001))
    sock.listen(5)
    while True:
    conn, addr = sock.accept()

    while True:
    # 获取消息类型
    message_type = recv_data(conn).decode('utf-8')
    if message_type == 'close': # 四次挥手,空内容。
    print("关闭连接")
    break
    # 文件:{'msg_type':'file', 'file_name':"xxxx.xx" }
    # 消息:{'msg_type':'msg'}
    message_type_info = json.loads(message_type)
    if message_type_info['msg_type'] == 'msg':
    data = recv_data(conn)
    print("接收到消息:", data.decode('utf-8'))
    else:
    file_name = message_type_info['file_name']
    print("接收到文件,要保存到:", file_name)
    recv_file(conn, file_name)

    conn.close()
    sock.close()


    if __name__ == '__main__':
    run()

  • 客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    import os
    import json
    import socket
    import struct


    def send_data(conn, content):
    data = content.encode('utf-8')
    header = struct.pack('i', len(data))
    conn.sendall(header)
    conn.sendall(data)


    def send_file(conn, file_path):
    file_size = os.stat(file_path).st_size
    header = struct.pack('i', file_size)
    conn.sendall(header)

    has_send_size = 0
    file_object = open(file_path, mode='rb')
    while has_send_size < file_size:
    chunk = file_object.read(2048)
    conn.sendall(chunk)
    has_send_size += len(chunk)
    file_object.close()


    def run():
    client = socket.socket()
    client.connect(('127.0.0.1', 8001))

    while True:
    """
    请发送消息,格式为:
    - 消息:msg|你好呀
    - 文件:file|xxxx.png
    """
    content = input(">>>") # msg or file
    if content.upper() == 'Q':
    send_data(client, "close")
    break
    input_text_list = content.split('|')
    if len(input_text_list) != 2:
    print("格式错误,请重新输入")
    continue

    message_type, info = input_text_list

    # 发消息
    if message_type == 'msg':

    # 发消息类型
    send_data(client, json.dumps({"msg_type": "msg"}))

    # 发内容
    send_data(client, info)

    # 发文件
    else:
    file_name = info.rsplit(os.sep, maxsplit=1)[-1]

    # 发消息类型
    send_data(client, json.dumps({"msg_type": "file", 'file_name': file_name}))

    # 发内容
    send_file(client, info)

    client.close()


    if __name__ == '__main__':
    run()

4. 阻塞和非阻塞

默认情况下我们编写的网络编程的代码都是阻塞的(等待),阻塞主要体现在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# ################### socket服务端(接收者)###################
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 8001))
sock.listen(5)

# 阻塞
conn, addr = sock.accept()

# 阻塞
client_data = conn.recv(1024)
print(client_data.decode('utf-8'))

conn.close()
sock.close()


# ################### socket客户端(发送者) ###################
import socket

client = socket.socket()

# 阻塞
client.connect(('127.0.0.1', 8001))

client.sendall('alex正在吃翔'.encode('utf-8'))

client.close()

如果想要让代码变为非阻塞,需要这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# ################### socket服务端(接收者)###################
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

sock.setblocking(False) # 加上就变为了非阻塞

sock.bind(('127.0.0.1', 8001))
sock.listen(5)

# 非阻塞
conn, addr = sock.accept()

# 非阻塞
client_data = conn.recv(1024)
print(client_data.decode('utf-8'))

conn.close()
sock.close()

# ################### socket客户端(发送者) ###################
import socket

client = socket.socket()

client.setblocking(False) # 加上就变为了非阻塞

# 非阻塞
client.connect(('127.0.0.1', 8001))

client.sendall('alex正在吃翔'.encode('utf-8'))

client.close()

image-20210216093805252

如果代码变成了非阻塞,程序运行时一旦遇到 acceptrecvconnect 就会抛出 BlockingIOError 的异常。

这不是代码编写的有错误,而是原来的IO阻塞变为非阻塞之后,由于没有接收到相关的IO请求抛出的固定错误。

非阻塞的代码一般与IO多路复用结合,可以迸发出更大的作用。

5. IO多路复用

I/O多路复用指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

IO多路复用 + 非阻塞,可以实现让TCP的服务端同时处理多个客户端的请求,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# ################### socket服务端 ###################
import select
import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(False) # 加上就变为了非阻塞
server.bind(('127.0.0.1', 8001))
server.listen(5)

inputs = [server, ] # socket对象列表 -> [server, 第一个客户端连接conn ]

while True:
# 当 参数1 序列中的socket对象发生可读时(accetp和read),则获取发生变化的对象并添加到 r列表中。
# r = []
# r = [server,]
# r = [第一个客户端连接conn,]
# r = [server,]
# r = [第一个客户端连接conn,第二个客户端连接conn]
# r = [第二个客户端连接conn,]
r, w, e = select.select(inputs, [], [], 0.05)
for sock in r:
# server
if sock == server:
conn, addr = sock.accept() # 接收新连接。
print("有新连接")
# conn.sendall()
# conn.recv("xx")
inputs.append(conn)
else:
data = sock.recv(1024)
if data:
print("收到消息:", data)
else:
print("关闭连接")
inputs.remove(sock)
# 干点其他事 20s
"""
优点:
1. 干点那其他的事。
2. 让服务端支持多个客户端同时来连接。
"""
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ################### socket客户端 ###################
import socket

client = socket.socket()
# 阻塞
client.connect(('127.0.0.1', 8001))


while True:
content = input(">>>")
if content.upper() == 'Q':
break
client.sendall(content.encode('utf-8'))

client.close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ################### socket客户端 ###################
import socket

client = socket.socket()
# 阻塞
client.connect(('127.0.0.1', 8001))


while True:
content = input(">>>")
if content.upper() == 'Q':
break
client.sendall(content.encode('utf-8'))

client.close() # 与服务端断开连接(四次挥手),默认会想服务端发送空数据。

IO多路复用 + 非阻塞,可以实现让TCP的客户端同时发送多个请求,例如:去某个网站发送下载图片的请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import socket
import select
import uuid
import os

client_list = [] # socket对象列表

for i in range(5):
client = socket.socket()
client.setblocking(False)

try:
# 连接百度,虽然有异常BlockingIOError,但向还是正常发送连接的请求
client.connect(('47.98.134.86', 80))
except BlockingIOError as e:
pass

client_list.append(client)

recv_list = [] # 放已连接成功,且已经把下载图片的请求发过去的socket
while True:
# w = [第一个socket对象,]
# r = [socket对象,]
r, w, e = select.select(recv_list, client_list, [], 0.1)
for sock in w:
# 连接成功,发送数据
# 下载图片的请求
sock.sendall(b"GET /nginx-logo.png HTTP/1.1\r\nHost:47.98.134.86\r\n\r\n")
recv_list.append(sock)
client_list.remove(sock)

for sock in r:
# 数据发送成功后,接收的返回值(图片)并写入到本地文件中
data = sock.recv(8196)
content = data.split(b'\r\n\r\n')[-1]
random_file_name = "{}.png".format(str(uuid.uuid4()))
with open(os.path.join("images", random_file_name), mode='wb') as f:
f.write(content)
recv_list.remove(sock)

if not recv_list and not client_list:
break

"""
优点:
1. 可以伪造除并发的现象。
"""

基于 IO多路复用 + 非阻塞的特性,无论编写socket的服务端和客户端都可以提升性能。其中

  • IO多路复用,监测socket对象是否有变化(是否连接成功?是否有数据到来等)。
  • 非阻塞,socket的connect、recv过程不再等待。

注意:IO多路复用只能用来监听 IO对象 是否发生变化,常见的有:文件是否可读写、电脑终端设备输入和输出、网络请求(常见)。

在Linux操作系统化中 IO多路复用 有三种模式,分别是:select,poll,epoll。(windows 只支持select模式)

监测socket对象是否新连接到来 or 新数据到来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
select

select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。
select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。

poll

poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。
poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。

epoll

直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

补充:socket + 非阻塞+ IO多路复用(IO操作对象都可以监测 + 文件)。

总结

  1. OSI 7层模型

    1
    应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。
  2. UDP和TCP的区别

    1
    2
    UDP,速度快但无法保证数据的准确性。
    TCP,需要先创建可靠连接,在进行收发数据(ack)。
  3. TCP的三次握手和四次挥手

  4. 为什么会有粘包?如何解决?

  5. 如何让socket请求变成非阻塞?

  6. IO多路复用的作用?

    1
    监测多个 IO对象 是否发生变化(可读/可写)。
    • IO多路复用 + 非阻塞 + socket服务端,可以让服务端同时处理多个客户端的请求。
    • IO多路复用 + 非阻塞 + socket客户端,可以向服务端同时发起多个请求。

作业(模块大作业)

请基于TCP协议实现一个网盘系统,包含客户端、服务端,各自需求如下:

  • 客户端

    • 用户注册,注册成功之后,在服务端的指定目录下为此用户创建一个文件夹,该文件夹下以后存储当前用户的数据(类似于网盘)。

    • 用户登录

    • 查看网盘目录下的所有文件(一级即可),ls命令

    • 上传文件,如果网盘已存在则重新上传(覆盖)。

    • 下载文件(进度条)

      1
      2
      3
      4
      5
      先判断要下载本地路径中是否存在该文件。
      - 不存在,直接下载
      - 存在,则让用户选择是否续传(继续下载)。
      - 续传,在上次的基础上继续下载。
      - 不续传,从头开始下载。
  • 服务端

    • 支持注册,并为用户初始化相关目录。

      1
      注册成功之后,将所有用户信息存储到特定的Excel文件中

      image-20210218163617245

    • 支持登录

    • 支持查看当前用户网盘目录下的所有文件。

    • 支持上传

    • 支持下载

day20 网络编程

image-20210220091551058

课程目标:掌握网络相关的基础知识并可以基于Python开发程序(基于网络进行数据传输)。

课程概要:

  • 网络必备基础
  • 网络编程(Python代码)
  • B/S和C/S架构

1.必备基础

你必须了解的网络相关设备和基础概念。

1.1 网络架构

假设 alex 上了一个野鸡大学买了一台电脑,电脑里存了1部小电影,整宿整宿的在宿舍反复的看。

image-20210204184431053

alex 如何想要和室友 于超 进行收发数据,可以通过一根网线来进行连接,并进行数据的传输。

image-20210204185417337

1.1.1 交换机

其他2位室友如何也想和他们的电脑相互连接然后进行资源的共享,此时就需要一个设备 【二层交换机】组件一个局域网。

1
2
3
4
5
当电脑接入交换机之后,我们需要为每台电脑分配一个IP,例如:
- 电脑1192.168.10.1
- 电脑2192.168.10.2
- 电脑3192.168.10.3
- 电脑4192.168.10.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
局域网内容个电脑之间是基于ARP协议来进行通信,例如:A电脑向 IP为192.168.10.3的另一个电脑发送消息。

第一步:A封装数据包,此时只知道目标IP不知道目标mac地址(未知mac地址时默认会设置为FF)。
第二步:将数据包发送到交换机,交换机通过广播的形式将数据发送给所有电脑。
第三步:目标电脑接收到数据包后,监测自己是否是目标IP。
- 是,收到数据并回复。
- 不是,则丢弃包。

为防止每次发送消息都是广播形式,每台电脑的内部都为维护了一个ARP表,接受到数据时(无论是否自己的)都会记录自己了解的IP和MAC的对应关系,例如:
Internet地址 物理地址
192.168.10.1 14-9d-da-2a-dd-0a
192.168.10.3 14-9d-da-2a-dd-0c
...

以便于下次在发送消息时,就知道了目标的mac地址,直接让交换机转发给指定的电脑(单播)。

同时,当有消息发送经过二层交换机时他的内容也会维护记录了交换机接口和连接的电脑的mac地址的对应关系,例如:
接口(网卡) mac地址
接口1 14-9d-da-2a-dd-0A
接口2 14-9d-da-2a-dd-0B
接口3 14-9d-da-2a-dd-0C
...
这样一来,交换机在进行数据转发时,效率就更高了。

注意:每台电脑出厂时在网卡中都设置了唯一的mac地址(不重复),网卡集成在主板上,如果更换了主板则mac地址也会变更。
image-20210221142111266

image-20210205143317465

1
2
头部信息:xxx
数据:你好
1
2
头部信息:xxx
数据:收到

1.1.2 路由器

多个宿舍之间想想要组建一个相互可以通信网络,此时需要【二层交换机】和【企业路由器】配合组建稍微大一点的局域网(同时也可缓解广播风暴)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
划分好网络结构之后,其实会给各宿舍的电脑分配IP和网关,例如:
宿舍A:
- 电脑1:192.168.10.1 网关:192.168.10.254
- 电脑2:192.168.10.2 网关:192.168.10.254
- 电脑3:192.168.10.3 网关:192.168.10.254
- 电脑4:192.168.10.4 网关:192.168.10.254
宿舍B
- 电脑1:192.168.20.1 网关:192.168.20.254
- 电脑2:192.168.20.2 网关:192.168.20.254
- 电脑3:192.168.20.3 网关:192.168.20.254
- 电脑4:192.168.20.4 网关:192.168.20.254

然后再在路由器中配置路由表(包含网段和路由器上的接口的对应关系),例如:
接口 IP
eth0 192.168.10.254(192.168.10网段)
eth1 192.168.20.254(192.168.20网段)

想与外部网络通信,需要配置网关,网关就是路由表中配置的指向此网段的IP。其实就类似于贸易出口都需要经过海关。
1
2
3
4
5
6
数据通信的过程结合了APR协议和IP协议,例如:宿舍A的电脑1向宿舍B的电脑3发送消息(目标IP:192.168.20.3)。

简化过程:
- 宿舍A的电脑1,通过广播或单播将数据发送到网管(路由器)
- 路由器接收到数据之后,再通过对应的接口把数据通过广播的形式发送到宿舍B。
注意:各自局域网内通过学习并记录相关mac地址后,就可以不再使用广播形式,而是使用单播来发送消息了。

image-20210204205902286

image-20210221145506980

1.1.3 三层交换机

三层交换机集成了 交换机 & 路由器的功能(大部分路由器功能),上述的三个设备其实可以用一个三层交换机就可以搞定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
按照下图,在三层交换机上分别做如下几件事:

1. 划分两个vlan,模拟出来路由器的两个接口。
2. 将交换机的接口划分给指定的vlan,例如:
接口123划分给一个vlan,相当于交换机连接上了路由器。
接口456划分给一个vlan,相当于交换机连接上了路由器。
3. 电脑连接上交换机。
4. 进行相应的配置。
宿舍A(左边)电脑配置:
- 电脑1192.168.10.1 网关:192.168.10.254 对应交换机接口:1
- 电脑2192.168.10.2 网关:192.168.10.254 对应交换机接口:2
- 电脑3192.168.10.3 网关:192.168.10.254 对应交换机接口:3
宿舍B(右边)电脑配置:
- 电脑1192.168.20.1 网关:192.168.20.254 对应交换机接口:4
- 电脑2192.168.20.2 网关:192.168.20.254 对应交换机接口:5
- 电脑3192.168.20.3 网关:192.168.20.254 对应交换机接口:6

交换机中的路由配置:
接口 IP
左vlan 192.168.10.254192.168.10网段)
右vlan 192.168.20.254192.168.20网段)

通过上述的配置之后,就可以实现宿舍A和宿舍B的网络通信了。

image-20210206092454106

1.1.4 小型企业基础网络架构

image-20210206225844012

1.1.5 家庭网络架构

家用路由器集成了是交换机和路由的功能(性能差、价格便宜)。

image-20210206231121896

1.1.6 互联网

image-20210206231602247

1.2 网络核心词汇

1.2.1 子网掩码和IP

之前说过,接入网络设备后,需要一个IP来代指次电脑,例如:192.168.10.1 。

IP其是一个32位的二进制,为了便于记忆就将它分为4组,每组8位,由小数点分开,例如:

1
2
3
4
5
6
二进制表示:00000000.10010111.11111111.00001111
十进制表示:251.151.255.15

0~255
192.178.11.211
192.178.11.311

在网络中的每台电脑都会有一个IP与之绑定,这样通过IP就可以找到相应的电脑。

一个IP地址可以划分为两个部分,即:网络地址 + 主机地址。

  • 问题1:如何确定网络地址和主机地址呢?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    通过子网掩码就可以确定IP的网络地址和主机地址。

    示例1:
    IP:192.168.1.199 11000000.10101000.00000001.11000111
    子网掩码:255.255.255.0 11111111.11111111.11111111.00000000
    此时,网络地址就是前24位 + 主机地址是后8位。你可能见过有些IP这样写 192.168.1.199/24,意思也是前24位是网络地址。


    示例2:
    IP:192.168.99.254 11000000.10101000.01100011.11111110
    子网掩码:255.255.240.0 11111111.11111111.11111100.00000000
    此时,网络地址就是前22位 + 主机地址是后10位。你可能见过有些IP这样写 192.168.99.254/22,意思也是前22位是网络地址。
  • 问题2:划分 网络地址 + 主机地址 的意义是什么?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    网络地址相同的IP,也称为属于同一个网段。
    在局域网内只有同一个网段的IP才能相互通信,不同网段IP想要通信需要借助路由的转发才能通信。

    当了解子网掩码之后,其实就可以确定某个网段可以容纳的主机个数,例如:
    【IP: 192.168.10.2 掩码:255.255.255.0】 和 【192.168.10.251 掩码:255.255.255.0】 数据同一个网段。

    示例网段的主机范围:11000000.10101000.00001010. 00000001 ~ 11000000.10101000.00001010. 11111110
    -------------------------- --------------------------
    网络地址 网络地址
    192.168.10.1 ~ 192.168.10.254

    【IP: 192.168.8.1 掩码:255.255.240.0】 和 【192.168.11.254 掩码:255.255.240.0】 数据同一个网段。
    子网掩码:255.255.240.0
    示例网段的主机范围:11000000.10101000.000010 00.00000001 ~ 11000000.10101000.000010 11.11111110
    11111111.11111111.111111 00.00000000
    ------------------------ ------------------------
    网络地址 网络地址
    192.168.8.1 ~ 192.168.11.254

    【IP: 192.168.96.1 掩码:255.255.240.0】 和 【192.168.99.254 掩码:255.255.240.0】 数据同一个网段。
    示例网段的主机范围:11000000.10101000.011000 00.00000001 ~ 11000000.10101000.011000 11.11111110

    ------------------------ ------------------------
    网络地址 网络地址
    192.168.96.1 ~ 192.168.99.254

    image-20210207001644433
    image-20210207135331788

1.2.2 DHCP

在一个局域网内想要给某台电脑分配IP有两种方式:

  • 手动设置,打开指定菜单栏在里面输入相应的IP信息。

  • 自动获取

    1
    2
    3
    4
    - 在电脑端,IP地址获取方式设置为自动。
    - 在路由器或三层交换机,开启DHCP服务,并设置IP地址池。(家用路由器上也是基于DHCP服务自动分配的IP)

    这样,电脑只要连接只该网络,DHCP服务就会为它自动分配IP、子网掩码、网关。

image-20210207143152690

image-20210207143340596

1.2.3 内网和公网IP

1
2
3
4
一般情况下,内网IP都用这些(潜规则):
- 10.0.0.0 到 10.255.255.255
- 172.16.0.0 到172.31.255.255
- 192.168.0.0 到192.168.255.255

之前我们自己在一个局域网内为电脑分配的IP都称为内网IP,基于内网IP可以在一个局域网内进行相互通信(也需要相关的配置)。

image-20210207211646642

如果想要通过互联网进行通信,就必须借助公网IP。例如,右边家庭电脑想访问左边某公司服务器上的部署的网站:

  • 第一步:左边公司,去运营商申请公网的固定IP(办理专线宽带时运营商会分配至少1个固定的IP地址),其实运营商就是将你拉的这个专线和固定IP创建绑定关系。(假设公网IP:123.206.15.88)
  • 第二步:配置公网IP与指定服务器的转发规则。
  • 第二步:右边家庭,如果想要访问某个公司服务器上的网网站,只需要执行指定IP:123.206.15.88,运营商就会根据IP找到与之关联的公司专线,并通过公司路由器、防火墙等设备找到指定的服务器。

按理说,每个从运营商接入网的用户都可以有一个外网IP,但由于全球用户太多而IP根本就不够分配,所以,运营商网络会进行划分,让多个家庭宽带用户共用一个公网IP(动态,可能每次上网公网IP都不一样)。

让家庭用户想要通过网络访问访问其他IP时,先发给运营商由运营商向外转发到其他IP。

注意:外部用户想要访问家庭宽带的IP时,运营商不会把请求转发到我们的电脑。

image-20210221173632272

所以,以后如果你想开发一个网站供全球的用户访问,那你就需要做以下几件事:

  • 拉专线,申请固定公网IP
  • 买一台服务器(就是性能好的电脑)
  • 公网IP绑定至此服务器
  • 将写好的代码放在服务器上并运行起来

这样就可以搞定了…

扩展:IPv4和IPv6

1
2
IPv4,长度为 32 位(4 个字节), 格式:A.B.C.D
IPv6,长度为 128 位(16 个字节),用":"分成8段,格式:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX(每个X是一个16进制数)。

1.2.4 云服务器

大家可能之前听说过:阿里云、腾讯云、亚马逊aws等之类的平台都在搞云服务器,那是个啥?

image-20210207214828047

简单的说:他们造了一个机房(网吧),买了很多很多的服务器(高性能电脑),然后将他们放在机房,然后通电+通网,主要对外去租赁这些服务器资源,让用户不必再自己 拉专线+配置网络+买服务器。

假设,你想要在腾讯云租一台服务器,就可以根据自己的需求去选择配置,腾讯云会根据配置在他的物理机上虚拟出一个服务器,并进行相应的环境初始化并绑定公网固定IP,这样你就可以快速拥有一台可以被大家访问的服务器了。

注意:一台性能非常高的物理机虚拟出很多虚拟机,类似于你在自己电脑上通过vmware、parallel等搞出多个虚拟机。

image-20210207215523168

1.2.5 端口

image-20210207221334711

假设,你在腾讯租了一台云服务器(外网IP:123.206.15.88),然后又开发了 2 个网站运行在服务器上。

那么问题来了,用户在自己的电脑或手机上如何来分别访问同一台服务器上两个程序呢?

其实,在计算机中有一个 端口 的概念,每个程序想要通过网络进行通讯都必须要指定一个端口,例如:

  • 网站A:使用8001端口,那么用户在自己电脑上或手机上访问时指定 IP和端口 即可,如: 123.206.15.88:8001
  • 网站B:使用8002端口,那么用户在自己电脑上或手机上访问时指定 IP和端口 即可,如: 123.206.15.88:8002

注意:端口的取值范围:0 ~ 65535,很多端口在计算机的内部已被使用,我们平时自定义时尽量选择5000之后的端口。

示例:访问百度

image-20210207230929229

提示:如果在浏览器上只写IP不写端口,则默认是80端口。

1.2.6 域名

假设你创业开发了一个网站,用户很难记住你的公网IP:123.206.15.88:80 ``123.206.15.88`。

所以,域名就诞生了,让域名和IP创建对应关系,用户只需要记住域名就可以了,例如:

1
2
3
www.baidu.com   -->  110.242.68.3
www.taobao.com --> 121.18.239.232
...

注意:域名只是和IP创建了对应关系,与端口无关 www.baidu.com:80

image-20210207221334711

在用户在自己的电脑或手机上输入域名去访问时,其实要执行两个步骤:

  • 根据域名寻找IP。(寻找IP)
  • 获得IP之后,再通过IP再去访问指定服务器。

在电脑上属如域名后,寻找IP的过程如下:

  • 第一步:在自己电脑的DNS缓存记录中寻找 域名对应的IP,如果未命中,则执行下一步。

  • 第二步:在自己电脑的hosts文件中寻找,如果未命中,则执行下一步。

    1
    2
    - mac系统:/etc/hosts 文件中
    - win系统:C:\Windows\System32\drivers\etc\hosts 文件中
    1
    2
    3
    4
    5
    # 内容示例
    127.0.0.1 localhost
    255.255.255.255 broadcasthost
    127.0.0.1 kubernetes.docker.internal
    192.168.1.55 www.pythonav.com
  • 第三步:在自己电脑上找到DNS配置的地址(本地域名服务器),去这个地址寻找域名对应的IP,如果未命中,则执行下一步。
    image-20210207233253951

    1
    2
    3
    4
    5
    6
    7
    8
    常见的DNS服务器地址:
    114.114.114.114114 DNS)
    223.5.5.5(阿里 AliDNS)
    8.8.8.8(Google DNS,随着Google在中国的没落和国内官方的限制,已经不是太好用了)
    ...
    各大运营商也有相应的DNS服务器...

    如果你选择的是自动获得DNS,那么就会使用本地运营商的DNS服务器了。
  • 第四步:去根域名服务器中询问(全球共13台根域名服务器,距离中国最近的一台是在日本)

    image-20210208102929953

问题来了

了解域名是怎么回事之后?现在你如果想要让自己的网站通过域名来访问,应该怎么办呢?【目前了解即可】

  • 租一个域名

    1
    2
    3
    ICANN,域名的总管理者(美国一个非营利机构),它仅制定域名政策,注册业务它会授权给一些顶级注册商。
    顶级注册商,可以对外销售域名,但要受国家 互联网络信息中心的管理。例如:中国万网(阿里云收购),中国新网,新网互联,商务中国,中国频道等。
    代理注册商,顶级注册上可以再招一些代理帮助他们卖域名。

    image-20210208110607388

  • 备案

    1
    2
    3
    4
    现在国内注册域名后,需要进行备案(提交一些网站、个人或企业 等信息)后才能使用。
    注册成功后,可按照引导备案:https://beian.aliyun.com/

    注意:国外的域名无需备案就能使用。
  • 域名解析

    1
    让域名和IP创建关联关系,并将关系同步到相关:本地域名服务器 和 根域名服务器(含顶级和二级域名服务器)。

    image-20210208110829796

    image-20210208110938038

2. 网络编程

image-20210207221334711

Python中内置了一个socket模块,可以快速实现网络之间进行传输数据。例如:

  • 服务端,放在左边云服务器中(有固定IP)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import socket

    # 1.监听本机的IP和端口
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('123.206.15.88', 8001)) # IP,端口
    sock.listen(5) # 支持排队等待5人

    while True:
    # 2.等待,有人来连接(阻塞)
    conn, addr = sock.accept() # 等待客户端来连接(阻塞)

    # 3.等待,连接者发送消息(阻塞)
    client_data = conn.recv(1024) # 等待接收客户端发来数据
    print(client_data.decode('utf-8')) # 字节

    # 4.给连接者回复消息
    conn.sendall("hello world".encode('utf-8'))

    # 5.关闭连接
    conn.close()

    # 6.停止服务端程序
    sock.close()
  • 客户端,放在右边用户电脑上

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import socket

    # 1. 向指定IP发送连接请求
    client = socket.socket()
    client.connect(('123.206.15.88', 8001)) # 向服务端发起连接(阻塞)10s

    # 2. 连接成功之后,发送消息
    client.sendall('hello'.encode('utf-8'))

    # 3. 等待,消息的回复(阻塞)
    reply = client.recv(1024)
    print(reply)

    # 4. 关闭连接
    client.close()

上述示例需要借助于互联网,你至少需要租一台云服务器才能通信。

为了节省学习成本,大家可以在自己电脑上模拟【服务端】和【客户端】,等以后项目开发完毕后,再租服务器并部署到服务器上。

注意:在自己本地运行上述代码时,要监听和连接时的IP地址。

image-20210208164809572

image-20210221190543727

image-20210221190426551

当然,你也可以把在自己的局域网内找两台电脑,A作为服务端,B作为客户端,这样两者也可以通信。

1
2
服务端的代码需修改:监听的IP修改为A的IP地址。
客户端的代码需修改:连接的IP修改为A的IP地址(客户端要去找到服务端,并与服务端创建连接)。

注意事项:

  • 本机:

    1
    服务端IP:127.0.0.1  / 192.168.28.92(局域网IP)
  • 局域网:

    1
    服务端IP:192.168.28.92(局域网IP)    
  • 互联网

    1
    服务端IP:123.206.15.88(外网IP)

案例:智障客服

  • 服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    import socket

    # 1.监听本机的IP和端口
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1', 8001)) # 127.0.0.1 或 查看自己局域网本地IP地址
    sock.listen(5)

    while True:
    # 2.等待,有人来连接(阻塞)
    conn, addr = sock.accept()
    print("有人来连接了...")

    # 3.连接成功后立即发送
    conn.sendall("欢迎使用xx系统,请输入您想要办理的业务!".encode("utf-8"))

    while True:
    # 3.等待接受信息
    data = conn.recv(1024)
    if not data:
    break
    data_string = data.decode("utf-8")

    # 4.回复消息
    conn.sendall("你说啥?".encode("utf-8"))
    print("断开连接了")
    # 5.关闭与此人的连接
    conn.close()

    # 6.停止服务端程序
    sock.close()

  • 客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import socket

    # 1. 向指定IP发送连接请求
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1', 8001))

    # 2.连接成功后,获取系统登录信息
    message = client.recv(1024)
    print(message.decode("utf-8"))

    while True:
    content = input("请输入(q/Q退出):")
    if content.upper() == 'Q':
    break
    client.sendall(content.encode("utf-8"))

    # 3. 等待,消息的回复
    reply = client.recv(1024)
    print(reply.decode("utf-8"))

    # 关闭连接,关闭连接时会向服务端发送空数据。
    client.close()

案例:文件上传

  • 服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    import socket

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('127.0.0.1', 8001)) # 127.0.0.1 或 查看自己局域网本地IP地址
    sock.listen(5)

    conn, addr = sock.accept()

    # 接收文件大小
    data = conn.recv(1024)
    total_file_size = int(data.decode('utf-8'))

    # 接收文件内容
    file_object = open('xxx.png', mode='wb')
    recv_size = 0
    while True:
    # 每次最多接收1024字节
    data = conn.recv(1024)
    file_object.write(data)
    file_object.flush()

    recv_size += len(data)
    # 上传完成
    if recv_size == total_file_size:
    break

    # 接收完毕,关闭连接
    conn.close()
    sock.close()

  • 客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    import time
    import os
    import socket

    client = socket.socket()
    client.connect(('127.0.0.1', 8001))

    file_path = input("请输入要上传的文件:")

    # 先发送文件大小
    file_size = os.stat(file_path).st_size
    client.sendall(str(file_size).encode('utf-8'))

    print("准备...")
    time.sleep(2)
    print("开始上传..")
    file_object = open(file_path, mode='rb')
    read_size = 0
    while True:
    chunk = file_object.read(1024) # 每次读取1024字节
    client.sendall(chunk)
    read_size += len(chunk)
    if read_size == file_size:
    break

    client.close()

3. B/S和C/S架构

image-20210207221334711

平时在开发或与人沟通时,经常会有人提到b/s和c/s架构,他们是啥意思呢?

  • C/S架构,是Client和Server的简称。开发这种架构的程序意味着你即需要开发客户端也需要开发服务端。

    1
    2
    3
    4
    例如:你电脑的上QQ、百度网盘、钉钉、QQ音乐 等安装在电脑上的软件。

    服务端:互联网公司会开发一个程序放在他们的服务器上,用于给客户端提供数据支持。
    客户端:大家在电脑安装的相关程序,内部会连接服务端进行收发数据并提供 交互和展示的功能。
  • B/S架构,是Browser和Server的简称。开发这种架构的程序意味着你开发服务端即可,客户端用用户电脑上的浏览器来代替。

    1
    2
    3
    4
    例如:淘宝、京东等网站。

    服务端:互联网公司开发一个网站,放在他们的服务器上。
    客户端:不需要开发,用现成的浏览器即可。

简而言之,B/S架构就是开发网站;C/S架构就是开发安装在电脑的软件。

总结

  1. 了解常见设备和网络架构。
  2. 掌握常见网络词汇的意思。
  3. 了解B/S和C/S架构的区别。
  4. 基于Python的socket模块实现网络编程。

作业

  1. 简述 二层交换机 & 路由器 & 三层交换机 的作用。

  2. 简述常见词:IP、子网掩码、DHCP、公网IP、端口、域名的作用。

  3. 实现远程用户认证系统。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    客户端:
    1. 运行程序,连接服务端并获取服务端发送的欢迎使用xx系统信息。
    2. 输入用户名和密码,并将用户名和密码发送到服务端去校验。
    3. 登录失败,重试(Q退出)。
    4. 登录成功,进入系统,提示登录成功
    服务端:
    1. 客户端连接上服务端,返回 欢迎使用xx系统信息。
    2. 等待客户端发送用户名和密码进行校验(用户名和密码在文件中)
    3. 登录失败,返回错误信息。
    4. 登录成功,返回成功提示的内容。

day18 面向对象进阶

image-20210127104158850

课程目标:掌握面向对象进阶相关知识点,能沟通更加自如的使用面向对象来进行编程。

今日概要:

  • 成员

    • 变量
      • 实例变量
      • 类变量
    • 方法
      • 绑定方法
      • 类方法
      • 静态方法
    • 属性
  • 成员修饰符(公有/私有)

  • “对象嵌套”

  • 特殊成员

1.成员

面向对象中的所有成员如下:

  • 变量
    • 实例变量
    • 类变量
  • 方法
    • 绑定方法
    • 类方法
    • 静态方法
  • 属性

通过面向对象进行编程时,会遇到很多种情况,也会使用不同的成员来实现,接下来我们来逐一介绍成员特性和应用场景。

1.1 变量

  • 实例变量,属于对象,每个对象中各自维护自己的数据。
  • 类变量,属于类,可以被所有对象共享,一般用于给对象提供公共数据(类似于全局变量)。

image-20210127111042911

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person(object):
country = "中国"

def __init__(self, name, age):
self.name = name
self.age = age

def show(self):
# message = "{}-{}-{}".format(Person.country, self.name, self.age)
message = "{}-{}-{}".format(self.country, self.name, self.age)
print(message)

print(Person.country) # 中国


p1 = Person("武沛齐",20)
print(p1.name)
print(p1.age)
print(p1.country) # 中国

p1.show() # 中国-武沛齐-20



提示:当把每个对象中都存在的相同的示例变量时,可以选择把它放在类变量中,这样就可以避免对象中维护多个相同数据。

易错点 & 面试题

第一题:注意读和写的区别。

image-20210127111042911

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Person(object):
country = "中国"

def __init__(self, name, age):
self.name = name
self.age = age

def show(self):
message = "{}-{}-{}".format(self.country, self.name, self.age)
print(message)

print(Person.country) # 中国

p1 = Person("武沛齐",20)
print(p1.name) # 武沛齐
print(p1.age) # 20
print(p1.country) # 中国
p1.show() # 中国-武沛齐-20

p1.name = "root" # 在对象p1中讲name重置为root
p1.num = 19 # 在对象p1中新增实例变量 num=19
p1.country = "china" # 在对象p1中新增实例变量 country="china"

print(p1.country) # china
print(Person.country) # 中国
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person(object):
country = "中国"

def __init__(self, name, age):
self.name = name
self.age = age

def show(self):
message = "{}-{}-{}".format(self.country, self.name, self.age)
print(message)

print(Person.country) # 中国

Person.country = "美国"


p1 = Person("武沛齐",20)
print(p1.name) # 武沛齐
print(p1.age) # 20
print(p1.country) # 美国

第二题:继承关系中的读写

image-20210127124925846

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Base(object):
country = "中国"


class Person(Base):

def __init__(self, name, age):
self.name = name
self.age = age

def show(self):
message = "{}-{}-{}".format(Person.country, self.name, self.age)
# message = "{}-{}-{}".format(self.country, self.name, self.age)
print(message)


# 读
print(Base.country) # 中国
print(Person.country) # 中国

obj = Person("武沛齐",19)
print(obj.country) # 中国

# 写
Base.country = "china"
Person.country = "泰国"
obj.country = "日本"

面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Parent(object):
x = 1


class Child1(Parent):
pass


class Child2(Parent):
pass


print(Parent.x, Child1.x, Child2.x) # 1 1 1

Child1.x = 2
print(Parent.x, Child1.x, Child2.x) # 1 2 1

Parent.x = 3
print(Parent.x, Child1.x, Child2.x) # 3 2 3

1.2 方法

  • 绑定方法,默认有一个self参数,由对象进行调用(此时self就等于调用方法的这个对象)【对象&类均可调用】
  • 类方法,默认有一个cls参数,用类或对象都可以调用(此时cls就等于调用方法的这个类)【对象&类均可调用】
  • 静态方法,无默认参数,用类和对象都可以调用。【对象&类均可调用】

image-20210127141701335

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Foo(object):

def __init__(self, name,age):
self.name = name
self.age = age

def f1(self):
print("绑定方法", self.name)

@classmethod
def f2(cls):
print("类方法", cls)

@staticmethod
def f3():
print("静态方法")

# 绑定方法(对象)
obj = Foo("武沛齐",20)
obj.f1() # Foo.f1(obj)


# 类方法
Foo.f2() # cls就是当前调用这个方法的类。(类)
obj.f2() # cls就是当前调用这个方法的对象的类。


# 静态方法
Foo.f3() # 类执行执行方法(类)
obj.f3() # 对象执行执行方法

在Python中比较灵活,方法都可以通过对象和类进行调用;而在java、c#等语言中,绑定方法只能由对象调用;类方法或静态方法只能由类调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import os
import requests


class Download(object):

def __init__(self, folder_path):
self.folder_path = folder_path

@staticmethod
def download_dou_yin():
# 下载抖音
res = requests.get('.....')

with open("xxx.mp4", mode='wb') as f:
f.write(res.content)

def download_dou_yin_2(self):
# 下载抖音
res = requests.get('.....')
path = os.path.join(self.folder_path, 'xxx.mp4')
with open(path, mode='wb') as f:
f.write(res.content)


obj = Download("video")
obj.download_dou_yin()

面试题:

在类中 @classmethod 和 @staticmethod 的作用?

1.3 属性

属性其实是由绑定方法 + 特殊装饰器 组合创造出来的,让我们以后在调用方法时可以不加括号,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Foo(object):

def __init__(self, name):
self.name = name

def f1(self):
return 123

@property
def f2(self):
return 123


obj = Foo("武沛齐")

v1 = obj.f1()
print(v1)

v2 = obj.f2
print(v2)

示例:以之前开发的分页的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Pagination:
def __init__(self, current_page, per_page_num=10):
self.per_page_num = per_page_num

if not current_page.isdecimal():
self.current_page = 1
return
current_page = int(current_page)
if current_page < 1:
self.current_page = 1
return
self.current_page = current_page

def start(self):
return (self.current_page - 1) * self.per_page_num

def end(self):
return self.current_page * self.per_page_num


user_list = ["用户-{}".format(i) for i in range(1, 3000)]

# 分页显示,每页显示10条
while True:
page = input("请输入页码:")

# page,当前访问的页码
# 10,每页显示10条数据
# 内部执行Pagination类的init方法。
pg_object = Pagination(page, 20)

page_data_list = user_list[ pg_object.start() : pg_object.end() ]
for item in page_data_list:
print(item)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Pagination:
def __init__(self, current_page, per_page_num=10):
self.per_page_num = per_page_num

if not current_page.isdecimal():
self.current_page = 1
return
current_page = int(current_page)
if current_page < 1:
self.current_page = 1
return
self.current_page = current_page

@property
def start(self):
return (self.current_page - 1) * self.per_page_num

@property
def end(self):
return self.current_page * self.per_page_num


user_list = ["用户-{}".format(i) for i in range(1, 3000)]

# 分页显示,每页显示10条
while True:
page = input("请输入页码:")

pg_object = Pagination(page, 20)
page_data_list = user_list[ pg_object.start : pg_object.end ]

for item in page_data_list:
print(item)

其实,除了咱们写的示例意外,在很多模块和框架的源码中也有porperty的身影,例如:requests模块。

1
2
3
4
5
6
7
8
9
10
11
12
import requests

# 内部下载视频,并将下载好的数据分装到Response对象中。
res = requests.get(
url="https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg",
headers={
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 FS"
}
)

# 去对象中获取text,其实需要读取原始文本字节并转换为字符串
res.text

关于属性的编写有两种方式:

  • 方式一,基于装饰器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class C(object):

    @property
    def x(self):
    pass

    @x.setter
    def x(self, value):
    pass

    @x.deleter
    def x(self):
    pass

    obj = C()

    obj.x
    obj.x = 123
    del obj.x
  • 方式二,基于定义变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class C(object):

    def getx(self):
    pass

    def setx(self, value):
    pass

    def delx(self):
    pass

    x = property(getx, setx, delx, "I'm the 'x' property.")

    obj = C()

    obj.x
    obj.x = 123
    del obj.x

Django源码一撇:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class WSGIRequest(HttpRequest):
def __init__(self, environ):
script_name = get_script_name(environ)
# If PATH_INFO is empty (e.g. accessing the SCRIPT_NAME URL without a
# trailing slash), operate as if '/' was requested.
path_info = get_path_info(environ) or '/'
self.environ = environ
self.path_info = path_info
# be careful to only replace the first slash in the path because of
# http://test/something and http://test//something being different as
# stated in https://www.ietf.org/rfc/rfc2396.txt
self.path = '%s/%s' % (script_name.rstrip('/'),
path_info.replace('/', '', 1))
self.META = environ
self.META['PATH_INFO'] = path_info
self.META['SCRIPT_NAME'] = script_name
self.method = environ['REQUEST_METHOD'].upper()
# Set content_type, content_params, and encoding.
self._set_content_type_params(environ)
try:
content_length = int(environ.get('CONTENT_LENGTH'))
except (ValueError, TypeError):
content_length = 0
self._stream = LimitedStream(self.environ['wsgi.input'], content_length)
self._read_started = False
self.resolver_match = None

def _get_scheme(self):
return self.environ.get('wsgi.url_scheme')

def _get_post(self):
if not hasattr(self, '_post'):
self._load_post_and_files()
return self._post

def _set_post(self, post):
self._post = post

@property
def FILES(self):
if not hasattr(self, '_files'):
self._load_post_and_files()
return self._files

POST = property(_get_post, _set_post)

写在最后,对属性进行一个补充:

由于属性和实例变量的调用方式相同,所以在编写时需要注意:属性名称 不要 实例变量 重名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Foo(object):

def __init__(self, name, age):
self.name = name
self.age = age

@property
def func(self):
return 123


obj = Foo("武沛齐", 123)
print(obj.name)
print(obj.func)

一旦重名,可能就会有报错。

1
2
3
4
5
6
7
8
9
10
11
12
class Foo(object):

def __init__(self, name, age):
self.name = name # 报错,错认为你想要调用 @name.setter 装饰的方法。
self.age = age

@property
def name(self):
return "{}-{}".format(self.name, self.age)


obj = Foo("武沛齐", 123)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Foo(object):

def __init__(self, name, age):
self.name = name
self.age = age

@property
def name(self):
return "{}-{}".format(self.name, self.age) # 报错,循环调用自己(直到层级太深报错)

@name.setter
def name(self, value):
print(value)


obj = Foo("武沛齐", 123)
print(obj.name)

如果真的想要在名称上创建一些关系,可以让实例变量加上一个下划线。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Foo(object):

def __init__(self, name, age):
self._name = name
self.age = age

@property
def name(self):
return "{}-{}".format(self._name, self.age)


obj = Foo("武沛齐", 123)
print(obj._name)
print(obj.name)

2.成员修饰符

Python中成员的修饰符就是指的是:公有、私有。

  • 公有,在任何地方都可以调用这个成员。
  • 私有,只有在类的内部才可以调用改成员(成员是以两个下划线开头,则表示该成员为私有)。

示例一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Foo(object):

def __init__(self, name, age):
self.__name = name
self.age = age

def get_data(self):
return self.__name

def get_age(self):
return self.age


obj = Foo("武沛齐", 123)


# 公有成员
print(obj.age)
v1 = self.get_age()
print(v1)

# 私有成员
# print(obj.__name) # 错误,由于是私有成员,只能在类中进行使用。
v2 = obj.get_data()
print(v2)

示例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Foo(object):

def get_age(self):
print("公有的get_age")

def __get_data(self):
print("私有的__get_data方法")

def proxy(self):
print("公有的proxy")
self.__get_data()


obj = Foo()
obj.get_age()

obj.proxy()

示例3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Foo(object):

@property
def __name(self):
print("公有的get_age")

@property
def proxy(self):
print("公有的proxy")
self.__name
return 1


obj = Foo()
v1 = obj.proxy
print(v1)

特别提醒:父类中的私有成员,子类无法继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base(object):

def __data(self):
print("base.__data")

def num(self):
print("base.num")


class Foo(Base):

def func(self):
self.num()
self.__data() # # 不允许执行父类中的私有方法


obj = Foo()
obj.func()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base(object):

def __data(self):
print("base.__data")

def num(self):
print("base.num")
self.__data() # 不允许执行父类中的私有方法


class Foo(Base):

def func(self):
self.num()


obj = Foo()
obj.func()

写在最后,按理说私有成员是无法被外部调用,但如果用一些特殊的语法也可以(Flask源码中有这种写法,大家写代码不推荐这样写)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Foo(object):

def __init__(self):
self.__num = 123
self.age = 19

def __msg(self):
print(1234)


obj = Foo()
print(obj.age)
print(obj._Foo__num)
obj._Foo__msg()

成员是否可以作为独立的功能暴露给外部,让外部调用并使用。

  • 可以,公有。
  • 不可以,内部其他放的一个辅助,私有。

3.对象嵌套

在基于面向对象进行编程时,对象之间可以存在各种各样的关系,例如:组合、关联、依赖等(Java中的称呼),用大白话来说就是各种嵌套。

下面我们就用示例来学习常见的嵌套的情景:

情景一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Student(object):
""" 学生类 """

def __init__(self, name, age):
self.name = name
self.age = age

def message(self):
data = "我是一名学生,我叫:{},我今年{}岁".format(self.name, self.age)
print(data)

s1 = Student("武沛齐", 19)
s2 = Student("Alex", 19)
s3 = Student("日天", 19)



class Classes(object):
""" 班级类 """

def __init__(self, title):
self.title = title
self.student_list = []

def add_student(self, stu_object):
self.student_list.append(stu_object)

def add_students(self, stu_object_list):
for stu in stu_object_list:
self.add_student(stu)

def show_members(self):
for item in self.student_list:
# print(item)
item.message()

c1 = Classes("三年二班")
c1.add_student(s1)
c1.add_students([s2, s3])

print(c1.title)
print(c1.student_list)

情景二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Student(object):
""" 学生类 """

def __init__(self, name, age, class_object):
self.name = name
self.age = age
self.class_object = class_object

def message(self):
data = "我是一名{}班的学生,我叫:{},我今年{}岁".format(self.class_object.title, self.name, self.age)
print(data)


class Classes(object):
""" 班级类 """

def __init__(self, title):
self.title = title


c1 = Classes("Python全栈")
c2 = Classes("Linux云计算")

user_object_list = [
Student("武沛齐", 19, c1),
Student("Alex", 19, c1),
Student("日天", 19, c2)
]

for obj in user_object_list:
print(obj.name,obj.age, obj.class_object.title)

image-20210127215951115

情景三:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Student(object):
""" 学生类 """

def __init__(self, name, age, class_object):
self.name = name
self.age = age
self.class_object = class_object

def message(self):
data = "我是一名{}班的学生,我叫:{},我今年{}岁".format(self.class_object.title, self.name, self.age)
print(data)


class Classes(object):
""" 班级类 """

def __init__(self, title, school_object):
self.title = title
self.school_object = school_object


class School(object):
""" 学校类 """

def __init__(self, name):
self.name = name


s1 = School("北京校区")
s2 = School("上海校区")

c1 = Classes("Python全栈", s1)
c2 = Classes("Linux云计算", s2)

user_object_list = [
Student("武沛齐", 19, c1),
Student("Alex", 19, c1),
Student("日天", 19, c2)
]
for obj in user_object_list:
print(obj.name, obj.class_object.title , obj.class_object.school_object.name)

image-20210127220654414

4.特殊成员

在Python的类中存在一些特殊的方法,这些方法都是 __方法__ 格式,这种方法在内部均有特殊的含义,接下来我们来讲一些常见的特殊成员:

  • __init__,初始化方法

    1
    2
    3
    4
    5
    6
    class Foo(object):
    def __init__(self, name):
    self.name = name


    obj = Foo("武沛齐")
  • __new__,构造方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Foo(object):
    def __init__(self, name):
    print("第二步:初始化对象,在空对象中创建数据")
    self.name = name

    def __new__(cls, *args, **kwargs):
    print("第一步:先创建空对象并返回")
    return object.__new__(cls)


    obj = Foo("武沛齐")
  • __call__

    1
    2
    3
    4
    5
    6
    7
    class Foo(object):
    def __call__(self, *args, **kwargs):
    print("执行call方法")


    obj = Foo()
    obj()
  • __str__

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Foo(object):

    def __str__(self):
    return "哈哈哈哈"


    obj = Foo()
    data = str(obj)
    print(data)
  • __dict__

    1
    2
    3
    4
    5
    6
    7
    8
    class Foo(object):
    def __init__(self, name, age):
    self.name = name
    self.age = age


    obj = Foo("武沛齐",19)
    print(obj.__dict__)
  • __getitem____setitem____delitem__

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Foo(object):

    def __getitem__(self, item):
    pass

    def __setitem__(self, key, value):
    pass

    def __delitem__(self, key):
    pass


    obj = Foo("武沛齐", 19)

    obj["x1"]
    obj['x2'] = 123
    del obj['x3']
  • __enter____exit__

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Foo(object):

    def __enter__(self):
    print("进入了")
    return 666

    def __exit__(self, exc_type, exc_val, exc_tb):
    print("出去了")


    obj = Foo()
    with obj as data:
    print(data)
    1
    2
    3
    4
    超前知识:数据连接,每次对远程的数据进行操作时候都必须经历。
    1.连接 = 连接数据库
    2.操作数据库
    3.关闭连接
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class SqlHelper(object):

    def __enter__(self):
    self.连接 = 连接数据库
    return 连接

    def __exit__(self, exc_type, exc_val, exc_tb):
    self.连接.关闭



    with SqlHelper() as 连接:
    连接.操作..


    with SqlHelper() as 连接:
    连接.操作...
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 面试题(补充代码,实现如下功能)

    class Context:

    def do_something(self):
    print('内部执行')


    with Context() as ctx:
    print('内部执行')
    ctx.do_something()

    上下文管理的语法。

  • __add__ 等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Foo(object):
    def __init__(self, name):
    self.name = name

    def __add__(self, other):
    return "{}-{}".format(self.name, other.name)


    v1 = Foo("alex")
    v2 = Foo("sb")

    # 对象+值,内部会去执行 对象.__add__方法,并将+后面的值当做参数传递过去。
    v3 = v1 + v2
    print(v3)
  • __iter__

    • 迭代器

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      # 迭代器类型的定义:
      1.当类中定义了 __iter__ 和 __next__ 两个方法。
      2.__iter__ 方法需要返回对象本身,即:self
      3. __next__ 方法,返回下一个数据,如果没有数据了,则需要抛出一个StopIteration的异常。
      官方文档:https://docs.python.org/3/library/stdtypes.html#iterator-types

      # 创建 迭代器类型 :
      class IT(object):
      def __init__(self):
      self.counter = 0

      def __iter__(self):
      return self

      def __next__(self):
      self.counter += 1
      if self.counter == 3:
      raise StopIteration()
      return self.counter

      # 根据类实例化创建一个迭代器对象:
      obj1 = IT()

      # v1 = obj1.__next__()
      # v2 = obj1.__next__()
      # v3 = obj1.__next__() # 抛出异常

      v1 = next(obj1) # obj1.__next__()
      print(v1)

      v2 = next(obj1)
      print(v2)

      v3 = next(obj1)
      print(v3)


      obj2 = IT()
      for item in obj2: # 首先会执行迭代器对象的__iter__方法并获取返回值,一直去反复的执行 next(对象)
      print(item)

      迭代器对象支持通过next取值,如果取值结束则自动抛出StopIteration。
      for循环内部在循环时,先执行__iter__方法,获取一个迭代器对象,然后不断执行的next取值(有异常StopIteration则终止循环)。
    • 生成器

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      # 创建生成器函数
      def func():
      yield 1
      yield 2

      # 创建生成器对象(内部是根据生成器类generator创建的对象),生成器类的内部也声明了:__iter__、__next__ 方法。
      obj1 = func()

      v1 = next(obj1)
      print(v1)

      v2 = next(obj1)
      print(v2)

      v3 = next(obj1)
      print(v3)


      obj2 = func()
      for item in obj2:
      print(item)

      如果按照迭代器的规定来看,其实生成器类也是一种特殊的迭代器类(生成器也是一个中特殊的迭代器)。
    • 可迭代对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      # 如果一个类中有__iter__方法且返回一个迭代器对象 ;则我们称以这个类创建的对象为可迭代对象。

      class Foo(object):

      def __iter__(self):
      return 迭代器对象(生成器对象)

      obj = Foo() # obj是 可迭代对象。

      # 可迭代对象是可以使用for来进行循环,在循环的内部其实是先执行 __iter__ 方法,获取其迭代器对象,然后再在内部执行这个迭代器对象的next功能,逐步取值。
      for item in obj:
      pass
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      class IT(object):
      def __init__(self):
      self.counter = 0

      def __iter__(self):
      return self

      def __next__(self):
      self.counter += 1
      if self.counter == 3:
      raise StopIteration()
      return self.counter


      class Foo(object):
      def __iter__(self):
      return IT()


      obj = Foo() # 可迭代对象


      for item in obj: # 循环可迭代对象时,内部先执行obj.__iter__并获取迭代器对象;不断地执行迭代器对象的next方法。
      print(item)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      # 基于可迭代对象&迭代器实现:自定义range
      class IterRange(object):
      def __init__(self, num):
      self.num = num
      self.counter = -1

      def __iter__(self):
      return self

      def __next__(self):
      self.counter += 1
      if self.counter == self.num:
      raise StopIteration()
      return self.counter


      class Xrange(object):
      def __init__(self, max_num):
      self.max_num = max_num

      def __iter__(self):
      return IterRange(self.max_num)


      obj = Xrange(100)

      for item in obj:
      print(item)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      class Foo(object):
      def __iter__(self):
      yield 1
      yield 2


      obj = Foo()
      for item in obj:
      print(item)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      # 基于可迭代对象&生成器 实现:自定义range

      class Xrange(object):
      def __init__(self, max_num):
      self.max_num = max_num

      def __iter__(self):
      counter = 0
      while counter < self.max_num:
      yield counter
      counter += 1


      obj = Xrange(100)
      for item in obj:
      print(item)

      常见的数据类型:

      1
      2
      3
      v1 = list([11,22,33,44])

      v1是一个可迭代对象,因为在列表中声明了一个 __iter__ 方法并且返回一个迭代器对象。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      from collections.abc import Iterator, Iterable

      v1 = [11, 22, 33]
      print( isinstance(v1, Iterator) ) # false,判断是否是迭代器;判断依据是__iter__ 和 __next__。
      v2 = v1.__iter__()
      print( isinstance(v2, Iterator) ) # True



      v1 = [11, 22, 33]
      print( isinstance(v1, Iterable) ) # True,判断依据是是否有 __iter__且返回迭代器对象。

      v2 = v1.__iter__()
      print( isinstance(v2, Iterable) ) # True,判断依据是是否有 __iter__且返回迭代器对象。

总结

  1. 面向对象编程中的成员

    • 变量
      • 实例变量
      • 类变量
  • 方法
    - 绑定方法
    - 类方法
    - 静态方法
    • 属性
  1. 成员修饰符
  2. 对象中的数据嵌套
  3. 特殊成员
  4. 重要概念:
    • 迭代器
    • 生成器
    • 可迭代对象

作业

  1. 列举面向对象的成员并简述他们的特点。

  2. @staticmethod 和 @classmethod的作用是什么?

  3. 面向对象中如何让成员变为私有。

  4. __new__方法的作用?

  5. 简述你理解的:迭代器、生成器、可迭代对象。

  6. 看代码写结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    class Foo(object):
    a1 = 1

    def __init__(self,num):
    self.num = num

    def show_data(self):
    print(self.num+self.a1)

    obj1 = Foo(666)
    obj2 = Foo(999)

    print(obj1.num)
    print(obj1.a1)

    obj1.num = 18
    obj1.a1 = 99

    print(obj1.num)
    print(obj1.a1)

    print(obj2.a1)
    print(obj2.num)
    print(obj2.num)
    print(Foo.a1)
    print(obj1.a1)
  7. 看代码写结果,注意返回值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Foo(object):

    def f1(self):
    return 999

    def f2(self):
    v = self.f1()
    print('f2')
    return v

    def f3(self):
    print('f3')
    return self.f2()

    def run(self):
    result = self.f3()
    print(result)

    obj = Foo()
    v1 = obj.run()
    print(v1)
  8. 看代码写结果【如果有错误,则标注错误即可,并且假设程序报错可以继续执行】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Foo(object):

    def f1(self):
    print('f1')

    @staticmethod
    def f2():
    print('f2')

    obj = Foo()
    obj.f1()
    obj.f2()

    Foo.f1()
    Foo.f2()
  9. 看代码写结果【如果有错误,则标注错误即可,并且假设程序报错可以继续执行】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Foo(object):

    def f1(self):
    print('f1')
    self.f2()
    self.f3()

    @classmethod
    def f2(cls):
    print('f2')

    @staticmethod
    def f3():
    print('f3')

    obj = Foo()
    obj.f1()
  10. 看代码写结果【如果有错误,则标注错误即可,并且假设程序报错可以继续执行】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Base(object):
    @classmethod
    def f2(cls):
    print('f2')

    @staticmethod
    def f3():
    print('f3')

    class Foo(Base):
    def f1(self):
    print('f1')
    self.f2()
    self.f3()

    obj = Foo()
    obj.f1()
  11. 看代码写结果【如果有错误,则标注错误即可,并且假设程序报错可以继续执行】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Foo(object):
    a1 = 1
    __a2 = 2

    def __init__(self,num):
    self.num = num
    self.__salary = 1000

    def show_data(self):
    print(self.num+self.a1)

    obj = Foo(666)

    print(obj.num)
    print(obj.a1)
    print(obj.__salary)
    print(obj.__a2)
    print(Foo.a1)
    print(Foo.__a2)
    obj.show_data()
  12. 看代码写结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Foo(object):

    def __init__(self, age):
    self.age = age

    def display(self):
    print(self.age)


    data_list = [Foo(8), Foo(9)]
    # print(data_list[0].age)
    # data_list[1].display()

    for item in data_list:
    print(item.age, item.display())
  13. 看代码写结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Base(object):
    def __init__(self, a1):
    self.a1 = a1

    def f2(self, arg):
    print(self.a1, arg)


    class Foo(Base):
    def f2(self, arg):
    print('666')


    obj_list = [Base(1), Foo(2), Foo(3)]

    for item in obj_list:
    item.f2(1)
  14. 看代码写结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Foo(object):
    def __init__(self, num):
    self.num = num

    v1 = [Foo for i in range(10)]
    v2 = [Foo(5) for i in range(10)]
    v3 = [Foo(i) for i in range(10)]

    print(v1)
    print(v2)
    print(v3)
  15. 看代码写结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class StarkConfig(object):

    def __init__(self, num):
    self.num = num

    def changelist(self, request):
    print(self.num, request)


    config_obj_list = [ StarkConfig(1), StarkConfig(2), StarkConfig(3) ]
    for item in config_obj_list:
    print(item.num)
  16. 看代码写结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class StarkConfig(object):

    def __init__(self, num):
    self.num = num

    def changelist(self, request):
    print(self.num, request)


    config_obj_list = [StarkConfig(1), StarkConfig(2), StarkConfig(3)]
    for item in config_obj_list:
    item.changelist(666)
  17. 看代码写结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    class StarkConfig(object):

    def __init__(self, num):
    self.num = num

    def changelist(self, request):
    print(self.num, request)

    def run(self):
    self.changelist(999)


    class RoleConfig(StarkConfig):

    def changelist(self, request):
    print(666, self.num)


    class AdminSite(object):
    def __init__(self):
    self._registry = {}

    def register(self, k, v):
    self._registry[k] = v


    site = AdminSite()

    site.register('武沛齐', StarkConfig(19))
    site.register('root', StarkConfig(20))
    site.register("admin", RoleConfig(33))

    print(len(site._registry))

    for k, row in site._registry.items():
    row.changelist(5)

  18. 看代码写结果(如有报错,请标注报错位置)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class StarkConfig(object):
    def __init__(self, num):
    self.num = num
    def run(self):
    self()
    def __call__(self, *args, **kwargs):
    print(self.num)

    class RoleConfig(StarkConfig):
    def __call__(self, *args, **kwargs):
    print(345)
    def __getitem__(self, item):
    return self.num[item]

    v1 = RoleConfig('alex')
    v2 = StarkConfig("wupeiqi")

    print(v1[1])
    print(v2[2])
  19. 补全代码

    1
    2
    3
    4
    5
    6
    class Context:
    pass


    with Context() as ctx:
    ctx.do_something()
  20. 看代码写结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class Department(object):
    def __init__(self,title):
    self.title = title

    class Person(object):
    def __init__(self,name,age,depart):
    self.name = name
    self.age = age
    self.depart = depart

    def message(self):
    msg = "我是%s,年龄%s,属于%s" %(self.name,self.age,self.depart.title)
    print(msg)


    d1 = Department('人事部')
    d2 = Department('销售部')

    p1 = Person('武沛齐',18,d1)
    p2 = Person('alex',18,d1)

    p1.message()
    p2.message()
  21. 分析代码关系,并写出正确的输出结果。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Node(object):
    def __init__(self, title):
    self.title = title
    self.children = []

    def add(self, node):
    self.children.append(node)

    def __getitem__(self, item):
    return self.children[item]


    root = Node("中国")

    root.add(Node("河南省"))
    root.add(Node("河北省"))

    print(root.title)
    print(root[0])
    print(root[0].title)
    print(root[1])
    print(root[1].title)
  22. 分析代码关系,并写出正确的输出结果。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    class Node(object):
    def __init__(self, title):
    self.title = title
    self.children = []

    def add(self, node):
    self.children.append(node)

    def __getitem__(self, item):
    return self.children[item]


    root = Node("中国")

    root.add(Node("河南省"))
    root.add(Node("河北省"))
    root.add(Node("陕西省"))
    root.add(Node("山东省"))

    root[1].add(Node("石家庄"))
    root[1].add(Node("保定"))
    root[1].add(Node("廊坊"))

    root[3].add(Node("潍坊"))
    root[3].add(Node("烟台"))
    root[3].add(Node("威海"))

    root[1][1].add(Node("雄安"))
    root[1][1].add(Node("望都"))

    print(root.title)
    print(root[0].title)
    print(root[1].title)
    print(root[1][0].title)
    print(root[1][2].title)
    print(root[1][1][0].title)

第三模块 面向对象&网络&并发编程

image-20210117230507748

从今天开始,我们将进入系列课程第3个模块的的学习,此模块包含如下三大部分知识:

  • 面向对象,Python中支持两种编程方式来写代码,分别是:函数式编程面向对象式编程

    • 函数式

      1
      2
      3
      4
      5
      6
      # 定义函数,在函数中实现功能
      def func():
      print("一个NB的功能")

      # 执行函数
      func()
    • 面向对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      # 定义类
      class Foo(object):
      # 在类中定义方法
      def func(self):
      print("一个NB的功能")

      # 实例化类的对象
      obj = Foo()
      # 执行类中的方法
      obj.func()

    Python支持两种编程方式(其他很多语言只支持一种),所以初学者在刚开始学习往往不知道应如何选择,并且行业内对于 函数式编程 vs 面向对象编程 之间谁更好的讨论也是难分胜负,其实在开发中无论要实现什么样的功能,两种编程模式都能实现,那种让我们能更好实现就选择谁?不必非纠结于那种方式更好,编程语言支持工具,最重要的是把功能实现。

    初学者在选择编程方式时候,可以遵循如下规则:

    • 函数式,推荐初学者使用。理由:上手快且逻辑简单清晰。

    • 面向对象,推荐有一些代码经验后使用。理由:面向对象的思想需要有一定的项目积累之后(写多了&看的多)才能真正理解其精髓,基于面向对象可以编写出扩展性更强的代码(在一定程序上也可以简化代码)。

    现阶段,大家在学习面向对象时更重要的是:掌握相关知识点 & 读懂源码 & 编写简单的基于面向对象的程序。

  • 网络编程,学习网络知识后,可以让我们的程序通过网络来进行数据交互和传输 并 掌握其本质。

    image-20210117224420539

  • 并发编程,一个程序想要执行的速度更快是必须要掌握并发编程的相关知识。

    1
    2
    3
    例如:下载10个抖音视频,每个需要2分钟。
    - 按照以前的思路,逐一下载就需要20分钟。
    - 按照并发的思路,创建10个线程/进程来实现,大概需要2分钟就可以完成。

day17 面向对象基础

image-20210118204107085

课程目标:了解面向对象并可以根据面向对象知识进行编写代码。

课程概要:

  • 初识面向对象
  • 三大特性(面向对象)
    • 封装
    • 继承
    • 多态
  • 再看数据类型

1. 初识面向对象

想要通过面向对象去实现某个或某些功能时需要2步:

  • 定义类,在类中定义方法,在方法中去实现具体的功能。

  • 实例化类并的个一个对象,通过对象去调用并执行方法。

1
2
3
4
5
6
7
8
9
class Message:

def send_email(self, email, content):
data = "给{}发邮件,内容是:{}".format(email,content)
print(data)


msg_object = Message() # 实例化一个对象 msg_object,创建了一个一块区域。
msg_object.send_email("wupeiqi@live.com","注册成功")

注意:1.类名称首字母大写&驼峰式命名;2.py3之后默认类都继承object;3.在类种编写的函数称为方法;4.每个方法的第一个参数是self。

类中可以定义多个方法,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Message:

def send_email(self, email, content):
data = "给{}发邮件,内容是:{}".format(email, content)
print(data)

def send_wechat(self, vid, content):
data = "给{}发微信,内容是:{}".format(vid, content)
print(data)


msg_object = Message()
msg_object.send_email("wupeiqi@live.com", "注册成功")
msg_object.send_wechat("武沛齐", "注册成功")

你会发现,用面向对象编程写的类有点像归类的意思:将某些相似的函数划分到一个类中。

但,这种编写方式让人感觉有些鸡肋,直接用 函数 写多好呀。对吧?

别着急,记者往下看。

1.1 对象和self

在每个类中都可以定义个特殊的:__init__ 初始化方法 ,在实例化类创建对象时自动执行,即:对象=类()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Message:

def __init__(self, content):
self.data = content

def send_email(self, email):
data = "给{}发邮件,内容是:{}".format(email, self.data)
print(data)

def send_wechat(self, vid):
data = "给{}发微信,内容是:{}".format(vid, self.data)
print(data)

# 对象 = 类名() # 自动执行类中的 __init__ 方法。

# 1. 根据类型创建一个对象,内存的一块 区域 。
# 2. 执行__init__方法,模块会将创建的那块区域的内存地址当self参数传递进去。 往区域中(data="注册成功")
msg_object = Message("注册成功")

msg_object.send_email("wupeiqi@live.com") # 给wupeiqi@live.com发邮件,内容是:注册成功
msg_object.send_wechat("武沛齐") # 给武沛齐发微信,内容是:注册成功

image-20210123195714019

通过上述的示例,你会发现:

  • 对象,让我们可以在它的内部先封装一部分数据,以后想要使用时,再去里面获取。
  • self,类中的方法需要由这个类的对象来触发并执行( 对象.方法名 ),且在执行时会自动将对象当做参数传递给self,以供方法中获取对象中已封装的值。

注意:除了self默认参数以外,方法中的参数的定义和执行与函数是相同。

当然,根据类也可以创建多个对象并执行其中的方法,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Message:

def __init__(self, content):
self.data = content

def send_email(self, email):
data = "给{}发邮件,内容是:{}".format(email, self.data)
print(data)

def send_wechat(self, vid):
data = "给{}发微信,内容是:{}".format(vid, self.data)
print(data)


msg_object = Message("注册成功")
msg_object.send_email("wupeiqi@live.com") # 给wupeiqi@live.com发邮件,内容是:注册成功
msg_object.send_wechat("武沛齐")


login_object = Message("登录成功")
login_object.send_email("wupeiqi@live.com") # 给wupeiqi@live.com发邮件,内容是:登录成功
login_object.send_wechat("武沛齐")

image-20210123195714019

面向对象的思想:将一些数据封装到对象中,在执行方法时,再去对象中获取。

函数式的思想:函数内部需要的数据均通过参数的形式传递。

  • self,本质上就是一个参数。这个参数是Python内部会提供,其实本质上就是调用当前方法的那个对象。
  • 对象,基于类实例化出来”一块内存“,默认里面没有数据;经过类的 __init__方法,可以在内存中初始化一些数据。

1.2 常见成员

在编写面向对象相关代码时,最常见成员有:

  • 实例变量,属于对象,只能通过对象调用。
  • 绑定方法,属于类,通过对象调用 或 通过类调用。

注意:还有很多其他的成员,后续再来介绍。

image-20210126140807446

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person:

def __init__(self, n1, n2):
# 实例变量
self.name = n1
self.age = n2

# 绑定方法
def show(self):
msg = "我叫{},今年{}岁。".format(self.name, self.age)
print(msg)

def all_message(self):
msg = "我是{}人,我叫{},今年{}岁。".format(Person.country, self.name, self.age)
print(msg)

def total_message(self):
msg = "我是{}人,我叫{},今年{}岁。".format(self.country, self.name, self.age)
print(msg)
1
2
3
4
5
6
7
8
9
10
11
12
13
# 执行绑定方法
p1 = Person("武沛齐",20)
p1.show()
# 或
# p1 = Person("武沛齐",20)
# Person.show(p1)


# 初始化,实例化了Person类的对象叫p1
p1 = Person("武沛齐",20)



1.3 应用示例

  1. 将数据封装到一个对象,便于以后使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    class UserInfo:
    def __init__(self, name, pwd,age):
    self.name = name
    self.password = pwd
    self.age = age


    def run():
    user_object_list = []
    # 用户注册
    while True:
    user = input("用户名:")
    if user.upper() == "Q":
    break
    pwd = input("密码")

    # user_object对象中有:name/password
    user_object = UserInfo(user, pwd,19)
    # user_dict = {"name":user,"password":pwd}

    user_object_list.append(user_object)
    # user_object_list.append(user_dict)

    # 展示用户信息
    for obj in user_object_list:
    print(obj.name, obj.password)

    总结:
    - 数据封装到对象,以后再去获取。
    - 规范数据(约束)

    注意:用字典也可以实现做封装,只不过字典在操作值时还需要自己写key,面向对象只需要 . 即可获取对象中封装的数据。

  2. 将数据分装到对象中,在方法中对原始数据进行加工处理。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    user_list = ["用户-{}".format(i) for i in range(1,3000)]

    # 分页显示,每页显示10条
    while True:
    page = int(input("请输入页码:"))

    start_index = (page - 1) * 10
    end_index = page * 10

    page_data_list = user_list[start_index:end_index]
    for item in page_data_list:
    print(item)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    class Pagination:
    def __init__(self, current_page, per_page_num=10):
    self.per_page_num = per_page_num

    if not current_page.isdecimal():
    self.current_page = 1
    return
    current_page = int(current_page)
    if current_page < 1:
    self.current_page = 1
    return
    self.current_page = current_page

    def start(self):
    return (self.current_page - 1) * self.per_page_num

    def end(self):
    return self.current_page * self.per_page_num


    user_list = ["用户-{}".format(i) for i in range(1, 3000)]

    # 分页显示,每页显示10条
    while True:
    page = input("请输入页码:")

    # page,当前访问的页码
    # 10,每页显示10条数据
    # 内部执行Pagination类的init方法。
    pg_object = Pagination(page, 20)
    page_data_list = user_list[ pg_object.start() : pg_object.end() ]
    for item in page_data_list:
    print(item)

    还有这个示例:将数据封装到一个对象中,然后再方法中对已封装的数据进行操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    import os
    import requests


    class DouYin:
    def __init__(self, folder_path):
    self.folder_path = folder_path

    if not os.path.exists(folder_path):
    os.makedirs(folder_path)


    def download(self, file_name, url):
    res = requests.get(
    url=url,
    headers={
    "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 FS"
    }
    )
    file_path = os.path.join(self.folder_path, file_name)
    with open(file_path, mode='wb') as f:
    f.write(res.content)
    f.flush()

    def multi_download(self, video_list):
    for item in video_list:
    self.download(item[0], item[1])


    if __name__ == '__main__':
    douyin_object = DouYin("videos")

    douyin_object.download(
    "罗斯.mp4",
    "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg"
    )

    video_list = [
    ("a1.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300fc20000bvi413nedtlt5abaa8tg"),
    ("a2.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0d00fb60000bvi0ba63vni5gqts0uag"),
    ("a3.mp4", "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg")
    ]
    douyin_object.multi_download(video_list)


  3. 根据类创建多个对象,在方法中对对象中的数据进行修改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    class Police:
    """警察"""

    def __init__(self, name, role):
    self.name = name
    self.role = role
    if role == "队员":
    self.hit_points = 200
    else:
    self.hit_points = 500

    def show_status(self):
    """ 查看警察状态 """
    message = "警察{}的生命值为:{}".format(self.name, self.hit_points)
    print(message)

    def bomb(self, terrorist_list):
    """ 投炸弹,炸掉恐怖分子 """
    for terrorist in terrorist_list:
    terrorist.blood -= 200
    terrorist.show_status()

    """
    p1 = Police("武沛齐","队员")
    p1.show_status()
    p1.bomb(["alex","李杰"])

    p2 = Police("日天","队长")
    p2.show_status()
    p2.bomb(["alex","李杰"])
    """



    class Terrorist:
    """ 恐怖分子 """

    def __init__(self, name, blood=300):
    self.name = name
    self.blood = blood

    def shoot(self, police_object):
    """ 开枪射击某个警察 """
    police_object.hit_points -= 5
    police_object.show_status()

    self.blood -= 2

    def strafe(self, police_object_list):
    """ 扫射某些警察 """
    for police_object in police_object_list:
    police_object.hit_points -= 8
    police_object.show_status()

    def show_status(self):
    """ 查看恐怖分子状态 """
    message = "恐怖分子{}的血量值为:{}".format(self.name, self.blood)
    print(message)

    """
    t1 = Terrorist('alex')
    t2 = Terrorist('李杰',200)
    """

    def run():
    # 1.创建3个警察
    p1 = Police("武沛齐", "队员")
    p2 = Police("苑昊", "队员")
    p3 = Police("于超", "队长")

    # 2.创建2个匪徒
    t1 = Terrorist("alex")
    t2 = Terrorist("eric")


    # alex匪徒射击于超警察
    t1.shoot(p3)

    # alex扫射
    t1.strafe([p1, p2, p3])

    # eric射击苑昊
    t2.shoot(p2)

    # 武沛齐炸了那群匪徒王八蛋
    p1.bomb([t1, t2])

    # 武沛齐又炸了一次alex
    p1.bomb([t1])


    if __name__ == '__main__':
    run()

总结:

  • 仅做数据封装。
  • 封装数据 + 方法再对数据进行加工处理。
  • 创建同一类的数据且同类数据可以具有相同的功能(方法)。

2. 三大特性

面向对象编程在很多语言中都存在,这种编程方式有三大特性:封装、继承、多态。

2.1 封装

封装主要体现在两个方面:

  • 将同一类方法封装到了一个类中,例如上述示例中:匪徒的相关方法都写在Terrorist类中;警察的相关方法都写在Police类中。
  • 将数据封装到了对象中,在实例化一个对象时,可以通过__init__初始化方法在对象中封装一些数据,便于以后使用。

2.2 继承

传统的理念中有:儿子可以继承父亲的财产。

在面向对象中也有这样的理念,即:子类可以继承父类中的方法和类变量(不是拷贝一份,父类的还是属于父类,子类可以继承而已)。

1
2
3
4
5
父类
子类

基类
派生类

image-20210126182724313

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base:

def func(self):
print("Base.func")

class Son(Base):

def show(self):
print("Son.show")

s1 = Son()
s1.show()
s1.func() # 优先在自己的类中找,自己没有才去父类。

s2 = Base()
s2.func()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Base:
def f1(self):
pass

class Foo(Base):

def f2(self):
pass

class Bar(Base):

def f3(self):
pass

o1 = Foo()
o1.f2()
o1.f1()

练习题

1
2
3
4
5
6
7
8
9
10
11
class Base:
def f1(self):
print('base.f1')

class Foo(Base):
def f2(self):
print('foo.f2')

obj = Foo()
obj.f1()
obj.f2()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base:
def f1(self):
print('base.f1')

class Foo(Base):
def f2(self):
print('before')
self.f1() # 调用了f1方法 obj.f1()
print('foo.f2')

obj = Foo()
obj.f2()

>>> before
>>> base.f1
>>> foo.f2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base:
def f1(self):
print('base.f1')

class Foo(Base):
def f2(self):
print("before")
self.f1() # obj,Foo类创建出来的对象。 obj.f1
print('foo.f2')
def f1(self):
print('foo.f1')

obj = Foo()
obj.f1() # obj对象到底是谁?优先就会先去谁里面找。
obj.f2()

>>> before
>>> foo.f1
>>> foo.f2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Base:
def f1(self):
print('before')
self.f2() # slef是obj对象(Foo类创建的对象) obj.f2
print('base.f1')

def f2(self):
print('base.f2')

class Foo(Base):
def f2(self):
print('foo.f2')

obj = Foo()
obj.f1() # 优先去Foo类中找f1,因为调用f1的那个对象是Foo类创建出来的。


>>> before
>>> foo.f2
>>> base.f1

b1 = Base()
b1.f1()

>>> before
>>> base.f2
>>> base.f1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class TCPServer:
def f1(self):
print("TCPServer")

class ThreadingMixIn:
def f1(self):
print("ThreadingMixIn")

class ThreadingTCPServer(ThreadingMixIn, TCPServer):
def run(self):
print('before')
self.f1()
print('after')

obj = ThreadingTCPServer()
obj.run()

>>> before
>>> ThreadingMixIn
>>> after
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class BaseServer:
def serve_forever(self, poll_interval=0.5):
self._handle_request_noblock()
def _handle_request_noblock(self):
self.process_request(request, client_address)

def process_request(self, request, client_address):
pass

class TCPServer(BaseServer):
pass

class ThreadingMixIn:
def process_request(self, request, client_address):
pass

class ThreadingTCPServer(ThreadingMixIn, TCPServer):
pass

obj = ThreadingTCPServer()
obj.serve_forever()

image-20210126192330261

小结:

  • 执行对象.方法时,优先去当前对象所关联的类中找,没有的话才去她的父类中查找。
  • Python支持多继承:先继承左边、再继承右边的。
  • self到底是谁?去self对应的那个类中去获取成员,没有就按照继承关系向上查找 。

2.3 多态

多态,按字面翻译其实就是多种形态。

  • 其他编程语言多态
  • Python中多态

其他编程语言中,是不允许这样类编写的,例如:Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Cat{  
public void eat() {
System.out.println("吃鱼");
}
}

class Dog {
public void eat() {
System.out.println("吃骨头");
}
public void work() {
System.out.println("看家");
}
}


public class Test {
public static void main(String[] args) {
obj1 = Cat()
obj2 = Cat()
show(obj1)
show(obj2)

obj3 = Dog()
show(obj3)
}

public static void show(Cat a) {
a.eat()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
abstract class Animal {  
abstract void eat();
}

class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
}

class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
public void work() {
System.out.println("看家");
}
}


public class Test {
public static void main(String[] args) {
obj1 = Cat()
show(obj1)

obj2 = Dog()
show(obj2)
}

public static void show(Animal a) {
a.eat()
}
}

在java或其他语言中的多态是基于:接口 或 抽象类和抽象方法来实现,让数据可以以多种形态存在。

在Python中则不一样,由于Python对数据类型没有任何限制,所以他天生支持多态。

1
2
3
4
5
6
def func(arg):
v1 = arg.copy() # 浅拷贝
print(v1)

func("武沛齐")
func([11,22,33,44])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Email(object):
def send(self):
print("发邮件")


class Message(object):
def send(self):
print("发短信")



def func(arg):
v1 = arg.send()
print(v1)


v1 = Email()
func(v1)

v2 = Message()
func(v2)

在程序设计中,鸭子类型(duck typing)是动态类型的一种风格。在鸭子类型中,关注点在于对象的行为,能作什么;而不是关注对象所属的类型,例如:一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟可以被称为鸭子。

小结:

  • 封装,将方法封装到类中 或 将数据封装到对象中,便于以后使用。

  • 继承,将类中的公共的方法提取到基类中去实现。

  • 多态,Python默认支持多态(这种方式称之为鸭子类型),最简单的基础下面的这段代码即可。

    1
    2
    3
    4
    5
    6
    def func(arg):
    v1 = arg.copy() # 浅拷贝
    print(v1)

    func("武沛齐")
    func([11,22,33,44])

3. 扩展:再看数据类型

在初步了解面向对象之后,再来看看我们之前学习的:str、list、dict等数据类型,他们其实都一个类,根据类可以创建不同类的对象。

image-20210118203012874

1
2
3
4
5
6
7
# 实例化一个str类的对象v1
v1 = str("武沛齐")

# 通过对象执行str类中的upper方法。
data = v1.upper()

print(data)

总结

  1. 类和对象的关系。

  2. 面向对象编程中常见的成员:

    • 绑定方法
    • 实例变量
  3. self到底是什么?

  4. 面向对象的三大特性。

  5. 面向对象的应用场景

    1. 数据封装。
    2. 封装数据 + 方法再对数据进行加工处理。
    3. 创建同一类的数据且同类数据可以具有相同的功能(方法)。
  6. 补充:在Python3中编写类时,默认都会继承object(即使不写也会自动继承)。

    1
    2
    3
    4
    5
    class Foo:
    pass

    class Foo(object):
    pass

    这一点在Python2是不同的:

    • 继承object,新式类
    • 不继承object,经典类

作业

  1. 简述面向对象三大特性?

  2. 将以下函数改成类的方式并调用 :

    1
    2
    def func(a1):   
    print(a1)
  3. 面向对象中的self指的是什么?

  4. 以下代码体现 向对象的什么特性?

    1
    2
    3
    4
    5
    6
    7
    8
    class Person(object):
    def __init__(self, name, age, gender):
    self.name = name
    self.age = age
    self.gender = gender


    obj = Person('武沛齐', 18, '男')
  5. 以下代码体现 向对象的 么特点?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Message(object):
    def email(self):
    """
    发送邮件
    :return:
    """
    pass

    def msg(self):
    """
    发送短信
    :return:
    """
    pass

    def wechat(self):
    """
    发送微信
    :return:
    """
    pass
  6. 看代码写结果

    1
    2
    3
    4
    5
    6
    7
    class Foo:
    def func(self):
    print('foo.func')

    obj = Foo()
    result = obj.func()
    print(result)
  7. 看代码写结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    class Base1:
    def f1(self):
    print('base1.f1')

    def f2(self):
    print('base1.f2')

    def f3(self):
    print('base1.f3')
    self.f1()


    class Base2:
    def f1(self):
    print('base2.f1')


    class Foo(Base1, Base2):
    def f0(self):
    print('foo.f0')
    self.f3()


    obj = Foo()
    obj.f0()
  8. 看代码写结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Base:
    def f1(self):
    print('base.f1')

    def f3(self):
    self.f1()
    print('base.f3')


    class Foo(Base):
    def f1(self):
    print('foo.f1')

    def f2(self):
    print('foo.f2')
    self.f3()


    obj = Foo()
    obj.f2()
  9. 补充代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    user_list = []
    while True:
    user = input("请输入用户名:")
    pwd = input("请输入密码:")
    email = input("请输入邮箱:")

    """
    # 需求
    1. while循环提示 户输 : 户名、密码、邮箱(正则满足邮箱格式)
    2. 为每个用户创建一个个对象,并添加到user_list中。
    3. 当列表中的添加 3个对象后,跳出循环并以此循环打印所有用户的姓名和邮箱
    """
  10. 补充代码:实现 户注册和登录。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    class User:
    def __init__(self, name, pwd):
    self.name = name
    self.pwd = pwd


    class Account:
    def __init__(self):
    # 用户列表,数据格式:[user对象,user对象,user对象]
    self.user_list = []

    def login(self):
    """
    用户登录,输入用户名和密码然后去self.user_list中校验用户合法性
    :return:
    """
    pass

    def register(self):
    """
    用户注册,没注册一个用户就创建一个user对象,然后添加到self.user_list中,表示注册成功。
    :return:
    """
    pass

    def run(self):
    """
    主程序
    :return:
    """
    pass


    if __name__ == '__main__':
    obj = Account()
    obj.run()

day16 阶段总结

课程目标:对第二模块 “函数和模块” 阶段的知识点进行总结和考试,让学员更好的掌握此模块的相关知识。

课程概要:

  • 知识补充
  • 阶段总结(思维导图)
  • 考试题

1.知识补充

1.1 nolocal关键字

在之前的课程中,我们学过global关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
name = 'root'


def outer():
name = "武沛齐"

def inner():
global name
name = 123

inner()
print(name)


outer()
print(name)

其实,还有一个nolocal关键字,用的比较少,此处作为了解即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
name = 'root'


def outer():
name = "武沛齐"

def inner():
nonlocal name
name = 123

inner()
print(name)


outer()
print(name)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
name = 'root'


def outer():
name = 'alex'

def func():
name = "武沛齐"

def inner():
nonlocal name
name = 123

inner()
print(name)

func()
print(name)


outer()
print(name)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
name = 'root'


def outer():
name = 'alex'

def func():
nonlocal name
name = "武沛齐"

def inner():
nonlocal name
name = 123

inner()
print(name)

func()
print(name)


outer()
print(name)

1.2 yield from

在生成器部分我们了解了yield关键字,其在python3.3之后有引入了一个yield from。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def foo():
yield 2
yield 2
yield 2


def func():
yield 1
yield 1
yield 1
yield from foo()
yield 1
yield 1


for item in func():
print(item)

1.3 深浅拷贝

  • 浅拷贝

    • 不可变类型,不拷贝。

      1
      2
      3
      4
      5
      6
      7
      import copy

      v1 = "武沛齐"
      print(id(v1)) # 140652260947312

      v2 = copy.copy(v1)
      print(id(v2)) # 140652260947312

      按理说拷贝v1之后,v2的内存地址应该不同,但由于python内部优化机制,内存地址是相同的,因为对不可变类型而言,如果以后修改值,会重新创建一份数据,不会影响原数据,所以,不拷贝也无妨。

    • 可变类型,只拷贝第一层。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      import copy

      v1 = ["武沛齐", "root", [44, 55]]
      print(id(v1)) # 140405837216896
      print(id(v1[2])) # 140405837214592

      v2 = copy.copy(v1)
      print(id(v2)) # 140405837214784
      print(id(v2[2])) # 140405837214592

      image-20210106151332792

  • 深拷贝

    • 不可变类型,不拷贝

      1
      2
      3
      4
      5
      6
      7
      import copy

      v1 = "武沛齐"
      print(id(v1)) # 140188538697072

      v2 = copy.deepcopy(v1)
      print(id(v2)) # 140188538697072

      特殊的元组:

      • 元组元素中无可变类型,不拷贝

        1
        2
        3
        4
        5
        6
        7
        import copy

        v1 = ("武沛齐", "root")
        print(id(v1)) # 140243298961984

        v2 = copy.deepcopy(v1)
        print(id(v2)) # 140243298961984
      • 元素元素中有可变类型,找到所有【可变类型】或【含有可变类型的元组】 均拷贝一份

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        import copy

        v1 = ("武沛齐", "root", [11, [44, 55], (11, 22), (11, [], 22), 33])
        v2 = copy.deepcopy(v1)

        print(id(v1)) # 140391475456384
        print(id(v2)) # 140391475456640

        print(id(v1[2])) # 140352552779008
        print(id(v2[2])) # 140352552920448

        print(id(v1[2][1])) # 140642999940480
        print(id(v2[2][1])) # 140643000088832

        print(id(v1[2][2])) # 140467039914560
        print(id(v2[2][2])) # 140467039914560

        print(id(v1[2][3])) # 140675479841152
        print(id(v2[2][3])) # 140675480454784
    • 可变类型,找到所有层级的 【可变类型】或【含有可变类型的元组】 均拷贝一份

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      import copy

      v1 = ["武沛齐", "root", [11, [44, 55], (11, 22), (11, [], 22), 33]]
      v2 = copy.deepcopy(v1)

      print(id(v1)) # 140391475456384
      print(id(v2)) # 140391475456640

      print(id(v1[2])) # 140352552779008
      print(id(v2[2])) # 140352552920448

      print(id(v1[2][1])) # 140642999940480
      print(id(v2[2][1])) # 140643000088832

      print(id(v1[2][2])) # 140467039914560
      print(id(v2[2][2])) # 140467039914560

      print(id(v1[2][3])) # 140675479841152
      print(id(v2[2][3])) # 140675480454784
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      import copy

      v1 = ["武沛齐", "root", [44, 55]]
      v2 = copy.deepcopy(v1)

      print(id(v1)) # 140405837216896
      print(id(v2)) # 140405837214784


      print(id(v1[2])) # 140563140392256
      print(id(v2[2])) # 140563140535744

      image-20210106152704591

2.阶段总结

image-20210106143928141

3.考试题

考试题的目的是让大家对自己近期知识点学习练习 以及 自测,请大家务必【独立】完成(切勿翻看笔记 & 切勿网上搜索 )。

  • 第一步:自己独立完成(编程题目可以在pycharm中编写)

  • 第二步:做完之后,翻看自己笔记去修改和更正。

  • 第三步:觉自己做的没问题了,最后再去看考试题的参考答案和讲解。

详情见附件《第二阶段考试题.md》文件。

day15 内置模块和开发规范

image-20210104122555935

目标:掌握常见的内置模块的使用及了解软件开发的规范。

今日概要:

  • 内置模块
    • json
    • time
    • datetime
    • re
  • 开发规范
    • 主文件
    • 配置文件
    • 数据
    • 附件
    • 业务代码

1. 内置模块

1.1 json

json模块,是python内部的一个模块,可以将python的数据格式 转换为json格式的数据,也可以将json格式的数据转换为python的数据格式。

json格式,是一个数据格式(本质上就是个字符串,常用语网络数据传输)

1
2
3
4
5
6
7
8
9
# Python中的数据类型的格式
data = [
{"id": 1, "name": "武沛齐", "age": 18},
{"id": 2, "name": "alex", "age": 18},
('wupeiqi',123),
]

# JSON格式
value = '[{"id": 1, "name": "武沛齐", "age": 18}, {"id": 2, "name": "alex", "age": 18},["wupeiqi",123]]'

1.1.1 核心功能

json格式的作用?

1
2
3
4
5
6
7
跨语言数据传输,例如:
A系统用Python开发,有列表类型和字典类型等。
B系统用Java开发,有数组、map等的类型。

语言不同,基础数据类型格式都不同。

为了方便数据传输,大家约定一个格式:json格式,每种语言都是将自己数据类型转换为json格式,也可以将json格式的数据转换为自己的数据类型。

image-20210104123415566

Python数据类型与json格式的相互转换:

  • 数据类型 -> json ,一般称为:序列化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import json

    data = [
    {"id": 1, "name": "武沛齐", "age": 18},
    {"id": 2, "name": "alex", "age": 18},
    ]

    res = json.dumps(data)
    print(res) # '[{"id": 1, "name": "\u6b66\u6c9b\u9f50", "age": 18}, {"id": 2, "name": "alex", "age": 18}]'

    res = json.dumps(data, ensure_ascii=False)
    print(res) # '[{"id": 1, "name": "武沛齐", "age": 18}, {"id": 2, "name": "alex", "age": 18}]'
  • json格式 -> 数据类型,一般称为:反序列化

    1
    2
    3
    4
    5
    6
    7
    import json

    data_string = '[{"id": 1, "name": "武沛齐", "age": 18}, {"id": 2, "name": "alex", "age": 18}]'

    data_list = json.loads(data_string)

    print(data_list)

练习题

  1. 写网站,给用户返回json格式数据

    • 安装flask模块,协助我们快速写网站(之前已安装过)

      1
      pip3 install flask
    • 使用flask写网站

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      import json
      from flask import Flask

      app = Flask(__name__)


      def index():
      return "首页"


      def users():
      data = [
      {"id": 1, "name": "武沛齐", "age": 18},
      {"id": 2, "name": "alex", "age": 18},
      ]
      return json.dumps(data)


      app.add_url_rule('/index/', view_func=index, endpoint='index')
      app.add_url_rule('/users/', view_func=users, endpoint='users')

      if __name__ == '__main__':
      app.run()
  2. 发送网络请求,获取json格式数据并处理。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import json
    import requests

    url = "https://movie.douban.com/j/search_subjects?type=movie&tag=%E7%83%AD%E9%97%A8&sort=recommend&page_limit=5&page_start=20"

    res = requests.get(
    url=url,
    headers={
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
    }
    )

    # json格式
    print(res.text)

    # json格式转换为python数据类型
    data_dict = json.loads(res.text)
    print(data_dict)

1.1.2 类型要求

python的数据类型转换为 json 格式,对数据类型是有要求的,默认只支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+-------------------+---------------+
| Python | JSON |
+===================+===============+
| dict | object |
+-------------------+---------------+
| list, tuple | array |
+-------------------+---------------+
| str | string |
+-------------------+---------------+
| int, float | number |
+-------------------+---------------+
| True | true |
+-------------------+---------------+
| False | false |
+-------------------+---------------+
| None | null |
+-------------------+---------------+
1
2
3
4
data = [
{"id": 1, "name": "武沛齐", "age": 18},
{"id": 2, "name": "alex", "age": 18},
]

其他类型如果想要支持,需要自定义JSONEncoder 才能实现【目前只需要了解大概意思即可,以后项目开发中用到了还会讲解。】,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import json
from decimal import Decimal
from datetime import datetime

data = [
{"id": 1, "name": "武沛齐", "age": 18, 'size': Decimal("18.99"), 'ctime': datetime.now()},
{"id": 2, "name": "alex", "age": 18, 'size': Decimal("9.99"), 'ctime': datetime.now()},
]


class MyJSONEncoder(json.JSONEncoder):
def default(self, o):
if type(o) == Decimal:
return str(o)
elif type(o) == datetime:
return o.strftime("%Y-%M-%d")
return super().default(o)


res = json.dumps(data, cls=MyJSONEncoder)
print(res)

1.1.3 其他功能

json模块中常用的是:

  • json.dumps,序列化生成一个字符串。

  • json.loads,发序列化生成python数据类型。

  • json.dump,将数据序列化并写入文件(不常用)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import json

    data = [
    {"id": 1, "name": "武沛齐", "age": 18},
    {"id": 2, "name": "alex", "age": 18},
    ]

    file_object = open('xxx.json', mode='w', encoding='utf-8')

    json.dump(data, file_object)

    file_object.close()
  • json.load,读取文件中的数据并反序列化为python的数据类型(不常用)

    1
    2
    3
    4
    5
    6
    7
    8
    import json

    file_object = open('xxx.json', mode='r', encoding='utf-8')

    data = json.load(file_object)
    print(data)

    file_object.close()

1.2 时间处理

  • UTC/GMT:世界时间

  • 本地时间:本地时区的时间。

Python中关于时间处理的模块有两个,分别是time和datetime。

1.2.1 time

1
2
3
4
5
6
7
8
9
10
11
import time

# 获取当前时间戳(自1970-1-1 00:00)
v1 = time.time()
print(v1)

# 时区
v2 = time.timezone

# 停止n秒,再执行后续的代码。
time.sleep(5)

1.2.2 datetime

在平时开发过程中的时间一般是以为如下三种格式存在:

  • datetime

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from datetime import datetime, timezone, timedelta

    v1 = datetime.now() # 当前本地时间
    print(v1)

    tz = timezone(timedelta(hours=7)) # 当前东7区时间
    v2 = datetime.now(tz)
    print(v2)

    v3 = datetime.utcnow() # 当前UTC时间
    print(v3)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from datetime import datetime, timedelta

    v1 = datetime.now()
    print(v1)

    # 时间的加减
    v2 = v1 + timedelta(days=140, minutes=5)
    print(v2)

    # datetime类型 + timedelta类型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from datetime import datetime, timezone, timedelta

    v1 = datetime.now()
    print(v1)

    v2 = datetime.utcnow() # 当前UTC时间
    print(v2)

    # datetime之间相减,计算间隔时间(不能相加)
    data = v1 - v2
    print(data.days, data.seconds / 60 / 60, data.microseconds)

    # datetime类型 - datetime类型
    # datetime类型 比较 datetime类型
  • 字符串

    1
    2
    3
    4
    # 字符串格式的时间  ---> 转换为datetime格式时间
    text = "2021-11-11"
    v1 = datetime.strptime(text,'%Y-%m-%d') # %Y 年,%m,月份,%d,天。
    print(v1)
    1
    2
    3
    4
    # datetime格式 ----> 转换为字符串格式
    v1 = datetime.now()
    val = v1.strftime("%Y-%m-%d %H:%M:%S")
    print(val)
  • 时间戳

    1
    2
    3
    4
    # 时间戳格式 --> 转换为datetime格式
    ctime = time.time() # 11213245345.123
    v1 = datetime.fromtimestamp(ctime)
    print(v1)
    1
    2
    3
    4
    # datetime格式 ---> 转换为时间戳格式
    v1 = datetime.now()
    val = v1.timestamp()
    print(val)

image-20210104171741529

练习题

  1. 日志记录,将用户输入的信息写入到文件,文件名格式为年-月-日-时-分.txt

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    from datetime import datetime

    while True:
    text = input("请输入内容:")
    if text.upper() == "Q":
    break

    current_datetime = datetime.now().strftime("%Y-%m-%d-%H-%M")
    file_name = "{}.txt".format(current_datetime)

    with open(file_name, mode='a', encoding='utf-8') as file_object:
    file_object.write(text)
    file_object.flush()
  2. 用户注册,将用户信息写入Excel,其中包含:用户名、密码、注册时间 三列。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    import os
    import hashlib
    from datetime import datetime

    from openpyxl import load_workbook
    from openpyxl import workbook


    BASE_DIR = os.path.dirname(os.path.abspath(__file__))
    FILE_NAME = "db.xlsx"


    def md5(origin):
    hash_object = hashlib.md5("sdfsdfsdfsd23sd".encode('utf-8'))
    hash_object.update(origin.encode('utf-8'))
    return hash_object.hexdigest()


    def register(username, password):
    db_file_path = os.path.join(BASE_DIR, FILE_NAME)
    if os.path.exists(db_file_path):
    wb = load_workbook(db_file_path)
    sheet = wb.worksheets[0]
    next_row_position = sheet.max_row + 1
    else:
    wb = workbook.Workbook()
    sheet = wb.worksheets[0]
    next_row_position = 1

    user = sheet.cell(next_row_position, 1)
    user.value = username

    pwd = sheet.cell(next_row_position, 2)
    pwd.value = md5(password)

    ctime = sheet.cell(next_row_position, 3)
    ctime.value = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    wb.save(db_file_path)


    def run():
    while True:
    username = input("请输入用户名:")
    if username.upper() == "Q":
    break
    password = input("请输入密码:")
    register(username, password)


    if __name__ == '__main__':
    run()

1.3 正则表达式相关

当给你一大堆文本信息,让你提取其中的指定数据时,可以使用正则来实现。例如:提取文本中的邮箱和手机号

1
2
3
4
5
6
import re

text = "楼主太牛逼了,在线想要 442662578@qq.com和xxxxx@live.com谢谢楼主,手机号也可15131255789,搞起来呀"

phone_list = re.findall("1[3|5|8|9]\d{9}", text)
print(phone_list)

1.3.1 正则表达式

1. 字符相关
  • wupeiqi 匹配文本中的wupeiqi

    1
    2
    3
    4
    5
    import re

    text = "你好wupeiqi,阿斯顿发wupeiqasd 阿士大夫能接受的wupeiqiff"
    data_list = re.findall("wupeiqi", text)
    print(data_list) # ['wupeiqi', 'wupeiqi'] 可用于计算字符串中某个字符出现的次数
  • [abc] 匹配a或b或c 字符。

    1
    2
    3
    4
    5
    import re

    text = "你2b好wupeiqi,阿斯顿发awupeiqasd 阿士大夫a能接受的wffbbupqaceiqiff"
    data_list = re.findall("[abc]", text)
    print(data_list) # ['b', 'a', 'a', 'a', 'b', 'b', 'c']
    1
    2
    3
    4
    5
    import re

    text = "你2b好wupeiqi,阿斯顿发awupeiqasd 阿士大夫a能接受的wffbbupqcceiqiff"
    data_list = re.findall("q[abc]", text)
    print(data_list) # ['qa', 'qc']
  • [^abc] 匹配除了abc意外的其他字符。

    1
    2
    3
    4
    5
    import re

    text = "你wffbbupceiqiff"
    data_list = re.findall("[^abc]", text)
    print(data_list) # ['你', 'w', 'f', 'f', 'u', 'p', 'e', 'i', 'q', 'i', 'f', 'f']
  • [a-z] 匹配a~z的任意字符( [0-9]也可以 )。

    1
    2
    3
    4
    5
    import re

    text = "alexrootrootadmin"
    data_list = re.findall("t[a-z]", text)
    print(data_list) # ['tr', 'ta']
  • . 代指除换行符以外的任意字符。

    1
    2
    3
    4
    5
    import re

    text = "alexraotrootadmin"
    data_list = re.findall("r.o", text)
    print(data_list) # ['rao', 'roo']
    1
    2
    3
    4
    5
    import re

    text = "alexraotrootadmin"
    data_list = re.findall("r.+o", text) # 贪婪匹配
    print(data_list) # ['raotroo']
    1
    2
    3
    4
    5
    import re

    text = "alexraotrootadmin"
    data_list = re.findall("r.+?o", text) # 非贪婪匹配
    print(data_list) # ['rao']
  • \w 代指字母或数字或下划线(汉字)。

    1
    2
    3
    4
    5
    import re

    text = "北京武沛alex齐北 京武沛alex齐"
    data_list = re.findall("武\w+x", text)
    print(data_list) # ['武沛alex', '武沛alex']
  • \d 代指数字

    1
    2
    3
    4
    5
    import re

    text = "root-ad32min-add3-admd1in"
    data_list = re.findall("d\d", text)
    print(data_list) # ['d3', 'd3', 'd1']
    1
    2
    3
    4
    5
    import re

    text = "root-ad32min-add3-admd1in"
    data_list = re.findall("d\d+", text)
    print(data_list) # ['d32', 'd3', 'd1']
  • \s 代指任意的空白符,包括空格、制表符等。

    1
    2
    3
    4
    5
    import re

    text = "root admin add admin"
    data_list = re.findall("a\w+\s\w+", text)
    print(data_list) # ['admin add']
2. 数量相关
  • * 重复0次或更多次

    1
    2
    3
    4
    5
    import re

    text = "他是大B个,确实是个大2B。"
    data_list = re.findall("大2*B", text)
    print(data_list) # ['大B', '大2B']
  • + 重复1次或更多次

    1
    2
    3
    4
    5
    import re

    text = "他是大B个,确实是个大2B,大3B,大66666B。"
    data_list = re.findall("大\d+B", text)
    print(data_list) # ['大2B', '大3B', '大66666B']
  • ? 重复0次或1次

    1
    2
    3
    4
    5
    import re

    text = "他是大B个,确实是个大2B,大3B,大66666B。"
    data_list = re.findall("大\d?B", text)
    print(data_list) # ['大B', '大2B', '大3B']
  • {n} 重复n次

    1
    2
    3
    4
    5
    import re

    text = "楼主太牛逼了,在线想要 442662578@qq.com和xxxxx@live.com谢谢楼主,手机号也可15131255789,搞起来呀"
    data_list = re.findall("151312\d{5}", text)
    print(data_list) # ['15131255789']
  • {n,} 重复n次或更多次

    1
    2
    3
    4
    5
    6
    import re

    text = "楼主太牛逼了,在线想要 442662578@qq.com和xxxxx@live.com谢谢楼主,手机号也可15131255789,搞起来呀"
    data_list = re.findall("\d{9,}", text)
    print(data_list) # ['442662578', '15131255789']

  • {n,m} 重复n到m次

    1
    2
    3
    4
    5
    import re

    text = "楼主太牛逼了,在线想要 442662578@qq.com和xxxxx@live.com谢谢楼主,手机号也可15131255789,搞起来呀"
    data_list = re.findall("\d{10,15}", text)
    print(data_list) # ['15131255789']
3. 括号(分组)
  • 提取数据区域

    1
    2
    3
    4
    5
    import re

    text = "楼主太牛逼了,在线想要 442662578@qq.com和xxxxx@live.com谢谢楼主,手机号也可15131255789,搞起来呀"
    data_list = re.findall("15131(2\d{5})", text)
    print(data_list) # ['255789']
    1
    2
    3
    4
    5
    import re

    text = "楼主太牛逼了,在线想要 442662578@qq.com和xxxxx@live.com谢谢楼主,手机号也可15131255789,搞起来15131266666呀"
    data_list = re.findall("15(13)1(2\d{5})", text)
    print(data_list) # [ ('13', '255789') ]
    1
    2
    3
    4
    5
    import re

    text = "楼主太牛逼了,在线想要 442662578@qq.com和xxxxx@live.com谢谢楼主,手机号也可15131255789,搞起来呀"
    data_list = re.findall("(15131(2\d{5}))", text)
    print(data_list) # [('15131255789', '255789')]
  • 获取指定区域 + 或条件

    1
    2
    3
    4
    5
    import re

    text = "楼主15131root太牛15131alex逼了,在线想要 442662578@qq.com和xxxxx@live.com谢谢楼主,手机号也可15131255789,搞起来呀"
    data_list = re.findall("15131(2\d{5}|r\w+太)", text)
    print(data_list) # ['root太', '255789']
    1
    2
    3
    4
    5
    import re

    text = "楼主15131root太牛15131alex逼了,在线想要 442662578@qq.com和xxxxx@live.com谢谢楼主,手机号也可15131255789,搞起来呀"
    data_list = re.findall("(15131(2\d{5}|r\w+太))", text)
    print(data_list) # [('15131root太', 'root太'), ('15131255789', '255789')]
    练习题
  1. 利用正则匹配QQ号码

    1
    [1-9]\d{4,}
  2. 身份证号码

    1
    2
    3
    4
    5
    import re

    text = "dsf130429191912015219k13042919591219521Xkk"
    data_list = re.findall("\d{17}[\dX]", text) # [abc]
    print(data_list) # ['130429191912015219', '13042919591219521X']
    1
    2
    3
    4
    5
    import re

    text = "dsf130429191912015219k13042919591219521Xkk"
    data_list = re.findall("\d{17}(\d|X)", text)
    print(data_list) # ['9', 'X']
    1
    2
    3
    4
    5
    import re

    text = "dsf130429191912015219k13042919591219521Xkk"
    data_list = re.findall("(\d{17}(\d|X))", text)
    print(data_list) # [('130429191912015219', '9'), ('13042919591219521X', 'X')]
    1
    2
    3
    4
    5
    import re

    text = "dsf130429191912015219k13042919591219521Xkk"
    data_list = re.findall("(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X)", text)
    print(data_list) # [('130429', '1919', '12', '01', '521', '9'), ('130429', '1959', '12', '19', '521', 'X')]
  3. 手机号

    1
    2
    3
    4
    5
    import re

    text = "我的手机哈是15133377892,你的手机号是1171123啊?"
    data_list = re.findall("1[3-9]\d{9}", text)
    print(data_list) # ['15133377892']
  4. 邮箱地址

    1
    2
    3
    4
    5
    import re

    text = "楼主太牛逼了,在线想要 442662578@qq.com和xxxxx@live.com谢谢楼主,手机号也可15131255789,搞起来呀"
    email_list = re.findall("\w+@\w+\.\w+",text)
    print(email_list) # ['442662578@qq.com和xxxxx']
    1
    2
    3
    4
    5
    6
    import re

    text = "楼主太牛逼了,在线想要 442662578@qq.com和xxxxx@live.com谢谢楼主,手机号也可15131255789,搞起来呀"
    email_list = re.findall("[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+", text, re.ASCII)
    print(email_list) # ['442662578@qq.com', 'xxxxx@live.com']

    1
    2
    3
    4
    5
    import re

    text = "楼主太牛逼了,在线想要 442662578@qq.com和xxxxx@live.com谢谢楼主,手机号也可15131255789,搞起来呀"
    email_list = re.findall("\w+@\w+\.\w+", text, re.ASCII)
    print(email_list) # ['442662578@qq.com', 'xxxxx@live.com']
    1
    2
    3
    4
    5
    import re

    text = "楼主太牛44266-2578@qq.com逼了,在线想要 442662578@qq.com和xxxxx@live.com谢谢楼主,手机号也可15131255789,搞起来呀"
    email_list = re.findall("(\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*)", text, re.ASCII)
    print(email_list) # [('44266-2578@qq.com', '-2578', '', ''), ('xxxxx@live.com', '', '', '')]
  5. 补充代码,实现获取页面上的所有评论(已实现),并提取里面的邮箱。

    1
    2
    3
    # 先安装两个模块
    pip3 install requests
    pip3 install beautifulsoup4
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import re
    import requests
    from bs4 import BeautifulSoup

    res = requests.get(
    url="https://www.douban.com/group/topic/79870081/",
    headers={
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36',
    }
    )
    bs_object = BeautifulSoup(res.text, "html.parser")
    comment_object_list = bs_object.find_all("p", attrs={"class": "reply-content"})
    for comment_object in comment_object_list:
    text = comment_object.text
    print(text)
    # 请继续补充代码,提取text中的邮箱地址

4. 起始和结束

上述示例中都是去一段文本中提取数据,只要文本中存在即可。

但,如果要求用户输入的内容必须是指定的内容开头和结尾,比就需要用到如下两个字符。

  • ^ 开始
  • $ 结束
1
2
3
4
5
import re

text = "啊442662578@qq.com我靠"
email_list = re.findall("^\w+@\w+.\w+$", text, re.ASCII)
print(email_list) # []
1
2
3
4
5
import re

text = "442662578@qq.com"
email_list = re.findall("^\w+@\w+.\w+$", text, re.ASCII)
print(email_list) # ['442662578@qq.com']

这种一般用于对用户输入数据格式的校验比较多,例如:

1
2
3
4
5
6
7
8
import re

text = input("请输入邮箱:")
email = re.findall("^\w+@\w+.\w+$", text, re.ASCII)
if not email:
print("邮箱格式错误")
else:
print(email)
5. 特殊字符

由于正则表达式中 * . \ { } ( ) 等都具有特殊的含义,所以如果想要在正则中匹配这种指定的字符,需要转义,例如:

1
2
3
4
5
import re

text = "我是你{5}爸爸"
data = re.findall("你{5}爸", text)
print(data) # []
1
2
3
4
5
import re

text = "我是你{5}爸爸"
data = re.findall("你\{5\}爸", text)
print(data)

1.3.2 re模块

python中提供了re模块,可以处理正则表达式并对文本进行处理。

  • findall,获取匹配到的所有数据

    1
    2
    3
    4
    5
    import re

    text = "dsf130429191912015219k13042919591219521Xkk"
    data_list = re.findall("(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X)", text)
    print(data_list) # [('130429', '1919', '12', '01', '521', '9'), ('130429', '1959', '12', '19', '521', 'X')]
  • match,从起始位置开始匹配,匹配成功返回一个对象,未匹配成功返回None

    1
    2
    3
    4
    5
    import re

    text = "大小逗2B最逗3B欢乐"
    data = re.match("逗\dB", text)
    print(data) # None
    1
    2
    3
    4
    5
    6
    7
    import re

    text = "逗2B最逗3B欢乐"
    data = re.match("逗\dB", text)
    if data:
    content = data.group() # "逗2B"
    print(content)
  • search,浏览整个字符串去匹配第一个,未匹配成功返回None

    1
    2
    3
    4
    5
    6
    import re

    text = "大小逗2B最逗3B欢乐"
    data = re.search("逗\dB", text)
    if data:
    print(data.group()) # "逗2B"
  • sub,替换匹配成功的位置

    1
    2
    3
    4
    5
    import re

    text = "逗2B最逗3B欢乐"
    data = re.sub("\dB", "沙雕", text)
    print(data) # 逗沙雕最逗沙雕欢乐
    1
    2
    3
    4
    5
    import re

    text = "逗2B最逗3B欢乐"
    data = re.sub("\dB", "沙雕", text, 1)
    print(data) # 逗沙雕最逗3B欢乐
  • split,根据匹配成功的位置分割

    1
    2
    3
    4
    5
    import re

    text = "逗2B最逗3B欢乐"
    data = re.split("\dB", text)
    print(data) # ['逗', '最逗', '欢乐']
    1
    2
    3
    4
    5
    import re

    text = "逗2B最逗3B欢乐"
    data = re.split("\dB", text, 1)
    print(data) # ['逗', '最逗3B欢乐']
  • finditer

    1
    2
    3
    4
    5
    6
    import re

    text = "逗2B最逗3B欢乐"
    data = re.finditer("\dB", text)
    for item in data:
    print(item.group())
    1
    2
    3
    4
    5
    6
    import re

    text = "逗2B最逗3B欢乐"
    data = re.finditer("(?P<xx>\dB)", text) # 命名分组
    for item in data:
    print(item.groupdict())
    1
    2
    3
    4
    5
    text = "dsf130429191912015219k13042919591219521Xkk"
    data_list = re.finditer("\d{6}(?P<year>\d{4})(?P<month>\d{2})(?P<day>\d{2})\d{3}[\d|X]", text)
    for item in data_list:
    info_dict = item.groupdict()
    print(info_dict)

小结

到此,关于最常见的内置模块就全部讲完了(共11个),现阶段只需要掌握这些模块的使用即可,在后续的课程和练习题中也会涉及到一起其他内置模块。

  • os
  • shutil
  • sys
  • random
  • hashlib
  • configparser
  • xml
  • json
  • time
  • datetime
  • re

2. 项目开发规范

现阶段,我们在开发一些程序时(终端运行),应该遵循一些结构的规范,让你的系统更加专业。

2.1 单文件应用

当基于python开发简单应用时(一个py文件就能搞定),需要注意如下几点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
"""
文件注释
"""

import re
import random

import requests
from openpyxl import load_workbook

DB = "XXX"


def do_something():
""" 函数注释 """

# TODO 待完成时,下一期实现xxx功能
for i in range(10):
pass


def run():
""" 函数注释 """

# 对功能代码进行注释
text = input(">>>")
print(text)


if __name__ == '__main__':
run()
image-20210105160728297

2.2 单可执行文件

新创建一个项目,假设名字叫 【crm】,可以创建如下文件和文件夹来存放代码和数据。

1
2
3
4
5
6
7
crm
├── app.py 文件,程序的主文件(尽量精简)
├── config.py 文件,配置文件(放相关配置信息,代码中读取配置信息,如果想要修改配置,即可以在此修改,不用再去代码中逐一修改了)
├── db 文件夹,存放数据
├── files 文件夹,存放文件
├── src 包,业务处理的代码
└── utils 包,公共功能

示例程序见附件:crm.zip

image-20210105163335127

2.3 多可执行文件

新创建项目,假设名称叫【killer】,可以创建如下文件和文件夹来存放代码和数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
killer
├── bin 文件夹,存放多个主文件(可运行)
│   ├── app1.py
│   └── app2.py
├── config 包,配置文件
│   ├── __init__.py
│   └── settings.py
├── db 文件夹,存放数据
├── files 文件夹,存放文件
├── src 包,业务代码
│   └── __init__.py
└── utils 包,公共功能
└── __init__.py

示例程序见附件:killer.zip

image-20210105164740583

总结

  1. json格式和json模块
  2. json模块处理特殊的数据类型
  3. datetime格式与字符串、时间戳以及相关之间的转换。
  4. datetime格式时间与timedelta的加减。
  5. 两个datetime相减可以计算时间间隔,得到的是一个timedelta格式的时间。
  6. 了解正则表达式的编写方式和python中re模块的使用。
  7. 项目开发规范。

作业:开发短视频资讯平台

  • 有video.csv视频库文件,其中有999条短视频数据,格式如下:【 video.csv 文件已为大家提供好,在day15课件目录下。 】

    image-20210105223331765

  • 项目的核心功能有:

    • 分页看新闻(每页显示10条),提示用户输入页码,根据页码显示指定页面的数据。

      • 提示用户输入页码,根据页码显示指定页面的数据。
      • 当用户输入的页码不存在时,默认显示第1页
    • 搜索专区

      • 用户输入关键字,根据关键词筛选出所有匹配成功的短视频资讯。
      • 支持的搜索两种搜索格式:
        • id=1715025,筛选出id等于1715025的视频(video.csv的第一列)。
        • key=文本,模糊搜索,筛选包含关键字的所有新闻(video.csv的第二列)。
    • 下载专区

      • 用户输入视频id,根据id找到对应的mp4视频下载地址,然后下载视频到项目的files目录。

        • 视频的文件名为:视频id-年-月-日-时-分-秒.mp4

        • 视频下载代码示例

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          import requests

          res = requests.get(
          url='https://video.pearvideo.com/mp4/adshort/20210105/cont-1715046-15562045_adpkg-ad_hd.mp4'
          )

          # 视频总大小(字节)
          file_size = int(res.headers['Content-Length'])

          download_size = 0
          with open('xxx.mp4', mode='wb') as file_object:
          # 分块读取下载的视频文件(最多一次读128字节),并逐一写入到文件中。 len(chunk)表示实际读取到每块的视频文件大小。
          for chunk in res.iter_content(128):
          download_size += len(chunk)
          file_object.write(chunk)
          file_object.flush()
          message = "视频总大小为:{}字节,已下载{}字节。".format(file_size, download_size)
          print(message)
          file_object.close()

          res.close()
        • 下载的过程中,输出已下载的百分比,示例代码如下:

          1
          2
          3
          4
          5
          6
          7
          8
          9
          import time

          print("正在下载中...")
          for i in range(101):
          text = "\r{}%".format(i)
          print(text, end="")
          time.sleep(0.2)

          print("\n下载完成")

附赠

自动采集梨视频1000条资讯的爬虫脚本。梨视频平台系统更新后可能会导致下载失败,到时候需根据平台调整再来修改代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
"""
下载梨视频的:视频ID,视频标题,视频URL地址 并写入到本次 video.csv 文件中。

运行此脚本需要预先安装:
pip install request
pip install beautifulsoup4

"""
import requests
from bs4 import BeautifulSoup


def get_mp4_url(video_id):
data = requests.get(
url="https://www.pearvideo.com/videoStatus.jsp?contId={}".format(video_id),
headers={
"Referer": "https://www.pearvideo.com/video_{}".format(video_id),
}
)
response = data.json()
image_url = response['videoInfo']['video_image']
video_url = response['videoInfo']['videos']['srcUrl']
middle = image_url.rsplit('/', 1)[-1].rsplit('-', 1)[0]
before, after = video_url.rsplit('/', 1)
suffix = after.split('-', 1)[-1]
url = "{}/{}-{}".format(before, middle, suffix)
return url


def download_video():
file_object = open('video.csv', mode='w', encoding='utf-8')
count = 0
while count <= 999:
res = requests.get(
url="https://www.pearvideo.com/category_loading.jsp?reqType=14&categoryId=&start={}".format(count)
)
bs = BeautifulSoup(res.text, 'lxml')
a_list = bs.find_all("a", attrs={'class': "vervideo-lilink"})
for tag in a_list:
title = tag.find('div', attrs={'class': "vervideo-title"}).text.strip()
video_id = tag.get('href').split('_')[-1]
mp4_url = get_mp4_url(video_id)
row = "{},{},{}\n".format(video_id, title, mp4_url)
file_object.write(row)
file_object.flush()
count += 1
message = "已下载{}个".format(count)
print(message)
file_object.close()


if __name__ == '__main__':
download_video()