Python_Day2:内存,实验,List内部机制与数组对比,字符串与bytes底层
Part1_Python_Day2:无论c语言还是python电脑中的内存都是一样的不同程序使用方式不同。1.内存把你电脑的内存想象成一排极其长的储物柜每个柜子有编号从 0 开始一直排到几十亿柜号: 0 1 2 3 4 5 6 ... 内容: 01001100 00101011 11100010 00011101 ...............每个柜子存 8 个比特也就是 1 个字节。这是计算机唯一能存的物理状态——高电平或低电平0 或 1。你写 Python 时看到的hello、3.14、[1,2,3]都是在这层物理现实之上抽象出来的幻象。因为 CPU 访问任意一个柜子的时间是相同的。给定柜号addrCPU 内部的地址总线直接把电信号送过去一个时钟周期内数据就回来了。这叫做随机访问。但同时物理世界的柜子是静止的、固定的。要在柜子 3 和柜子 4 之间插入一个新柜子不可能——除非你把柜子 4 开始的内容全部往后挪。这就是算法复杂度的物理根源物理事实算法推论给定地址一步拿到数据数组的按下标访问是 O(1)柜子之间是固定的无法凭空插入数组中间插入/删除需要移动后续全部元素O(n)柜子有固定编号必须靠编号访问如果没有编号就不能直接飞过去只能一个个找O(n)那么在python中是什么样的呢2.实验python创建一个int对象aa是标签指向5a5;ba;a6后b还是5a5 print(id(a))#140731651873336 ba a6 print(b,id(b)) print(a,id(a)) #5 140731651873336 #6 140731651873368结果表示python中更改int的值是将它重新指向另一个值并不是修改内存详情见day1模块一3.手写一个交换函数也是如此。defswap(a,b):a,bb,aprint(a,b)print(id(a),id(b))a5b6print(id(a),id(b))swap(a,b)结果140730647534136 140730647534168 6 5 140730647534168 140730647534136python不可变int,str,tuple内容创建之后不能修改任何修改的操作都是创建新对象可变listdict,set原地修改a[1,2] print(id(a));a.append(3);print(id(a)) 2500605958336 2500605958336结果并未改变说明list是在原地修改的shello;print(id(s));s world;print(id(s)) 2445510941440 2445511397296结果改变说明s已经是新创建的对象s(1,2,[23,4]) print(id(s)) s[2].append(5) print(id(s)) s[2][:][7,8] print(id(s)) 1827330370496 1827330370496 1827330370496结果都一样说明元组不可变但是元组内的列表可变。3.List内部机制与数组对比list存的是PyObject指针数组list[5]是base58拿到指针再解引用。Python list是动态的——append、insert、pop的内部行为完全不同原理见day1模块2此处手写一个函数模拟Python list的内部结构用C概念写一个DynamicArray类包含_capacity、_size、_array实现append和resize (2)用timeit对比list.append vs list.insert(0,item)在100/1000/10000个元素时的耗时class DynamicArray: def __init__(self) - None: self.capacity 4 self.size 0 self.array [None] * self.capacity def resize(self, new_capacity: int): new_array [None] * new_capacity for i in range(self.size): new_array[i] self.array[i] self.array new_array self.capacity new_capacity def append(self, value): if self.size self.capacity: self.resize(2 * self.capacity) self.array[self.size] value self.size 1 def __str__(self): return str(self.array[:self.size]) def __len__(self): return self.size import timeit import matplotlib.pyplot as plt # 设置中文字体防止图表中的中文显示为方块 plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS] # Windows用SimHei, Mac用Arial Unicode MS plt.rcParams[axes.unicode_minus] False # 正常显示负号 def benchmark_list_operations(): # 测试的数据规模 sizes [100, 1000, 10000] # 存储耗时结果 append_times [] insert_times [] # 测试代码的预设每次测试前先创建好对应大小的列表避免重复初始化带来的误差 setup_template import sys # 预先创建列表使得测试时列表已经有 {size} 个元素 test_list list(range({size})) # 测试 append 的语句 append_stmt test_list.append(999) # 测试 insert(0, item) 的语句 insert_stmt test_list.insert(0, 999) # 运行基准测试 for size in sizes: print(f正在测试规模: {size} 个元素...) setup_code setup_template.format(sizesize) # number1000 表示将目标语句重复执行1000次以获取更稳定的耗时 # 为了防止测试时间过长10000规模时可以适当减少执行次数这里统一1000次 t_append timeit.timeit(stmtappend_stmt, setupsetup_code, number1000) t_insert timeit.timeit(stmtinsert_stmt, setupsetup_code, number1000) append_times.append(t_append) insert_times.append(t_insert) print(f - append 耗时: {t_append:.6f} 秒) print(f - insert 耗时: {t_insert:.6f} 秒) # 绘制曲线图 plt.figure(figsize(10, 6)) # 绘制两条曲线 plt.plot(sizes, append_times, markero, linewidth2, labellist.append(item) - O(1)) plt.plot(sizes, insert_times, markers, linewidth2, labellist.insert(0, item) - O(N)) # 设置图表属性 plt.title(Python List 操作耗时对比: append vs insert(0), fontsize16) plt.xlabel(列表元素数量, fontsize14) plt.ylabel(执行 1000 次的总耗时 (秒), fontsize14) plt.xticks(sizes) plt.legend(fontsize12) plt.grid(True, linestyle--, alpha0.6) # 显示图表 plt.show() if __name__ __main__: benchmark_list_operations()Python的list使用over-allocation策略每次扩容为(newsize3)(newsize9?3:6)字节。list.insert(0,x)需要移动N个指针所以O(N)。list.pop()在末尾O(1)末尾删除O(1)leetcode26class Solution(object): def removeDuplicates(self, nums): :type nums: List[int] :rtype: int i0 for j in range(1,len(nums)): if nums[j] ! nums[i]: i1 nums[i]nums[j] return i1i是慢指针j是快指针如果i与j相等则跳过i与j不等则将i1修改为j的值这样i所指向的每一个都不相等而总数则为i1。j一直向前跑每次都与i比较。时间复杂度O(n),空间复杂度O(1)。leetcode27class Solution(object): def removeElement(self, nums, val): :type nums: List[int] :type val: int :rtype: int i0 for j in range(0,len(nums)): if nums[j] ! val: nums[i] nums[j] i1 return i与上题类似。4.字符串与bytes底层分别用’ascii’/‘utf-8’/gbk’编码同一段中文→观察字节长度def compare_encodings(text: str): print(f原始文本: {text} (Unicode长度: {len(text)} 个字符)\n) print(f{编码方式:10} | {编码结果 (十六进制):45} | {字节长度}) print(- * 75) encodings [ascii, utf-8, gbk] for enc in encodings: try: # 将字符串编码为字节 encoded_bytes text.encode(enc) # 转换为十六进制形式方便观察 hex_str .join([f{b:02X} for b in encoded_bytes]) byte_len len(encoded_bytes) print(f{enc:10} | {hex_str:45} | {byte_len} bytes) except UnicodeEncodeError as e: # ASCII 无法编码中文捕获异常 print(f{enc:10} | 编码失败: {e}) if __name__ __main__: chinese_text 代码 compare_encodings(chinese_text) 原始文本: 代码 (Unicode长度: 2 个字符) 编码方式 | 编码结果 (十六进制) | 字节长度 --------------------------------------------------------------------------- ascii | 编码失败: ascii codec cant encode characters in position 0-1: ordinal not in range(128) utf-8 | E4 BB A3 E7 A0 81 | 6 bytes gbk | B4 FA C2 EB | 4 bytes1. ASCII直接报错诞生背景最古老的编码专为英语设计。规则它只使用1个字节的后7位0-127来表示字符包括英文字母、数字和常见标点。为什么失败中文字符成千上万根本不在0-127的映射表里。所以 ASCII无法表示中文强行编码会抛出UnicodeEncodeError。2. UTF-8变长编码本例中每个中文占 3 字节诞生背景为了统一全球所有语言的编码而诞生Unicode 的一种实现方式。规则它是变长编码根据字符的 Unicode 码点大小决定使用几个字节英文字母/数字1 字节与 ASCII 完全兼容拉丁文、希腊字母等2 字节大部分中日韩汉字3 字节极少用的生僻字/Emoji4 字节为什么是 3 字节汉字的 Unicode 码点通常落在0x4E00-0x9FFF范围内UTF-8 需要使用 3 个字节的空间才能装下这些码点并加上用于同步的标识位。所以 “代码” 2个字占了 6 字节。3. GBK定长/变长编码本例中每个中文占 2 字节诞生背景中国国家标准局制定的中文编码扩展了早期的 GB2312。规则也是变长编码但策略与 UTF-8 不同英文字母/数字1 字节兼容 ASCII所有中文字符固定 2 字节为什么是 2 字节GBK 的设计目标很专一——只要能表示中英文就行。它利用了 2 个字节最高位为1的组合可以表示 128×25632768128×25632768 种状态这足以囊括所有的简繁体中文约2万多个汉字。因为不需要像 UTF-8 那样兼顾全球几百种语言所以它可以用更紧凑的 2 字节装下所有中文。所以 “代码” 2个字占了 4 字节。总结对比编码设计范围英文占字节中文占字节优点缺点ASCII纯英文1❌ 无法表示极其节省空间只能表示128个字符GBK中英文12中文空间利用率高2字节定长解析快国际通用性差在其他国家系统容易乱码UTF-8全球语言13全球通用绝无乱码Web标准中文比GBK多耗50%存储/带宽空间注hexdump 风格是经典三栏式十六进制 dump左边偏移地址、中间十六进制字节、右边对应 ASCII 文本每行固定 16 字节排版整齐、便于对照二进制与字符。用hexdump风格打印bytes的每个字节值def hexdump(data: bytes, length: int 16): 以 hexdump 风格打印 bytes 数据 :param data: 字节流 :param length: 每行显示的字节数默认16 # 过滤器用于将不可打印字符或破坏多字节编码的字符替换为 . # 避免在终端打印 ASCII 列时出现乱码 printable_filter bytes(range(32, 127)).decode(ascii) for i in range(0, len(data), length): chunk data[i:ilength] # 1. 偏移量8位十六进制不足补零 offset f{i:08X} # 2. 十六进制区域每字节2位大写每8字节加一个额外空格分隔 hex_parts [] for j in range(length): if j len(chunk): hex_parts.append(f{chunk[j]:02X}) else: hex_parts.append( ) # 不足16字节时补空格对齐 # 分为左右两半中间加额外空格 hex_left .join(hex_parts[:8]) hex_right .join(hex_parts[8:]) hex_line f{hex_left} {hex_right} # 3. ASCII 区域可打印字符原样输出其余替换为 . ascii_line for b in chunk: char chr(b) ascii_line char if char in printable_filter else . print(f {offset} {hex_line} |{ascii_line}|) def compare_encodings_with_dump(text: str): print(f原始文本: {text} (Unicode长度: {len(text)} 个字符)\n) encodings [ascii, utf-8, gbk] for enc in encodings: print( * 70) print(f编码方式: {enc}) try: encoded_bytes text.encode(enc) print(f字节长度: {len(encoded_bytes)} bytes\n) hexdump(encoded_bytes) except UnicodeEncodeError as e: print(f编码失败: {e}\n) print() if __name__ __main__: chinese_text 代码A1 compare_encodings_with_dump(chinese_text) 原始文本: 代码A1 (Unicode长度: 4 个字符) 编码方式: ascii 编码失败: ascii codec cant encode characters in position 0-1: ordinal not in range(128) 编码方式: utf-8 字节长度: 8 bytes 00000000 E4 BB A3 E7 A0 81 41 31 |......A1| 编码方式: gbk 字节长度: 6 bytes 00000000 B4 FA C2 EB 41 31 |....A1|练习1.判断一个文件是不是PNG——读前8个字节对比PNG魔术字节此处使用codegeex生成def is_png_file(filepath: str) - bool: 判断一个文件是否为PNG图片 通过读取前8个字节对比PNG的魔术字节来实现 # PNG文件的魔术字节 (十进制表示: 137 80 78 71 13 10 26 10) PNG_MAGIC_BYTES b\x89PNG\r\n\x1a\n try: # 以二进制只读模式打开文件 with open(filepath, rb) as f: # 读取前8个字节 header f.read(8) # 对比读取的字节和魔术字节是否完全一致 return header PNG_MAGIC_BYTES except FileNotFoundError: print(f文件未找到: {filepath}) return False except IOError as e: print(f读取文件出错: {e}) return False # 测试代码 if __name__ __main__: import os # 1. 创建一个假的PNG文件用于测试 fake_png test_fake.png with open(fake_png, wb) as f: # 写入正确的PNG头部 f.write(b\x89PNG\r\n\x1a\n) # 后面随便写点内容 f.write(bsome image data...) # 2. 创建一个普通的文本文件强行改名为 .png not_png test_not_png.png with open(not_png, w, encodingutf-8) as f: f.write(我是一个文本文件只是后缀名叫png) # 3. 测试判断 print(f{fake_png} 是PNG吗? - {is_png_file(fake_png)}) print(f{not_png} 是PNG吗? - {is_png_file(not_png)}) # 清理测试文件 os.remove(fake_png) os.remove(not_png) test_fake.png 是PNG吗? - True test_not_png.png 是PNG吗? - FalsePython内部str使用柔性数组PyASCIIObject→PyCompactUnicodeObject→PyUnicodeObject三级根据字符范围自动选最省内存的表示。join比‘’快因为join预先算出总长度一次分配PyASCIIObject 存 纯英文、数字、符号 每个字符占 1 字节最省内存 PyCompactUnicodeObject 存 中文、日文、特殊符号 每个字符占 2 字节 或 4 字节 PyUnicodeObject 最复杂、最大的结构很少用使用拼接字符每一次就要创建一个新字符串每次都要重新申请内存 拷贝数据次数越多越慢浪费极大用 str.join () 拼接快先遍历所有字符串算出总长度一次性申请一大块连续内存把所有内容一次性拷贝进去只创建 1 个对象没有反复复制 .join(str(i) for i in [1,2,3])#数字需先转换为str再进行拼接2写一个函数读取任意文件的魔术字节判断文件类型支持PNG/JPEG/PDF/ZIP此处使用codegeex生成import os # 文件类型魔术字节字典 # 格式: 类型名: (偏移量, 魔术字节) # 偏移量: 魔术字节开始的位置0表示文件开头 MAGIC_DICT { PNG: (0, b\x89PNG\r\n\x1a\n), JPEG: (0, b\xff\xd8\xff), # JPEG 常见的前3个字节第三字节通常是 e0, e1, db 等 PDF: (0, b%PDF-), # PDF 以 %PDF- 开头 ZIP: (0, bPK\x03\x04), # ZIP 以及基于ZIP的格式(docx, xlsx, jar等) } def detect_file_type(filepath: str) - str: 读取文件魔术字节判断文件类型 :param filepath: 文件路径 :return: 文件类型字符串 (如 PNG, JPEG)未知返回 UNKNOWN出错返回 None if not os.path.isfile(filepath): print(f文件不存在: {filepath}) return None # 找出字典中需要读取的最大偏移量和最长魔术字节决定一次性读取多长 # 这样做可以避免多次读取文件IO提高效率 max_offset max(offset for offset, _ in MAGIC_DICT.values()) max_magic_len max(len(magic) for _, magic in MAGIC_DICT.values()) read_length max_offset max_magic_len try: with open(filepath, rb) as f: header f.read(read_length) # 遍历字典进行匹配 for file_type, (offset, magic) in MAGIC_DICT.items(): # 从读取的 header 中截取对应偏移量和长度的切片进行对比 if header[offset:offsetlen(magic)] magic: return file_type return UNKNOWN except IOError as e: print(f读取文件出错: {e}) return None # 测试代码 if __name__ __main__: # 准备测试文件 test_files { test.png: b\x89PNG\r\n\x1a\n bsome png data, test.jpg: b\xff\xd8\xff\xe0 bsome jpeg data, test.pdf: b%PDF-1.4 bsome pdf data, test.zip: bPK\x03\x04 bsome zip data, test_fake.pdf: bThis is not a PDF, just a text file, } # 写入测试文件 for filename, content in test_files.items(): with open(filename, wb) as f: f.write(content) # 检测并打印结果 print(f{文件名:20} | {检测结果}) print(- * 35) for filename in test_files.keys(): ftype detect_file_type(filename) print(f{filename:20} | {ftype}) # 清理测试文件 for filename in test_files.keys(): os.remove(filename)