IO操作
一、读
1、读文件
-
过程
a、找到文件
b、打开文件
c、读取文件的内容
d、关闭文件
-
找到文件
绝对路径:从根目录开始链接的路径
相对路径:不是从根目录开始链接的路径
path = r"file.txt"
-
打开文件
-
原型
def open(file, mode='r', buffering=None, encoding=None, errors=None, newline=None, closefd=True)
-
参数
file:要打开的文件的路径
mode:打开方式
encoding:编码格式
errors:错误处理方式(ignore表示直接忽略)
-
返回值
文件描述符,从当前的位置操作当前打开的文件
-
打开方式
方式 说明 r 以只读的方式打开文件,文件的引用(描述符)将会被放在文件开头 rb 以二进制格式打开只读文件,文件的引用(描述符)将会被放在文件开头 r+ 以读写的方式打开文件,文件的引用(描述符)将会被放在文件开头 w 以只写的方式打开文件,如果该文件存在,则将其内容覆盖,如果文件不存在则会创建该文件 wb 以二进制格式打开只写文件,如果该文件存在,则将其内容覆盖,如果文件不存在则会创建该文件 w+ 以读写的方式打开文件,如果该文件存在,则将其内容覆盖,如果文件不存在则会创建该文件 a 打开一个文件用于追加内容,如果该文件存在,文件描述符会被放到文件的末尾,如果文件不存在则会创建该文件 ab 打开一个文件用于追加写,如果文件存在,文件描述符将会放到文件末尾 不存在则创建 a+ 打开一个文件用于读写,如果该文件存在,文件描述符会被放到文件的末尾,如果文件不存在则会创建该文件 -
打开
打开普通文件
fp = open(path, "r")
打开二进制文件
fp = open(path, "rb")
指定编码格式
fp = open(path, "r", encoding="utf-8")
指定错误处理方式
fp = open(path, "r", encoding="utf-8", errors="ignore")
-
-
读取文件的内容
函数名称 函数说明 read() 读取文件的全部内容 readline() 读取整行内容 readlines 读取所有行并返回一个列表 seek(offset[,1whence]) 方法用于移动文件读取指针到指定位置whence:可选,默认值为 0。给offset参数一个定义,表示要从哪个位置开始偏移;0代表从文件开头开始算起,1代表从当前位置开始算起,2代表从文件末尾算起 next(file) 返回文件下一行 -
读取文件的全部内容
str1 = fp.read() print(str1)
-
读取指定字节数的内容
str2 = fp.read(4) print(str2)
-
读取整行内容(包括\n字符)
str3 = fp.readline() print("*"+str3+"*")
-
读取指定字节数内容
str4 = fp.readline(13) print("*"+str4+"*")
-
读取所有行并返回一个列表,列表中的元素是每行内容
list5 = fp.readlines() print(list5)
-
-
修改文件描述符的位置
str6 = fp.read(4) print("*"+str6+"*") str7 = fp.read(4) print("*"+str7+"*") # 修改文件描述符的位置 fp.seek(1) str8 = fp.read(4) print("*"+str8+"*")
-
关闭文件
-
注意
文件使用过后必须关闭
-
原因
释放资源,系统能打开的文件个数是有限制的,所以需要释放相应文件的文件描述符
-
关闭方式
程序结束自动关闭:程序结束时会释放文件对象的空间,文件会关闭,但是不建议这样来做,最好手动关闭
手动关闭:调用代码关闭
-
示例
fp.close()
-
2、读文件完整过程
try:
fp = open("file.txt", "r")
print(fp.read())
finally:
if fp:
fp.close()
3、读文件简写方式
with open("file.txt", "r") as fp:
print(fp.read())
二、写
1、写文件
-
过程
- 找到文件
- 打开文件
- 将内容写入缓冲区,此时内容没有写入文件
- 刷新缓冲区,直接把缓存区中的数据立刻写入文件
- 关闭文件
-
刷新缓冲区方式
- 程序结束
- 关闭文件
- 手动刷新
- 缓冲区满了
- 遇到\n
-
函数
函数名 函数说明 file.write() 将字符串写入文件,没有返回值。 file.writelines() 向文件写入一个序列字符串列表,如果需要换行则要自己加入每行的换行符。没有返回值 file.flush() 刷新文件内部缓冲,直接把内部缓冲区的数据立刻写入文件, 而不是被动的等待输出缓冲区写入。 file.tell() 返回文件当前位置。 -
找打文件
path = "file.txt"
-
打开文件
fp = open(path, "w")
-
将内容写入缓冲区
fp.write("lucky good")
-
手动刷新缓冲区
fp.flush()
-
关闭文件
fp.close()
2、写文件完整过程
try:
fp = open("file.txt", "w")
fp.write("cool man")
finally:
if fp:
fp.close()
3、文件关闭
你可以反复调用write()
来写入文件,但是务必要调用f.close()
来关闭文件。当我们写文件时,操作系统往往不会立刻把数据写入磁盘,而是放到内存缓存起来,空闲的时候再慢慢写入。只有调用close()
方法时,操作系统才保证把没有写入的数据全部写入磁盘。忘记调用close()
的后果是数据可能只写了一部分到磁盘,剩下的丢失了。所以,还是用with
语句来得保险
不使用with的情况
file = open("test.txt","r")
for line in file.readlines():
print(line)
file.close()
这样直接打开文件,如果出现异常,如读取过程中文件不存在或异常,则直接出现错误,close方法无法执行,文件无法关闭
使用with的情况
file= open("test.txt","r")
try:
for line in file.readlines():
print line
except:
print "error"
finally:
file.close()
with语句作用效果相当于上面的try-except-finally
4、写文件简写方式
with open("file.txt", "w") as fp:
fp.write("cool man")
三、编码与解码
Unicode:统一码,也叫万国码、单一码(Unicode)是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。
如果把各种文字编码形容为各地的方言,那么Unicode就是世界各国合作开发的一种语言。
UTF-8是针对Unicode的一种可变长度字符编码;它可以用来表示Unicode标准中的任何字符,而且其编码中的第一个字节仍与ASCII相容,使得原来处理ASCII字符的软件无须或只进行少部份修改后,便可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。
1、编码
with open("file.txt", "wb") as fp:
s = "lucky is a good man"
s = s.encode("utf-8")
fp.write(s)
2、解码
with open("file.txt", "rb") as fp:
s = fp.read()
s = s.decode("utf-8")
print(s)
编码与解码 :使用什么编码 就使用什么解码
3、案例
-
文件加密
file = open('1.py','r') myfile = open('2.py','a+') x = file.read() for i in x: z = chr(ord(i)+10) myfile.write(z)
-
文件解密
file = open('4.py','r') myfile = open('5.py','a+') x = file.read() for i in x: z = chr(ord(i)-10) myfile.write(z)
4、chardet模块
-
作用
使用chardet检测编码,支持检测中文、日文、韩文等多种语言
-
安装
pip install chardet
-
使用
import chardet # 注意:数据量小,猜测的不准 data = "lucky是一个好男人".encode("utf-8") # print(data) ret = chardet.detect(data) print(ret, type(ret)) ''' encoding:表示编码格式 confidence:表示检测的正确的概率 language:表示数据的语言 ''' ''' GBK是GB2312的超集,两者是同一种编码 ''' info = data.decode(ret["encoding"]) print(info)
四、pickle 序列化的操作
使用说明:可以将序列 序列化到 文件里 也就是 可以做到 原样写入 原样拿出 以二进制写进文件里 并以二进制的形式读取到内存里
1、list、tuple、dict、set的文件操作
-
pickle模块
持久化保存对象,将list、tuple、dict、set等数据序列化存储到文件
import pickle
-
函数
函数名 函数说明 dump() 将数据序列化后写入到 文件里 load() 将数据反序列化 取出来 dumps() 将数据序列化 直接返回 loads() 将dumps序列化后的 进制 转换成 普通的数据类型 -
存储
import pickle user = {"account": "lucky", "passwd": "666"} with open("file.txt", "wb") as fp: pickle.dump(user, fp)
-
读取
with open("file.txt", "rb") as fp: user = pickle.load(fp) print(user, type(user))
2、StringIO
-
作用
数据的读写不一定都是文件,也可以是内存中读写,StringIO可以在内存中读写字符串
StringIO顾名思义就是在内存中读写str。
-
导入
from io import StringIO
-
写
要把str写入StringIO,我们需要先创建一个StringIO,然后,像文件一样写入即可:
getvalue()
方法用于获得写入后的str。fp = StringIO() fp.write("lucky is\n") fp.write(" a good ") fp.write("man!") # 获取写入的内容 print(fp.getvalue())
-
读
要读取StringIO,可以用一个str初始化StringIO,然后,像读文件一样读取
fp.seek(0) print(fp.read()) fp.seek(0) print(fp.readline()) fp.seek(0) print(fp.readlines())
循环读
from io import StringIO f = StringIO('Hello!\nHi!\nGoodbye!') while True: s = f.readline() if s == '': break print(s.strip())
注意:文件使用后关闭文件
3、BytesIO
-
作用
StringIO操作的只能是str,如果要操作二进制数据,就需要使用BytesIO。
-
写入
from io import BytesIO f = BytesIO() f.write('lucky老师'.encode('utf-8')) print(f.getvalue())
请注意,写入的不是str,而是经过UTF-8编码的bytes。
-
读取
from io import BytesIO f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87') print(f.read())
4、二进制文件
原因大概有三个:
第一是[二进制文件]比较节约空间,这两者储存[字符型数据]时并没有差别。但是在储存数字,特别是实型数字时,二进制更节省空间,比如储存 Real*4 的数据:3.1415927,文本文件需要 9 个字节,分别储存:3 . 1 4 1 5 9 2 7 这 9 个 ASCII 值,而[二进制文件]只需要 4 个字节(DB 0F 49 40)
第二个原因是,内存中参加计算的数据都是用二进制无格式储存起来的,因此,使用二进制储存到文件就更快捷。如果储存为文本文件,则需要一个转换的过程。在数据量很大的时候,两者就会有明显的速度差别了。
第三,就是一些比较精确的数据,使用二进制储存不会造成有效位的丢失。
五、os模块
1、os中常用属性和方法
作用:包含了基本的操作系统功能,提供了非常丰富的方法用来处理文件和目录
-
属性
名称 说明 name 操作系统的类型,nt表示windows,posix表示Linux、Unix uname 获取操作系统的信息,linux、Unix下使用 environ 获取系统中的环境变量,environ.get()可以获取环境变量的值 curdir 返回当前的目录 -
方法
名称 说明 getcwd() 返回当前工作目录的绝对路径 listdir() 返回指定目录下的所有文件和目录 mkdir() 创建指定目录,注意目录已经存在时会报错,目录路径中存在不存在的层级时报错 rmdir() 删除目录,注意目录不存在则报错 rename() 重命名 stat() 获取文件属性 remove() 删除普通文件 system() 运行shell命令
运行shell命令 shutdown -s -f/ shutdown -a -
示例
import os # 操作系统的类型 # nt windows # posix Linux、Unix print(os.name) # 获取操作系统的信息,linux、Unix下使用 print(os.uname()) # 获取系统中的环境变量 print(os.environ) # 获取指定环境变量的值 print(os.environ.get("PATH")) # 返回当前的目录 print(os.curdir) # 返回当前工作目录的绝对路径 print(os.getcwd()) # 返回指定目录下的所有文件和目录 print(os.listdir(r"C:\Users\sunck\Desktop\code")) # 创建指定目录,注意目录已经存在时会报错,目录路径中存在不存在的层级时报错 os.mkdir(r"C:\Users\sunck\Desktop\file") os.mkdir(r"C:\Users\sunck\Desktop\file\a\b") # 删除目录,注意目录不存在则报错 os.rmdir(r"C:\Users\sunck\Desktop\file") # 重命名 os.rename(r"C:\Users\sunck\Desktop\code\file1.txt", r"C:\Users\sunck\Desktop\code\file2.txt") # 获取文件属性 print(os.stat(r"C:\Users\sunck\Desktop\code\file2.txt")) # 删除普通文件 os.remove(r"C:\Users\sunck\Desktop\code\file2.txt") # 运行shell命令 os.system("notepad") os.system("shutdown -s -t 10") os.system("shutdown -a")
2、os.path中常用方法
操作文件和目的函数一部分在os模块中,还有一部分在os.path中
名称 | 说明 |
---|---|
abspath | 返回指定路径的绝对路径 |
join | 拼接路径(不论是否存在) |
split | 拆分路径(不论是否存在) |
splitdrive | 以路径第一个'/'为分隔,分隔驱动器名与路径 |
splitext | 获取文件的扩展名(不论是否存在) |
basename | 获取目录或文件名(不论是否存在) |
getsize | 获取属性 |
getctime | 获取属性 |
isdir | 判断是否是目录 |
isfile | 判断是否是文件 |
exists | 判断目录和文件是否存在 |
isabs | 判断是否是绝对路径(不论是否存在) |
import os
# 返回指定路径的绝对路径
print(os.path.abspath("."))
# 拼接路径(不论是否存在)
print(os.path.join(r"C:\Users\sunck\Desktop\file", "a.txt"))
# 拆分路径(不论是否存在)
print(os.path.split(r"C:\Users\sunck\Desktop\file"))
print(os.path.split(r"C:\Users\sunck\Desktop\file\sunck.txt"))
# 以路径第一个'/'为分隔,分隔驱动器名与路径
print(os.path.splitdrive(r"C:\Users\sunck\Desktop\file"))
print(os.path.splitdrive(r"C:\Users\sunck\Desktop\file\sunck.txt"))
# 获取文件的扩展名(不论是否存在)
print(os.path.splitext(r"C:\Users\sunck\Desktop\file"))
print(os.path.splitext(r"C:\Users\sunck\Desktop\file\sunck.txt"))
# 获取目录名(不论是否存在)
print(os.path.basename(r"C:\Users\sunck\Desktop\file"))
# 获取文件名(不论是否存在)
print(os.path.basename(r"C:\Users\sunck\Desktop\file\sunck.txt"))
# 获取属性
print(os.path.getsize(r"C:\Users\sunck\Desktop\code\test.py"))
print(os.path.getctime(r"C:\Users\sunck\Desktop\code\test.py"))
# 判断是否是目录
print(os.path.isdir(r"C:\Users\sunck\Desktop\code"))
print(os.path.isdir(r"C:\Users\sunck\Desktop\file"))
print(os.path.isdir(r"C:\Users\sunck\Desktop\code\test.py"))
# 判断是否是文件
print(os.path.isfile(r"C:\Users\sunck\Desktop\code\test.py"))
print(os.path.isfile(r"C:\Users\sunck\Desktop\code\1.txt"))
print(os.path.isfile(r"C:\Users\sunck\Desktop\code"))
# 判断目录和文件是否存在
print(os.path.exists(r"C:\Users\sunck\Desktop\code\test.py"))
print(os.path.exists(r"C:\Users\sunck\Desktop\code\2.py"))
print(os.path.exists(r"C:\Users\sunck\Desktop\code"))
print(os.path.exists(r"C:\Users\sunck\Desktop\file"))
# 判断是否是绝对路径(不论是否存在)
print(os.path.isabs(r"C:\Users\sunck\Desktop\code"))
print(os.path.isabs(r"C:\Users\sunck\Desktop\file"))
print(os.path.isabs(r".\code"))
python删除一个非空文件夹
import shutil
shutil.rmtree('c:\\test')
六、目录遍历
1、递归遍历目录
# 返回所有文件的绝对路径
def traverseDir(dirPath):
absPathList = []
import os
filesList = os.listdir(dirPath)
for fileName in filesList:
absPath = os.path.join(dirPath, fileName)
if os.path.isdir(absPath):
# 目录
absPathList += traverseDir(absPath)
else:
# 文件
# print(absPath)
absPathList.append(absPath)
return absPathList
absPathList = traverseDir(r"C:\Users\lucky\Desktop\file")
print(absPathList)
print(len(absPathList))
# for absPath in absPathList:
# print(absPath)
2、递归遍历 统计大小
import os
# 递归 输出 所有的文件名
path = r'C:\Users\xlg\Desktop\python安装文件'
def myfile(path):
sum = 0 # 文件大小初始化 为 0
myfilelist = os.listdir(path) #返回当前文件夹下的所有的文件 和 目录
for i in myfilelist: #遍历
newpath = os.path.join(path,i) #将文件和路径拼凑在一起 形成一个新的完整的文件路径
if os.path.isdir(newpath): #判断是否是 目录
sum += myfile(newpath) #如果是目录 就进去 继续统计
if os.path.isfile(newpath): #如果是文件 直接统计大小
sum += os.path.getsize(newpath) #累加文件的大小
return sum
print(myfile(path))
3、检索指定路径下后缀是 py 的所有文件
获取文件后缀
def getfile_fix(filename):
return filename[filename.rfind('.')+1:]
print(getfile_fix('lucky.txt'))
方法二
filename[-3:].upper()=='.PY'
方法三
#myList = x.split('.')
#print(myList[len(myList)-1])
完整示例
import os
import os.path
#path = 'D:/UC/'
ls = []
def getAppointFile(path,ls):
fileList = os.listdir(path)
try:
for tmp in fileList:
pathTmp = os.path.join(path,tmp)
if os.path.isdir(pathTmp):
getAppointFile(pathTmp,ls)
elif pathTmp[pathTmp.rfind('.')+1:].upper()=='PY':
#相等filename[-3:].upper()=='.PY': #不是目录,则比较后缀名
#myList = x.split('.')
#print(myList[len(myList)-1])
ls.append(pathTmp)
except PermissionError:
pass
def main():
while True:
path = input('请输入路径:').strip()
if os.path.isdir(path) == True:
break
getAppointFile(path,ls)
#print(len(ls))
print(ls)
print(len(ls))
main()
进阶操作
import os
import time
def getSuffix(path,suffixList,searchSuffix):
fileList = os.listdir(path)
for file in fileList:
#拼接新的路径
newPath = os.path.join(path,file)
#判断是否是目录 是的话递归
if os.path.isdir(newPath):
getSuffix(newPath,suffixList,searchSuffix)
#判断是否是py后缀的文件
# elif newPath[newPath.rfind('.')+1:].upper() == "PY":
elif newPath[newPath.rfind('.')+1:].upper() == searchSuffix.upper():
suffixList.append(file)
def suffixShow(path,searchSuffix='py'):
myList = []
#path 当前查找数据的目录
#myList 存储后缀数据的名字
getSuffix(path,myList,searchSuffix)
# print(myList)
#判断是否有数据
length = len(myList)
#有数据进行显示
if length:
print("你所查找py后缀文件在{}目录下的个数为{} 这些文件都为...".format(path,length))
time.sleep(2)
for file in myList:
print(file)
else:
print("在当前目录下 没有找到你要寻找后缀的文件")
while True:
searchDir = input("请输入你要查找的目录")
searchSuffix = input("请输入你要查找的后缀 不输入默认为py")
#判断目录是否存在
if os.path.exists(searchDir):
if searchSuffix:
suffixShow(searchDir,searchSuffix)
else:
suffixShow(searchDir)
else:
print(searchDir,"不存在 请重新输入")
time.sleep(2)
4、递归删除 文件
path = './a'
import os
def d(path):
List = os.listdir(path)
for i in List:
newPath = os.path.join(path,i)
if os.path.isdir(newPath):
d(newPath)
if os.path.isfile(newPath):
os.remove(newPath)
os.rmdir(path)
d(path)
七、栈与队列
1、栈结构
栈和队列:两种数据存储格式
-
特点:先进后出
列表方法使得列表可以很方便的作为一个堆栈来使用,堆栈作为特定的数据结构,最先进入的元素最后一个被释放(后进先出)。用 append() 方法可以把一个元素添加到堆栈顶。用不指定索引的 pop() 方法可以把一个元素从堆栈顶释放出来。
-
实例
myStack = [] # 压栈(往栈结构中存储数据) myStack.append(1) print(myStack) myStack.append(2) print(myStack) myStack.append(3) print(myStack) # 出栈(从栈结构中提取数据) myStack.pop() print(myStack) myStack.pop() print(myStack) myStack.pop() print(myStack)
-
堆栈实现 获取文件下所有的文件名(深度优先)
import os path = r'C:\Users\xlg\Desktop\python安装文件'm mylist = [] #创建 一个存储 目录的空列表 mylist.append(path) #把路径添加进去 while len(mylist) != 0: #判断我的目录列表是否为空 mypath = mylist.pop() #将目录弹出来 mylistdir = os.listdir(mypath) #获取该目录下的所有文件 for filename in mylistdir: #把该目录下的文件 进行 遍历 逐个获取 newpath = os.path.join(mypath,filename) #把每个文件 或者 目录 拼凑成一个完整的 路径 if os.path.isdir(newpath): #判断是否是目录 # print('目录名',filename) mylist.append(newpath) #是目录 就添加到 目录列表里 else: print('文件名',filename) #是文件 就输出
2、队列结构
-
特点
先进先出
模块:collections中deque的使用
deque是为了向list中删除和插入的效率更高,用法与list相似,而list自带的append和pop方法(尾部插入和删除)速度慢
-
示例
import collections #引入 其它的数据类型 que = collections.deque() #使用队列 mystr = 'abcdefg' for i in mystr: que.append(i) print(que.popleft()) print(que)
-
队列实现获取所有的文件名(广度优先)
import collections import os path = r'C:\Users\xlg\Desktop\python安装文件' que = collections.deque() que.append(path) while len(que) != 0: mypath = que.popleft() mylist = os.listdir(mypath) for filename in mylist: newpath = os.path.join(mypath,filename) if os.path.isdir(newpath): que.append(newpath) else: print("文件名为",filename)
3、堆栈扩展知识
栈区:由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈
堆区:一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表
全局区(静态区):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束后由系统释放
文字常量区:常量字符串就是放在这里的,程序结束后由系统释放
程序代码区:存放函数体的二进制代码
-
堆栈对比
-
申请方式
stack:系统自动分配
heap:需要程序员自己申请,并指明大小
-
申请大小的限制
stack:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小
heap:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大
-
申请后系统的响应
stack:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出
heap:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中
-
申请效率的比较
stack:系统自动分配,速度较快。但程序员是无法控制的
heap:由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便
-
堆和栈中的存储内容
stack:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行
heap:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排
存取效率的比较
-
stack:相对较高
heap:相对较低