python

date
Oct 18, 2024
slug
python
status
Published
tags
python
summary
python概念
type
Post
接口:包括(前端请求—>路由到后端处理请求—>返回处理结果给前端)的整个过程
开始
Linux
安装python
  • sudo apt-get install python3,安装该发行版的默认 python3 版本
  • 安装前可使用 apt show python3 查看安装默认安装的 python3 包信息(包括版本号、包的大小)
  • 安装后可使用 which python3 查看 python3 的安装位置
创建虚拟环境
  1. 安装 virtualenv 库 sudo pip install virtualenv
pip
虚拟环境
通用约定
Bug & 漏洞
  • Bug 和漏洞是两个相近但并不完全等同的概念
  • Bug——是指软件程序中存在的错误或缺陷,它可能导致程序运行不正常、死机、数据丢失、非正常中断等现象。有些程序错误会造成计算机安全隐患,此时叫漏洞。
  • 漏洞——是指软件程序中存在的安全隐患,攻击者可以利用这些漏洞来获取未授权的访问权限或控制计算机系统。
  • 两者的主要区别在于:
    • Bug 的范围更广,包括所有软件错误,而漏洞仅指存在安全隐患的错误。
    • Bug 不一定會被攻击者利用,而漏洞则可能被攻击者利用造成安全问题。
  • Bug 例子:
    • 程序计算结果错误
    • 程序界面显示异常
    • 程序运行时崩溃
  • 漏洞 例子:
    • 缓冲区溢出漏洞
    • SQL 注入漏洞
    • 跨站脚本攻击漏洞
python开发工程师必备技能
  • 框架——Flask/Django/Tornado
  • 语言——Python
  • 数据库——MySQL/NoSQL/Redis(Sql调优)
  • 网络编程、Python服务端编程、Python的多线程/进程/协程
  • Linux Shell常用命令,RESTful规范,Http协议
  • 异步编程、事件编程
 

概念与知识

python语言特性
面向对象
  • 通常在定义一个 class 时,实际上定义了一种数据类型,这里的数据类型和python内置的数据类型是一个概念(比如list、dict、tuple、str等)
  • 【封装】
【继承】
  • 在类的继承中,子类通常会继承父类的非私有属性和方法。这意味着,除了那些声明为私有(在某些编程语言中用 private 关键词标记)的成员变量(属性)或方法外,子类会继承父类的所有其他成员。
  • 子类只需要新增自己的方法,也可以覆盖重写父类的方法
  • 当子类和父类都存在相同的方法时,就说子类覆盖了父类的方法,调用时总会运行子类中的方法。通常通过在子类中重写父类的方法以达到特别的需求
【多态】
  • 对于一个函数,传入一个类型,即可调用该类型的方法。新增该类型的一个子类型,无需修改这个函数,仍然可以正常调用该类型的方法
  • 对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在Animal、Dog、Cat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:
    • 对扩展开放:允许新增Animal子类;
    • 对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。
代码实例
class Animal(object): def run(self): print('Animal is running......') class Dog(Animal): def run(self): print('Dog is running......') class Cat(Animal): def run(self): print('Cat is running......') def run_twice(animal): animal.run() animal.run() run_twice(Animal()) run_twice(Dog()) run_twice(Cat()) # 输出 # Animal is running...... # Animal is running...... # Dog is running...... # Dog is running...... # Cat is running...... # Cat is running...... # 新增 Animal 的子类 class Tortoise(Animal): def run(self): print('tortoise is running......') # 可直接调用 run_twice() 方法(不用对调用方做出改变) run_twice(Tortoise()) # 输出 # tortoise is running...... # tortoise is running......
动态类型语言
  • (动态类型语言)在变量使用时不需要指定类型,类型会在运行时确定(动态类型)
    • 对于python 这样的动态语言的“鸭子类型”,调用方法时不一定需要传入 的对象是Animal 类型,只需要传入的对象中有 run() 方法即可完成调用
    • 动态语言的“鸭子类型”对继承并不严格要求,只需要“看起来像鸭子,走起路来想鸭子”,那这个对象就可以被看做是鸭子
  • (静态类型语言)比如Java必须要事先声明变量的类型才能使用变量,并且如果方法中调用的变量是 Animal 类型,那么传入的对象必须是 Animal 类型或它的子类,否则无法调用该类型的 run() 方法
可移植性和跨平台
属性访问机制
  1. 点号访问属性:
      • Python 首先会尝试调用 __getattribute__ 方法(所有对象都有这个方法)。只有当 __getattribute__ 引发 AttributeError 时,才会尝试调用 __getattr__ 方法。
      a. 首先,Python 调用 getattribute
      b. 如果 getattribute 成功找到属性,则返回该属性。
      c. 如果 getattribute 引发 AttributeError,且定义了 getattr ,则调用 getattr
      d. 如果 getattr 也失败或未定义,则最终引发 AttributeError。
  1. 方括号访问:
      • 使用方括号(如 obj[key])确实是通过 __getitem__ 方法实现的。
      • 这适用于序列类型(如列表、元组)和映射类型(如字典)。
补充说明:
  • __getattr__ 是一个后备方法,只在常规属性查找失败时被调用。
  • __getattribute__ 是对所有属性访问的底层方法,包括存在的属性。
  • 有些类(如 dict)同时实现了点号访问和方括号访问,但它们的行为可能不同。
总结:
  • 点号访问主要依赖于常规属性查找和 __getattribute__,__getattr__ 是一个可选的后备机制。
  • 方括号访问确实主要依赖于 __getitem__ 方法。
Python标准库
os 库
属性
os.name 返回操作系统的类型
  • 如果是posix,说明系统是Linux、Unix或Mac OS X,如果是nt,就是Windows系统。
  • os.environ 返回操作系统所有的环境变量
  • os.environ.get('PATH') &os.environ['PATH'] 返回指定的系统环境变量内容
  • os.path
    • abspath = os.path.abspath(filename) 获取文件filename的绝对路径, os.path.abspath('.') 获取当前目录的绝对路径, os.path.abspath(__file__) 获取当前文件的决定路径
    • os.path.join('/Users/michael', 'testdir') 拼接目录和文件名,返回结果'/Users/michael/testdir'
    • os.mkdir(dirname) 创建一个目录
    • os.rmdir(dirname) 删除一个目录
    • os.rename(filename, new_filename) 重命名一个文件
    • os.rename(filename) 删除一个文件
    • dirname = os.path.dirname(filename) # 获取文件的目录路径
    • os.listdir(dirname) 返回指定目录的子文件和子目录(仅一层), os.listdir('.') 返回当前目录的子文件和子目录(仅一层,不会输出子目录下的文件)
    • os.path.isdir(x) 判断x是否是目录,返回值为布尔值
    • os.path.isfile(x) 判断x是否是文件,返回值为布尔值
    • os.path.split('/path/to/file.txt') 获取文件路径和文件名,返回一个元组 ('/path/to', 'file.txt')
    • os.path.splitext('/path/to/file.txt') 获取文件的扩展名,返回一个元组 ('/path/to/file', '.txt')
    • 操作示例
      • # 列出当前目录下的所有目录 [x for x in os.listdir('.') if os.path.isdir(x)] # 输出 # ['.lein', '.local', '.m2', '.npm', '.ssh', '.Trash', '.vim', 'Applications', 'Desktop', ...] # 列出所有的.py文件 [x for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1]=='.py'] # 输出 # ['apis.py', 'config.py', 'models.py', 'pymonitor.py', 'test_db.py', 'urls.py', 'wsgiapp.py']
  • lis = os.listdir(dirname) # 获取该目录下的所有文件和文件夹
os.walk(top, topdown=True, onerror=None, followlinks=False)

参数

  • top: 要遍历的顶层目录的路径。
  • topdown (可选): 如果为 True(默认值),则先遍历顶层目录,然后是子目录。如果为 False,则先遍历子目录,最后是顶层目录。
  • onerror (可选): 是一个函数,它会在遇到错误时调用,例如无法访问一个目录的权限问题。该函数接收一个 OSError 实例作为参数。如果未指定,错误将被忽略。
  • followlinks (可选): 如果为 True,则会遍历符号链接指向的目录。

返回值

os.walk() 返回一个迭代器,每次迭代它会产生一个三元组 (dirpath, dirnames, filenames):
  • dirpath: 字符串,表示当前正在遍历的目录的路径。
  • dirnames: 列表,包含 dirpath 下的所有子目录名。通过修改这个列表,您可以影响后续迭代的目录,比如可以排除特定的子目录。
  • filenames: 列表,包含 dirpath 下的所有非目录文件的名字。
遍历是从 top 目录开始,并递归遍历每个子目录。每个目录都是根据 topdown 参数的值遍历的。

示例用法

以下是使用 os.walk() 遍历目录的简单示例:
python复制代码 import os # 要遍历的顶级目录 top_dir = '/path/to/directory' for dirpath, dirnames, filenames in os.walk(top_dir): print(f'Accessing directory: {dirpath}') # 打印所有子目录for dirname in dirnames: print(f'Sub-directory: {dirname}') # 打印所有文件for filename in filenames: print(f'File: {filename}')
示例
for root, dirs, files in os.walk(dirname): print(root, dirs, files, sep='\n') print("结束") # 目录结构如下 dirname |_ data_cache | | | a.json | ...... | |_ test(空文件夹) | 0.json 1.json data.csv ...... # 输出: # 第一次遍历,遍历 dirname 文件夹 .../.../dirname ['data_cache', 'test'] ['0.json', '1.json', ......] # 第二次遍历, 遍历子目录 data_cache .../.../dirname /data_cache [] ['a.json', ......] # 第三次遍历,遍历子目录test .../.../dirname/test [] [] 结束
  • 从 dirname 开始,遍历该目录及内部的所有目录,获得对应的目录名(root)、目录下的子目录列表(dirs)、子文件列表(files)
在这个示例中,替换 '/path/to/directory' 为你想要遍历的实际目录路径。
记住,由于 os.walk() 是一个生成器,因此它不会立即执行完整的遍历。每进行一次迭代,它会返回下一个目录的信息。这使得它在处理非常大的目录结构时非常有效率,因为不需要一次性将所有内容加载到内存中。
  • os.rename(origin_filename, new_filename) 修改文件名
  • os.remove(file_name) 删除文件
  • operator 库(结合Sort方法使用)
urllib 库
  • 概述:在Python中,可以使用标准库urllib.parse来提取URL的各个部分。这个模块提供了一系列方法用于操作URL。
  • 示例
    • from urllib.parse import urlparse url = 'http://www.example.com/some/path/?a=10&b=20#SomeAnchor' parsed_url = urlparse(url) print(f"协议(scheme):{parsed_url.scheme}") print(f"网络位置(netloc):{parsed_url.netloc}") print(f"路径(path):{parsed_url.path}") print(f"查询(query):{parsed_url.query}") print(f"片段(fragment):{parsed_url.fragment}") # 协议(scheme):http # 网络位置(netloc):www.example.com # 路径(path)):/some/path/ # 查询(query):a=10&b=20 # 片段(fragment):SomeAnchor
  • urlparse函数返回一个6元组,其中包含以下部分:协议(scheme)、网络位置(netloc)、路径(path)、参数(params)、查询(query)、片段(fragment)。这些部分覆盖了URL的主要组成部分。此外,该模块还提供了其他功能,比如urlsplit(类似于urlparse但不单独解析参数)、urljoin(用于构造绝对URL)等。
  • tqdm 库
json 库——实现数据的序列化和反序列化
【dumps & loads】
  • 将data转化为json格式,json_data = json.dunms(data)
  • 读取json数据为源格式,data= json.loads(json_data)
【dump & load】
  • 将data保存为json格式文件,json.dunm(data, f),其中f为文件指针或打开的文件(f = open(filename, ‘w’))
with open(file_name, 'w') as file: json.dump(data, file, default=custom_converter, indent=4)
  • 读取json格式文件为源格式,data = json.load(f)
json.dump 和 json.load 是 Python json 模块中的两个主要方法,用于将 Python 数据结构序列化为 JSON 格式(写入文件),以及从 JSON 格式反序列化回 Python 数据结构(读取文件)。 json.dump 方法: 当您使用 json.dump 方法时,它会将一个 Python 对象转换(序列化)成一个 JSON 格式的字符串,并将该字符串写入一个 file-like 对象(通常是一个打开的文件)。json.dump 可以处理以下 Python 数据类型: 字典 (dict) 列表 (list) 元组 (tuple) - 会被当作列表序列化 字符串 (str) 数字 (整型 int 和浮点型 float) 布尔值 (True 和 False) None 请注意,虽然元组在 Python 中是一种不同的数据类型,但在序列化为 JSON 时,它们会被当作数组处理(即与列表相同)。 json.load 方法: 相反地,json.load 会从一个 file-like 对象中读取 JSON 格式的字符串,并将这个字符串反序列化(解码)为一个 Python 对象。JSON 格式支持的数据类型有限,因此解码后你会得到以下 Python 数据类型的对象: 对象(在 JSON 中以大括号表示 {})会被转换为字典 (dict) 数组(在 JSON 中以方括号表示 [])会被转换为列表 (list) 字符串会保持为字符串 (str) 数字会保持原样(整数和浮点数) 布尔值会转换为 Python 中的布尔值 (True 或 False) null 会转换为 Python 中的 None 因此,json.dump 和 json.load 是一对互补的方法,用于在 Python 数据结构与 JSON 格式之间进行转换。需要注意的是,当你使用 json.load 从 JSON 字符串加载数据时,你得到的 Python 数据类型可能与最初传递给 json.dump 的类型略有不同。例如,如果你将一个元组传递给 json.dump,当你使用 json.load 读取数据时,你会得到一个列表而不是元组。
pickle 库——实现数据的序列化和反序列化
关于序列化和反序列化
  • 对象的序列化——将对象转化为字节序列的过程(例如数据写入)
  • 对象的反序列化——将字节序列恢复为对象的过程(例如读取文件)
只能在python中使用,只支持python的基本数据类型。
pickle是Python特有的序列化模块,它用来将Python对象转换成字节流(一种可以保存到文件或通过网络传输的格式)。因为pickle的格式是Python专用的,所以通常情况下无法在其他编程语言中被解析或正确使用。同样,pickle设计之初就是为了处理Python中的各种对象,包括但不限于基本数据类型(如字符串、整数、浮点数和列表)。
可以处理复杂的序列化语法。(例如自定义的类的方法,游戏的存档等)
pickle模块能够序列化许多不同类型的Python对象,包括用户自定义的类实例。当你序列化一个类实例时,pickle会尝试保存其状态与足够的信息,以便在反序列化时重建该实例。
序列化的时候,只是序列化了整个序列对象,而不是内存地址。
这一点表明pickle在序列化一个对象时,并不是简单地存储对象的内存地址,而是保存对象的内容和结构。这意味着它会保存对象的属性值、包含的数据以及足够的信息以便在另一个Python环境中重建该对象。序列化后的数据是对象的一个忠实副本,但是完全独立于原始对象所在的内存地址。
举例来说,如果你有一个包含多个元素的列表,使用pickle进行序列化时,pickle会将列表中的所有元素转换成一串连续的字节。当你反序列化这串字节时,你会得到一个内容与原来相同的新列表对象,但它将占据不同的内存空间。其中,第三条强调了pickle序列化过程中的一个关键方面:它创建的是对象内容的一个独立副本,而不是对原始对象内存位置的引用。
【dump】&( dumps )
with open(test,'wb') as f: #f.write( pickle.dumps(info) ) pickle.dump(info,f) #跟上面的f.write( pickle.dumps(info) )语意完全一样
【load】& ( loads )
with open(test,'rb') as f: # data = pickle.loads(f.read()) data = pickle.load(f) #跟上面的data = pickle.loads(f.read())语意完全一样。
time 库& timeit 库
  1. time.time():
      • 用途: 返回当前时间的时间戳(自纪元以来的秒数,即1970年1月1日00:00:00 UTC到现在的秒数)。它通常用于获取当前时间或测量事件间隔。
      • 精确度: 精确度通常取决于操作系统。大多数系统上time()的分辨率至少可以达到微秒(10^-6秒),但实际精确度可能因系统而异。
time.perf_counter():
  • 用途: 提供了一个更高精度的计时器,用于测量短时间内的性能。perf_counter()包括了系统睡眠的时间,且它不反映系统时间的任何更改。
  • 精确度: 这是一个针对性能计时优化的计数器,其分辨率通常比time()高,可以达到纳秒级别(10^-9秒)。它是建议用于性能测试的计时器。
import time def my_function(): # 你的函数内容 pass start_time = time.perf_counter() my_function() end_time = time.perf_counter() print(f"函数运行时间:{end_time - start_time}秒")
timeit.timeit():
  • 用途: 是一个独立于time模块的高级接口,专门用于测量小段代码的执行时间。它通常用循环执行代码片段以获得更准确的平均执行时间。
  • 精确度: timeit()使用的是系统上最精确的定时器(如perf_counter()),因此也具有很高的精确度。
import timeit def my_function(): # 你的函数内容 pass execution_time = timeit.timeit('my_function()', globals=globals(), number=1000) print(f"函数平均运行时间:{execution_time / 1000}秒") 在这个例子中,number=1000 意味着my_function将会被调用1000次,timeit会给出总共耗费的时间。然后你可以除以执行次数来得到平均运行时间。 注意,timeit默认禁用垃圾收集来减少测量误差,而time.perf_counter()不会这样做。此外,timeit还提供了一个命令行接口,可以直接通过命令行工具进行测试。
datetime 库
datetime.datetime 类
  • datetime.datetime.now() 返回当前本地时间戳
  • datetime.datetime.utcnow() 返回当前UTC时间,是根据世界标准时间返回的一个时间戳
datetime.datetime(year, month, day, hour, minute, second, microsecond, tzinfo) 创建一个 datetime对象
  • 年份 (year):四位数的年份。
  • 月份 (month):范围从 1(一月)到 12(十二月)。
  • 日 (day):根据给定月份的天数,通常从 1 到 28、29、30 或 31。
  • 小时 (hour):(可选参数,默认为0)24小时制中的小时数,范围从 0(午夜)到 23(晚上11点)。
  • 分钟 (minute):(可选参数,默认为0)小时中的分钟数,范围从 0 到 59。
  • 秒 (second):(可选参数,默认为0)分钟中的秒数,范围从 0 到 59。
  • 微秒 (microsecond):(可选参数,默认为0)秒中的微秒数,范围从 0 到 999999。
  • 时区 (tzinfo):(可选参数,默认为None)表示时区信息
from datetime import datetime, timezone, timedelta dt1 = datetime(2024, 2, 11, 15, 30, 10, 12345) # 2024-02-11 15:30:10.12345 tz = timezone(timedelta(hours=1)) # 创建 UTC+1 时区 dt2 = datetime(2024, 2, 11, 15, 30, 10, 12345, tzinfo=tz) # 2024-02-11 15:30:10.12345+01:00
datetime 对象的 strftime() 和 strptime() 方法

strftime() 方法

strftime() 方法将datetime对象格式化为一个字符串。它采用一个格式字符串,其中包含有各种不同的格式代码,代表日期和时间的不同部分。
下面是一些常用的格式代码:
  • %Y: 四位数年份(例如,2023)
  • %m: 月份(01到12)
  • %d: 月份中的一天(01到31)
  • %H: 小时(00到23)
  • %M: 分钟(00到59)
  • %S: 秒(00到59)
使用示例:
python复制代码 from datetime import datetime now = datetime.now() formatted_date = now.strftime("%Y-%m-%d %H:%M:%S") print(formatted_date)# 输出如: "2023-03-29 17:42:21"

strptime() 方法

相对地,strptime() 方法用于解析一个符合特定格式的时间字符串,并将其转换为datetime对象。它也使用格式字符串来指明输入字符串中日期和时间数据的结构。
使用示例:
python复制代码 from datetime import datetime date_string = "2023-03-29 17:42:21" date_object = datetime.strptime(date_string, "%Y-%m-%d %H:%M:%S") print(date_object)# 输出: 2023-03-29 17:42:21
在这个例子中,strptime() 接受一个时间字符串和一个描述该时间字符串格式的格式字符串,然后返回一个相应的datetime对象。
需要注意的是,strftime() 和 strptime() 的格式字符串必须匹配日期时间数据的实际格式,否则可能会导致错误或意外的行为。
  • strftime中的“f”代表“format”,意味着这个方法是用来格式化datetime对象为一个字符串。
  • strptime中的“p”代表“parse”,意味着这个方法是用来解析一个符合特定格式的字符串以创建一个datetime对象
总结:
  • strftime 用于从 datetime 对象生成字符串。
  • strptime 用于从字符串生成 datetime 对象。
datetime.timezone 类
timezone 是 Python datetime 模块中的一个类,用于表示一个固定偏移的时区。它是 tzinfo 抽象基类的一个具体实现,用于给 datetime 对象提供时区信息。
tzinfo 是一个用于处理时区信息的基类。datetime 和 time 类都可以接受一个 tzinfo 子类的实例作为参数来处理时区相关的问题。但是 tzinfo 自身并不包含具体的时区数据,因此通常需要继承 tzinfo 并且提供实际的时区实现,或者使用 datetime 模块提供的 timezone 类。

基本使用

timezone 类只有一个重要的参数,即 offset,它代表了与协调世界时(UTC)的时间差。offset 是一个 timedelta 对象,可以表示时间的偏移量,也就是这个时区相对于UTC的时差。
python复制代码 from datetime import timezone, timedelta # 创建一个UTC+1的时区 utc_plus_one = timezone(timedelta(hours=1)) # 创建一个UTC-5的时区 utc_minus_five = timezone(timedelta(hours=-5))

示例

下面是如何使用 timezone 的一些示例:
python复制代码 from datetime import datetime, timezone, timedelta # 创建一个UTC时区(没有时差) utc_timezone = timezone.utc # 创建一个UTC+1时区(东1区) utc_plus_one = timezone(timedelta(hours=1)) # 使用UTC+1时区创建一个aware类型的datetime对象 dt_with_tz = datetime(2024, 2, 11, 15, 30, tzinfo=utc_plus_one) print(dt_with_tz)# 输出: 2024-02-11 15:30:00+01:00
在这个示例中,dt_with_tz 是一个意识到(aware)时区的 datetime 对象,它知晓自己所在的时区,即UTC+1。

注意事项

虽然 timezone 类可以方便地表示一个固定的时区偏移,但它不支持夏令时或其他更复杂的时区变化。对于这些情况,你可能需要使用第三方库,如 pytz 或 Python 3.9及以后版本中引入的 zoneinfo 模块,它们提供了更完整的时区支持和对历史时区数据的考虑。
python复制代码 # 使用Python 3.9+中的zoneinfo模块from zoneinfo import ZoneInfo # 创建一个aware类型的datetime对象,指定时区为纽约 dt_new_york = datetime(2024, 2, 11, 15, 30, tzinfo=ZoneInfo("America/New_York")) print(dt_new_york)# 输出将会根据当时是否是夏令时而有不同的UTC偏移
zoneinfo 会自动处理像夏令时这样的时区转换,而 timezone 只提供简单的固定偏移功能。
datetime.timedelta 类
timedelta 是 Python datetime 模块中的一个类,用于表示两个日期或时间之间的差异,也就是时间间隔。这个类对于执行时间上的加法和减法运算非常有用,因为它可以用来表示一段时间的长度(例如天、小时、分钟、秒、微秒等)。

基本使用

timedelta 对象可以用具体的时间单位来创建,在构造函数中可以包括以下参数:
  • days:天数
  • seconds:秒数
  • microseconds:微秒数
  • milliseconds:毫秒数
  • minutes:分钟数
  • hours:小时数
  • weeks:周数
所有的参数都是可选的,并且默认值都是 0。当创建 timedelta 对象时,传入的参数值累加起来表示总的时间间隔。

示例

下面是一些创建和使用 timedelta 的示例:
python复制代码 from datetime import timedelta # 创建一个代表1天的timedelta对象 delta1 = timedelta(days=1) # 创建一个代表3小时15分钟的timedelta对象 delta2 = timedelta(hours=3, minutes=15) # 创建一个代表10分钟30秒的timedelta对象 delta3 = timedelta(minutes=10, seconds=30)

运算

timedelta 对象可以与其他 datetime 或 timedelta 对象进行数学运算。例如:
python复制代码 from datetime import datetime, timedelta # 当前日期和时间 now = datetime.now() # 创建一个timedelta对象表示48小时后 two_days = timedelta(days=2) # 将timedelta加到当前datetime上得到未来的时间点 future_date_time = now + two_days # 相减得到过去的时间点 past_date_time = now - two_days
除了加减运算,timedelta 对象还支持其他类型的运算,包括乘法和除法,以及通过-运算符得到相反的时间间隔。

正规化

当你创建一个 timedelta 对象时,如果传入的时间单位组合导致其中某些部分超出了它们自然的范围(比如分钟数大于60),timedelta 会自动将这些超出部分正规化到各自的上限。例如:
python复制代码 from datetime import timedelta # 创建一个表示5000秒的timedelta对象# 由于5000秒超过了1小时(3600秒),它会被正规化为1小时余1400秒 delta4 = timedelta(seconds=5000) print(delta4)# 输出: 1:23:20,表示1小时23分钟20秒
timedelta 是处理日期和时间计算中一个非常重要的工具,它提供了灵活的方式来表示和操作时间间隔。
random 库
  1. random.random():
      • 生成一个[0.0, 1.0)范围内的随机浮点数。
  1. random.randint(a, b):
      • 生成一个[a, b]范围内的随机整数。
  1. random.uniform(a, b):
      • 生成一个[a, b]范围内的随机浮点数。
  1. random.randrange(start, stop[, step]):
      • 从指定范围按指定基数递增的集合中返回一个随机数,类似range()。
  1. random.choice(seq):
      • 从非空序列seq返回一个随机元素。
      • 作用于——实现了序列协议(实现了 __getitem__ 和 __len__ 方法)的对象(可以是python内置对象,也可以是自定义对象)
  1. random.sample(population, k):
      • 从总体序列或集合中选择k个独立的随机元素,用于不重复的随机抽样。
  1. random.shuffle(x[, random]):
      • 将序列x随机排序(打乱顺序)。
  1. random.seed(a=None):
      • 初始化随机数生成器的内部状态。如果不提供参数或者参数为None,使用系统时间初始化生成器。
argparse 模块——解析命令行参数和选项
  • 概述
  • 使用过程
    • 创建解析器 parser = argparse.ArgumentParser([description str])
    • 添加参数 parser.add_argument(name_or_flags, ..., **kwargs)
      • name_or_flags:
        • 可以是位置参数的名称(如 'input'),也可以是选项参数的格式(如 '--verbose')
        • 位置参数:应该提供一个不带前缀的名称。默认必填
        • 可选参数:应以 - 或  前缀开始。默认非必填,可通过指定 required=True 将其设为必填
      • type:
        • 指定参数的类型,例如 int, float, str 等。如果未指定,默认类型为 str。
      • help:
        • 参数的帮助文本,用于描述该参数的用途。
      • action:
        • 指定参数被解析时应采取的行动。常见的值包括:
          • 'store': 储存参数值(默认操作)。
          • 'store_true': 如果该参数出现在命令行中,则将对应的值设置为 True;否则为 False。
      • default:
        • 指定当未提供参数时使用的默认值。
      • required:
        • 对于可选参数,设定是否必须提供此参数(默认为 False)。
    • 解析参数 args = parser.parse_args()
    • 使用参数
  • 使用示例
    • import argparse # 创建解析器,并附上描述信息(可选) parser = argparse.ArgumentParser(description="This is a example.") # 添加参数 ## 位置参数 parser.add_argument('input', type=str, help="输入") ## 可选参数 parser.add_argument('--debug', action="store_true", help="是否为调试模式") parser.add_argument('--count', type=int, default=1, help="次数,默认为1") # 解析参数 args = parser.parse_args() # 使用参数 print(f'输入文件: {args.input}') print(f'详细模式: {args.debug}') print(f'处理次数: {args.count}')
python关键字
raise
  • 引发异常
  • 【语法】raise ValueError(”A message exploring of the error”) 抛出 ValueError 异常,括号内的内容是对异常的原因描述
continue
  • continue语句用于跳过当前循环的剩余代码,并返回到循环的顶部。
break
  • break 语句用于终止当前循环
try……except...(else)...finally……
  • 主要用于错误处理,代码执行流程如下:
    • 正常运行 try 中的内容
    • 当出现错误时 except 将捕获指定的错误并运行其下的内容(故可以有多个 except 语句)
    • 如果定义了else语句,则没有错误时会运行其下的语句
    • 最后运行 finally 语句下的内容
示例:
try: print('try...') r = 10 / int('2') print('result:', r) except ValueError as e: print('ValueError:', e) except ZeroDivisionError as e: print('ZeroDivisionError:', e) else: print('no error!') finally: print('finally...') print('END') # 输出 # try... # result: 5.0 # no error! # finally... # END
  • 错误也是一种类型,捕获父类也会将其子类一网打尽
Python官方错误类继承
BaseException ├── BaseExceptionGroup ├── GeneratorExit ├── KeyboardInterrupt ├── SystemExit └── Exception ├── ArithmeticError │ ├── FloatingPointError │ ├── OverflowError │ └── ZeroDivisionError ├── AssertionError ├── AttributeError ├── BufferError ├── EOFError ├── ExceptionGroup [BaseExceptionGroup] ├── ImportError │ └── ModuleNotFoundError ├── LookupError │ ├── IndexError │ └── KeyError ├── MemoryError ├── NameError │ └── UnboundLocalError ├── OSError │ ├── BlockingIOError │ ├── ChildProcessError │ ├── ConnectionError │ │ ├── BrokenPipeError │ │ ├── ConnectionAbortedError │ │ ├── ConnectionRefusedError │ │ └── ConnectionResetError │ ├── FileExistsError │ ├── FileNotFoundError │ ├── InterruptedError │ ├── IsADirectoryError │ ├── NotADirectoryError │ ├── PermissionError │ ├── ProcessLookupError │ └── TimeoutError ├── ReferenceError ├── RuntimeError │ ├── NotImplementedError │ └── RecursionError ├── StopAsyncIteration ├── StopIteration ├── SyntaxError │ └── IndentationError │ └── TabError ├── SystemError ├── TypeError ├── ValueError │ └── UnicodeError │ ├── UnicodeDecodeError │ ├── UnicodeEncodeError │ └── UnicodeTranslateError └── Warning ├── BytesWarning ├── DeprecationWarning ├── EncodingWarning ├── FutureWarning ├── ImportWarning ├── PendingDeprecationWarning ├── ResourceWarning ├── RuntimeWarning ├── SyntaxWarning ├── UnicodeWarning └── UserWarning
 
模块
  • 模块是一组Python代码的集合(也即一个Python文件),可以使用其他模块,也可以被其他模块调用。
  • 包是一个内含 __init__.py 文件的目录,通常是一组模块的集合
    • Python会将包含了 __init__.py 文件的目录视作一个包
  • 模块与包
    • 一个abc.py的文件就是一个名字叫abc的模块,一个xyz.py的文件就是一个名字叫xyz的模块。
      现在,假设我们的abc和xyz这两个模块名字与其他模块冲突了,于是我们可以通过包来组织模块,避免冲突。方法是选择一个顶层包名,比如mycompany,按照如下目录存放:
      mycompany ├─ __init__.py ├─ abc.py └─ xyz.py
      引入了包以后,只要顶层的包名不与别人冲突,那所有模块都不会与别人冲突。现在,abc.py模块的名字就变成了mycompany.abc,类似的,xyz.py的模块名变成了mycompany.xyz。
      请注意,每一个包目录下面都会有一个__init__.py的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包。__init__.py可以是空文件,也可以有Python代码,因为__init__.py本身就是一个模块,而它的模块名就是mycompany
  • 创建自己的模块时,要注意:
    • 模块名要遵循Python变量命名规范,不要使用中文、特殊字符;
    • 模块名不要和系统模块名冲突,最好先查看系统是否已存在该模块,检查方法是在Python交互环境执行import abc,若成功则说明系统存在此模块。
数据类型和变量
整形
  • python允许在数字中间以 _ 分隔,方便区分0的个数
  • 【示例】 10000000 可写成 1000_0000 ,十六进制也可写成 0xa1b2_c3d4
  • 浮点数
字符串
  • 使用单引号 ‘……’ 、双引号“……”或三引号’’’……’’’表示,三引号可表示多行内容。注意:引号不属于字符串内容
  • 【示例】字符串 'abc'只有 a , b , c 这三个字符
转义字符 \
  • \n 换行
  • \r 回车
  • \t 表示一个制表符
  • \0 表示一个空字符
  • 使用 r'......' 可表示字符串内部不转义,如 r'a\tb' 打印出来则是 a\tb ,不加则打印出来为 a b
布尔值
  • 只有True和False两种值
常见的Falsy值
  • False
  • None
  • 0
  • 空列表([])
  • 空字符串('')
  • 空字典({})
  • 空元组(())
  • False:布尔类型的False值。
  • 0.0:浮点数类型的0。
  • 0j:复数类型的0(虚部和实部都是0)。
  • range(0):长度为0的区间。
  • set():空集合。
  • frozenset():空的不可变集合。
  • bytes() 或 b'':空的字节序列。
  • bytearray():空的字节数组。
  • memoryview(b''):空的内存视图。
 
变量
  • 字母数字下划线的组合,注意:不能数字开头
  • python的变量通过赋值符号“=”来确定引用的对象,根本上属于指向作用(指明对象的地址),显式表现出来的变量的类型不固定(随遇而安)
  • 这种变量本身类型不固定的语言称之为动态类型语言
  • 【示例】对于 a='abc' ,在计算机内存中python做了两件事
      1. 在内存中创建一个‘abc’对象
      1. 创建变量a,并将变量a指向这个对象(在变量a中存储对象的地址)
常量
  • 常量属于约定俗成的概念,通常使用全大写字母表示
  • 在python中没有确切的定义,严格上来说也属于变量(即也可以重复赋值)
条件判断
  • if...elif...elif...else
match...case 模式匹配
普通模式匹配
score = 'v' match score: case 'A': print('Good') case 'B': print('Go on') case 'C': print('up!up!') case 'D': print('Study!') case _: # _表示匹配任意情况 print('Sorry!The score is empty.')
复杂模式匹配
age = 15 match age: case x if x < 10: # 条件匹配 print(f'< 10 years old: {x}') case 10:# 匹配多个 print('10 years old.') case 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18: print('11~18 years old.') case 19: print('19 years old.') case _: # 匹配任意 print('not sure.') args = ['gcc', 'hello.c', 'world.c'] # args = ['clean'] # args = ['gcc'] match args: # 匹配列表 case ['gcc']: print('gcc: missing source file(s).') case ['gcc', file1, *files]: print('gcc compile: ' + file1 + ', ' + ', '.join(files)) case ['clean']: print('clean') case _: print('invalid command.') # 第一个case ['gcc']表示列表仅有'gcc'一个字符串,没有指定文件名,报错; # 第二个case ['gcc', file1, *files]表示列表第一个字符串是'gcc',第二个字符串绑定到变量file1,后面的任意个字符串绑定到*files,它实际上表示至少指定一个文件; # 第三个case ['clean']表示列表仅有'clean'一个字符串; # 最后一个case _表示其他所有情况。
== 判断两个变量的内容(指向的值)是否相等
is 判断两个变量的内存地址(id)是否相等,即两个变量是否是同一个变量
循环
for 循环
  • 一般建议最小化内层循环的工作量
    • 尽量将较大的列表放在外层循环,将较小的列表放在内层循环,以减小内层循环的工作量。这是为了提高缓存效率和减少内层循环的开销
python数据结构
Python的可变和不可变数据类型
  • 可变不可变有点“一一对应”的意思,该数据类型的某一个变量在一块内存中只能有一个值
  • 当数据类型a对应变量的值发生了改变,如果它对应的内存地址也会发生改变,则称a为不可变数据类型;如果它对应的内存地址不会发生改变,则称a为可变数据类型
  • 不可变数据类型:int(整形)、string(字符串)、tuple(元组)
  • 可变数据类型:set(集合)、list(列表)、dict(字典)、set(集合)
容器类型与序列类型
容器类型——可以包含一个或多个其他对象的数据结构
  • 容器类型包括:列表,元组,字典,集合,字符串等
  • len(a) 用于获取容器长度,性能并不会受到容器中元素个数的影响。
    • 这个操作是常数时间操作(O(1)复杂度),因为容器对象维护了一个内部计数器来跟踪其大小。这意味着无论容器中有多少元素,调用 len(a) 所花费的时间都大致相同,不会随着容器长度的增加而变慢。
序列类型——特定种类(拥有一些共通性质和行为)的容器
  • 特定性质:保持元素顺序、支持索引访问、支持切片操作、可被迭代、元素的连续存储
  • 序列类型包括:字符串、列表、元组、字节数组
  • 所有的序列类型都是容器类型,但非所有的容器类型都是序列类型
列表
  1. 创建列表
    1. 使用方括号直接创建,如 list_0 = [1, 2, 3, 4]
    2. 使用list(),将序列对象转化为列表
        • list(a) ,a 为列表,则创建 a 的副本
        • 将字符串转化为列表 list_0 = list(’Hello’)
        • 将元组转化为列表 list_1 = list((1, 2, 3))
  1. 访问列表
    1. 使用索引访问, a = list_0[index]
    2. 切片, a = list_0[start: end: step]
  1. 修改列表
    1. 直接赋值, list_0[0]='a'
    2. 使用 list.insert(),向指定索引位置插入元素, list_0.insert(1, 'b')
    3. 使用 list.append(), 向尾部追加元素,list_0.append('a')
    4. 使用 list.remove(),删除指定元素, list_0.remove('a')
    5. 使用 list.pop(), 删除指定索引位置的元素,默认删除尾部元素, list_0.pop(1)
    6. 使用del 关键字,删除指定索引的元素, del list_0[1]
  1. 列表常规操作
    1. 使用 index() 获取指定元素的索引
    2. 使用 in运算符 判断列表中是否存在指定元素, ’a’ in list_0
    3. 使用 len() 获取列表长度, len(list_0)len(a) 用于获取容器长度,性能并不会受到容器中元素个数的影响。
    4. 使用 list.sort() 对列表进行排序(修改原列表, 默认升序), 升序list_0.sort(reverse=False)
    5. 使用 list.reverse() 对列表进行反转(修改原列表)
    6. 使用 list[:] 复制列表, list_1 = list_0[:]
    7. 使用 +运算符 连接列表, new_list = list_0 + list_1
    8. 使用 for循环
        • 遍历列表
        • 列表生成式
字典
  • 特点——时间换空间,字典内部是无序的
    • 【保证键的唯一性】键必须是可哈希的。只有值永不可变的对象才是可哈希的,包括字符串、数字、元组(其元素均为不可变对象)等。
      # 通过python内置的 hash() 编写函数判断对象是否是可哈希的 def fixed(o): try: hash(o) except TypeError: return False return True
    • 【无视数据量大小的快速访问】用空间换时间,查询的复杂度为 O(1),操作无视数据量大小
    • 【空间开销大】python 字典必须使用散列表,而散列表必须是稀疏的
内部原理
  • 字典中的key-value是通过散列表(Hash Table,哈希表)实现的
  • 查找原理
    • notion imagenotion image
    • 向字典传入key值,python会调用hash(serach_key)来计算出serach_key的值对应的散列值,
    • 然后取散列值最低的几位数字,在散列表里查找表元(具体几位,根据散列表的大小决定)。
    • 如果找到的表元是空的,则抛出KeyError异常。若不是空的,表元中会有一对:found_key:found_value。
    • python这时候会检验search_key==found_key是否为真,如果为真,则返回found_value。
    • 如果serach_key与found_key不匹配,这种情况被称为散列冲突。发生这种情况是因为,散列表所做的其实是把随机的元素映射到只有几位的数字上,而散列表本身的索引又只依赖于这个数字的一部分。为了解决散列冲突,算法会在散列值中另外再取几位,然后用特殊的方法处理一下,把新得到的数字再当作索引来寻找表元。若这次找到的表元是空的,则同样抛出 KeyError;若非空,或者键匹配,则返回这个值;或者又发现了散列冲突,则重复以上的步骤。
  • 新增和更新原理
    • 对于新增,在发现空表元的时候会放入一个新元素
    • 对于更新操作,在找到相对应的表元后,原表里的值对象会被替换成新值
合并两个键相同但值为列表的两个字典
使用zip函数并将结果平坦化
如果count_dict[k]和unique_user_dict[k]中的列表长度总是匹配的,可以使用zip函数结合列表推导式来简化代码。使用zip和嵌套列表推导式可以使代码更加清晰,并且易于处理任意长度的列表,而不必硬编码索引值。
给定两个字典 count_dict 和 unique_user_dict,其中键相同,我们想要合并它们对应的列表值。我们希望交替地从每个字典中取出对应键的列表元素,并将它们组合成一个新的列表。为了实现这一点,我们使用了 zip 函数和列表推导式。
python复制代码 merged_dic = { k: [item for pair in zip(count_dict[k], unique_user_dict[k]) for item in pair] for k in count_dict }
以下是代码的逐步解释:
  1. {}:表明我们要创建一个新的字典。
  1. for k in count_dict:这是字典推导式的一部分,它遍历 count_dict 字典的所有键。对于每个键 k,我们会在新字典 merged_dic 中创建一个条目。
  1. zip(count_dict[k], unique_user_dict[k]):zip 函数接受两个或多个序列(在我们的例子中是列表)作为输入,并返回一个迭代器。这个迭代器生成一系列元组,每个元组包含来自每个输入序列的相应元素。例如,如果 count_dict[k] 为 [1, 2, 3],而 unique_user_dict[k] 为 [4, 5, 6],那么 zip 的结果会是 [(1, 4), (2, 5), (3, 6)]。
  1. [item for pair in ... for item in pair]:这是一个嵌套列表推导式。首先,它取出由 zip 函数生成的每个元组(即变量 pair),然后迭代这些元组中的每个元素(变量 item)。这样,我们把每个元组内的元素依次加入到列表中,最终得到一个扁平化的列表。
  1. k: [...]:这定义了字典推导式中的键值对。键是 k,而值是通过之前描述的方式生成的扁平化列表。
整体上看,这段代码对于每个键 k,同时处理两个字典中的列表,把它们的元素按顺序交错合并到一个新列表中,然后将这个列表作为值与键 k 关联起来存储在新的字典 merged_dic 中。
假设 count_dict 和 unique_user_dict 中,对应于键 'js' 的列表分别是 [1, 2] 和 [7, 8],则 merged_dic 中 'js' 的值将会是 [1, 7, 2, 8]。
普通循环合并
用于合并两个字典 d1 和 d2。它创建了一个新的字典 merged,其中包含从两个原始字典中提取的键和值。让我们分解这段代码来进行详细解释:
python复制代码 merged = {k: [d1.get(k, []), d2.get(k, [])] for k in set(d1) | set(d2)}
  1. {}:表明我们正在创建一个新的字典。
  1. k::这里 k 是新字典中的键。
  1. [d1.get(k, []), d2.get(k, [])]:这是对应于键 k 的值,它是一个列表。这个列表包含两个元素:
      • d1.get(k, []):这是从字典 d1 中获取与键 k 对应的值。如果 d1 中不存在键 k,则返回一个空列表 []。
      • d2.get(k, []):这是从字典 d2 中获取与键 k 对应的值。同样地,如果 d2 中不存在键 k,则返回一个空列表 []。
  1. for k in set(d1) | set(d2):这部分遍历 d1 和 d2 的所有键的集合。set(d1) 返回一个包含 d1 所有键的集合,set(d2) 返回一个包含 d2 所有键的集合。竖线 | 表示集合的并集操作,所以 set(d1) | set(d2) 给出所有在 d1 和/或 d2 中出现的键的集合。
  1. 整体来看,这个字典推导式为每个唯一键 k 创建了一个条目,并将该键在两个字典中的对应值作为一个列表赋给了这个键。如果一个键仅在一个字典中存在,那么对应另一个字典会得到一个空列表,确保每个键都映射到一个包含两个元素的列表。
如何判断字典推导式中的循环嵌套结构
在字典推导式中,循环的嵌套结构可以通过它们在表达式中的位置来识别。推导式从左到右读取循环。
对于表达式:
{k: i for i in range(10) for k in range(9)}
这里是如何区分内外循环的:
  1. 由左向右阅读时,最先遇到的for循环是外层循环。
  1. 紧随其后的for循环是内层循环。
因此,根据上面的例子:
  • for i in range(10) 是外层循环,它首先开始迭代。
  • for k in range(9) 是内层循环,在外层循环的每次迭代中,内层循环都会完整地执行一次。
为了更好地理解发生了什么,让我们手动模拟这个推导式的执行过程:
  1. 外层循环开始,i 被赋值为 0。
  1. 内层循环开始,它遍历 k 的值从 0 到 8,对于每个 k 的值,都创建或更新键 k 在字典中的值为当前的 i(也就是 0)。
  1. 内层循环结束,外层循环继续,增加 i 的值,现在 i 是 1。
  1. 内层循环再次开始,重复步骤2的过程,这次是将字典中每个键 k 的值更新为 1。
  1. 这个过程一直持续,直到外层循环结束;在最后一轮迭代中,所有的键 k 都会被设置为最后的 i 值 9。
最终结果是,字典中每个键的值都是外层循环的最后一个值 9,因为这是内层循环在每次迭代中所设置的。
使用json库格式化输出字典
  • json.dumps(dict, indent=4, ensure_ascii=False),缩进4个单位,不对中文进行解码
常用方法
  • get(key, default_value=None),获取字典中指定的key, key不存在时返回默认值(初始默认值为None)
集合
  • 特点
    • 无序
    • 无重复元素
  • 交(&)、并(|)、差集合运算
    • s1 = set([1, 2, 3]) s2 = set([2, 3, 4]) jiaoji = s1 & s2 # {2, 3} bingji = s1 | s2 # {1, 2, 3, 4} chaji = s1 - s2 # {1} chaji = s1.difference(s2) # {1}
    • 其中差集推荐使用 difference() 方法,效率更高
  • rset.discard(’_id’),调用集合的 discard 方法将元素‘_id’从集合 rset 中移除,如果没有这个元素则不做任何操作
(fset ^ rset) <= fset 是Python集合操作的一部分,它结合了差集(异或)和小于等于(<=)两种运算。
  • 首先,fset ^ rset 是对两个集合fset和rset执行差集(异或)操作。在集合理论中,差集(异或)返回的是在两个集合中分别存在但不同时存在的元素的集合,即在fset中有但在rset中没有,或者在rset中有但在fset中没有的那些元素。
  • 然后,(fset ^ rset) <= fset 是一个比较运算,检查前一步得到的差集是否为fset的子集,也就是说,差集中的所有元素是否也都存在于fset中。如果差集中的任何元素不在fset中,则这个表达式的结果为False;否则,结果为True。这实际上可以用来判断rset中独有的元素是否都不在fset中。
元组
  • 相较于列表,元组在性能中的一些优势
    • Python 编译器求解元组字面量时,经过一次操作即可生成元组常量的字节码。求解列表字面量时,生成的字节码将每个元素当作独立的常量推入数据栈,然后构建列表。
    • 给定一个元组 t,tuple(t) 直接返回 t 的引用,不涉及复制。相比之下,给定一个列表 l,list(l) 创建 l 的副本。
    • tuple 实例长度固定,分配的内存空间正好够用。而 list 实例的内存空间要富余一些,时刻准备追加元素。
    • 对元组中项的引用存储在元组结构体内的一个数组中,而列表把引用数组的指针存储在别处。二者不存储在同一个地方的原因是列表可以变长,一旦超出当前分配的空间,Python 就需要重新分配引用数组来腾出空间,而这会导致 CPU 缓存效率较低。
元组tuple
  • 元组的用法
    • 不可变列表
    • 无字段名称的记录
      • 位置决定数据内容的意义
  • 定义空元组
    • a = ()
    • a = tuple()
python规定使用括号且括号内为纯数字时表示自然数,所以当元组只有一个元素且为数字时,通常使用 a = (1,) 来定义(即在数字后添加一个逗号)
a = (1,) b = (1) print(type(a), type(b), sep='\n') # 输出结果 # <class 'tuple'> # <class 'int'>
元组不可变的本质
  • 示例
    • notion imagenotion image
  • 元组的不可变性指的是元组中的元素指向永远不可变。元组中的元素在内存中是存储为指针的,指向元素实际的位置,不变即指的是指针的值(地址)不变。
  • 只有值永不可变的对象才是可哈希的。不可哈希的元组不能作为字典的键,也不能作为集合的元素
  • 常用方法
    • 对于变量 t,tuple(t) 如果 t 为元组则直接返回 t 的引用,否则将 t 指向的数据转化为元组类型并返回(即以元数据为源,创建一个新的元组类型的对象并包含源中的数据,然后返回这个元组类型对象)。
具名元组
import collections User = collections.namedtuple(’User’, ‘name sex age’)# 创建一个User类 # user = User._make(iterable) # 使用User类的_make属性创建,传入一个可迭代对象 # user = User(name='zhangsan', sex='man', age=12) # 名称传参创建 user = User('zhangsan', 'man', 12) # 位置传参创建
字符串
str.strip()
去除字符串两端的空白(空格、制表符、换行符等),并返回新的字符串
str.rstrip()
去除字符串右侧的空白
str.lstrip()
去除字符串左侧的空白
str.join(iterable)
join() 方法不会修改原始的可迭代对象,而是返回一个新的字符串。
1、解释说明 ''.join() 是 Python 中的一个字符串方法,用于将一个可迭代对象(如列表、元组等)中的元素连接成一个字符串。
join() 方法会遍历 iterable 中的每个元素,并将它们用 str 连接起来,最后返回一个新的字符串。需要注意的是,iterable 中的元素必须是字符串类型,否则需要先将其转换为字符串。
2、使用示例 下面是一个使用 ''.join() 方法的示例:
python复制代码 # 定义一个包含多个字符串的列表 words = ['Hello', 'World', 'Python'] # 使用 ''.join() 方法将列表中的字符串连接成一个新的字符串 result = ''.join(words) # 输出结果print(result)# 输出:HelloWorldPython
3、注意事项
  • join() 方法只能用于字符串类型的可迭代对象,如果 iterable 中的元素不是字符串类型,需要先将其转换为字符串。例如:
python复制代码 # 定义一个包含整数的列表 numbers = [1, 2, 3] # 使用 ''.join() 方法将列表中的整数连接成一个新的字符串 result = ''.join(map(str, numbers)) # 输出结果print(result)# 输出:123
str.split(sep, max_split_times=-1)
split() 方法作用于一个字符串并返回一个子字符串列表
语法是:
<string>.split(sep,maxsplit)
在上面的语法中:
  • <string> 是任何的有效的 Python 字符串。
  • sep 是你用来作为拆分基准的分隔符。它应该被指定为一个字符串。
例如,如果你想在出现逗号时拆分
<string>
sep = ","
  • sep 是一个可选参数。默认情况下,此方法基于空格拆分字符串。
  • maxsplit 是一个可选参数,指示你要拆分 <string> 的次数。
  • maxsplit 有一个默认值 1,它基于所有出现的 sep 拆分字符串。
str.replace(old, new[, max])
replace() 方法不会修改原始的字符串对象,而是返回一个新的字符串。
str.upper()
返回一个新的全部大写的字符串
str.lower()
返回一个新的全部小写的字符串
str.title()
返回一个新的仅首字母大写的字符串
python函数
  • 关于同名函数,python只知道最近一次定义的函数,【原因】python不支持方法重载,即最近定义的函数会覆盖之前定义的函数
  • 无副作用性(pure function)——不改变输入参数的状态
  • 参数
    • 位置参数
    • 默认参数(缺省参数)【强烈建议指向不可变对象,且必须位于位置参数后面】,原因如下
      原因:Python函数在定义的时候,默认参数L的值就被计算出来了,即[],因为默认参数L也是一个变量,它指向对象[],每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]了。
      notion imagenotion image
      可变参数【*args】
      • 收集所有没有被捕获的位置参数(那些没有被命名的参数)到一个元组中
      • 可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple,arg变量变成元组调用
      • 可变参数之前的参数都只能位置传参(否则报错),可变参数后面的参数都只能名称传参(否则会被归为可变参数之中),建议可变参数放在第一个位置
      关键字参数【**kwargs】
      • 收集所有未被捕获的命名参数(那些被命名了的参数)到一个字典中
      • 关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict
      • 关键字参数只能作为最后一个参数,前面的参数可以按照位置赋值或名称赋值
函数调用
在Python中,函数是组织好的、可重复使用的、用来执行特定任务的代码块。函数调用是程序执行过程中的一个核心概念,它允许我们执行函数定义的代码。
函数调用的基本概念
当你调用一个函数时,程序执行流程会跳转到该函数的定义处,并开始执行其中的代码。一旦函数执行完成,控制权返回到函数被调用的地方,程序继续序列化执行。
函数调用的基本语法如下:
python复制代码 # 定义函数def my_function(param1, param2): # do something with param1 and param2return result # 调用函数 result = my_function(arg1, arg2)
  • my_function 是函数名。
  • param1 和 param2 是函数定义时的参数(形式参数)。
  • arg1 和 arg2 是函数调用时传递给函数的实际参数(实参)。
  • result 是函数处理后返回的值。
函数调用的运作过程
函数调用涉及多个层面的操作,尤其是在内存管理方面。下面是一个高级别的概述:
  1. 名称解析: 当你尝试调用函数时,Python首先需要解析函数名称以确定它引用了哪个函数对象。
  1. 创建新的栈帧: 每个函数调用都会在调用栈上创建一个新的栈帧(frame)。栈帧是存储函数的局部变量、参数和其他信息的内存区域。
  1. 传递参数: 调用者提供的实参会被赋值给被调函数的形式参数。这通常通过位置或关键字匹配实现。
  1. 函数体执行: 一旦设置好参数和局部变量,函数体就开始执行。
  1. 返回值: 函数执行完毕后,可能会有一个返回值。使用 return 语句可以提供返回值。如果没有 return 语句,则函数默认返回 None。
  1. 栈帧弹出: 在函数执行完成后,当前的栈帧将从调用栈上弹出,释放相应的资源,控制权回到函数被调用的代码点。
  1. 恢复状态: 之前保存的执行上下文状态会被恢复,程序继续从函数调用后的下一条指令执行。
内存中的运作过程
以下是更详细的内存中的运作过程:
  1. 命名空间管理: Python使用命名空间来管理不同作用域中的变量。当函数被调用时,会创建一个新的局部命名空间。
  1. 调用栈管理: Python有一个内部的调用栈,用于管理函数调用的顺序和局部变量。每当发生函数调用时,当前函数的执行环境会被压入栈中,包括局部变量、参数值和返回地址。
  1. 参数传递: 实参会被评估,并将它们的值绑定到相应的形参上。这个过程可能涉及创建新的对象(如传递的是可变类型时的副本)或简单地引用对象(对于不可变类型)。
  1. 代码执行: 解释器会执行函数体中的代码。任何局部变量都会存储在当前的栈帧中。
  1. 异常处理: 如果在函数执行期间抛出异常且未在函数内部捕获,则异常将传播回调用链,直到找到相应的异常处理程序。
  1. 清理工作: 函数完成执行后,栈帧被丢弃,局部变量和参数会被销毁。如果这些变量持有的是对象的引用,那么它们的引用计数会减少。如果某个对象的引用计数变为0,则该对象会被垃圾收集器回收。
  1. 返回结果: 如果函数有返回值,则这个值会被推送到调用栈上,供调用者使用。如果没有显式的返回值,则隐式返回 None。
函数调用和返回是Python执行程序的基础机制,理解它们背后的内存与执行模型对于编写高效、可维护的代码至关重要。
  • 函数调用的简单理解
      1. 函数名与内存地址: 在Python中,函数名实际上是一个指向函数对象的引用。当你定义一个函数时,Python将这个函数编译成一个函数对象,并存储在内存中。函数名就像一个标签,指向函数对象的内存地址。
      1. 查找函数代码: 当函数被调用时,解释器通过函数名查找到这个函数对象。
      1. 参数传递: 函数调用还涉及将实参绑定到形参,这也是在内存中的操作。
      1. 代码解释: Python是一种解释型语言。这意味着每次运行程序时,Python解释器会读取源代码(或者是预编译的字节码),然后一条条指令地执行。所以说函数代码被“解释为机器语言”其实不太准确,因为通常不会直接翻译成机器语言,而是转换成字节码,然后由Python虚拟机执行。但是在某些实现里,如PyPy,可能使用即时编译(JIT)技术来将字节码编译成机器码以提高效率。
      1. 执行函数代码: 解释器执行函数体中的代码,如果有返回值,处理返回值并继续执行调用该函数的代码。
      1. 结束执行: 函数执行完毕后,控制权返回到函数被调用的地方。
      以下是这个过程的简化表示:
      +-------------------+ | 代码 | | | | def foo(): | +---------------+ | pass | ---> | 函数对象 foo | | | | 内存地址: 0x1A | | foo() | +---------------+ | | +-------------------+ 1. 定义函数 foo,编译并创建函数对象,存储在内存中。 2. 调用 foo 时,通过名称找到对应的函数对象。 3. 传递参数(如果有的话),执行函数内部的代码。 4. 函数执行完成,返回结果(如果有的话)。
      综上所述,可以说Python的函数调用包括了命名解析、参数传递、代码解释、执行及返回值处理等步骤。
  • 高阶函数
    • 【int】 int(obj)
      在 Python 中,int() 函数是一个内置函数,用于将一个值转换成整数类型。这个函数主要有以下几种用法:

      1. 转换数字到整数

      如果参数是一个浮点数,int() 会去掉小数部分,返回整数部分,这是通过「向下取整」(也就是截断小数部分)实现的。
      python复制代码 print(int(3.14))# 输出: 3print(int(3.99))# 输出: 3

      2. 转换字符串到整数

      如果参数是代表整数的字符串,int() 会解析字符串并返回对应的整数。字符串中的内容必须为合法的整数表示,否则会抛出 ValueError。
      python复制代码 print(int('123'))# 输出: 123
      注意:字符串不能表示浮点数,否则会抛出错误。
      python复制代码 try: print(int('123.45')) except ValueError as e: print(e)# 输出: invalid literal for int() with base 10: '123.45'

      3. 指定进制转换

      int() 可以接受一个可选的 base 参数,用来指定转换时使用的进制(默认是十进制)。进制可以是 0 或 2 到 36 之间的任何整数。
      python复制代码 print(int('101', base=2))# 解析二进制字符串 '101',输出: 5print(int('ff', base=16))# 解析十六进制字符串 'ff',输出: 255
      特别地,如果 base 设置为 0,则会根据字符串的格式自动确定进制(例如,前缀 '0b' 表示二进制,'0x' 表示十六进制,'0o' 表示八进制)。
      python复制代码 print(int('0b101', base=0))# 输出: 5

      4. 无参数调用

      当不传递任何参数时,默认返回整数 0。
      python复制代码 print(int())# 输出: 0

      错误情况

      如果 int() 函数无法转换给定的参数到整数形式(比如参数是非数字字符串,或者整数超出了 Python 整数的大小限制),它会抛出 ValueError 或者 OverflowError。
      总结一下,在数据处理和用户输入中,int() 方法是非常常见且有用的,因为它提供了将不同类型的数据转换为整数的简单方式。然而,总是需要注意处理可能发生的异常情况,并确保提供给 int() 的是有效的、预期的数据类型。
      【float】 float(obj)
      在 Python 中,float() 函数用于将一个数值或字符串转换成浮点数。这个函数相当灵活,因为它可以处理多种类型的输入,并且尝试把它们转换为 float 类型的对象。
      以下是关于 float() 函数使用的一些详细说明:
      1. 转换数字到浮点数:如果参数是一个整数或其他类型的数字,float() 会返回该数字的浮点数表示。
        1. python复制代码 float(10)# 输出: 10.0
      1. 转换字符串到浮点数:如果参数是代表数字的字符串,float() 会解析字符串并返回对应的浮点数。字符串可以是整数或浮点数表示,也可以包含正负号。
        1. python复制代码 float('3.1415')# 输出: 3.1415float('-123.456')# 输出: -123.456float('+7')# 输出: 7.0
      1. 没有参数时:调用 float() 时如果不传递任何参数,它将返回 0.0。
        1. python复制代码 float()# 输出: 0.0
      1. 空格处理:float() 可以处理开头和结尾的空白字符,但中间不能有非法字符。
        1. python复制代码 float(' -123.45 ')# 输出: -123.45
      1. 特殊值:float() 还能将一些特殊的字符串转换为浮点数,比如 'inf', 'infinity', 'nan' 分别对应无穷大和非数(Not a Number)。
        1. python复制代码 float('inf')# 输出: inffloat('nan')# 输出: nan
      1. 错误情况:如果 float() 无法将给定的参数转换为浮点数,比如参数是一个非法格式的字符串,它会抛出一个 ValueError 异常。
        1. python复制代码 try: float('abc') except ValueError as e: print(e)# 输出: could not convert string to float: 'abc'
      总之,float() 是一个非常实用的内置函数,广泛应用于需要进行数值转换的场景中。当处理用户输入或从文本数据中解析数字时,它尤其有用。然而,要留意浮点数的精度问题,因为浮点数在计算机中的存储通常是近似值,可能导致精确度上的误差。
      【open】 open(filepath, mode, encoding)
      • 用于打开文件对象,操作流程:以特定模式打开文件对象——进行读写操作——关闭文件对象
      普通用法
      # 防止文件对象未关闭 try: f = open('/path/to/file', 'r') print(f.read()) # 一次性读取并输出所有文件 # for line in f.readlines(): # 一次读取所有内容并按行返回list # print(line.strip()) finally: if f: f.close()
      with 语句(以防文件对象未关闭,使用此方法会自动关闭)
      with open('/path/to/file', 'r') as f: print(f.read())
      【len】 len(obj)
      • len() 方法获取对象的长度,实际上在该方法的内部调用了传入对象的 __len__() 方法来返回对象的长度
      • 可以通过在自定义的类(型)中定义 __len__() 方法,便可以实现对该类的实例对象调用 len() 方法
      【dir】 dir(obj)
      • 返回值是一个list
      • 用于获取对象的所有属性和方法
      【callable】 callable(obj)
      • 判断一个对象是否是可调用对象
      • 返回值为布尔值
      【hasattr、getatrr、setattr】 hasattr(obj, attr) getattr(obj, attr) setattr(obj, attr, value)
      • 判断对象是否存在该属性/方法hasattr(obj, attr)
      • 获取对象的该属性/方法getattr(obj, attr) ,如果不存在就报错AttributeError ,可以设置默认值 get(obj, attr, default_value)
      • 设置对象的该属性/方法setattr(obj, attr, value)
      【type】 type(obj) || type(class_name_str, parent_class_tuple, class_attribute_dict)
      【isinstance】 isinstance(obj, classinfo)
      • 接收两个参数,检查第一个参数是否是第二个参数的实例或子类,第二个参数必须是类型参数,返回值为bool值(检查一个对象是否是指定的类型)
        • obj——一个实例对象或类对象
        • classinfo——一个类对象(或多个类对象组成的类元组)
      • 类似于 type() ,但 type() 不考虑继承关系, isinstance() 考虑继承关系
      numbers = [1, 2, 3, 4, 2, 5] result = isinstance(numbers, list) result = isinstance(numbers, (list, str)) print(result) # Output: True
      【issubclass】 issubclass(class, classinfo)
      • 接收两个参数,用于检查class是否是classinfo的子类(派生类)
        • class——一个类对象
        • classinfo——一个类对象(或多个类对象组成的类元组)
      class A(): pass class B(A): pass res = issubclass(B, A) # True
      【map】 map(func, iterable)
      • 参数
        • 一个应用于每个元素且返回布尔值的函数
        • 一个可迭代对象,
      • 返回值
        • 一个迭代器
      • map()函数返回的是一个迭代器,因此需要使用list()或其他方法将其转换为列表或其他可迭代对象以进行进一步操作。
      1、解释说明
      在Python中,map()函数是一个内置的高阶函数,它接收一个函数和一个可迭代对象(如列表、元组等)作为参数。map()函数会将传入的函数应用于可迭代对象的每个元素,并返回一个新的可迭代对象,其中包含应用函数后的结果。需要注意的是,map()函数并不会直接修改原始的可迭代对象,而是返回一个新的可迭代对象。
      2、使用示例
      python复制代码 def square(x): return x * x numbers = [1, 2, 3, 4, 5] squared_numbers = map(square, numbers) # 将结果转换为列表并打印print(list(squared_numbers))# 输出:[1, 4, 9, 16, 25]
      在这个示例中,我们定义了一个名为square的函数,用于计算一个数的平方。然后,我们创建了一个包含数字的列表numbers。接下来,我们使用map()函数将square函数应用于numbers列表中的每个元素,并将结果存储在squared_numbers变量中。最后,我们将squared_numbers转换为列表并打印出来。
      • 3、注意事项
        • 如果传入的函数有多个参数,可以使用偏函数functools.partial()来固定部分参数,然后将新的函数传递给map()。
        • 如果需要对结果进行排序或过滤等操作,可以先将map()的结果转换为其他可迭代对象,然后再进行操作。
      【reduce】 reduce(func, iterable)
      • python3中, from functools import reduce
      • reduce 接受两个参数,将函数(第一个参数)作用在一个序列(第二个参数)上。reduce 把结果继续和序列中的下一个元素做累积运算,效果如下:
        • reduce(f, [t1, t2, t3, t4, t5]) = f(f(f(f(t1, t2), t3), t4), t5)
      • rduce 结合 map实现 str2int()
        • from functools import reduce # python3 def str2int(s): def char2num(n): d = {'0':0, '1':1, '2':2, '3':3, '4':4, '5':5, '6':6, '7':7, '8':8, '9':9} return d[n] return reduce(lambda x,y:x*10+y, map(char2num, s))
      【filter】 filter(func, iterable)
      • 参数
        • 一个应用于每个元素且返回布尔值的函数
        • 一个可迭代对象,
      • 返回值
        • 一个迭代器
      • filter 是一个过滤函数,将函数作用于序列的每一个元素,并根据函数返回的结果为 True 还是 False 来判断是否保留这个元素
      def is_odd(n): return n % 2 == 0 list(filter(is_odd, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
      练习
      • 素数问题
          1. 求指定范围内的素数
      【sorted】 sorted(iterable, key(可选), reverse=False)
      sorted排序的根据是什么,为什么传入一个可迭代对象iterable,就能进行排序
      • sorted 函数能够对任何可迭代对象进行排序,因为它依赖于Python中的比较操作符来确定元素间的顺序。Python中的每个对象都可以通过比较方法(如 __lt__, __gt__, 等)定义其自己的顺序行为,这些方法在使用比较操作符时被调用(例如 <, >, 等)。sorted 函数利用这些比较操作符来决定两个元素哪个应该先出现在排序后的序列中。
      • 当你传递一个可迭代对象给sorted时,排序算法(Python使用了一种称为Timsort的高效排序算法)会迭代该对象,并对每一对元素使用比较操作符来确定它们之间的顺序关系。如果没有提供key函数,将直接比较元素本身;如果提供了,则比较的是key函数返回的值。
      • sorted函数工作原理:
          1. 迭代:sorted函数从可迭代对象中获取元素。
          1. 键函数:如果提供了key参数,将对每个元素应用这个函数以产生用于排序的键。
          1. 比较:排序算法比较键函数生成的键(或者在未提供键函数时比较元素本身),根据比较结果安排它们的顺序。
          1. 构造列表:sorted函数构建一个新的列表,包含按照指定顺序排列的所有元素。
          1. 返回:返回这个排好序的列表。
      无需key函数时sorted 函数
      例1——sorted按数字大小对列表进行排序,由于列表包含数值类型,所以sorted直接使用默认的比较操作来完成排序。
      python复制代码 numbers = [3, 1, 4, 1, 5, 9, 2] sorted_numbers = sorted(numbers) print(sorted_numbers)# 输出: [1, 1, 2, 3, 4, 5, 9]
      例2——对于字符串列表,sorted将根据字典顺序来排序字符串:
      python复制代码 words = ["banana", "apple", "cherry"] sorted_words = sorted(words) print(sorted_words)# 输出: ["apple", "banana", "cherry"]
      按照元素的某个特定属性或复杂规则进行排序,可以使用 key 参数:
      在这个示例中,lambda 表达式被用作 key 函数,它返回了每个字典中与 'age' 关联的值。基于这些值,sorted() 函数对原始列表的元素进行了排序。
      # 假设我们有一个字典列表,我们希望根据每个字典中的 'age' 键对列表进行排序 people = [ {'name': 'Alice', 'age': 30}, {'name': 'Bob', 'age': 25}, {'name': 'Charlie', 'age': 35} ] # 使用 key 参数通过 'age' 键来排序 sorted_people = sorted(people, key=lambda person: person['age']) # 打印排序后的列表for person in sorted_people: print(person) # 输出: # {'name': 'Bob', 'age': 25} # {'name': 'Alice', 'age': 30} # {'name': 'Charlie', 'age': 35}

      Python 的 sorted() 函数在排序时遵循以下过程,具体到按照列表 people 中字典的 age 键进行排序时:

      python复制代码 # 假设这是我们的初始列表 people = [ {'name': 'Alice', 'age': 30}, {'name': 'Bob', 'age': 25}, {'name': 'Charlie', 'age': 35} ]
      以下为排序过程的步骤:
      1. 提取键值:
          • sorted() 函数根据你提供的 key 函数(在本例中为 lambda person: person['age'])来遍历列表 people 中的每个元素。
          • key 函数被应用到列表中的每一个字典上,提取出用于比较的键值,在本例中就是各个字典的 age 值。
      1. 比较和排序:
          • Python 内部使用 "Timsort" 算法对这些键值(即年龄)进行排序。"Timsort" 是一种混合排序算法,主要结合了归并排序和二分插入排序的特性。
          • 在此过程中,元素间的比较是基于它们的键值(年龄)进行的。
          • 排序算法会识别数据中已经有序的片段,并尽可能地利用这些片段,这使得 "Timsort" 在处理部分有序数据时非常高效。
          • "Timsort" 也是一个稳定的排序算法,这意味着如果两个元素的键值相同,它们在原始列表中的顺序将被保留。
      1. 构建结果列表:
          • 根据排序后的键值顺序,sorted() 函数构建一个新的列表。在本例中,列表元素的顺序将根据他们的 age 值从小到大排列。
          • 因为 "Timsort" 是稳定的,所以如果两个人的年龄相同,它们在新列表中的相对位置会与原始列表中的相对位置相同。
      1. 返回新列表:
          • 完成排序后,sorted() 生成并返回一个新的列表,包含排序后的字典。
      在这个具体示例中,排序过程如下:
      • 应用 key 函数,得到年龄列表:[30, 25, 35]
      • 对年龄列表排序,得到排序后的索引:[25, 30, 35]
      • 根据排序后的索引重组原列表,得到排序后的 people 列表:
      python复制代码 [ {'name': 'Bob', 'age': 25},# 25 是最小的年龄 {'name': 'Alice', 'age': 30},# 30 是第二小的年龄 {'name': 'Charlie', 'age': 35}# 35 是最大的年龄 ]
      最终返回的是根据 age 值排序后的新列表。
      【lambda】 lambda x, y, z....: x+y*z
      • 不限参数数量,可以设置无限多个参数
      • 只能有一个表达式
      • 返回值是这个表达式的结果
      【enumerate】enumerate(iterable)
      • enumerate()方法用于将一个可迭代对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标。enumerate()返回对象的每个元素都是一个元组,每个元组包括两个值,一个是计数,一个是sequence的值,计数是从start开始的,start默认为0。
      • enumerate()方法返回的对象是一个生成器,它可以被用于迭代可迭代对象。
      • enumerate()方法的返回值是一个生成器的原因是,它需要在每次迭代时才生成下一个索引和数据。如果它返回一个列表或元组,那么列表或元组中的所有元素都将在创建时生成,这可能会导致内存占用过多。因此,enumerate()方法返回一个生成器,可以提高程序的效率和内存利用率。
      【functools.partial】偏函数
      偏函数是一个固定了某些参数值的新函数,源自一个已有的函数。在 Python 的 functools 模块中,partial 函数用于创建偏函数。
      这在你希望减少函数需要的参数数量,或者预设某些默认值时非常有用。
      from functools import partial def multiply(x, y): return x * y # 创建一个新的函数 double,它是 multiply 的偏函数,其中 x 固定为 2 double = partial(multiply, 2) result = double(4) print(result)# 输出: 8
      在上面的例子中,double 是一个新定义的函数,它通过固定 multiply 函数的第一个参数 x 为 2 来创建。现在调用 double 只需要一个参数 y。
返回函数
python类
新式类与经典类
  • python2.x版本中,继承object的类都是新式类,不写父类的都是经典类
  • python3.x版本中,只有新式类,没有经典类
  • 新式类中保持了MyClass.__class__与type(MyClass)结果统一
  • class属性只有新式类拥有,经典类没有这个属性
  • 创建类
    • 【class关键字】
      • s = NewStyle(),表示创建并初始化了一个名为NewStyle()的类的实例对象,并将变量s指向(引用)这个实例对象
      【type元类】通过type() 函数创建类。它的第一个参数是类的名称,第二个参数是类的父类元组,第三个参数是类的属性字典。
      • MyClass2 = type("MyClass2", (object,), {"sm": staticmethod(sm)}),其中MyClass2是名称,(object,)是父类元组——父类是object,{"sm": staticmethod(sm)}是属性字典——key即第一个sm表示函数名,value表示函数,表示将sm(第二个)转化为静态属性的方法
      • type 是构造类对象的工厂函数。因此,MyClass2 类是由 type 元类创建的,MyClass2 的实例会是 MyClass2 类型,而 MyClass2 类本身的类型是 type
      类的属性字典:
      • 类的属性字典中的 key 表示属性名,value 表示属性值。
      • 属性名可以是任意字符串,但通常使用驼峰式命名。属性值可以是任意类型的对象,包括函数、类、字符串、数字等。
      • 类的属性字典用于存储类的属性值。属性值可以是类的静态属性,也可以是类的实例属性。
      • 静态属性是类的属性,与类的实例无关。静态属性可以直接通过类名来访问。
      • 实例属性是类的实例的属性。实例属性只能通过类的实例来访问。
      type 元类与 object 基类
      在Python中,object 基类也是由 type 元类创建的。实际上,type 是所有新风格类及其实例的基础。在Python的类型体系中,type 是一个非常特殊的元素,它既是自己的实例又是大多数其他所有新风格类的元类。
      下面是几个关于Python类和对象的关键点:
      • type本身是一个元类,专门用来创建类。
      • object是最顶层的基类,所有新风格的类,包括它自己,都直接或间接地继承自它。
      • type同时也是object的实例,这表明即便是object这个根基类type也是由创建的。
      • object类的元类是type,意味着type创建了object。
      这样看起来似乎很矛盾,但这是Python语言中一系列精心设计的规则,使得Python在内部可以保持一致性。在Python的世界里,几乎一切皆对象,包括类和函数。
      以下是一些代码示例,帮助说明这些概念:
      # type 是它自己的实例 print(isinstance(type, type))# 输出:True # object 是 type 的实例 print(isinstance(object, type))# 输出:True # type 是 object 的元类 print(type(object))# 输出:<class 'type'> # object 是最顶层基类 print(object.__bases__)# 输出:(),表示没有父类 # 任何新风格的类默认都有 type 作为它们的元类,class MyNewStyleClass(object): passprint(type(MyNewStyleClass))# 输出:<class 'type'>
      这些关系构成了Python OOP(面向对象编程)的核心,所有的类都继承自 object,而所有类(包括 object 自身)都是由 type 元类创建的。
类是创建对象的模版,类的实例是对象;类本身也是对象,元类是创建类的模版。示例:
class NewStyle(Object): def __new__(cls, *args, **kwargs): print("instance will be create...") instance = super(NewStyle, cls).__new__(cls, *args, **kwargs) print("instance is created...") return instance def __init__(self, *arg, **kwargs): print("__init__ is invoked...") if __name__ == "__main__": s = NewStyle() print(s) # instance will be create... # instance is created... # __init__ is invoked... # <__main__.NewStyle object at 0x000002AB05CDBE20>
  • 在 if __name__ == "__main__": 语句块中,调用 NewStyle() 函数来创建一个 NewStyle 类的实例。NewStyle() 函数会先调用 __new__() 方法来创建实例,然后调用 __init__() 方法来初始化实例。
输出中 <__main__.NewStyle object at 0x000002AB05CDBE20> 的解释如下:
<__main__.NewStyle object at 0x7f0e1b255690> 表示的就是这个 NewStyle 类的实例。
具体来说,<__main__.NewStyle object at 0x7f0e1b255690> 的各个部分表示如下:
  • < 和 > 表示一个对象的引用。
  • __main__ 表示 NewStyle() 函数所在的模块名。
  • NewStyle 表示 NewStyle() 函数所创建的类的名称。
  • object 表示 NewStyle() 函数所创建的类的类型。
  • at 0x7f0e1b255690 表示 NewStyle() 函数所创建的类的实例的地址。
因此,<__main__.NewStyle object at 0x7f0e1b255690> 可以理解为:
在 __main__ 模块中,一个类型为 NewStyle 的对象,地址为 0x7f0e1b255690。
  • 类的属性(只能使用类对象调用)
    • 类的自身属性
      • 【__new__】方法用于创建类的实例,在创建类的实例对象时会调用该方法
      • 【__init__】方法用于初始化类的实例,在创建类的实例对象后会调用该方法初始化类的实例对象(此时类的实例对象已被创建)
        • 可通过 __init__() 方法向类传入参数
        class MyClass(object): def __init__(self, x, y): self.x = x self.y = y def add(self): return self.x + self.y if__name__=='__main__': myclass = MyClass(1, 2) res = myclass.add() print(res) # 3
      • 【__class__】用于获取创建该对象使用的模版类(即创建它的类)。对于实例对象,返回的是创建它的类;对于类对象,返回的是元类type。
      • 【__base__】用于获取该类的直接父类。对于类对象,返回该类的第一个父类(父类元组的的第一个元素)
      • 【__bases__】用于获取该类的父类元组。对于类对象,返回该类继承的父类元组,但不包含继承树更上层的其他类(比如父类的父类)
      • 【__mro__】用于获取该类的MRO列表
      类的实例属性
      • 实例对象会继承类的实例属性,可以使用点 . 形式访问属性,优先访问实例属性,如果没有该实例属性才访问类属性,仍然没有则会报错AttributeError
      • 在Python中,实例对象和类(class)拥有不同的属性和方法集。当你创建一个类的实例时,该实例主要继承了类中定义的那些面向实例的属性和方法。而一些特殊的属性,比如__bases__和__class__,并不是作为实例的一部分而是属于类的内部机制。
      • 实例属性比类属性优先级高,故存在同名的属性会优先访问实例属性
      • 在模版类的属性基础上,可以通过点 . 的方式在外部设置新的实例属性,例如:
        • class Student(): name = 'Marry' # 类的属性 def __init__(self, name, score): self.name = name # 实例属性 self.score = score # 实例属性 if__name__ == '__main__': bart = Student('Bart', 80) bart.age = 20 # 设置新的属性 print(bart.name) # Bart print(Student.name) # Marry
实例变量(也称为实例属性) & 类变量(也称为类属性)
  • 实例变量(也称为实例属性):
    • 属于类的实例的变量,每个对象或实例都有自己独立的一套实例变量,也称为实例属性。
    • 定义在类的方法中(通常是在__init__方法内),并且通过self来引用。
    • 当创建一个类的新实例时,每个实例变量都会得到自己的一份拷贝。
    • 实例变量在类的方法中通常通过 self 关键字来定义和访问。
    • 可通过在类中定义特殊变量 __slots__ 的属性来限制实例的属性,但是仅对当前类的实例起作用,对其子类不起作用
      • 当一个类定义了__slots__属性,它告诉Python解释器不要为这个类使用普通的基于字典的对象表示形式。相反,为了节省内存和提高访问速度,它会为实例创建一个更加静态的、固定大小的字段集合。
      class Stu(): __slots__ = ('name', 'age') pass s = Stu() s.name = 'Mi' # 可以正常设置 s.age = 30 # 可以正常设置 s.score = 80 # 报错AttributeError: 'Stu' object has no attribute 'score' # 在其子类中就不起作用了 class Stu1(Stu): pass s = Stu1() # 均可正常设置 s.name = 'F' s.age = 20 s.score = 30
  • 类变量(也称为类属性):
    • 属于类本身的变量,由所有实例共享,也称为类属性。
    • 类变量是在类定义的顶层声明的,而非在方法内部(即定义在类的内部,但在所有方法之外)
    • 类变量在所有实例之间是共享的,这意味着如果某个实例改变了类变量的值,这个变化会反映到所有其他实例上
例如:
python复制代码 class ExampleClass: class_variable = 0# 类变量 def __init__(self, instance_variable_value): self.instance_variable = instance_variable_value# 实例变量# 创建两个ExampleClass的实例 instance1 = ExampleClass(1) instance2 = ExampleClass(2) # 每个实例的实例变量是独立的 print(instance1.instance_variable)# 输出: 1 print(instance2.instance_variable)# 输出: 2 # 类变量在所有实例之间共享 print(ExampleClass.class_variable)# 输出: 0 print(instance1.class_variable)# 输出: 0 print(instance2.class_variable)# 输出: 0 # 更改类变量将影响所有实例 ExampleClass.class_variable = 10 print(instance1.class_variable)# 输出: 10 print(instance2.class_variable)# 输出: 10
super方法
  • super().method(*args, **kwargs)
  • 【super().init()、super().func()等等】表示执行第一个调用 super() 的类的 MRO 列表中除自己外第一个父类的类的 init 方法(func 方法)
super()调用的是MRO(Method Resolution Order)列表中当前类之后的第一个父类的方法。并且这个 MRO 列表始终指的是第一个调用 super() 的类的 MRO 列表。如果存在多重继承,则super()指代的对象一直按照MRO列表来调用(即在连续调用过程中Python会自动确定super()方法的两个参数,在此过程中即使自己指定了这两个参数也不会起作用),例如:
class A: def do_something(self): print("Method in A") class B(A): def do_something(self): print("Method in B") super(B, self).do_something() class C(A): def do_something(self): print("Method in C") class D(B, C): def do_something(self): print("Method in D") super().do_something() d_instance = D() d_instance.do_something() # 输出 # Method in D # Method in B # Method in C
  • 【注意】
    • python2 中的 super() 需要传入两个参数,第一个是调用它的类(即子类),第二个参数是该子类的实例
    • python3(隐式参数) 中 super() 能够自动确定调用它的类和第二个参数(即self),所以直接使用即可
示例代码
class FooParent(object): def __init__(self): self.parent = 'I\'m the parent.' print ('Parent') def bar(self,message): print ("%s from Parent" % message) class FooChild(FooParent): def __init__(self): # super(FooChild,self) 首先找到 FooChild 的父类(就是类 FooParent),然后把类 FooChild 的对象转换为类 FooParent 的对象 super(FooChild,self).__init__() print ('Child') def bar(self,message): super(FooChild, self).bar(message) print ('Child bar fuction') print (self.parent) if __name__ == '__main__': fooChild = FooChild() fooChild.bar('HelloWorld')
私有方法 & 私有变量
  • 私有方法
    • python中约定私有方法为类中的前缀带下划线的方法,类似_xxx和__xxx这样的函数或变量就是非公开的(private),不应该被直接引用,比如_abc,__abc等;之所以我们说,private函数和变量“不应该”被直接引用,而不是“不能”被直接引用,是因为Python并没有一种方法可以完全限制访问private函数或变量,但是,从编程习惯上不应该引用private函数或变量。
  • 私有变量
    • python中私有变量是以下划线为前缀的变量,包括单下划线前缀的变量和双下划线前缀的变量
    • 双下划线前缀的变量(类变量( Student 类中的 __students )或实例变量( Student 类中的 self.__name )),python解释器会自动将私有变量的变量名 __name 起别名 _Student__name ,在类的内部可以通过两种变量名的任意一种进行访问,但在外部只能通过 _Student_name 访问和修改
    • 非常不推荐在外部直接进行访问和修改,建议在类的内部定义私有变量相应的访问和修改方法,这样不仅可以对私有变量进行操作,还可以做相应的检查
      • 【注意】实例变量(也称为实例属性) & 类变量(也称为类属性)
      • class Student(): __students = [] # 类变量 def __init__(self, name, score) -> None: self.__name = name # 实例变量 self.score = score Student.__students.append(name) # 类变量 def get_name(self): return self.__name def set_name(self, name): if not isinstance(name, str): raise 'name is str object ,please modify again' self.__name = name
    • 单下划线前缀的变量(不分情况,如 _name ),这样的实例变量外部是可以访问的。但是,按照约定俗成的规定,当看到这样的变量时,意思是“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
    • 【注意】Python中以双下划线开头并且以双下划线结尾的变量名(类似__xxx__)是特殊变量,不是private变量。特殊变量是可以直接访问的,所以,定义私有变量不能用__name__、__score__这样的变量名。
    • class Student(): __students = [] # 类变量 def __init__(self, name, score) -> None: self.__name = name # 实例变量 self.score = score Student.__students.append(name) # 类变量 def print_score(self): print('%s的分数%s' % (self.__name, self.score)) if __name__ == '__main__': bart = Student('Bart', 80) print(bart.score) # 80 print(bart.__name) # (报错)AttributeError: 'Student' object has no attribute '__name' print(bart._Student__name) # Bart # 设置新的属性, bart.__name = 'New name' print(bart.__name) # New name print(bart._Student__name) # Bart
  • 成员函数
    • 实例成员函数(又名【成员方法】【类实例方法】【类对象方法】,属于类的实例对象,需要实例化该类之后才能调用):实例成员函数可以访问类的所有成员,包括成员变量和成员函数。实例成员函数在调用时,(类实例对象)会自动传入一个参数self,代表类的实例。
      • 不是静态成员函数的均可视为实例成员函数
      • 注意:
        • 实例成员函数必有self参数,如果在定义时没写,那么在调用时会自动传入。故在此情况下调用该方法会报错
        • 使用类本身调用会报错没有 self 参数
      静态成员函数(又名【静态方法】【类方法】,属于类,可不实例化该类直接通过类调用):静态成员函数只能访问类的静态成员,不能访问类的实例成员。静态成员函数在调用时,不需要传入任何参数。
      • 使用@staticmethod装饰器进行装饰,即表示被装饰的函数为静态成员函数
      • 注意:
        • 静态成员函数不需要self参数,即调用该方法 python 不会自动传入 self 参数
        • 静态成员函数是该类的命名空间内的普通函数,与该类并没有关联关系
  • 类定义中的self是类的实例
  • 超类(Superclass),指的是一个类在继承关系中处于更高层的类。
    • 子类(Subclass)可以通过继承超类的属性和方法,从而在其基础上实现自己的功能。超类可以被多个子类继承,这些子类可以进一步定义自己的属性和方法,以满足特定的需求。
  • MRO(方法解析顺序)
    • 获取类的MRO列表的两种方法
      1. 【MyClass.__mro__】调用类的mro属性,返回该类的MRO列表
      1. 【MyClass.mro()】调用类的mro方法,返回该类的MRO列表
    • MRO的【单调性原则】——在MRO列表中所有子类都在父类的前面
        1. 子类在其父类之前。
        1. 如果类有多个父类,那么基类的列表顺序就是这些类被检查的顺序。
        1. 如果存在多条路径可以到达同一个类,Python不允许该类在MRO中重复出现。
多重继承
  • MixIn
    • 在设计类的继承关系时,通常主线都是单一继承下来的,例如,Ostrich (鸵鸟)继承自Bird。但是,如果需要“混入”额外的功能,通过多重继承就可以实现,比如,让Ostrich除了继承自Bird外,再同时继承Runnable (通常将除主继承之外的其他继承都使用 xxxMixIn 表示,此处的 Runable 通常表示为 RunableMixIn )。这种设计通常称之为MixIn。
  • 定制类
    • __init__(self, [...])
      __init__ 是一个构造器方法,当创建一个类的新实例时会被自动调用。它用于初始化对象的状态或属性。
      class MyClass: def __init__(self, value): self.value = value
      __str__(self) & __repr__(self)
      • 两种方法均是用来定义类的字符串描述(简单来说,就是类的信息介绍)。不同的是, __str__ 方法在于可读性强, __repr__ 方法在于准确性高。
      • 在输出类的实例时,使用 python 内置的 print() 和 format() 方法则优先调用 __str__ 方法,找不到该方法才调用 __repr__ 方法;交互模式下直接输入类的实例对象名称再按回车,调用的是 __repr__ 方法
        • obj = MyObject("example") # 使用 `format()` 默认会调用 `__str__` formatted_str = format(obj) print(formatted_str) # 输出:MyObject(example) which is great for str # 如果直接打印对象,也会调用 `__str__` print(obj) # 输出:MyObject(example) which is great for str # 使用 `repr()` 会调用 `__repr__` repr_str = repr(obj) print(repr_str) # 输出:MyObject(example)
          在上面的代码中: 使用format(obj)得到的结果是调用obj.__str__()方法得到的字符串。 使用print(obj)打印 对象时,同样是输出由__str__方法产生的字符串。 使用repr(obj)则会输出由__repr__方法产生的字符串。 因此,当你只是想要将对象转换为字符串而不需要额外的格式化信息时,format()函数会默认调用对象的__str__方法。
      __str__ 方法定义了当对象被转换成字符串(例如使用 str() 函数,或是在打印时)应该如何展示给用户。通常用于创建对象的“非正式”或可打印的字符串表示。
      class MyClass: def __str__(self): return "A custom string representation of the object"
      __repr__ 方法定义了对象的“正式”字符串表示,它的目标是明确无误地表达出如何与这个对象相关联的信息。理想情况下,__repr__ 返回的字符串应能被 eval() 使用,并且返回一个等价的对象。
      class MyClass: def __repr__(self): return "MyClass()"
      __iter__(self) & __next__(self)
      __iter__ 方法需要返回一个迭代器对象,也就是包含有 __next__ 方法的对象。而 __next__ 用来获取序列中的下一个元素。
      class MyIterable: def __init__(self): self.data = [1, 2, 3] def __iter__(self): self.index = 0 return self def __next__(self): if self.index < len(self.data): result = self.data[self.index] self.index += 1 return result else: raise StopIteration
      __getitem__(self, key), __setitem__(self, key, value) & __delitem__(self, key)
      这些方法用于自定义对象的索引和切片行为。
      __getitem__ 允许通过索引访问元素。
      # 使得类的实例对象(假如为 fib )以及使用 fib[n] 这种方式访问元素 class Fib(): def __getitem__(self, n): a, b = 0, 1 for _ in range(n): a, b = b, a+b return a # 使得类的实例对象(假如为 fib )以及使用 fib[n]以及切片的方式访问元素 class Fib1(): def __getitem__(self, n): if isinstance(n, int): a, b = 0, 1 for _ in range(n): a, b = b, a+b return a if isinstance(n, slice): start = n.start stop = n.stop if not start: start = 0 L = [] a, b = 0, 1 for _ in range(stop-1): if _ >= start: L.append(a) a, b = b, a+b return L
      • __setitem__ 允许设置指定索引处的元素值。
      • __delitem__ 允许删除指定索引处的元素。
      class MyCollection: def __init__(self): self.data = {} def __getitem__(self, key): return self.data[key] def __setitem__(self, key, value): self.data[key] = value def __delitem__(self, key): del self.data[key]
      __getattribute__(self, key), _getattr__(self, name)&__setattr__(self, name, value) & hasattr(self, name) & __delattr__(self, key)
      这些方法用于自定义对象属性的访问和修改。
      • __getattribute__ 所有新式类都有这个方法,从object继承而来。当访问对象属性时,会优先调用__getattribute__(self, key) ,当查找失败引发 AttributeError 错误时才会调用_getattr__(self, name)
      • __getattr__ 【动态调用】在尝试访问不存在的属性时被调用。即只有在没有找到属性的情况下,才调用__getattr__,已有的属性,比如name,不会在__getattr__中查找。
        • # 利用动态调用实现自适应API的生成 class Chain(object): def __init__(self, path=''): self.__path = path def __getattr__(self, path): if path == 'user': return lambda username:Chain(f'{self.__path}/{username}') return Chain('%s/%s' % (self.__path, path)) def __str__(self): return self.__path __repr__ = __str__ Chain().status.user('michael').timeline.list # /status/michael/timeline/list
      • __setattr__ 所有新式类都有这个方法,从object继承而来。在属性被赋值时调用。
        • 会拦截所有属性的的赋值语句。
        • 当在__setattr__方法内对属性进行赋值时,不可使用self.attr = value,因为他会再次调用self.__setattr__("attr", value),则会形成无穷递归循环,最后导致堆栈溢出异常。应该通过对属性字典做索引运算来赋值任何实例属性,也就是使用self.__dict__['name'] = value.
        • 【注意】如果定义了这个方法,self.arrt = value 就会变成self.__setattr__("attr", value)
        • 如果类自定义了__setattr__方法,当通过实例获取属性尝试赋值时,就会调用__setattr__。常规的对实例属性赋值,被赋值的属性和值会存入实例属性字典__dict__中。
        • 如果类自定义了__setattr__,对实例属性的赋值就会调用它。类定义中的self.attr也同样,所以在__setattr__下还有self.attr的赋值操作就会出现无线递归的调用__setattr__的情况。自己实现__setattr__有很大风险,一般情况都还是继承object类的__setattr__方法。
      • hasattr() 内置函数,但可以通过重写 __getattr__ 来改变 hasattr() 的行为。
        • class MyClass: def __getattr__(self, name): return f"{name} attribute is not defined" def __setattr__(self, name, value): print(f"Setting {name} to {value}") self.__dict__[name] = value
      • __delattr__ 所有新式类都有这个方法,从object继承而来。删除对象的属性时调用
      __call__(self, [...])
      __call__ 方法允许对象像函数一样被调用。你可以定义任何参数,当对实例进行调用时,这些参数都会传递给 __call__ 方法。
      • 在Python中,__call__ 方法是一个特殊方法(也称为魔术方法),它允许你定义一个类的实例的行为,使得该实例可以像函数一样被调用。换句话说,当你创建了一个类的对象之后,你可以直接使用这个对象加上括号和参数来调用这个方法。
      • 定义 __call__ 方法的类的实例,在被当作函数调用时,就会执行 __call__ 方法内部的代码。
      • 下面是一个简单的例子,我们将创建一个 Adder 类,其实例可以像函数那样被调用,并且返回一个将传入参数与实例初始化时给定值相加的结果。
        • class Adder: def __init__(self, n): self.n = n def __call__(self, x): return self.n + x # 创建一个Adder的实例,此时n被设置为3 add_three = Adder(3) # 因为Adder类定义了__call__方法,所以add_three实例可以被当做函数调用 result = add_three(10)# 等价于调用 add_three.__call__(10)print(result)# 输出 13,因为 10 + 3 = 13
  • 元类
    • type
    • metaclass
枚举类
枚举(Enumerations)是一种用于创建命名常量组的方法。枚举成员是常量,并且不可更改。Python 通过内置的 enum 模块提供了对枚举的支持。在 Python 中,<enum 'Month'> 表示 Month 是一个枚举类型。
函数式 API (Enum 函数) 定义枚举:
from enum import Enum # 函数式API(Enum)定义枚举类 Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
  • 优点
      1. 简洁性:当枚举值只需要默认的递增整数时,函数式 API 可以更简洁地定义枚举。
      1. 动态性:如果你需要在运行时动态创建枚举(例如,基于外部数据源),函数式 API 可能更方便。
class 语法定义枚举的优点:
from enum import Enum # class语法定义枚举类 class Month(Enum): JANUARY = 1 FEBRUARY = 2 MARCH = 3 APRIL = 4 MAY = 5 JUNE = 6 JULY = 7 AUGUST = 8 SEPTEMBER = 9 OCTOBER = 10 NOVEMBER = 11 DECEMBER = 12
  • 优点
      1. 可读性:对于熟悉 Python 类和面向对象编程的开发者来说,使用 class 语法可能更直观易懂。
      1. 定制性:如果你需要自定义方法或特殊行为,class 语法可以提供更多的灵活性。你可以轻松添加方法或改变枚举成员的默认行为。
      1. 文档与工具支持:一些代码编辑器和静态分析工具可能对 class 语法有更好的支持,包括自动补全、代码跳转等功能。
定义枚举类示例:
from enum import Enum # 函数式API(Enum)定义枚举类 Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')) # class语法定义枚举类 class Month(Enum): JANUARY = 1 FEBRUARY = 2 MARCH = 3 APRIL = 4 MAY = 5 JUNE = 6 JULY = 7 AUGUST = 8 SEPTEMBER = 9 OCTOBER = 10 NOVEMBER = 11 DECEMBER = 12
上述代码定义了一个名为 Month 的枚举类,其中每个月份都是一个枚举成员,并且与一个整数值相关联。例如,JANUARY 对应于数值 1,FEBRUARY 对应于数值 2,依此类推。
当你尝试打印枚举类时,Python 解释器会显示枚举类型的名称。例如:
将会得到以下输出: <enum 'Month'>
这表明 Month 是一个枚举类型的对象,但这并不展示枚举中的任何成员或它们的值。若要查看所有的枚举成员和它们的值,可以遍历枚举:
for month in Month: print(month.name, month.value)
输出将是:
JANUARY 1 FEBRUARY 2 MARCH 3 APRIL 4 MAY 5 JUNE 6 JULY 7 AUGUST 8 SEPTEMBER 9 OCTOBER 10 NOVEMBER 11 DECEMBER 12
这显示了 Month 枚举的每个成员及其关联的数值。使用枚举的好处是它们是不可变的,可哈希的,可以轻松地进行比较、迭代和确保值的唯一性。
相对合适性:
  • 如果你正在进行快速的原型设计,或者你的枚举类定义非常简单(比如仅仅是一组命名常量),那么使用函数式 API 可能更合适。
  • 而如果你的枚举需要附加的方法、文档字符串或是复杂的值,那么使用 class 语法将会更合适。
在大多数情况下,class 语法是首选,因为它提供了更好的可读性和扩展性。不过,在某些特定的情况下,函数式 API 提供的简洁和动态性可能更受欢迎。最终这个决定应该基于项目需求和开发团队的标准来做出。
常见类的主要作用
MixinHandler 混入类,提供了一些通用的方法,主要作用是为其他类提供扩展方法使用,通过继承的方式使其他类获得方法
  • 这个处理器类被称为混入类(Mixin class),是因为它提供了一组额外的功能,可以通过继承的方式添加到其他类中,而不是作为一个独立的、完整的类来设计。混入类通常不会被直接实例化,它们的目的是为了通过多重继承被其他类复用,提供一种灵活的方式来扩展类的功能。
  • BaseHandler 基础处理器类,定义一些基本的方法和属性
  • ErrorHandler 错误处理器类,主要用于处理错误
闭包
  • 在函数A的内部再定义一个函数B,并且函数B引用了函数A的变量,这种结构叫做闭包,函数B叫做闭包函数
    • def count1(): x = 0 def f(): nonlocal x x = x+1 # 对外部变量 x 进行了修改 return x return f def count2(): x = 0 def f(): nonlocal x return x+1 # 没有修改外部变量 x return f # 对count1() 和count2()每个均调用5次,count1()输出结果为5,count2输出结果为1 # 主要在于count1()在的内部函数引用了外部变量x并且进行了修改,而count2()则是直接引用了变量x并没有进行修改
  • 在 Python 中,闭包函数确实记住了其外部作用域中的变量(或者说记住了外部命名空间),但这种记忆是通过引用来实现的。所以当闭包被创建时,它不是复制变量 i 的值,而是保持对变量 i 的引用。由于 Python 中的变量是名字到对象的绑定,所以闭包函数记住的实际上是变量名 i 而不是其具体的内存地址。然后当闭包被调用时,Python 解释器会查找这个名字 i 当前所引用的对象,这也是为什么闭包可能反映外部作用域中变量的最新状态,因为它使用的是变量名来获取当前的值。
  • 闭包意味着内部函数 f 记住了它从外部作用域(count 函数的作用域)中引用的变量 i 的值,而不是该变量某一时刻的副本。但是,由于 Python 的延迟绑定行为,这些内部函数实际上并没有在创建时立即计算出 i 的值,而是在调用时才去查找 i 的值。
  • 理解闭包时,关键是要知道闭包引用的是变量本身而不是变量的值。这样就可以解释为什么即使闭包在循环中创建,但直到闭包被调用时才会求得真正的值——这个值是根据闭包引用的那个变量在闭包调用时刻所具有的值。
外部函数运行时闭包在内存中的变化
def outer_function(A_data): # 函数A中创建一些数据 A_local_data = 'local data in A' # 定义闭包函数B def inner_function(B_data): # 函数B可以访问外围函数A的局部变量 print(A_local_data) print(B_data) # 函数A返回闭包函数B return inner_function # 创建闭包实例 closure_instance = outer_function('data for A')

函数 A 的运行过程以及闭包函数 B 在内存中的变化

  1. 函数 A 运行之前
    1. 在外部函数 outer_function 被调用之前,它仅仅是在内存中定义好的一个函数对象。此时,还没有任何闭包被创建。因此,内存中还没有与闭包相关的任何内容。
  1. 函数 A 运行中
    1. 当 outer_function 被调用时 (closure_instance = outer_function('data for A')),函数 A 开始执行,并且会为其局部变量 A_local_data 分配内存和存储数据 'local data in A'。接着,函数 A 中定义了一个闭包函数 B,即 inner_function。
      此时,在函数 A 的上下文环境中,闭包函数 B 被创建。因为闭包函数 B 会引用到函数 A 中的局部变量 A_local_data,这个变量会被包含在闭包的环境中。该过程称为词法闭包或者静态作用域,表示函数 B 记住了它被定义时的环境。
  1. 函数 A 运行结束
    1. 当外部函数 outer_function 执行完毕并返回 inner_function 时,通常情况下,函数 A 的局部变量在函数退出时会被销毁。但由于有闭包存在,A_local_data 不会被销毁;相反,它将继续存在因为闭包函数 B 引用了它。这意味着 inner_function 继承了它被定义时的环境,保留了对 A_local_data 的引用。
      外部函数 outer_function 返回闭包函数 B 的引用,并且这个引用被赋值给变量 closure_instance。尽管外部函数 A 的执行已经完成,但是闭包函数 B 内部仍然可以访问 outer_function 的局部变量 A_local_data。
在这个过程中,“闭包”这个术语指的是函数 B 及其周围状态(即函数 A 中的局部变量)。闭包允许你保存这些状态,即使外部函数的执行已经完成。
如果你现在调用 closure_instance('data for B'):
python复制代码 closure_instance('data for B')
闭包函数 B 会打印出 'local data in A' 和 'data for B',证明虽然外部函数 A 已经执行结束,闭包函数 B 仍然持有并能够访问 A_local_data。所以在内存中,A_local_data 仍被闭包引用,直到闭包自身不再被需要并被垃圾回收器回收。
装饰器
  • 语法糖格式含义:@log 等价于 func = log(func) , @log('execute') 等价于 func = log('execute')(func)
  • Decorator 是返回函数的高级函数,可以在不改变原有函数代码的情况下为该函数增加新的功能
普通装饰器
# 计时器 def calc_func_runtime_no_wrapped_funtools(func): def wrapper(*args, **kwargs): start = time.perf_counter() res = func(*args, **kwargs) end = time.perf_counter() print(f'{func.__name__} execute in {end-start}s') return res return wrapper def calc_func_runtime(func): # 返回扩展过功能的函数 @functools.wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() res = func(*args, **kwargs) end = time.perf_counter() print(f'{func.__name__} execute in {end-start}s') return res return wrapper @calc_func_runtime_no_wrapped_funtools # 等价于now = calc_func_runtime_no_wrapped_funtools(now) def now(): print('2024.2.4') @calc_func_runtime # 等价于now = calc_func_runtime(now) def now_functols_wrapped(): print('2024.2.4') # 输出函数名 print(now.__name__) # wrapper print(now_functols_wrapped.__name__) # now_functols_wrapped
可传参数的装饰器
# 记录日志函数 # 不带参数 def log(func): def wrapper(): print("Logging execution") func() print("Finished logging") return wrapper def log_of_param(param): def decorator(func): def wrapper(): print(f"Logging with param: {param}") func() print(f"Finished logging with param: {param}") return wrapper return decorator @log # 等价于 now = log(now) @log_of_parameter('execute') # 等价于 now = log_of_parameter('execute')(now),第一步log_of_parameter('execute')返回decorator函数,第二步返回装饰后的函数 def now(): print('2024.2.4') now() # 执行结果 # Logging execution # Logging with param: execute # 2024.2.4 # Finished logging with param: execute # Finished logging
  • 如果装饰器本身需要传入参数,那么可以定义一个返回decorator的高级函数
  • 当有多个装饰器时,它们的执行顺序是从靠近被装饰对象的的开始由内向外执行;对于函数对象,运行函数时时从外向内执行,如可传参数的装饰器
  • 使用 @functools.wraps 是为了同步原函数的属性信息(比如__name__(函数名))
  • python内置装饰器
    • @staticmethod:表示这个函数是静态方法,因为某种原因放在此类中,实际上与此类毫无关系,但是可以并且只能通过此类名来调用被装饰的函数
    • @classmethod:在重构类时不需要修改构造函数(__init__),如果想增加新的方法,此时要用到父类,只需要给该方法添加@classmethod即可实现
      • 在已写好初始类的情况下,想给初始类再新添功能,不需要改初始类,只要在新类继承初始类并在内部新写一个方法,方法用@classmethod装饰一下即可。
      • 此修饰器装饰的方法不需要实例化,不需要 self 参数,但第一个参数需要是表示自身类的 cls 参数,可以来调用类的属性,类的方法,实例化对象等。
      # 初始类:classData_test(object): day=0 month=0 year=0 def__init__(self,year=0,month=0,day=0): self.day=day self.month=month self.year=year defout_date(self): print "year :" print self.year print "month :" print self.month print "day :" print self.day # 新增功能:classStr2IntParam(Data_test): @classmethod defget_date(cls, string_date): #这里第一个参数是cls, 表示调用当前的类名year,month,day=map(int,string_date.split('-')) date1=cls(year,month,day) #返回的是一个初始化后的类return date1 # 使用:r= Str2IntParam.get_date("2016-8-1") r.out_date() # 输出:year : 2016 month : 8 day : 1
      @property:创建只读属性,将方法转化为同名称的属性,使方法像属性一样被访问
      • 使用@property装饰器可以将一个方法转换为同名的只读属性,这意味着当你尝试访问这个属性时,实际上是在调用一个方法
      property 装饰器是 Python 中的一个内置装饰器,用于创建只读属性。它允许将类中的方法转换为看似访问属性一样的方式进行调用,这有助于实现数据封装和数据隐藏。通过使用 property 装饰器,您可以在不改变类接口的情况下,更改属性背后的实现细节。

      使用 property 装饰器

      python复制代码 class MyClass: def __init__(self): self._my_attribute = None # 通常使用单下划线前缀表示“私有”属性 @property def my_attribute(self): """这是一个只读属性""" return self._my_attribute
      在上面的例子中,my_attribute 方法被 @property 装饰器修饰,这意味着你可以像访问数据属性一样访问 my_attribute 方法:
      python复制代码 obj = MyClass() value = obj.my_attribute # 不需要括号,就像访问属性一样,调用了类中的方法
      但是,只用 @property 并不能设置属性的值。为了能够设置属性的值,还需要定义一个 setter 函数:

      添加 setter 函数

      python复制代码 class MyClass: def __init__(self): self._my_attribute = None @property def my_attribute(self): """获取属性值""" return self._my_attribute @my_attribute.setter def my_attribute(self, value): """设置属性值""" # 在这里可以添加任何验证或处理代码 self._my_attribute = value
      现在,你既可以获取也可以设置 my_attribute 的值了,并且在设置值时可以包含任何必要的逻辑,比如类型检查、验证等:
      python复制代码 obj = MyClass() obj.my_attribute = 42# 设置属性值print(obj.my_attribute)# 获取并打印属性值

      添加 deleter 函数

      如果你还想要控制删除属性的行为,可以添加一个 deleter 函数:
      python复制代码 class MyClass: def __init__(self): self._my_attribute = None @property def my_attribute(self): """获取属性值""" return self._my_attribute @my_attribute.setter def my_attribute(self, value): """设置属性值""" self._my_attribute = value @my_attribute.deleter def my_attribute(self): """删除属性""" del self._my_attribute
      使用 deleter 可以执行清理操作或者其他特定逻辑:
      python复制代码 obj = MyClass() del obj.my_attribute# 删除属性

      总结

      property 装饰器使得开发者可以在不改变类的外部调用方式的情况下,对属性的访问进行更精细的控制。通过在属性的 getter、setter 和 deleter 方法中添加逻辑,您可以确保属性值总是符合预期,并且可以方便地随着项目的变更而更新底层逻辑。
      示例
      class A(object): # 属性默认为类属性(可以给直接被类本身调用) num = "类属性" # 实例化方法(必须实例化类之后才能被调用) def func1(self): # self : 表示实例化类后的地址id print("func1") print(self) # 类方法(不需要实例化类就可以被类本身调用) @classmethod def func2(cls): # cls : 表示没用被实例化的类本身 print("func2") print(cls) print(cls.num) cls().func1() # 不传递传递默认self参数的方法(该方法也是可以直接被类调用的,但是这样做不标准,标准的做法是使用 @staticmethod 装饰) # @staticmethod def func3(): print("func3") print(A.num) # 属性是可以直接用类本身调用的 # A.func1() 这样调用是会报错:因为func1()调用时需要默认传递实例化类后的地址id参数,如果不实例化类是无法调用的 A.func2() A.func3()
  • 类装饰器
    • 不带参数的类装饰器
      • 在类的初始化函数 _init__() 方法中定义装饰的内容
      • 使用 @Student 装饰函数等价于 now = Student(now)
      • 【注意】传入的是函数对象,返回值是一个类的实例对象
      • 示例
        class Student(): def __init__(self, func): print('__init__') func() def __call__(self): print('__call__') # 等价于 now = Student(now) @Student def now(): print('2024.02.06') print('Today is 2024.02.06') now()
    • 带参数的类装饰器
      • 在类的 __call__() 方法中定义装饰的内容
      • 使用 @Student(arg) 装饰函数等价于 now = Student(arg)(now) ,即先执行 Student(arg) 获得类的实例对象,接着调用类的实例对象并传入一个函数。
      • 【注意】返回值是一个函数对象
      • 示例
        class Decorator(): def __init__(self, arg): self.__arg = arg def __call__(self, func): print('__call__') def wrapper(*args, **kwargs): print('wrapper') result = func(*args, **kwargs) print(self.__arg) return result return wrapper # 等价于 add = Decorator('Decorator')(add) @Decorator('Decorator') def add(a, b): print('add') return a+b print('This is Main') res = add(1, 3) print(res)
缓存装饰器
import functools from datetime import datetime def redis_cache(func): @functools.wraps(func) # 常见的用法是@wraps,这是因为在引入模块functools时直接导入了wraps def wrapper(*args, **kwargs): # 如果使用了位置参数,在args中按位置访问参数 start_date = args[0] end_date = args[1] # 如果使用了关键字参数,从kwargs字典中获取start_date和end_date: start_date = kwargs.get('start_date') end_date = kwargs.get('end_date') # 这里可以根据start_date和end_date进行一些操作,比如生成缓存键等 # 执行实际的函数调用并返回结果 return func(*args, **kwargs) return wrapper @redis_cache def get_data(start_date: datetime, end_date: datetime): # 实际函数实现 pass
计时装饰器
import time from functools import wraps def calculate_runtime(func): @wraps(func) def wrapper(*args, **kwargs): start_time = time.perf_counter() result = func(*args, **kwargs) end_time = time.perf_counter() print(f'{func.__name__} execute in {end_time - start_time}s') return result return wrapper
日志记录装饰器
import functools # 简单日志装饰器 def log(func): @functools.wraps(func) def wrapper(*args, **kwargs): print('call log') print(f'{func.__name__} start execute') result = func(*args, **kwargs) print('end log') return result return wrapper # 可带参数的日志装饰器 def log_of_parameter(arg): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f'call log:{arg}') print(f'{func.__name__} is execute') result = func(*args, **kwargs) print('end log') return result return wrapper return decorator # 自适应的日志装饰器,可带参数可不带参数 def log_enhance_mode(arg): if callable(arg): @functools.wraps(arg) def wrapper(*args, **kwargs): print('call log') print(f'{arg.__name__} is execute') result = arg(*args, **kwargs) print('end log') return result return wrapper def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f'call log:{arg}') print(f'{func.__name__} is execute') result = func(*args, **kwargs) print('end log') return result return wrapper return decorator
递归
  • 【主要思想】将复杂问题分解成基础问题来解决
  • 【主要方法】通过重复将问题分解为同类的子问题而解决问题
  • 递归函数的定义通常包含以下两个部分:
    • 递归条件: 指问题的终止条件,即问题无法再分解为子问题时该如何处理。
    • 递归步骤: 指如何将问题分解为子问题,以及如何解决子问题。
  • 优点是定义简单,逻辑清晰
  • 缺点是容易导致栈溢出。
    • 在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。
    • 由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。
解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。
  • 尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
  • 具体来说:
    • import time import sys def cal_runtime(func, n): start_time = time.perf_counter() result = func(n) end_time = time.perf_counter() print(f"函数运行时间:{end_time - start_time}秒") def fact(n): if n == 1: return 1 return n * fact(n-1) def fact_1(n): return item(n, 1) def item(num, product): if num == 1: return product return item(num-1, num*product) if __name__ == '__main__': # python的递归深度默认为1000,超过1000层会报错RecursionError: maximum recursion depth exceeded while calling a Python object sys.setrecursionlimit(10000) # 设置python的递归深度 a = cal_runtime(fact, 3000) b = cal_runtime(fact_1, 3000)
  • 【注意】:python标准并没有做尾递归优化,所以使用了尾递归优化的递归函数相比用于未优化的递归函数并没有什么优势,甚至尾递归花销更大
常见递归问题
  • 常见的递归优化
    • 动态规划(Dynamic Programming, DP)
      动态规划是一种算法设计技巧,常用于解决具有重叠子问题和最优子结构的问题。在这类问题中,可以通过将大问题分解为小问题来解决,并且小问题的解可以重复使用以解决更大的问题。
      关键特点:
      • 重叠子问题:一个问题的解可以由其子问题的解构成,并且子问题会被多次计算。
      • 最优子结构:问题的最优解包含了其子问题的最优解。
      • 记忆化:通常通过存储已经计算过的子问题的结果,在后续需要时直接查找,避免了重复计算,这个存储的过程也称之为记忆化或缓存。
      • 状态转移方程:描述了问题的各个状态是如何从一个或多个其他状态转换而来。
      状态压缩(State Compression)
      • 状态压缩是一种优化动态规划空间复杂度的技术,它通过合理地利用数据表示方式,减少存储多余信息的需求。在某些情况下,DP 表格中的当前状态只依赖于前一个或几个状态,不必存储整个DP表。
      • 例如,如果在计算斐波那契数列的过程中,我们发现当前的状态仅依赖于前两个状态,那么就没有必要存储整个序列,只需要存储前两个数即可。
      • 状态压缩通常意味着使用变量代替数组
      • 在某些情况下,状态压缩可能使用位操作来存储状态,这在处理子集、排列等组合问题时尤其有用。例如,使用位掩码来表示集合的子集,可以有效地执行集合操作。
切片
在Python中,切片操作允许你获取序列的一个子集。切片的语法是 sequence[start:stop:step] ,其中 start 是切片开始的索引,stop 是切片结束的索引,而 step 是步长。如果省略 start 和 stop,默认会使用序列的起始和终止位置。如果 step 是负数,切片将会逆序取值。
  • 所有的sequence都支持slicing,为什么不包括最后一项,排除历史原因,主要是因为sequence都是从0开始索引,所以不包括最后一个item,有几个好处(如下):
    • 能方便地知道 range 生成的 list 和 slice 截取的长度
      • lis = range(9) # 长度为9 lis[:3] # 长度为3
    • 能方便地算出slicing后的sequence长度
      • lis[3:9] # 长度为 9 - 3 = 6
  • 支持切片操作的类型
      1. 字符串(str)
        1. s = "Hello, World!" slice_s = s[1:5]# 结果是 "ello"
      1. 列表(list)
        1. lst = [1, 2, 3, 4, 5] slice_lst = lst[1:4]# 结果是 [2, 3, 4]
      1. 元组(tuple)
        1. tpl = (1, 2, 3, 4, 5) slice_tpl = tpl[1:4]# 结果是 (2, 3, 4)
      1. 字节串(bytes)
        1. b = b'Hello, World!' slice_b = b[1:5]# 结果是 b'ello'
      1. 字节数组(bytearray)
        1. ba = bytearray(b'Hello, World!') slice_ba = ba[1:5]# 结果是 bytearray(b'ello')
      1. 内存视图(memoryview)
        1. mv = memoryview(b'Hello, World!') slice_mv = mv[1:5]# 结果是 <memory at 0x...>
      1. Numpy 数组(如果你使用 Numpy 库的话)
        1. import numpy as np arr = np.array([1, 2, 3, 4, 5]) slice_arr = arr[1:4]# 结果是 array([2, 3, 4])
  • 对于可切片对象S, S[0] 和 S[:1] 的行为是不同的
    • S[0] :表示取索引0的元素,当对象S为空时,会引发异常报错IndexError
    • S[:1] :表示切片操作,当对象为空时,切片操作会返回一个空对象,不会引发异常
  • 逆序用法
    • [-1] :获取最后一个元素
    • [:-1] :除了最后一个元素,获取其他所有的元素;
    • [::-1]:对第一个到最后一个元素进行倒序之后取出;
    • [n::-1]:对第一个到第n+1个元素(从开始到索引为n(包括n)的元素)进行倒序后取出。
      例如[5::-1] ,它的含义如下:
      • start: 5 —— 切片从索引为5的地方开始(包括索引5)。
      • stop: 省略 —— 由于没有明确指定结束索引,并且步长为负,因此切片会一直进行到序列的开始。
      • step: -1 —— 为负数表示反向取值,每次向前移动一个元素。
      a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] print(a[5::-1])# 输出: [5, 4, 3, 2, 1, 0]
      从列表 a 的第5个元素 5 开始,以 -1 的步长反向切片,直到列表的起始位置。
  • 切片操作会返回一个新的序列对象
    • 列表切片——创建并返回一个新的列表对象
    • 元组切片——始终创建并返回一个新的元组对象
    • 字符串切片——始终创建并返回一个新的字符串对象
解包(拆包)
  • 在Python中,*可以用于以下两种场景:
    • 列表、元组、字符串等可迭代对象的解包。在解包操作中,*表示剩余的元素。
    • 函数的可变参数。
  • 函数参数调用的解包
    • 在参数前使用 * 和 ** 的确有解包(unpacking)的作用。
    • :用于解包列表、元组或任何可迭代对象,将其中的每个元素都作为单独的位置参数传递给函数。
    • *:用于解包字典,将字典中的每个键值对作为单独的关键字参数传递给函数。
    • 在你提供的代码示例中:
      def test(arg1, arg2, arg3, arg4, arg5): print(arg1, arg2, arg3, arg4, arg5) arg_list = [1, 3, 5] kwarg_dict = {'arg4': 2, 'arg5': 4} test(*arg_list, **kwarg_dict)
      这里 *arg_list 会将列表 arg_list 中的元素解包为位置参数,相当于传递了 1, 3, 5 给函数 test。而 **kwarg_dict 会将字典 kwarg_dict 中的键值对解包成关键字参数,相当于传递了 arg4=2, arg5=4 给函数 test。最终 test 函数接收到的参数就好像直接调用了 test(1, 3, 5, arg4=2, arg5=4)
  • 一般是将对象中的元素按照顺序依次赋值给多个变量,主要针对的是序列(sequence)对象或者可迭代(iterable)对象
在 Python 中,解包(unpacking)一般适用于任何序列(sequence)类型的对象或可迭代(iterable)类型的对象。常见的对象类型包括:
  1. 元组 (Tuple): 元组是不可变序列,经常用于解包操作。
    1. python复制代码 a, b = (1, 2)
  1. 列表 (List): 列表是可变序列,同样可以进行解包。
    1. python复制代码 a, b = [1, 2]
  1. 字符串 (String): 字符串虽然是文本序列,但每个字符都可以单独解包。
    1. python复制代码 a, b, c = "ABC"
  1. 集合 (Set): 集合是无序的可迭代对象,也可以进行解包,但由于是无序的,实际应用中较少这么做。
    1. python复制代码 a, b = {1, 2}
  1. 字典 (Dictionary): 解包一个字典时,默认情况下只会解包其键(keys),但也可以通过调用 .items()、.values() 或 .keys() 方法来解包键值对、值或键。
    1. python复制代码 key, value = {'a': 1, 'b': 2}.popitem()# 解包最后一个键值对
  1. 生成器 (Generator): 生成器也是可迭代对象,可以进行解包。
    1. python复制代码 a, b = (x for x in range(2))
使用解包时需要注意,解包的变量数量必须与序列中的元素数量匹配,否则会引发 ValueError。可以通过下面两种方式来避免这种错误:
  1. 对于序列解包:
      • 先对序列进行切片,得到新的序列,再对新的序列进行解包操作
  1. Python 3 引入了扩展的可变长度解包,可以使用星号 * 来捕获多余的元素。
    1. 例如:
      python复制代码 first, *middle, last = [1, 2, 3, 4, 5]# first = 1, middle = [2, 3, 4], last = 5
      这里 middle 变量会捕获列表中除了第一个和最后一个之外的所有元素。
推导式(生成式)
列表推导式 [ a for i in b ] ,生成一个新的列表
执行过程
  1. 进入可迭代对象 b。
  1. 从 b 中取出一个元素,命名为 i。
  1. 执行表达式 a,其中 a 通常依赖于 i。
  1. 收集表达式 a 的结果,并添加到新列表中。
  1. 重复上述步骤,直到 b 中所有的元素都被处理完毕。
  1. 返回最终收集到的结果列表。
  • 【简单理解】
    • 从可迭代对象 b 中取出一个元素 i 就执行一次表达式 a 并添加到列表中
    • 取多少次就执行多少次表达式 a
    • a 可以与 i 有关,也可以与 i 无关
实例代码
# 表达式依赖取出的元素 list1 = [["_"] * 3 for i in range(3)] # [["_", "_", "_"], ["_", "_", "_"], ["_", "_", "X"]] # 解释 # 表达式分两部分:列表推导([["_"] * 3 for i in range(3)])和 range 函数。 # step1:range(3) # step2:["_"] * 3 # step3:列表推导 # 对 range(3) 生成的每个数字都执行一次 ["_"] * 3 操作,创建了三个分开的列表。每次循环产生的列表都是独立的一个新列表。所以当你更改其中一个子列表时,其余的子列表不受影响,因为它们各自都有自己的内存空间。 # 备注:列表中的每一个小列表["_", "_", "_"]都是独立创建的,即它们在内存中有自己的地址块,并非是相互引用 # 表达式与取出的元素无关 squares = [x**2 for x in range(5)] # [0, 1, 4, 9, 16] # step1:range(5) # step2:x**2 # step3:列表推导 ## 对比结果 list1 = [["_"] * 3 for i in range(3)] list2 = [["_"] * 3] * 3 list3 = ["a"]*3 # list1 = [["_", "_", "_"], ["_", "_", "_"], ["_", "_", "_"]] # list2 = [["_", "_", "_"], ["_", "_", "_"], ["_", "_", "_"]] # lsit3 = ["a", "a", "a"] ## 修改list list1[2][2] = "Y" list2[2][2] = "Y" list3[2] = "b" # list1 = [["_", "_", "_"], ["_", "_", "_"], ["_", "_", "Y"]] # list2 = [["_", "_", "Y"], ["_", "_", "Y"], ["_", "_", "Y"]] # list3 = ["a", "a", "b"] ## 原因 ## list1 # 使用使用["_"] * 3 创建了一个包含三个指向相同字符串 "_" 的引用的列表["_", "_", "_"] # 接着通过列表推导式创建了三次 ["_", "_", "_"],每一次都是独立创建,它们有自己的内存空间 # 所以修改一个子列表中的元素并不影响其他的子列表 ## list2 # 使用使用["_"] * 3 创建了一个包含三个指向相同字符串 "_" 的引用的列表["_", "_", "_"] # 然后通过[...] * 3 又对这个新创建的列表进行了3次引用,即结果列表中的三个子列表都指向这个新创建的列表的内存空间, # 所以修改其中一个子列表中的元素,其他的子列表也会镜像修改 ## list3 # 使用["a"] * 3 创建了一个包含三个指向相同字符串 "a" 的引用的列表。 # 由于字符串是不可变的,故在尝试修改 list3 中的任何一个元素时,比如通过 list3[2] = "b",实际上是在该位置创建了一个新的字符串 "b" 的引用,而不是改变原有的字符串 "a"。 # 这就是为什么改变 list3[0] 不会影响 list3[1] 和 list3[2]。
 
变化
  • python 2.x版本中,列表推导式中的临时变量会污染全局的同名变量
  • python 3.x版本中,列表推导式有自己的作用域,即便外部存在同名变量,也不会混淆
 
集合推导式 { number for number in numbers},生成一个新的集合(无序,元素具有唯一性)
numbers = [1, 2, 2, 3, 4, 5, 5, 6, 6, 7, 8, 8, 9] distinct_numbers = {number for number in numbers} # {1, 2, 3, 4, 5, 6, 7, 8, 9}
  • 注意:在一个生成式中,for前面的if ... else是表达式,而for后面的if是过滤条件,不能带else
生成器
  • 生成器(generator)是一种特殊的迭代器,它在每次迭代时才会生成下一个值。生成器可以提高程序的效率,因为它只会在需要时才生成数据。
  • 获取生成器的值
    • 使用next(),每调用一次next()方法,就返回一个生成器的值
    • for
  • 创建生成器
    • 方法一:将列表生成式的 [] 改为 () ,得到的结果即为generator
      colors = ["red", "blue"] sizes = ["M", "S"] for tshirt in ("{color} {size}".format(color=color, size=size) for color in colors for size in sizes): print(tshirt)
      • 在生成器表达式 ("{color} {size}".format(color=color, size=size) for color in colors for size in sizes) 中,我们可以把两个 for 循环看作是嵌套的。这里的 "外部循环" 和 "内部循环" 的概念用于描述循环的嵌套结构,它们决定了组合元素生成的顺序。
      • 当我们说 “对于 colors 中的每种颜色,内部循环都会为 sizes 中的每个尺寸生成一个字符串” 时,我们实际上是在描述循环的顺序。第一个(或最左边)的 for 循环是“最外层”的循环,它设定了迭代的主要步骤,而接下来的 for 循环则是嵌套于主循环之中的“内部”循环。
      具体到这个例子中:
      • "for color in colors" 是外部循环,它遍历 colors 列表。
      • "for size in sizes" 是内部循环,它在外部循环的每一次迭代中完整地遍历 sizes 列表。
      • 所谓的外部循环并不是由它在代码中位置决定的,而是由它在逻辑上如何控制迭代顺序决定的。在这个生成器表达式中,外部循环(color)设置了主迭代轴,然后对于每种颜色,内部循环(size)会针对每个尺码运行,从而形成颜色和尺寸的所有可能组合。换句话说,"外部"与"内部"这两个词用于描述嵌套循环的执行顺序。在这个例子中,颜色的改变发生得更缓慢,每种颜色都会与所有的尺码组合一遍,然后颜色变化,再与所有尺码组合,如此往复直到完成所有颜色的遍历。
      方法二:定义generator函数,调用generator函数将返回一个generator
      def odd(): print('step 1') yield 1 print('step 2') yield(3) print('step 3') yield(5) # 正确调用 o = odd() # o 为generator对象 next(o) # step 1 # 1 next(o) # step 2 # 2 # 错误调用 # 每调用一次 odd() 方法就生成一个新的 generator 对象,多次调用会创建多个相互独立的 generator 对象 # 故每次都返回一样的内容 next(odd()) # step 1 # 1 next(odd()) # step 1 # 1
      【练习】打印杨辉三角
      杨辉三角定义如下: 1 / \ 1 1 / \ / \ 1 2 1 / \ / \ / \ 1 3 3 1 / \ / \ / \ / \ 1 4 6 4 1 / \ / \ / \ / \ / \ 1 5 10 10 5 1 把每一行看做一个list,试写一个generator,不断输出下一行的list def triangles(): L = [1] while True: yield L # 返回当前行数据 L = [0] + L + [0] L = [L[f]+L[f+1] for f in range(len(L)-1)] # def triangles(): # row = [1] # while True: # yield row # 返回当前行数据 # row = [x + y for x, y in zip([0] + row, row + [0])] n = 0 results = [] for t in triangles(): results.append(t) n = n + 1 if n == 10: break for t in results: print(t) if results == [ [1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1], [1, 5, 10, 10, 5, 1], [1, 6, 15, 20, 15, 6, 1], [1, 7, 21, 35, 35, 21, 7, 1], [1, 8, 28, 56, 70, 56, 28, 8, 1], [1, 9, 36, 84, 126, 126, 84, 36, 9, 1] ]: print('测试通过!') else: print('测试失败!')
       
迭代器
1、解释说明
  • 可迭代对象( Iterable)
    • 能够使用 for 循环遍历的有两类:一类是集合类型数据,比如: list 、 dict、 tuple、 set、 str等;一类是 generator ,包括生成器和带 yield 的generator function。
    • 这种可直接使用 for 循环遍历的对象称为可迭代对象(Iterable)
  • 迭代器
    • 可以被 next() 调用不断返回下一个值的对象称为迭代器(Iterator)
    • 迭代器是一个惰性序列,即请求一次计算一次结果
    • 生成器都是迭代器,但是集合类型数据(如list,str,dict等)是可迭代对象却不是迭代器,但可以通过 iter() 方法将可迭代对象转化为迭代器
  • 可使用 isinstance 方法来判断一个对象是否是 iterable以及 Iteator
    • from collections.abc import Iterable, Iterator result1 = isinstance(obj, Iterable) result2 = isinstance(obj, Iterator)
2、使用示例
在Python中,迭代器是一个可以遍历或迭代的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。迭代器有两个基本的方法:__iter__() 和 __next__()。__iter__() 方法返回迭代器对象本身。__next__() 方法返回下一个值。当没有更多的元素时,__next__() 会抛出 StopIteration 异常。
# 创建一个列表 my_list = [1, 2, 3, 4, 5] # 获取列表的迭代器 my_iterator = iter(my_list) # 使用 next() 函数输出迭代器的结果print(next(my_iterator))# 输出:1print(next(my_iterator))# 输出:2print(next(my_iterator))# 输出:3print(next(my_iterator))# 输出:4print(next(my_iterator))# 输出:5# 当迭代器中没有更多元素时,再次调用 next() 函数会抛出 StopIteration 异常print(next(my_iterator))# 抛出 StopIteration 异常
3、注意事项
  • 在使用迭代器时,需要确保迭代器对象是可迭代的,可以通过 iter() 函数创建迭代器对象。
  • 在迭代过程中,如果迭代器中没有更多元素,需要捕获 StopIteration 异常,避免程序崩溃。
并发编程:线程、进程与协程
基本概念概述
  • 线程是计算机可以被CPU调度的最小单元(即最小执行单元)
  • 进程是计算机资源分配的最小单元,一个进程可以有多个线程,这些线程共享这个进程的资源
  • 线程干活,进程为线程提供资源,他们之间的关系可类比为:
    • 一个工厂,至少拥有一个车间,并且车间至少拥有一名工人,最后是工人在干活
    • 一个程序,至少拥有一个进程,并且进程中至少拥有一个线程,最后是线程在干活
  • 一般开发的程序,都是以串行的形式执行,排队逐一执行,前面没完成,后面就不能继续
  • 多进程比多线程的开销大
  • 并发与并行
    • 并发:同一时间段内执行多个任务,但同一时刻只能执行一个任务,是虚假的多任务。CPU通过“时间片流转”、“优先级调度”等策略实现并发,让人觉得好像同一时刻有多个任务在执行
    • 并行:同一时刻可以执行多个任务,是真正的多任务
    • 无论是并行还是并发,在用户眼里看起来都是同时运行的,不管是线程还是进程,都只是一个任务,真正干活的CPU,而同一个CPU在同一时刻只能执行一个任务。
  • 线程安全
    • 当多个线程同时访问某个方法时,不管系统是交替还是按照什么规则安排这些线程去调用这个方法,我们在主程序中不用做任何的同步,这个类的结果行为都是我们设想的正确行为,我们称这个类是线程安全的
  • 耦合、内聚
    • 一个软件是由多个程序组成的,一个程序是由多个模块组成的
    • 耦合:描绘各个程序之间的相互依赖程度
    • 内聚:描述程序内部各个模块之间的相互依赖程度
GIL锁
  • 全局解释器锁(global interpreter lock),是CPython解释器特有的一个属性,让一个进程中同一时刻只有一个线程可以被CPU调度
    • notion imagenotion image
  • 利用CPU多核优势,使用多进程
    • notion imagenotion image
  • 如何合理使用多线程与多进程
    • 常见开发程序中,计算操作需要使用CPU多核优势,IO操作不需要利用CPU的多核优势
    • 计算密集型,用多进程,如:大量的数据计算【累加计算示例】
    • IO密集型,用多线程,如:文件读写、网络数据传输【下载视频示例】
关于进程的一些问题解答
  • multiprocessing模块的主要用途是创建新的进程,并且提供了Process类以及Pool类来分别管理单个进程和进程池。 还需要注意的是,这个模块也提供了支持进程间通信的工具,如队列(Queue)和管道(Pipe),以及同步原语,比如锁(Lock)。
  • 【创建新进程】
    • 首先导入multiprocessing,然后使用Process类并设置其target参数为想要执行的函数和args参数为传递给该函数的参数。接着使用start()来启动进程,使用join()等待进程完成。这说明你掌握了进程创建的流程。👌 需要注意的是,尽管join()可以阻塞调用它的程序直到进程的任务结束,但它不是必须的。如果不调用join(),主进程会继续执行其他任务,而子进程则在后台运行。
  • 【在Windows平台上需要在if __name__ == '__main__':保护代码块内创建进程的原因】
    • 确保了只有当模块作为主程序运行时才会执行进程创建代码,避免了无限创建新进程的可能性。Windows平台中python创建新进程采用spawn的方式,表示创建的新进程会导入当前运行程序所在的模块,不在保护代码块内创建则会导致新进程也会创建新进程,导致无限创建新进程的现象。
  • GIL影响的是多线程执行,它限制了同一时刻只有一个线程可以执行Python字节码。因此,对于CPU密集型任务,多线程在一个Python进程中并不会带来太大的性能提升,因为线程会因为GIL而无法真正并行执行。相反,多进程没有这个限制,因为每个进程有自己的解释器和内存空间,所以GIL不会影响到进程之间的并行执行
  • 进程间通信(IPC)
    • 进程间通信(Inter-Process Communication,简称IPC)是指在不同进程之间传递数据或信号的一些方法。Python的multiprocessing模块提供了几种实现IPC的方法,其中包括:
      1. Queues(队列):
          • Queue是线程和进程安全的,可以用来从一个进程向另一个进程发送对象。
          • 使用方法类似于标准库中的队列,提供FIFO(先进先出)的数据结构。
      1. Pipes(管道):
          • Pipe方法返回一对连接对象,代表管道的两端。
          • 每个连接对象都有send()和recv()方法,可用于在进程之间传输信息。
      除了队列和管道,还有其他几种IPC机制,例如共享内存、信号量(Semaphores)、事件(Events)和条件变量(Condition),但队列和管道是最常用的方法。
      其中,共享状态可以通过使用共享内存(例如Value或Array)来实现,也可以使用服务器进程(Manager Server Process)来实现,它允许不同进程共享Python对象
进程 & 多进程
  • 查看进程号
    • os.getpid() 查看当前进程号
    • os.getppid() 查看当前进程的父进程的进程号
创建进程
Linux创建进程
  • os.fork()
import os print('Process (%s) start...' % os.getpid()) # Only works on Unix/Linux/Mac: pid = os.fork() if pid == 0: print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid())) else: print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
当你在一个Linux系统上执行包含os.fork()的Python程序时,下面是详细的执行过程:
  1. 程序开始执行,并且由单个进程(父进程)运行。程序首先打印出父进程的PID。
python复制代码 print('Process (%s) start...' % os.getpid())
  1. 当父进程到达os.fork()调用时,操作系统创建了一个新的子进程。
      • 子进程是父进程的副本,拥有一份相同的内存图像和执行状态。
      • fork()函数在父进程中返回子进程的PID,而在子进程中返回0。
  1. 此时,父进程和子进程将并发执行,因为操作系统的调度器会分别给它们CPU时间。
  1. 在父进程中:
      • os.fork()返回子进程的非零PID。
      • 因此,if条件(pid == 0)不满足,执行流进入else分支。
      • 父进程打印出它自己的PID以及刚刚创建的子进程的PID。
python复制代码 else: print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
  1. 在子进程中:
      • os.fork()返回0。
      • 所以,if条件 (pid == 0) 满足,执行流进入if分支。
      • 子进程打印出它自己的PID和父进程的PID。
python复制代码 if pid == 0: print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
  1. 最终,你在屏幕上看到的输出顺序取决于操作系统调度父进程和子进程的顺序,这可能会有所不同。但通常,父进程和子进程几乎同时执行,所以你通常会看到如下输出:
复制代码 Process (876) start... I (876) just created a child process (877). I am child process (877) and my parent is 876.
os.fork()执行过程相关
  1. 子进程的创建:当执行os.fork()时,确实会创建一个新的子进程,这个新进程是父进程的副本。它们共享代码段,但数据段和堆栈段会复制给子进程。
  1. 并发执行:子进程将从fork()调用之后的地方开始执行,就像父进程一样。如果系统是多核心的,父进程和子进程有可能同时在不同的核心上运行,但这完全取决于操作系统的调度器如何安排这两个进程的执行。即使在单核心系统中,由于时间分片机制,父进程和子进程也会看似同时运行。
  1. 输出顺序:打印到屏幕的输出先后顺序并不是固定的,因为它依赖于操作系统调度器的具体决策和调度策略。操作系统可能会先调度父进程继续执行,也可能会先调度子进程。因此,哪个进程的消息先打印出来是不确定的。
  1. 运行在核心上:您提到的“系统会在另一个核心中以当前进度运行子进程”这一点需要注意,因为操作系统的调度器并不总是将父子进程分配到不同的核心上。调度器可能会根据系统当前的负载情况和调度策略来决定如何分配CPU资源。在多核心系统上,父进程和子进程可以并行运行在不同的核心上;在单核心系统上,它们将共享同一个CPU核心,并通过操作系统调度器的时间分片机制轮流运行。
当os.fork()被调用时,操作系统使用的是称为“写时复制”(copy-on-write)的技术来处理内存。在Unix-like系统上,fork()系统调用通常不会立即复制整个父进程的内存空间给子进程,而是采用更高效的方法:
  1. 共享代码段:在大多数现代操作系统中,代码是只读的,并且可以被同一个程序的所有进程所共享。所以,父进程和子进程实际上共享相同的物理代码段内存。这种共享是安全的,因为代码是不会被更改的。
  1. 数据段和堆栈段:对于父进程的数据段和堆栈段,在初次进行fork()时,并不会执行完全的物理拷贝。相反,操作系统将这些内存区域标记为“写时复制”。这意味着父进程和子进程在内存层面上看起来好像有各自独立的副本,但实际上它们都指向相同的物理内存页面。只有当其中一个进程试图修改这些页面中的内容时,操作系统才会创建一份真正的物理副本给那个进程,以确保其他进程的内存视图不受影响。
  1. 编译的代码:当你运行一个Python程序时,.py源代码文件首先被解释器编译成字节码,这在Python中是.pyc文件。当你使用os.fork()时,正在运行的程序已经是字节码在Python虚拟机上执行。因此,子进程获得的是父进程内存中已经编译的字节码副本,而不需要重新编译源代码。
在这个过程中,并没有重新编译代码段——无论是在硬盘上的源代码文件还是内存中的字节码或已经加载到内存并正在执行的机器代码。子进程继承了父进程内存中的代码状态,并从fork()调用后的点开始执行。由于写时复制策略,这使得fork()在内存使用方面非常高效,因为只有在必要的情况下才会发生实际的内存复制。
multiprocessing.Process
创建进程的类:Process([group [, target [, name [, args [, kwargs]]]]])
在 Python 的 multiprocessing 库中,Process 类用于创建一个新的进程。Process 构造函数接受以下参数:
  • group: 这个参数是保留的,用于将来扩展,并且目前不使用。它的默认值是 None,你应该也把它留作 None。
  • target: 这个参数指定了一个可调用对象(函数或方法),将在这个新建的子进程中运行。
  • name: 可以给每个进程对象指定一个名称,主要用于识别目的。如果不指定,则系统会自动分配一个唯一的名称。
  • args: 这是一个元组,里面包含传递给 target 函数的参数。
  • kwargs: 这是一个字典,包含以关键字方式传递给 target 函数的参数。
参数之所以这样展示,是因为它们是按照其在构造函数中出现的位置排序的,其中后面的参数都有默认值,是可选的。
不过,上述格式实际上并不标准,通常情况下参数列表会被更清晰地表示,像这样:
Process(group=None, target=None, name=None, args=(), kwargs={})
这里没有多层嵌套的意思,只是表明每个参数有默认值,如果调用时不提供这些参数,它们会自动设置为这些默认值。
创建一个 Process 对象的例子如下:
from multiprocessing import Process def my_function(arg1, arg2): print(f"arg1: {arg1}, arg2: {arg2}") # 创建 Process 实例 p = Process(target=my_function, args=('hello', 'world')) # 启动进程 p.start() # 等待进程结束 p.join()
在这个例子中,我们创建了一个进程,它会执行 my_function 函数,传递 'hello' 和 'world' 作为参数。可以看到 args 接受一个元组,kwargs(如果需要)将接受一个字典。
类的方法
假设p = Process()
  • p.start():启动进程
  • p.run():进程启动时运行的方法,用于调用target来指定需要执行的函数
  • p.terminate():强制终止进程p,并不会做任何的清理操作,如果p下还创建了子进程,那么这个子进程并没有父进程处理,这个子进程就称之为僵尸进程。
  • p.is_alive():判断p是否还在运行,如果还在运行则返回True
  • p.join([timeout]):主线程等待p终止,也可以理解为回收计算机资源,值得注意的是,主线程从始至终处于等待的状态,而p是处于运行的状态,join只能作用于start的进程,而不能作用于run的进程
注意 p.join()只能作用start的进程,不能作用于run的进程 ,【原因】p.start() ,Python会安排底层系统创建一个新的进程,在这个进程中去调用执行p.run();p.run()则相当于函数调用,在当前线程执行
在 Python 的 multiprocessing 模块中,join() 方法是用来阻塞当前上下文中的线程直到调用它的那个进程终止。这里有两个方法需要区分:start() 和 run()。
  • start(): 这个方法用于启动 Process 实例(创建并开始执行一个新的进程)。当你调用 start() 方法时,Python 会安排底层操作系统创建一个新的进程,并且在这个新进程内部去调用 run() 方法。
  • run(): 这个方法定义了进程运行时实际执行的代码。通常情况下,run() 方法由 start() 方法间接地调用,而不是直接被用户代码调用。
当文档或某些资源提到 "join() 只能作用于 start() 的进程,而不能作用于 run() 的进程",它实际上意味着:
  • 你只能对已经通过 start() 方法启动的进程使用 join() 方法。也就是说,你只能对已经运行在自己独立执行流中的进程进行 "join" 操作。
  • 如果你仅仅调用了 run() 而不是 start(),那么你实际上没有创建一个新的并发执行流(即没有创建一个新的进程),而是在当前的执行流(当前的进程)中同步地执行了 run() 方法里的代码。因此,没有必要也无法对这种情况下的 run() "join",因为它只是当前进程中的一个普通函数调用。
下面是创建和启动进程并使用 join() 的示例:
python复制代码 from multiprocessing import Process def task(): print("Task is running") # 创建进程实例 p = Process(target=task) # 启动进程 p.start() # 阻塞等待进程结束 p.join()
在这个例子中,调用 p.start() 导致 task() 函数在一个新的进程中异步执行。调用 p.join() 使得主程序等待直到 p 进程结束。如果我们替换掉 p.start() 为 p.run(),那么 task() 将会在当前的进程中同步执行,并不会有任何新的进程生成;因此,在这种情况下使用 p.join() 是没有意义的,同时也是不允许的,会抛出异常。
类的属性
p.daemon:默认值为False,如果设置为True,则代表p为后台的守护进程,也就是前边提到的后台运行的守护进程等待用户与之发生交互。当p的父进程终止的时候,p也随之终止 ,并且设定为True之后是不能创建自己的新进程的,改设置必须在start之前设置
p.name:进程的名称
p.pid:进程的PID,和linux的PID类似
multiprocessing.Pool
在 Python 中,multiprocessing.Pool 类用于并行执行多个进程。这是通过将任务分配给进程池中的工作进程来实现的。以下是 multiprocessing.Pool 的一些主要使用方法和属性:

创建 Pool

python复制代码 from multiprocessing import Pool # 创建一个包含特定数量工作进程的进程池,默认大小是系统 CPU 核心数量 pool = Pool(processes=N)
这里,N 是进程池中进程的数量。如果不指定 processes,则默认值为 os.cpu_count() 的返回值。

方法

  • apply(func, args=(), kwds={}): 使用阻塞方式调用 func 函数,args 为函数参数的元组,kwds 为函数参数的字典。返回函数的结果。
  • apply_async(func, args=(), kwds={}, callback=None, error_callback=None): 异步版本的 apply() 方法。它返回一个 multiprocessing.pool.AsyncResult 对象,可以用来查询或获取结果。callback 参数是一个可调用对象,它将在任务完成时被调用,并且接收函数结果作为参数;error_callback 是当任务抛出异常时会被调用的可调用对象。
  • map(func, iterable, chunksize=None): 类似于内置的 map() 函数。它会将 iterable 集合中的每个元素应用到 func 函数上,将结果以列表形式返回。如果指定了 chunksize,则按照这个大小将 iterable 分成若干块进行提交,可以提高效率。
  • map_async(func, iterable, chunksize=None, callback=None, error_callback=None): map() 方法的异步版本。它返回一个 multiprocessing.pool.AsyncResult 对象。
  • imap(func, iterable, chunksize=1): 惰性版本的 map() 方法,返回一个迭代器而不是立即计算和返回整个列表。当有大量数据需要处理时,这个方法是很有用的,因为它可以开始产生结果而不必等待所有任务完成。
  • imap_unordered(func, iterable, chunksize=1): imap() 的一个变体,返回结果的顺序不保证和输入 iterable 相同。
  • starmap(func, iterable, chunksize=None): 和 map() 类似,但 iterable 必须产生一个可迭代的元组序列,每个元组都作为参数传递给 func。
  • starmap_async(func, iterable, chunksize=None, callback=None, error_callback=None): starmap() 的异步版本。
  • close(): 阻止更多的任务提交到池。已经提交的任务会继续执行。
  • terminate(): 立即停止所有工作进程,不再处理任何未完成的任务。应该在 close() 或 terminate() 后调用 join() 方法。
  • join(): 等待所有工作进程退出,必须在 close() 或 terminate() 之后调用。

属性

  • _state: 表示池的状态,如运行、关闭等。
  • _taskqueue: 一个队列,存放还未分配给工作进程的任务。
  • _result_handler: 一个线程,用于处理异步任务的结果。

示例

以下是一个使用 multiprocessing.Pool 的简单例子:
普通使用
from multiprocessing import Pool import os, time, random def long_time_task(name): print('Run task %s (%s)...' % (name, os.getpid())) start = time.time() time.sleep(random.random() * 3) end = time.time() print('Task %s runs %0.2f seconds.' % (name, (end - start))) if __name__=='__main__': print('Parent process %s.' % os.getpid()) p = Pool(4) for i in range(5): p.apply_async(long_time_task, args=(i,)) print('Waiting for all subprocesses done...') p.close() p.join() print('All subprocesses done.')
结合 with 语句使用
使用 with 语句来创建一个 multiprocessing.Pool 对象时,进程池会在 with 块的结束处自动关闭。这是因为 Pool 类实现了上下文管理器协议(__enter__ 和 __exit__ 特殊方法),允许它与 with 语句一起使用。这种用法被称为“上下文管理”,并且是处理资源的推荐方式,能确保资源像文件或网络连接以及进程池等在使用完成后被正确地清理。那么即便发生异常,也可以保证 __exit__ 方法被调用,进而调用 Pool 的 close 和 join 方法。在 with 块结束之后,__exit__ 方法会被触发,它会调用 close() 方法来防止新任务被提交到池中,并调用 join() 方法等待所有工作进程的退出。这避免了不小心忘记显式调用 close 和 join 方法的问题,使代码更加健壮和简洁:
from multiprocessing import Pool import os def f(x): return x*x if __name__ == '__main__': # 创建一个含有4个工作进程的进程池 with Pool(4) as p: # 输出当前进程的ID print("Current main process id:", os.getpid()) # map 方法批量执行函数 results = p.map(f, range(10)) print(results) # 使用 apply_async 异步执行函数 for i in range(10): result = p.apply_async(f, (i,)) print(result.get())# 输出单个结果 # 在这个块中可以自由地使用p # 一旦离开 'with' 块,进程池将自动关闭和等待所有进程结束,# 即隐式地执行了p.close() followed by p.join() # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] # 0 # 1 # 4 # 9 # 16 # 25 # 36 # 49 # 64 # 81
  • 请注意:
    • 在 Windows 和 macOS 平台上,由于 fork 方式的限制,必须在 if __name__ == '__main__': 代码块内创建和使用 Pool,否则可能导致程序挂起或其他问题。在 Linux 系统上,由于其默认使用 fork 来创建新进程,通常不需要这样做。
守护进程
在 Python 的 multiprocessing 模块中,守护进程(daemon process)是一种特殊类型的进程,其生命周期依赖于其创建者或父进程。当创建一个进程时,可以通过设置 daemon 属性为 True 来将其指定为守护进程。
守护进程的特点如下:
  1. 守护进程在后台运行并且其作用是提供服务或执行某些任务。
  1. 当主程序退出时,守护进程会自动终止,通常不需要显式地关闭。
  1. 守护进程通常不用于执行重要任务,因为它们可能在任意时间被强制终止。
  1. 守护进程不应该持有资源,如打开文件、数据库连接等,因为这些资源可能在未适当清理的情况下被释放。
  • 注意:主进程结束时,守护进程也会随之结束,非守护进程会执行完任务才结束,例如:
from multiprocessing import Process import time def game(name): print('%s 正在玩游戏。。。' % name) time.sleep(3) print('%s 玩完游戏了。。。' % name) def sing(name): print('%s 正在唱歌。。。' % name) time.sleep(3) print('%s 唱完歌了。。。' % name) if __name__ == '__main__': p1 = Process(target=game, args=('Chancey', ), daemon=True) p2 = Process(target=sing, args=('Chancey', )) p1.start() p2.start() print("进程一:", p1.name) print("进程二:", p2.name)
互斥锁
  • multiprocessing.Lock
  • lock = Lock() 获得锁对象
  • lock.acquire()上锁
  • lock.release()释放锁
多进程
利用多进程下载视频示例
import time import requests import multiprocessing url_ list = [ ( "东北F4模仿秀.mp4","https: //aweme . snssdk . com/ aweme/v1/playwm/ ?video_ id=v0300f570000bvbmace0gvch71o53oog"), ("卡特扣篮.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, ur1)) t.start()
利用多进程进行累加计算
进程间通信
线程 & 多线程
  • 多个线程的执行顺序默认有计算机决定(顺序不确定)
  • 主线程崩溃,主线程对应的所有的子线程都会崩溃
创建线程的两种方法
1. 使用 threading.Thread 模块
import time, threading # 新线程执行的代码: def loop(): print('thread %s is running...' % threading.current_thread().name) n = 0 while n < 5: n = n + 1 print('thread %s >>> %s' % (threading.current_thread().name, n)) time.sleep(1) print('thread %s ended.' % threading.current_thread().name) print('thread %s is running...' % threading.current_thread().name) t = threading.Thread(target=loop, name='LoopThread') t.start() t.join() print('thread %s ended.' % threading.current_thread().name) # thread MainThread is running... # thread LoopThread is running... # thread LoopThread >>> 1 # thread LoopThread >>> 2 # thread LoopThread >>> 3 # thread LoopThread >>> 4 # thread LoopThread >>> 5 # thread LoopThread ended. # thread MainThread ended.
  • 在调用 threading.Thread 时没有创建线程,只是实例化了一个 Thread 对象。
  • 在调用线程对象的 start() 方法是才会创建线程并让这个线程开始运行,调用join()方法会阻塞主线程直到当前子线程执行结束
  • threading.Thread 方法中的target参数要传入一个函数的地址(即函数名,如target=test,而test()是函数的调用)
  • Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread,子线程的名字在创建时指定,我们用LoopThread命名子线程。
2. 继承 threading.Thread 类,自定义一个线程类
import threading class MyThread(threading.Thread): def run(self): print("1") def main(): t = MyThread() t.start() if __name__ == "__main__": main()
  • 在自定义的线程类中必须继承 threading.Thread 类,且重写 run() 方法。原因是调用 start() 方法,,start() 方法内会调用 run() 方法去执行任务
ThreadLocal
  • 主要用于同一个线程下函数调用是传递局部变量
在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦,并且每个函数一层一层调用都这么传参数那还得了?用全局变量?也不行,因为每个线程处理不同的Student对象,不能共享。
defprocess_student(name): std = Student(name) # std是局部变量,但是每个函数都要用它,因此必须传进去: do_task_1(std) do_task_2(std) defdo_task_1(std): do_subtask_1(std) do_subtask_2(std) defdo_task_2(std): do_subtask_2(std) do_subtask_2(std)
使用一个全局dict存放所有的Student对象,然后以thread自身作为key获得线程对应的Student对象。这种方式理论上是可行的,它最大的优点是消除了std对象在每层函数中的传递问题,但是,每个函数获取std的代码有点丑。
global_dict = {} defstd_thread(name): std = Student(name) # 把std放到全局变量global_dict中: global_dict[threading.current_thread()] = std do_task_1() do_task_2() defdo_task_1(): # 不传入std,而是根据当前线程查找: std = global_dict[threading.current_thread()] ... defdo_task_2(): # 任何函数都可以查找出当前线程的std变量: std = global_dict[threading.current_thread()] ...
使用 ThreadLocal local = threading.loacl()
import threading # 创建全局ThreadLocal对象: local_school = threading.local() defprocess_student(): # 获取当前线程关联的student: std = local_school.student print('Hello, %s (in %s)' % (std, threading.current_thread().name)) defprocess_thread(name): # 绑定ThreadLocal的student: local_school.student = name process_student() t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A') t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B') t1.start() t2.start() t1.join() t2.join() # 输出 # Hello, Alice (in Thread-A) # Hello, Bob (in Thread-B)
全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。
可以理解为全局变量local_school是一个dict,不但可以用local_school.student,还可以绑定其他变量,如local_school.teacher等等。
ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
多线程下载时视频示例
import time import requests import threading url_ list = [ ("东北F4模仿秀. mp4","https://aweme.snssdk.com/aweme/v1/playwm/?video_ id=v0300f570000bvbmace0gvch71o53oog" ), ("卡特扣篮. 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()
协程
示例
import asyncio async def corotine_example(): await asyncio.sleep(1) print('task test') coro = corotine_example() loop = asyncio.get_event_loop() task = loop.create_task(coro) print('运行情况:', task) loop.run_until_complete(task) print('再看下运行情况:', task) loop.close() # 输出: # 运行情况: <Task pending name='Task-1' coro=<corotine_example() running at /home/fm02/python/test/task.py:2>> # task test # 再看下运行情况: <Task finished name='Task-1' coro=<corotine_example() done, defined at /home/fm02/python/test/task.py:2> result=None>
输出及解释
  • <Task pending name='Task-1' coro=<corotine_example() running at /home/fm02/python/test/task.py:2>>,表示 corotine_example() 协程对象已经被创建,但还没有执行
    • Task 表示这是一个异步任务
    • pending 表示这个协程当前处于挂起状态,尚未被事件循环调度
    • name=’…’ 表示这个协程的名字
    • coro=<corotine_example() running at /home/fm02/python/test/task.py:2 表示协程的运行状态,其中 running 表示协程正在运行
  • <Task finished name='Task-1' coro=<corotine_example() done, defined at /home/fm02/python/test/task.py:2> result=None>
    • finished 表示协程执行完成
    • done 表示协程运行完成
    • result=None 表示协程执行完成后返回结果None
正则表达式
表 3-2 常用的匹配规则
模  式
描  述
\w
匹配字母、数字及下划线
\W
匹配不是字母、数字及下划线的字符
\s
匹配任意空白字符,等价于 [\t\n\r\f]
\S
匹配任意非空字符
\d
匹配任意数字,等价于 [0-9]
\D
匹配任意非数字的字符
\A
匹配字符串开头
\Z
匹配字符串结尾,如果存在换行,只匹配到换行前的结束字符串
\z
匹配字符串结尾,如果存在换行,同时还会匹配换行符
\G
匹配最后匹配完成的位置
\n
匹配一个换行符
\t
匹配一个制表符
^
匹配一行字符串的开头
$
匹配一行字符串的结尾
.
匹配任意字符,除了换行符,当 re.DOTALL 标记被指定时,则可以匹配包括换行符的任意字符
[...]
用来表示一组字符,单独列出,比如 [amk] 匹配 a、m 或 k
[^...]
不在 [] 中的字符,比如 [^abc] 匹配除了 a、b、c 之外的字符
*
匹配 0 个或多个表达式
+
匹配 1 个或多个表达式
?
匹配 0 个或 1 个前面的正则表达式定义的片段,非贪婪方式
{n}
精确匹配 n 个前面的表达式
{n, m}
匹配 n 到 m 次由前面正则表达式定义的片段,贪婪方式
a
b
( )
匹配括号内的表达式,也表示一个组
表 3-3 匹配修饰符
修饰符
描  述
re.I
使匹配对大小写不敏感
re.L
做本地化识别(locale-aware)匹配
re.M
多行匹配,影响 ^ 和 $
re.S
使 . 匹配包括换行在内的所有字符
re.U
根据 Unicode 字符集解析字符。这个标志影响 \w、\W、\b 和 \B
re.X
该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解
匹配方法
  • re.match(pattern_str, str_content, 修饰符),从开始字符进行匹配,返回第一个匹配到的字符
  • re.search(pattern_str, str_content, 修饰符),从任意位置开始匹配,返回第一个匹配到的字符
  • re.findall(pattern_str, str_content, 修饰符),从任意位置开始匹配,返回所有匹配到的字符标记的组,其中每一个匹配结果标记的所有组放在一个元组,返回结果为列表包含元组
  • re.sub(pattern_str, 目标字符, str_content),将匹配到的字符全部替换为目标字符,返回替换后的字符
  • re.compile(pattern_str),将正则字符串编译为正则表达式对象,以便进行复用
import re content = 'Hello 1234567 World_This is a Regex Demo' # 匹配以字符串Hello开头(^Hello),后接一个空白字符(\s),三个数字(\d\d\d,等价于\d{3}),四个数字,一个空白字符,10个数字、字母或下划线 re.match('^Hello\s\d\d\d\d{4}\s\w{10}', content) # Hello 123 4567 World_This # 匹配以字符串Hello开头(^Hello),后接一个空白字符(\s),一个及以上的连续数字并标记为第一个组,一个空白字符,以及字符World # (\d+),括号表示标记了一个子表达式的开始和结束位置,被标记的每个子表达式会依次对应每一个分组,调用 group 方法传入分组的索引即可获取提取的结果。 re.match('^Hello\s(\d+)\sWorld', content) # Hello 1234567 World_This,第一个组1234567 # 匹配以字符串Hello开头(^Hello),后接通用匹配符(.*,尽可能匹配到多的字符,处于贪婪模式),以Demo结尾(Demo$) re.match('^Hello.*Demo$', content) # Hello 1234567 World_This is a Regex Demo # 匹配以字符串Hello开头(^Hello),后接通用匹配符(.*,但尽可能匹配到少的字符,处于非贪婪模式),一个及以上的数字并标记为一个组,贪婪通用匹配符,以Demo结尾(Demo$) # 非贪婪模式通用匹配符位于末尾位置则匹配不到任何内容 re.match('^He.*?(\d+).*Demo$', content) # Hello 1234567 World_This is a Regex Demo,第一个组1234567 # 转义匹配 content = '(百度) www.baidu.com' result = re.match('\(百度 \) www\.baidu\.com', content) # (百度) www.baidu.com # 替换字符 content = '54aK54yr5oiR54ix5L2g' content = re.sub('\d+', '', content) # aKyroiRixLg
python格式化输出
%
%s
s = 'hello' # %a.bs 表示先截取长度为b(从左向右截取),再设置输出长度为a # 若截取长度大于字符串长度,则取字符串本身;否则截取指定长度 # 若设置输出长度大于字符串长度,当 a>0 ,左侧以空格补齐输出;a<0,右侧以空格补齐输出。若输出长度小于字符串长度,则输出原字符串 output = 'String = %2s' % s # String = hello output = 'String = %7s' % s # String = **hello,以星号表示空格 output = 'String = %.2s' % s # String = he output = 'String = %7.4s' % s # String = ***hell output = 'String = %*.*s' % (7, 4 ,s) # String = ***hell,与 %7.2s 结果相同
%d
num = 14 # %a.bd 表示先补0后补空格,整数长度不够 b 时补0,够 b 但不够 a 时补空格 # %ad,表示整数长度大于 a 则显示原整数,否则左侧补空格 # %.bd,表示整数长度小于 b时,左侧补0,否则显示原整数 output = 'num = %1d' % num # num = 14, output = 'num = %3d' % num # num = *14,左侧补空格 output = 'num = %.1d' % num # num = 14, output = 'num = %03d' % num # num = 014,左侧补0 output = 'num = %.3d' % num # num = 014,效果同 %03d 和 %.00003d output = 'num = %.0003d' % num # num = 014,效果同 %03d 和 %.3d output = 'num = %*.*d' % (0,3,num) # num = 014,效果同 %.3d 、%03d 和 %.00003d
%f
f = 3.1415926535 # %a.bf,其中a表示浮点数长度,b表示浮点数精度(小数点后位数,默认6位) output = 'float = %f' % f # float = 3.141593 output = 'float = %.7f' % f # float = 3.1415927 output = 'float = %9f' % f # float = *3.141593 output = 'float = %10.3f' % f # float = *****3.142 output = 'float = %*.*f' % (10, 3, f) # float = *****3.142,效果同 %10.3f
format
# 默认 output = 'f = {}-{}-{}'.format('我', '是', '小明') # f = 我-是-小明 # 位置传入 output = 'f = {0}-{1}-{2}'.format('我', '是', '小明') # f = 我-是-小明,效果同上 output = 'f = {2}-{0}-{1}'.format('我', '是', '小明') # f = 小明-我-是 # 关键字传入 output = 'f = {who}-{am}-{name}'.format(who='我', am='是', name='小明') # f = 我-是-小明 output = 'f = {who}-{am}-{name}'.format(who='小明', am='是', name='我') # f = 小明-是-我
f-string
# 直接填写 output = f'name={"小明"}, age={18}' # name=小明, age=18 # 参数传入 name_ = '小明' age_ = 18 output = f'name={name_}, age={age_}' # name=小明, age=18 # 对齐方式 print(f'{"":15} | {"latitude":>9} | {"longitude":>9}') # 第一个设置宽度为15,整数格式化则对齐方式默认为左对齐 # 第二个和第三个设置宽度为9,显示设置对齐方式为右对齐 print(f'{name:15} | {lat:9.4f} | {lon:9.4f}') # 第一个同上 # lat和lon 变量设置宽度为9,保留4位小数(不足补0),浮点数格式化则对齐方式默认为右对齐(对于数字默认会向左侧填充空格)
python网络编程(socket编程)
原理架构图
notion imagenotion image
  • Python的socket编程,通常可分为TCP和UDP编程两种
    • TCP 是带连接的可靠传输服务,每次通信都要握手,结束传输也要挥手,数据会被检验,是使用最广的通用模式
    • UDP 是不带连接的传输服务,简单粗暴,不加控制和检查的一股脑将数据发送出去的方式,但是传输速度快,通常用于安全和可靠等级不高的业务场景,比如文件下载
TCP(面向 可靠连接 的 数据流式 协议)
HTTP 请求——直接使用 socket 库建立HTTP连接,进行发送和接收操作
import socket # 创建socket对象 # AF_INET 表示 IPv4,如果要用更先进的 IPv6,就指定为 AF_INET6 # SOCK_STREAM 表示使用面向流的 TCP 协议 # SOCK_DGRAM 表示使用 UDP 协议 s = socket.socket(socket.AF_INET, socket.STREAM) # 建立连接 # 传入一个地址元组,元组中HOST表示主机地址,PORT表示端口号 path = (HOST, PORT) s.connect(path) # 发送请求 request_header = b'GET / HTTP/1.1\r\nHOST: www.sina.com.cn\r\nConnection: close\r\n\r\n' s.send(request_header) # 接收响应内容 # 使用循环,以保证能够接受到全部的响应内容 response = b'' while True; data = s.recv(1024) # 每次最多接收 1k 字节 if not data: break response += data # 关闭连接 s.close() # 解析响应内容 header, html = response.decode('utf-8')
HTTPS 请求
import socket import ssl # 创建socket对象,并包装这个socket对象用于HTTPS连接 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) wrapped_socket = ssl.wrap_socket(s) HOST = 'www.sina.com.cn' path = '/' response = b'' try: # 建立 HTTPS 连接 wrapped_socket.connect(('www.sina.com.cn', 443)) # 构造请求头,并发送请求 request_header = f'GET {path} HTTP/1.1\r\nHOST: {HOST}\r\nConnection: close\r\nUser-Agnet: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\r\n\r\n' wrapped_socket.send(request_header.encode()) # 接收响应内容 # 使用循环,保证能够接受到全部的响应内容 while True: data = wrapped_socket.recv(4096) # 每次最多接收 4k 字节 if not data: break response += data finally: # 关闭连接 wrapped_socket.close() # 解析响应内容 res_text = response.decode('utf-8') res = res_text.split('\r\n\r\n', 1) header = res[0] html = res[1] if len(res) > 1 else '' print(header) with open('index.html', 'w', encoding='utf-8') as f: f.writelines(html)
客户端编程:主动连接服务端的IP和端口
  • 基本步骤
      1. 创建 socket 对象
        1. 如果是 HTTP 连接,则接下来可直接建立连接发送请求
        2. 如果是 HTTPS 连接,则接下来需要使用 ssl 库包装 socket 对象,再使用包装后的 socket 对象进行建立连接发送请求
      1. 建立连接
      1. 构造请求头,发送请求
      1. 接收响应内容
      1. 解析响应内容——获得响应头和响应主体
# client_socket.py # 普通的HTTP请求 import socket if __name__ =='__main__': s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) path = ('127.0.0.1', 9999) s.connect(path) print(s.recv(1024).decode('utf-8')) for data in [b'Michael', b'Tracy', b'Sarah']: s.send(data) print(s.recv(1024).decode('utf-8')) s.send(b'exit') s.close()
服务端编程:首先监听端口,其次对每一个新的连接,创建一个线程或进程来处理连接
  • 基本步骤
      1. 创建 socket 对象
      1. 绑定地址(主机地址和端口)
      1. 监听端口
        1. 接收数据
        2. 创建线程或进程处理数据
手动使用 socket 和 threading 模块实现多线程服务器
# server_socket.py import socket import threading import time import ssl def tcplick(sock, addr): print(f'Accept new connection from {addr}') sock.send(b'Welcome!') while True: data = sock.recv(1024) time.sleep(1) if not data or data.decode('utf-8') == 'exit': break sock.send(('Hello, {}'.format(data.decode('utf-8'))).encode('utf-8')) sock.close() print(f'Connection from {addr} closed') if __name__ == "__main__": # 创建普通的 socket 对象 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 绑定地址和端口 path = ('127.0.0.1', 9999) s.bind(path) # 监听端口,并设置等待连接的最大数量为 5 s.listen(5) print('Waiting for connection......') ## 对于HTTPS连接 ## 加载服务器的SSL密钥和证书 #context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) #context.load_cert_chain(certfile='server.crt', keyfile='server.key') while True: # 接受新的连接 client_socket, addr = s.accept() ## 对于HTTPS连接,需要对套接字进行包装以使用SSL # secure_sock = context.wrap_socket(client_socket, server_side=True) # 为每个客户端创建一个线程 t = threading.Thread(target=tcplick, args=(client_socket, addr)) t.start()
使用 python 的 socketserver 模块实现多线程服务器(生产环境下推荐使用此方案)
import socketserver class MyServer(socketserver.BaseRequestHandler): def handle(self): conn = self.request # self.request 对象封装了所有请求的数据 conn.sendall('欢迎访问socketserver服务器!'.encode('utf-8')) while True: data = conn.recv(1024).decode('utf-8') if data == 'exit': print('断开与{}的连接!'.format(self.client_address)) break print("来自{0}的客户端向你发来消息:{1}".format(self.client_address, data)) conn.sendall(('已收到你的消息<{}>'.format(data)).encode()) if __name__ == "__main__": # 创建一个多线程TCP服务器 ip_port = ('127.0.0.1', 9999) server = socketserver.ThreadingTCPServer(ip_port, MyServer) print('启动socketserver服务器!') server.serve_forever()
UDP(面向 无连接 的 数据报式 协议)
客户端编程
  • 基本步骤
      1. 创建 socket 对象
      1. 发送数据到指定地址
# UDP_client.py import socket HOST = '127.0.0.1' PORT = 9999 if __name__ == '__main__': s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for data in [b'Michael', b'Tracy', b'Sarah']: s.sendto(data, (HOST, PORT)) print(s.recv(1024).decode('utf-8')) s.close()
服务端编程
  • 基本步骤
      1. 创建socket对象
      1. 绑定端口号
      1. 监听端口,收发数据
# UDP_server.py import socket if __name__ == "__main__": # 创建 socket 对象 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 绑定主机地址和端口号 HOST = '127.0.0.1' PORT = 9999 s.bind((HOST, PORT)) # 监听端口,收发数据 while True: data, addr = s.recvfrom(1024) print('Receive from %s:%s' % addr) s.sendto(b'Hello, %s!'% data, addr)
socket 类型
socket类型
描述
socket.AF_UNIX
只能够用于单一的Unix系统进程间通信
socket.AF_INET
IPv4
socket.AF_INET6
IPv6
socket.SOCK_STREAM
流式socket , for TCP
socket.SOCK_DGRAM
数据报式socket , for UDP
socket.SOCK_RAW
原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。
socket.SOCK_SEQPACKET
可靠的连续数据包服务
创建TCP Socket:
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
创建UDP Socket:
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
socket 对象的方法
服务端方法
s.bind()
绑定地址(host,port)到套接字,在AF_INET下,以元组(host,port)的形式表示地址。
s.listen(backlog)
开始监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。
s.accept()
被动接受客户端连接,(阻塞式)等待连接的到来,并返回(conn,address)二元元组,其中conn是一个通信对象,可以用来接收和发送数据。address是连接客户端的地址。
客户端方法
s.connect(address)
客户端向服务端发起连接。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
s.connect_ex()
connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
公共方法
s.recv(bufsize)
接收数据,数据以bytes类型返回,bufsize指定要接收的最大数据量。
s.send()
发送数据。返回值是要发送的字节数量。
s.sendall()
完整发送数据。将数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
s.recvform()
接收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收的数据,address是发送数据的套接字地址。
s.sendto(data,address)
发送UDP数据,将数据data发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。
s.close()
关闭套接字,必须执行。
s.getpeername()
返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。
s.getsockname()
返回套接字自己的地址。通常是一个元组(ipaddr,port)
s.setsockopt(level,optname,value)
设置给定套接字选项的值。
s.getsockopt(level,optname[.buflen])
返回套接字选项的值。
s.settimeout(timeout)
设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())
s.gettimeout()
返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。
s.fileno()
返回套接字的文件描述符。
s.setblocking(flag)
如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常。
s.makefile()
创建一个与该套接字相关连的文件
Python I/O 编程
StringIO
from io import StringIO # 法一:创建一个StringIO对象,之后再写入内容 f = StringIO() f.write('Hello') f.write(' ') f.write('World!') print(f.getvalue()) # 法二:创建并使用一个str初始化StringIO对象 f = StringIO('Hello!\nHi!\nGoodBye!\n') print(f.getvalue())
BytesIO
from io import BytesIO # 法一:创建一个BytesIO对象,之后再写入内容 f = BytesIO() f.write('中文'.encode('utf-8')) print(f.getvalue()) # b'\xe4\xb8\xad\xe6\x96\x87' print(f.getvalue().decode('utf-8')) # 中文 # 法二:创建并使用一个bytes初始化BytesIO对象 f = BytesIO('中文'.encode('utf-8')) print(f.getvalue()) # b'\xe4\xb8\xad\xe6\x96\x87' print(f.getvalue().decode('utf-8')) # 中文 # BytesIO 对象的 write() 方法是从流(文件指针)的位置开始向该对象写入内容 f.write('你好'.encode('utf-8')) # 默认为覆盖式写入 print(f.getvalue().decode('utf-8')) # 你好 # 移动流的位置 f.seek(0, 2) # 0表示相对于末尾的偏移量,2表示从末尾开始计算 f.write('再见'.encode('utf-8')) print(f.getvalue().decode('utf-8')) # 你好再见
 
  • 两种IO文件对象在读取写入时,均是从文件流(文件指针)的位置开始进行操作,所以最好显示设置文件流的位置:
    • 读取时显示设置文件流的位置为文件起始位置 f.seek(0)
    • 写入时显示设置文件流的位置,如果覆盖式写入则设置为文件起始位置 f.seek(0) ,如果追加式写入则设置为文件末尾位置 f.seek(0, 2)
    • 查看文件流的位置 f.tell()
    • 设置文件流的位置 f.seek(0, 2) ,0表示相对于末尾的偏移量,2表示从末尾开始计算, 第一个offset: 偏移量,需要向前或向后的字节数,正为向后,负为向前;第二个whence: 可选值,默认为0,表示文件开头,1表示相对于当前的位置,2表示文件末尾
接口
  • 路由
  • 请求
  • 处理
常用测试
  • 一句话启动服务【python -m http.server 8000】在当前目录下启动一个Web服务器,并监听8000端口,python 2.x版本将 http.server 更换为 SimpleHTTPServer
流畅的Python
python面向对象编程
python框架获取值
  • self.get_argument(key, default_value)
  • self.request.files['bg_picture'][0] if 'bg_picture' in self.request.files.keys() else None
    • # self.request.files { 'bg_picture': [ { 'filename': '65af3f8c6b1d550e9808117d.jpg', 'body': b'…' }] }
       
 
python脚本
 
If you have any questions, please contact me.