Contents

Python_Note3

Beautiful landscape

Nature's Beauty

Explore the wonders of the world

格式化输出

format 函数

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
    3
    4
    5
    6
    7
    8
    
    # 创建互斥锁
    lock = threading.Lock()
    
    # 上锁
    lock.acquire()
    
    # 解锁
    lock.release()
    

    有了互斥锁之后,当两个线程访问同一资源时,即使没有使用 .join() 等待一方结束,也不会出现资源竞争的情况

需要小心出现 死锁,死锁通常在以下情况发生:
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(值可以是intstrlisttupledict)

# 根据【先进先出】的原则,取出队列中的值
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(生成器) 获取下一个值

创建方式

  • 列表推导式

  • 函数中使用了 yield 返回值
    yield 会暂停程序,返回变量值,并且保存程序的运行状态
    当执行 next 的时候就会从 yield 的位置继续向下执行

    while 循环中,使用 yieldsleep 可以实现基本的协程

生成器中可以使用 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__

    1
    
    __all__ = ['testA']
    

    仅有 testA 方法会被导入,testB 方法会被忽略