Nature's Beauty
Explore the wonders of the world
格式化输出
1
|
print("Batch: {:10}, Loss: {:.2f}, Accuracy: {:.2%}".format(123, 0.1234567, 0.89))
|
:10 表示输出的字符串长度为 10,由于值为 123,所以前面加了 7 个空格。注意若是 :1 或者 :2,小于 3 的,不影响结果
:.2f 表示保留两位小数,四舍五入
:.2% 将数据转换成百分比的形式,并保留两位小数
stack 与 unstack
https://blog.csdn.net/haley_yuen/article/details/108454313
stack() 把列(columns) 转成行索引(row index)
1
2
3
4
5
6
7
8
9
10
11
12
|
# Date A B
# 2020-01-01 10 30
# 2020-01-02 20 40
# |
# v
# Date Variable Value
# 2020-01-01 A 10
# 2020-01-01 B 30
# 2020-01-02 A 20
# 2020-01-02 B 40
|
原本 A、B 是列上的变量,stack 将它们移到行索引的内层,形成了 层级索引
就像是列被 堆叠 到行上
而 unstack() 正好相反,是上述的逆过程,把行索引的某一层再转回列,得到宽表(DataFrame)
高阶函数
把函数作为参数传入函数中
高阶函数是函数式编程的体现
abs 是内置的求绝对值的函数
下面将 abs 传入自定义的函数中来求两个数的绝对值之和
1
2
3
4
5
6
|
def add_num(a, b, f):
return f(a) + f(b)
result2 = add_num(-1, 2, abs)
print(result2)
|
部分内置高阶函数
map
map(func, lst) 将传入的函数变量 func 作用到 lst 变量的 每个元素
1
2
3
4
5
6
7
8
|
def func1(x):
return x ** 2
list1 = [1, 2, 3, 4, 5]
result = map(func1, list1)
print(result)
print(list(result)) # [1, 4, 9, 16, 25]
|
注意 Python3 的 map 返回的是 迭代器,需要手动通过 list 转换成列表形式
Python2 则是直接返回 列表
reduce
reduce(func(x,y),lst) 其中 func 必须有两个参数,每次 func 计算的结果继续和序列的下一个元素做 累计计算
1
2
3
4
5
6
7
8
9
10
11
|
import functools
def func2(a, b):
return a + b
list2 = [1, 2, 3, 4, 5]
# 计算 list2 的累加和
result = functools.reduce(func2, list2)
print(result)
|
filter
filter(func, lst) 用于过滤序列,过滤掉不符合条件的元素,返回⼀个 filter 对象
1
2
3
4
5
6
7
8
9
|
def func3(x):
return x % 2 == 0
# 返回布尔值,满足条件的为 True
list3 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
result = filter(func3, list3)
print(result)
print(list(result))
|
函数名 是一个特殊的变量,保存了 函数的地址
1
2
3
4
5
6
7
8
|
def test():
print("哈哈,我是一个test!")
test()
# 将函数名赋值给变量 ret,获取函数 test 的地址
ret = test
print(ret)
# 之后可以通过 ret 调用函数
ret()
|
将单方法的类转换为函数
有些类除了 __init__ 初始化和一个函数外没有其他函数
如果想要把这个类转换成一个函数,可以选择使用闭包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
from urllib.request import urlopen
class UrlTemplate:
def __init__(self, template):
# 定义要访问的链接变量
self.template = template
def myopen(self, **kwargs):
# 返回原本的链接 + 给定的参数
print('打开链接:', self.template.format_map(kwargs))
return urlopen(self.template.format_map(kwargs))
cloudmusic = UrlTemplate('http://music.163.com/song/media/outer/url?id={num_id}.mp3')
req = cloudmusic.myopen(num_id=1901371647)
req.read().decode('utf-8')
|
上述 UrlTemplate 类就是仅包含了一个初始化参数的 __init__ 方法以及 open 函数的类
下面使用闭包进行修改
1
2
3
4
5
6
7
8
9
10
11
12
|
# 定义一个嵌套函数
def urltemplate(template):
def myopen(**kwargs):
return urlopen(template.format_map(kwargs))
return myopen
cloudmusic = urltemplate('http://music.163.com/song/media/outer/url?id={num_id}.mp3')
req = cloudmusic(num_id=1901371647)
req.read().decode('utf-8')
|
当遇到 需要给某个函数增加额外的状态信息 的问题,都可以考虑使用闭包
闭包通常是一种更加简洁和优雅的方案
回调函数
带额外状态信息的回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
def apply_async(func, *args, callback):
# 计算结果
result = func(*args)
# 调用回调函数打印结果
callback(result)
# callback 放在了 *args 位置参数后面
# 调用时候必须以关键字参数的形式传入,即 callback=。。。
# 定义用于求和的函数
def add(x, y):
return x + y
# 定义用于打印结果的函数
def print_result(result):
print('Got:', result)
apply_async(add, 2, 3, callback=print_result)
# 注意不带括号,因为是传入 apply_async 中才被调用
|
参数 2, 3 被当作了一个元组传入 add 函数中
这里的 print_result() 作为回调函数,只接受一个参数 result,不能再传入其他信息
假设现在想要知道每次打印的结果是第几次计算的结果
该如何修改呢?
1
2
3
4
5
6
7
8
9
10
11
|
# 方法一,修改回调函数
class ResultHandler:
def __init__(self):
self.sequence = 0 # 用于记录当前是第几次结果
def handler(self, result):
self.sequence += 1
print('[{}] Got: {}'.format(self.sequence, result))
r = ResultHandler()
apply_async(add, 2, 3, callback=r.handler)
|
考虑使用类实现回调函数,上述定义了 ResultHandler 类,将 handler() 方法作为回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# 方法二,将方法一中的类转换成闭包形式
def make_handler():
sequence = 0
def handler(result):
nonlocal sequence
# nonlocal 使得变量 sequence 的作用域不只在 handler 中
# 每次调用这个函数时,参数 sequence 是共享的
sequence += 1
print('[{}] Got: {}'.format(sequence, result))
return handler
handler = make_handler()
apply_async(add, 2, 3, callback=handler)
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# 方法三,使用协程
def make_handler():
sequence = 0
while True:
result = yield
sequence += 1
print('[{}] Got: {}'.format(sequence, result))
# 创建协程对象 handler。此时协程尚未执行,只是初始化
handler = make_handler()
# 启动协程,执行到第一个 yield 处暂停
next(handler)
# handler.send 作为回调函数传入,在 apply_async 中,计算完结果后会调用 callback(result),即 handler.send(result)
apply_async(add, 2, 3, callback=handler.send)
# send(result) 会将 result 发送给协程,协程从 yield 处恢复,将 result 赋值给变量 result
|
协程是一种可以暂停和恢复的函数,非常适合用于这种需要维护状态的场景
while True 无限循环,表示这个协程可以持续处理回调
yield 是一个暂停点,协程会在这里暂停,等待外部发送数据
当外部调用 send 方法时,send 的值会被赋值给左边的 result,然后协程继续执行
每次回调时,计数器 sequence 加 1
套接字编程
注:这里整理的有点乱,推荐看黑马教程,看如何将问题拆解,以及实现模块调用的
https://github.com/cess-100/python-study2/blob/master/day03/03-%E5%88%9B%E5%BB%BAUDPsocket.py
1
2
|
# 使用标准库 socket 实现网络通信
import socket
|
创建套接字
1
2
3
4
5
6
7
8
|
mysocket = socket.socket(
# 协议类型
socket.AF_INET,
# 传输方式
socket.SOCK_STREAM
)
# 关闭套接字
mysocket.close()
|
1
2
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as mysocket:
# ...
|
上述两种该方法均可,更推荐使用 with 语句来管理套接字
with 可以确保即使在发生异常时套接字也能被正确关闭,并自动处理资源的释放
- 套接字的基本类型
- 流式
socket.SOCK_STREAM
提供有序、可靠、双向连接的字节流
常用于 TCP 协议
- 数据报
socket.SOCK_DGRAM
提供无连接的数据报服务
常用于 UDP 协议
UDP 连接,收发数据
1
2
3
4
5
|
# 发送数据
mysocket.sendto(
'hello!'.encode(),
('192.168.166.166', 8080)
)
|
'hello!'.encode() 是待发送数据的二进制格式
('192.168.166.166', 8080) 是发送方的 IP 和端口号
1
2
3
4
5
6
7
8
|
# 接收数据,从套接字中接受 1024 字节的数据
recv_data = mysocket.recvfrom(1024)
# 指定 UTF-8 的解码格式,且解码失败时忽略错误
recv_text = recv_data[0].decode(
encoding='UTF-8',
errors='ignore'
)
|
注意 recvfrom 方法会造成 程序的阻塞 直到收到数据
收到的数据为嵌套元组 (内容的二进制形式, (发送方的 IP, 发送方的端口号))
绑定端口
收、发端的绑定方法差不多
1
2
|
# 例如将套接字绑定在 10.6.6.6 的 IP 和 6666 端口上
mysocket.bind(('10.6.6.6', 6666))
|
当接收端所绑定的 IP 为 空字符串 时,若计算机有 多个网卡,则不同网卡的数据都能被接收
设置广播
1
2
3
4
5
6
7
8
9
|
# 默认不允许发送广播,需要开启相关权限
.setsocketopt(参数一:套接字, 参数二:广播, 参数三:广播属性布尔值)
# 示例
mysocket.setsocketopt(
socket.SOL_SOCKET,
socket.SO_BROADCAST, True
)
|
TCP 连接
TCP 客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import socket
# 创建套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接到服务器
client_socket.connect(('localhost', 12345))
try:
# 发送数据
message = 'hello, server'
client_socket.send(message.encode())
# 接收响应
response = client_socket.recv(1024) # 二进制形式
print(f'Received: {response.decode()}')
finally:
client_socket.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
24
25
26
27
28
29
30
|
import socket
# 创建套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定地址和端口
server_socket.bind(('localhost', 12345))
# 监听连接
server_socket.listen()
while True:
# 接受连接,在连接之前,程序都处于阻塞状态
client_socket, addr = server_socket.accept()
# 与客户端连接后,返回【新的套接字对象和一个包含IP和端口号的元组】
print(f'Connected by {addr}')
try:
while True:
data = client_socket.recv(1024)
if not data:
break
print(f'Received: {data.decode()}')
client_socket.send(data)
finally:
client_socket.close()
# 关闭服务器套接字
server_socket.close()
|
可以将 accept 和收发数据操作置于 while 循环中,模拟多个客户端前后接入的情况
文件下载器
客户端发送文件名给服务端
服务端根据文件名循环读取对应的文件内容,然后将内容发送给客户端
客户端接收文件数据,循环写入到本地的文件中
收到的数据不用再解码,直接 file.write(recv_data) 将二进制数据写入
模拟浏览器
通过套接字与目标 IP 建立 TCP 连接
再拼接出 请求协议,包括:请求行、请求头、请求空行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
request_line = 'GET / HTTP/1.1\r\n'
request_header = 'Host:www.com\r\n'
request_blank = '\r\n'
# 请求行、请求头、请求空行
request_data = request_line + request_header + request_blank
# 示例
"""
GET / HTTP/1.1
Host:www.com
"""
|
请求的资源在请求行中:GET / HTTP/1.1\r\n,这里是 /,则默认返回首页 /index.html
只需要通过 request_data.decode().find('\r\n') 方法找到第一次出现 \r\n 的位置,然后将字符串拆分即可
之后 send 请求协议,获取相应内容
模拟 Web 服务器
创建套接字后,设置 地址重用
1
2
3
4
|
tcp_server_socket.setsockopt(
socket.SOL_SOCKET,
socket.SO_REUSEADDR, True
)
|
然后绑定端口,设置监听
之后在 while 循环中,accept 客户端的连接
根据接收到的请求协议,判断 request_data 是否为空
为空则直接关闭客户端的连接
不为空则拼接 响应报文,包括:响应行、响应头、响应空行、响应主体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
response_line = 'HTTP/1.1 200 OK\r\n'
response_header = 'Server:Python20WS/2.1\r\n'
response_blank = '\r\n'
response_body = '<html><h>Hello World!</h></html>'
# 响应行、响应头、响应空行、响应主体
response_data = response_line + response_header + response_blank + response_body
# 示例
"""
HTTP/1.1 200 OK
Server:Python20WS/2.1
<html><h>Hello World!</h></html>
"""
|
向客户端发送响应报文,最后关闭连接
上述的响应主体可以根据客户端的请求协议 从指定文件中以二进制形式读取 后,与响应行等拼接后发送
其他响应行:
1
2
3
|
"""
HTTP/1.1 404 Not Found\r\n
"""
|
若要进行面向对象封装:
1、在 __init__ 方法中创建套接字,设置地址重用,绑定端口号,开启被动监听
2、定义启动服务器的函数,接受客户端连接
3、定义接收信息的函数
线程
创建子线程
1
2
3
4
5
6
7
8
9
10
11
|
import threading
# 创建子线程对象
thread_obj = threading.Thread(
target=自定义的函数,
args=(参数1, 参数2, ...)
)
# 启动子线程
thread_obj.start()
|
除了上述通过 元组 完成子线程传参,还有:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# 字典传参
threading.Thread(
target=自定义的函数,
kwargs={'参数名1': 值1, '参数名2': 值2, ...}
)
# 混合使用
threading.Thread(
target=自定义的函数,
args=(参数1, 参数2, ...),
kwargs={'参数名3': 值3, '参数名4': 值4, ...}
)
|
-
获取线程对象
1
2
3
4
5
|
# 获取当前线程对象
threading.current_thread()
# 获取正在活跃的线程列表
threading.enumerate()
|
线程守护
线程守护是指将一个线程设置为 守护线程
当主线程结束时,守护线程也会随之结束
1
2
3
|
t1_obj = threading.Thread(target=)
# 将 t1_obj 线程设置为守护线程
t1_obj.setDaemon(True)
|
自定义线程类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# 继承 threading.Thread 类
class MyThread(threading.Thread):
def __init__(self, num):
# 需要先调用父类的 init 方法
super().__init__()
self.num = num
# 重写父类的 run 方法
def run(self):
。。。
mythread = MyThread(10)
# 子类从父类继承了 start() 方法,可以直接使用
mythread.start()
|
多线程
1
2
3
4
5
6
7
8
9
|
# 创建 2 个子线程
t1 = threading.Thread(target=work1)
t2 = threading.Thread(target=work2)
# 启动线程
t1.start()
# 优先让 t1 线程执行,t1 执行完毕后,t2 才能执行
t1.join()
t2.start()
|
锁
多个子线程之间可以通过 定义全局变量来共享变量
但是需要注意防止线程之间 资源竞争,这可能导致变量计算结果有误
为了防止该现象,我们需要 对资源进行适时的锁定
需要小心出现 死锁,死锁通常在以下情况发生:
1、多个线程同时获取多个锁,但获取锁的顺序不一致
2、线程在持有一个锁的同时,尝试获取另一个锁,而另一个锁已经被其他线程持有
防止死锁的发生,可以:
1、使用 超时机制:在获取锁的时候设置超时时间,如果超过一定时间还未获取到锁,就放弃当前的锁并释放已经获取的锁,避免死锁的发生
2、避免嵌套锁:尽量避免在持有一个锁的情况下再去获取另一个锁
进程
创建子进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import multiprocessing
def work1():
pass
# 创建子进程对象,指定子进程名称为 p1
process_obj = multiprocessing.Process(
target=work1,
name='p1'
)
# 启动子进程
process_obj.start()
# 终止子进程
process_obj.terminate()
exit()
|
1
2
3
4
5
6
7
|
# 获取主进程的名称
multiprocessing.current_process()
# 获取进程的编号 process id(pid)
multiprocessing.current_process().pid()
# 或者通过 os 获取
os.getpid()
|
传参方式与 threading 类似
进程守护
1
2
3
|
p1_obj = multiprocessing.Process(target=)
# 设置 p1_obj 为守护进程
p1_obj.daemon = True
|
进程队列
1
2
3
4
5
6
7
8
|
# 创建队列,设置长度为 5
queue = multiprocessing.Queue(5)
# 值进队列
queue.put(值可以是int、str、list、tuple、dict)
# 根据【先进先出】的原则,取出队列中的值
queue.get()
|
若队列已满,此时继续有值进队列,则队列会进入阻塞状态
默认会等待先取出值再放入新的值
使用 .put_nowait(值) 虽然不会阻塞,但会直接报错
若队列为空,此时继续进行取值,也是默认阻塞
而使用 .get_nowait(值) 虽然不会阻塞,但也会直接报错
1
2
3
4
5
6
7
8
|
# 判断队列是否已满
queue.full()
# 队列里面值的个数
queue.qsize()
# 判断队列是否为空
queue.empty()
|
上述队列判断可能有坑!!!
进程之间的通信
思路:
创建两个进程和一个队列
其中一个进程向队列中 put 元素;另一个进程读取队列中的元素
注意需要使用 .join() 使前者先执行完,之后才能让后者读取数据
进程池
1
2
3
4
5
6
7
8
9
10
11
|
# 创建一个大小为 2 的进程池
mypool = multiprocessing.Pool(2)
# 异步对进程池中的内容执行某函数
mypool.apply_async(某函数)
# 进程池不再接收新任务
mypool.close()
# 主进程等待进程池中的任务结束后再退出
mypool.join()
|
-
进程池中的进程如何通信?
1
2
3
4
5
6
7
8
9
10
|
# 类似的,在进程池中创建队列
queue = multiprocessing.Manager().Queue(5)
result = mypool.apply_async(write_queue, (queue,))
result.wait()
mypool.apply_async(read_queue, (queue,))
# 先异步执行 write_queue 函数
# 然后等待其执行完毕后再异步执行 read_queue 函数
# 二者都是利用进程池中的进程来执行这些函数
# 注意需要等待写操作的进程结束后,才进行读操作
|
文件夹拷贝实例:
1
2
3
4
5
6
7
8
9
10
11
12
|
mypool = multiprocessing.Pool(3)
for filename in filelist:
mypool.apply_async(
copy_work,
(source_dir, dest_dir, filename)
)
mypool.close()
mypool.join()
# 第二个参数的元组即需要传入第一个参数 copy_work 中的参数
# 实现读取 source_dir 中的 filename 文件内容
# 然后写入到 dest_dir 文件夹中
|
迭代器、生成器
可迭代对象
-
检测一个对象是否是可迭代对象
1
2
3
4
|
from collections import Iterable
isinstance([1,2,3,4], Iterable)
|
若一个 自定义类 含有 __iter__() 方法,则它是一个可迭代对象;否则不是
所以判断一个对象是否是可迭代对象,可以看其是否包含了 __iter__() 方法
或者从 是否可遍历 来判断,例如列表、元组、字符串、字典都是可迭代对象,它们也都是可遍历的
迭代器
特点
迭代器能记录 遍历的位置,提供 下一个元素 的值
for 循环的本质:
先通过 iter(要遍历对象) 获取要遍历对象的迭代器
再通过 next(迭代器) 获取下一元素
此外 for 循环还自动帮我们捕获了 StopIteration 异常
1
2
3
4
5
6
7
8
9
10
11
|
mylist = [1, 3, 5]
# 获取迭代器
my_iterator = iter(mylist)
# 获取下一元素
value1 = next(my_iterator)
value2 = next(my_iterator)
value3 = next(my_iterator)
# 报错 StopIteration,因为 mylist 列表长度为 3,不存在下一元素
value4 = next(my_iterator)
|
若一个 自定义类 不仅有 __iter__() 方法,还有 __next__() 方法,则它是一个 迭代器类
应用
假设我们想实现下面这样的自定义列表,它是可以通过 for 循环进行遍历的
1
2
3
|
mylist = MyList()
for value in mylist:
print(value)
|
思路:
1、先定义 MyList 类
它具有 __init__() 方法;__iter__() 方法,对外提供迭代器;addItem() 方法,用来添加数据
2、再定义迭代器类 MyListIterator
应用于上述 MyList 类的 __iter__() 方法中。它具有 __init__() 方法;获取下一个元素值的 __next__() 方法
生成器
生成器是 特殊的迭代器,同样可以通过 next(生成器) 获取下一个值
创建方式
生成器中可以使用 return 来结束
协程的实现
调用 gevent 实现协程
gevent 能够自动识别程序中的耗时操作,在 耗时的时候自动切换到其他的任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import time
import gevent
# 指派任务
g1 = gevent.spawn(work1, 参数1, 参数2, ...)
g2 = gevent.spawn(work2, 参数1, 参数2, ...)
# 让主线程等待协程执行完毕再退出
g1.join()
g2.join()
# 或者
gevent.joinall([
gevent.spawn(work1, 参数1, 参数2, ...),
gevent.spawn(work2, 参数1, 参数2, ...)
])
|
闭包
闭包的结构:
1、存在函数的嵌套关系
2、内存可以使用外层的变量
3、外层返回内层的引用
简单闭包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
def func_out(num1):
print("外层函数的num1:", num1)
def func_inner(num2):
num1 = 999 # 内层函数定义了和外层同名的变量num1
print(num1)
print("内层函数的num2:", num2)
return func_inner
ret = func_out(100)
ret(200)
# 运行结果:
# 外层函数的num1: 100
# 内层函数的num1: 999
# 内层函数的num2: 200
|
如果内层函数中定义了和外层函数同名的变量,那么内层函数会优先使用自己的局部变量(Local),而不会去访问闭包作用域(Enclosing)中的同名变量
如果内层函数没有定义同名变量,则会向上查找闭包作用域中的变量
而此时若又在内层对该同名变量使用了 nonlocal 关键字,这会改变该变量的作用域,从而可能导致代码逻辑混乱,变量值被意外修改,甚至导致程序出现错误或异常
LEGB 变量作用域查找顺序
Local(局部)、Enclosing(嵌套)、Global(全局)、Built-in(内建) 四种作用域
当在代码中引用一个变量时,Python 解释器会按照这个顺序来查找变量的定义
- Local(局部)
函数内部定义的变量,只在 函数内部有效
- Enclosing(嵌套/闭包)
包含当前函数的外部函数中定义的变量
例如在一个函数内部还有另一个函数,那么 内部函数可以访问外部函数的变量
- Global(全局)
在模块层次上定义的变量,对 整个模块内部都有效
- Built-in(内建)
Python 内置的变量和函数名,如 print()、len() 等
装饰器
假设现在有一个 login 函数
1
2
3
|
def login(username, uuuid):
print(f'{username} 开始登录')
return f'uuuid 为 {uuuid}'
|
请问如何在不修改源代码的情况下,为其增加登录前的验证功能?
答案是使用装饰器
1
2
3
4
5
6
7
8
9
10
|
def function_out(func):
def function_in(username, uuuid='001'):
print("------开始验证-------", username)
# 这里是验证操作...
return func(username, uuuid) # 内部函数返回外部函数传入的参数(一个函数)
return function_in # 外部函数返回内部函数的函数名
|
-
方法一
1
2
3
4
5
|
# 往 function_out 传入 login 函数
login2 = function_out(login)
res = login2(username='iron', uuuid='023') # 相当于调用 function_in
print(res)
|
-
方法二
1
2
3
4
5
6
7
8
9
10
|
# 直接使用 @function_out 进行装饰
# 效果通方法一,但是步骤简化了
@function_out
def login(username, uuuid):
print(f'{username} 开始登录')
return f'uuuid 为 {uuuid}'
res = login(username='iron')
print(res)
|
那么,使用装饰器的时候如何传入参数呢?
考虑在 function_out 外层再套一个函数,使用局部变量即可
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
|
def myverify(step):
def function_out(func):
def function_in(username, uuuid='001'):
print("function_in step =", step)
print("------开始验证-------, username =", username)
# 这里是验证操作...
return func(username, uuuid)
return function_in
return function_out
@myverify("register 注册步骤")
def register(username, uuuid):
print(f'{username} 开始注册')
return f'uuuid 为 {uuuid}'
@myverify("login 登录步骤")
def login(username, uuuid):
print(f'{username} 开始登录')
return f'uuuid 为 {uuuid}'
print(register(username='iron'))
print(login(username='iron'))
|
上述的 @myverify("register 注册步骤") 装饰器可以分解为 2 步
1、执行 myverify("register 注册步骤"),传入了 step 参数值,并且返回 function_out 引用,得到 @function_out
2、之后就是同上,返回内部函数的引用 function_in
多重装饰器
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
|
# 定义一个让文字加粗的装饰器
def makeBold(func):
def function_in():
return "<b>" + func() + "</b>"
return function_in
# 定义一个让文字倾斜的装饰器
def makeItalic(func):
def function_in():
return "<i>" + func() + "</i>"
return function_in
@makeBold
def test():
return "hello world-1"
@makeItalic
def test2():
return "hello world-2"
@makeItalic
@makeBold
def test3():
return "hello world-3"
print(test()) # <b>hello world-1</b>
print(test2()) # <i>hello world-2</i>
print(test3()) # <i><b>hello world-3</b></i>
|
装饰器类
假设有下面的一个 Test 类,需要为其运行时添加验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
class Test(object):
def __init__(self):
print("----初始化----")
def run(self):
print("---正在运行---")
def __call__(self, *args, **kwargs):
print("---call---")
# 创建类对象
test = Test()
# 调用 run 方法
test.run()
test()
|
注意
当使用 对象名() 此时会去调用类中的 __call__() 方法
若没有定义 __call__() 方法,则会报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
# 装饰器类
class Test(object):
def __init__(self, func):
self.func = func
def _verify(self, *args, **kwargs):
username = kwargs.get('username')
print("------开始验证-------", username)
def __call__(self, *args, **kwargs):
# print(args, kwargs)
return self.func(*args, **kwargs)
@Test
def login(username, uuuid):
print(f'{username} 开始登录')
return f'uuuid 为 {uuuid}'
login._verify(username='iron')
res = login('iron', uuuid='023')
print(res)
|
当使用 @Test 装饰 login 时,相当于执行了 login = Test(login)
因此 login 不再是一个普通的函数,而是 Test 类的一个实例
可以调用 _verify 方法,而 login('', uuuid=) 则是在调用 Test 实例的 __call__ 方法
面向对象
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
|
class Washer:
def __init__(self, width, height):
# 添加实例属性
self.width = width
self.height = height
def __str__(self):
return '这是洗衣机的说明书'
def __del__(self):
print('对象已被删除')
def wash(self):
print('洗衣机洗衣服')
def print_info(self):
print('洗衣机的宽度是%d, 高度是%d' % (self.width, self.height))
# 创建Washer类对象
haier1 = Washer(10, 20) # 不传参数会报错
# 使用实例方法
haier1.wash()
# 添加实例属性
haier1.price = 3000
print(haier1)
|
使用 print 打印对象,默认打印的是对象的内存地址
而如果类定义了 __str__ 方法,就会打印 __str__ 方法中 return 的数据
名字 两侧带有双下划线 的函数叫做 魔法方法
__init__() 在创建⼀个对象时默认被调用,不需要手动调用
删除对象时,python 解释器会默认调用 __del__() 方法
方法中的 self 参数,python 解释器会自动把 当前的对象引用 传递过去
类的继承问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# 父类 A
class A(object):
def __init__(self):
self.num = 1
def info_print(self):
print(self.num)
# 子类 B 继承父类 A
class B(A):
pass
result = B()
result.info_print() # 子类默认继承父类的所有属性和方法
# A 所继承的 object 类是【顶级类或基类】,其他子类叫做【派生类】
|
.__mro__ 方法用于查看继承顺序
-
多继承的情况(注意和 多层继承 进行区别)
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 c1(object):
...
class c2(object):
...
class c3(object):
...
class myc(c3, c2, c1):
# 在 myc 中可以重新定义与父类同名的方法/属性
# 这也叫【重写方法/属性】
...
# 查看继承顺序
print(myc.__mro__)
# (
# <class '__main__.myc'>,
# <class '__main__.c3'>,
# <class '__main__.c2'>,
# <class '__main__.c1'>,
# <class 'object'>
# )
|
这里的 myc 类继承了多个类
若 c1、c2、c3 之间出现了同名属性和方法,则默认使用 c3 父类的
-
子类调用父类方法
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 FuelCar(object):
def __init__(self):
self.engine = '燃油发动机'
def drive(self):
print(f'使用{self.engine}驱动汽车')
# 电动车类
class ElectricCar(object):
def __init__(self):
self.engine = '电动机'
def drive(self):
print(f'使用{self.engine}驱动汽车')
# 混合动力车类
class HybridCar(FuelCar, ElectricCar):
def __init__(self):
self.engine = '混合动力系统'
def drive(self):
# 确保使用的是自己的engine
self.__init__()
print(f'使用{self.engine}驱动汽车')
# 调用燃油车父类的drive方法
def drive_with_fuel(self):
FuelCar.__init__(self)
FuelCar.drive(self)
# 调用电动车父类的drive方法
def drive_with_electric(self):
ElectricCar.__init__(self)
ElectricCar.drive(self)
# 创建混合动力车实例
my_car = HybridCar()
print(my_car.engine) # 混合动力系统
# 测试各种驱动方式
my_car.drive() # 使用混合动力系统驱动汽车
my_car.drive_with_fuel() # 使用燃油发动机驱动汽车
my_car.drive_with_electric() # 使用电动机驱动汽车
my_car.drive() # 使用混合动力系统驱动汽车
|
上述混动车类多继承了燃油和电动车类
我们可以观察到,每次若需要调用父类的方法,需要先调用父类的初始化
以确保正确使用对应父类的方法
-
super 初始化(多继承不适合使用,可能导致意外的调用顺序)
如果是单继承可以考虑 super 方法
1
2
3
4
5
6
7
|
# 1、super(当前类名, self).函数()
super(HybridCar, self).__init__()
super(HybridCar, self).drive()
# 2、super().函数()
super().__init__()
super().drive()
|
super() 可以自动查找父类,调用顺序遵循 __mro__ 类属性的顺序
调用时不需要写 FuelCar 或者 ElectricCar 类名,简洁也避免硬编码父类名
多态
有点像高阶函数,类里面的函数调用不同的类,会有不同的输出
模块
Module 是一个 Python 文件,以 .py 结尾,包含了 Python 对象定义和 Python 语句
模块能定义函数、类和变量,模块里也能包含可执行的代码
1
2
3
4
5
6
7
8
|
import math
print(math.sqrt(9))
# ==============================
from math import sqrt
# 或者 from math import *
print(sqrt(9))
|
1
2
3
|
# 为模块定义别名
import matplotlib.pyplot as plt
# 同理,模块中的函数也可以定义别名
|
我们自定义的模块文件可能会包含 if __name__ == '__main__': 这个写法
其中的 __name__ 是系统变量,模块标识符
自身模块运行时,__name__ 为 __main__
当被其他模块调用时,__name__ 为 当前模块的名字
-
模块的搜索顺序
当导入一个模块,Python 解析器会按照一下顺序搜索
1、当前目录
2、如果在当前目录找不到该模块,Python 则会去搜索 PYTHONPATH 下的每个目录
3、如果还找不到,Python 会查看默认路径。UNIX 下,默认路径一般为 /usr/local/lib/python/
模块搜索路径会存储在 system 模块的 sys.path 变量中
注意,自定义模块名不要和已有模块名重复,否则会导致模块功能无法使用
-
模块名和变量名
1
2
3
4
5
|
import time
print(time) # <module 'time' (built-in)>
time = 1 # 不会报错
|
time = 1 重新给 time 变量进行赋值,覆盖了之前的模块引用
由于 Python 的变量是动态类型的,这种操作是允许的,不会报错
但是这种覆盖通常是不推荐的,因为会导致代码可读性降低,甚至引发难以排查的错误
应该遵循命名规范,避免用模块名作为变量名
-
模块的导入
假设文件目录结构如下
1
2
3
4
|
- mypackage
- __init__.py
- mymodule1.py
- mymodule2.py
|
-
导入方法 1
1
2
3
|
import mypackage.mymodule1
mypackage.mymodule1.某方法()
|
-
导入方法 2
1
2
3
|
from mypackage import *
mymodule1.某方法()
|
-
控制允许导入的模块
当外部利用 from mypackage import * 导入模块时,我们希望只有 testA 被导入
__init__.py 中设置 __all__
仅有 testA 方法会被导入,testB 方法会被忽略