资讯动态

Python内置函数深度总结:核心用法、常见陷阱与工程实践

发布时间:2026/9/10 17:10:56 来源:尧图企业网站定制
这份笔记是我写Python这些年一直放在手边的东西。起因很简单——写久了之后发现自己还是会时不时卡在某些内置函数的细节上比如round到底怎么舍入、zip遇到不等长序列会怎样、sorted的key怎么写才最优雅。每次去翻官方文档虽然也能找到答案但太散而且网上很多整理只是把文档抄了一遍没有把真正会踩坑的点记下来。所以我把最常用的内置函数按自己的使用频率和踩坑经验重新整理了一份从什么时候用、为什么这么用、有什么坑的角度来写今天分享出来。适合两类人看一是已经学过Python基础、想系统过一遍内置函数的人二是面试前想突击、或者写代码时需要快速查阅的人。这份笔记不是官方文档的翻译它更像一个老开发在跟你面对面讲我平时是这么用这些函数的。1. 先搞清楚内置函数到底是什么值得专门整理一份吗1.1 内置函数和标准库函数的边界在哪很多初学者分不清内置函数和标准库函数的区别。简单说内置函数是Python解释器启动时就自动加载好的函数不需要import就能直接用比如print、len、type、range这些。它们住在builtins这个模块里解释器默认帮你导入了。标准库函数则是Python安装目录下带着的一堆模块os、sys、json、re那些用之前必须先import。两者最直观的区分方式就是在代码里直接敲名字不报NameError的就是内置函数。那为什么内置函数值得单独整理一份因为它们是Python语法之外最基础、使用频率最高的一批工具。写代码时不认识某个标准库函数影响不大最多多写几行但不了解内置函数你的代码会变得又臭又长而且会错过很多Python设计者已经帮你优化好的解决方案。1.2 如何快速查看全套内置函数想知道当前环境里有哪些内置函数直接在交互式命令行里敲dir(__builtins__)这会返回一个列表包含所有内置函数、异常类和几个特殊对象。其中以双下划线开头结尾的比如__import__、build_class属于解释器内部使用的接口业务代码里基本用不到忽略它们就好。想看某个函数的详细说明用helphelp(sorted) help(setattr)也可以一次性把整个builtins模块的文档拉出来看不过输出很长适合有空通读一遍、没空就按需查阅。1.3 这份笔记的组织逻辑官方文档是按字母序排列内置函数的这种排法对查阅有帮助但对记忆和掌握没有。我平时理解内置函数习惯按使用场景来归类数值运算时用哪些做类型转换和判断时用哪些处理序列列表、元组、字符串时用哪些写面向对象代码时用哪些做文件读写和输入输出时用哪些本文就按这个逻辑往下展开每个函数我都会给出典型的适用场景容易出错的边界情况和我实际用下来的心得。2. 数值运算类函数先把最常用的几个讲透2.1 abs、min、max、sum多一个可选参数少写三行判断这几个函数大家每天都在用但很多人不知道它们各自有容易被忽略的可选参数。abs()求绝对值参数可以是整数、浮点数甚至是复数——如果是复数返回的是它的模。求距离、算误差、统一正负号时很常用。min和max有两种调用形式min(1, 2, 3)这种传多个参数的形式以及min([1, 2, 3])这种传一个可迭代对象的形式。大家容易忽略的是key参数和default参数# 找出列表中最长的字符串 names [python, java, javascript] longest max(names, keylen) print(longest) # javascript # 空序列不会报错返回默认值 empty [] max_value max(empty, default0) print(max_value) # 0key参数的意思是在比较之前先把每个元素通过key指定的函数变换一下再按变换后的结果比较。这背后是Python内置函数里很统一的设计思想——sorted、min、max、itertools.groupby都支持key理解了key的机制这些函数就都能玩得转了。default参数则是Python 3.4以后才加的。没有它的话对空序列求max会直接抛ValueError: max() arg is an empty sequence。以前我只能先if判断序列是否为空现在一个default搞定。sum(iterable, start0)的第二个参数start也容易被忽略。start表示从什么初始值开始累加最常见的用法是给总和加一个基数# 等同于 sum(values) 100 values [1, 2, 3] print(sum(values, 100)) # 106还有一个冷门用法sum可以用来做一层列表展开。因为列表之间可以用号拼接sum([[1, 2], [3, 4]], [])就是[] [1,2] [3,4]结果是[1, 2, 3, 4]。不过我实际工作中几乎不用这个写法因为它可读性略差而且性能不如列表推导式这里提一句只是帮大家理解start参数。2.2 round的银行家舍入机制和它为什么看起来不对round可能是数值函数里坑最多、面试问得最多的一个。先看一个现象print(round(0.5)) # 0 print(round(1.5)) # 2 print(round(2.5)) # 2 print(round(3.5)) # 4看到没round(0.5)不是1而是0round(2.5)不是3而是2。这不是bug而是Python采用了银行家舍入Bankers Rounding当小数部分正好是0.5时round会舍入到最近的偶数而不是像小学数学那样一律入。为什么这么设计因为在大量统计计算中四舍五入会引入系统性偏差总是把0.5往上入会使结果偏高。银行家舍入让入和舍的几率大致均衡累积误差更小。钱相关的场景很多国家也要求这种舍入方式。另一个坑更隐蔽和浮点数精度有关print(round(2.675, 2)) # 2.67而不是期望中的2.68原因在于2.675在计算机内存里并不是精确的2.675而是一个略小于它的浮点数大概是2.6749999999999998所以四舍五入就变成了2.67。这不是round的问题是所有用二进制表示十进制的语言共同的问题。如果你的业务对舍入精度有严格要求比如金融、统计报表我的建议是不要直接用round而是用decimal模块from decimal import Decimal, ROUND_HALF_UP print(Decimal(2.675).quantize(Decimal(0.01), roundingROUND_HALF_UP)) # 2.68注意Decimal构造函数里传的是字符串不要传浮点数否则精度误差已经产生了。2.3 divmod、pow、bin、oct、hex冷门但关键时刻能救命divmod(a, b)同时返回商和余数一次调用拿到两个值。以前我写分页逻辑时经常需要total 95 per_page 20 pages, remainder divmod(total, per_page) print(pages) # 4 print(remainder) # 15虽然这里remainder用不上但divmod这个函数的语义就是一次拿到商和余数比分别用//和%两次运算更清晰。从Python 3.8开始divmod的第二个参数还支持0此时它会抛出ZeroDivisionError跟直接用//和%的行为一致。pow(x, y)和x ** y在多数场景下等价。但pow有一个两参数版本不具备的能力——三参数取模# 计算非常大数的模比如密码学、随机数采样里的场景 print(pow(2, 10, 1000)) # 24即 2^10 % 1000pow(x, y, mod)内部用快速幂算法比先算x ** y再取模快得多也避免了大数中间结果占内存。写RSA相关的加密代码、做哈希或采样时这个函数是标配。bin、oct、hex分别把整数转成二进制、八进制、十六进制字符串。写位运算调试、查看权限位、分析数据协议时经常用print(bin(10)) # 0b1010 print(oct(10)) # 0o12 print(hex(255)) # 0xff它们返回的字符串带前缀0b、0o、0x如果不需要前缀可以格式化输出比如format(10, b)只输出1010。3. 类型转换与类型判断类函数对象的变装与验明正身3.1 int、float、str等转换函数的边界条件类型转换函数是写脚本时用得最频繁的一批int()、float()、str()、list()、tuple()、set()、dict()。它们的共同点是接收一个对象尝试返回指定类型的对象。但每个函数都有自己的边界条件踩坑的往往就是这些边界。int(x, base10)可以把字符串按指定进制解析成整数print(int(ff, 16)) # 255 print(int(1010, 2)) # 10另外值得记住的是int在接收浮点数时是向零截断而不是四舍五入。int(3.9)的结果是3int(-3.9)的结果是-3这跟round的行为不一样。我见过有人拿int去做四舍五入结果数据整体偏小这个坑相当隐蔽。float(nan)会生成NaNNot a Number。NaN有一个反直觉的特性它不等于自己。这其实是IEEE 754的标准行为但在做数据清洗时需要注意——如果你看到一个值连它自己都不相等那它多半是NaN。str()和repr()的区别常被忽视。str的目标是给人看的repr的目标是能还原出这个对象的。调试时我几乎总是用repr因为字符串中的转义符、引号边界都能看得清清楚楚s a\nb print(str(s)) # a # b print(repr(s)) # a\nb在这段输出里str(s)打印出来是换行而repr(s)会显示转义序列。做日志输出、排查数据问题时repr可比str可靠多了。list()、tuple()、set()基本都接收可迭代对象。set()去重有一个前提元素必须是可哈希的。list、dict这些可变类型不能放进set会报TypeError: unhashable type: list。如果要在保持列表原始顺序的前提下去重用set直接转会丢顺序我一般这样写items [3, 1, 2, 3, 2, 4] unique list(dict.fromkeys(items)) print(unique) # [3, 1, 2, 4]原理是dict的键天然去重且保留插入顺序fromkeys用一个可迭代对象快速生成字典。dict()转换的场景更多dict([(a, 1), (b, 2)])可以接收键值对列表dict(a1, b2)可以用关键字参数创建。但要注意用dict(zip(keys, values))做键值配对时zip的截断行为后面第4章会讲可能让你悄悄丢数据。3.2 type和isinstance的区别写继承代码时尤其重要type(x)返回的是x的类型对象isinstance(x, T)判断x的类型是否属于T包括T的所有子类。两者看起来很像实际差别很大。最典型的案例是bool。在Python里bool继承自int所以print(isinstance(True, int)) # True print(type(True) is int) # False也就是说如果你用type(x) is int去判断用户输入的数字在Python里传True进来会被放走但isinstance(x, int)会把True当成int处理。业务上到底要不要把bool当int取决于你的语义约定但至少你要清楚这两者行为不同。在面向对象编程里我几乎总是用isinstance而不是type原因很简单isinstance尊重继承关系。一个子类实例用type去判断父类类型会得到False而isinstance会得到True。写通用函数、做类型分派时这直接决定了你的代码能不能跟别人写的子类配合。3.3 id和hash判断是不是同一个东西的两种方式id(x)返回对象在内存中的唯一标识is运算符比较两个变量是否指向同一个对象比较两个对象的值是否相等。这几个概念最经典的演示是小整数缓存a 256 b 256 print(a is b) # True因为小整数被缓存了 c 257 d 257 print(c is d) # 可能在交互式环境是False因为大整数每次都新建对象注意这个结果依赖具体实现和运行环境所以我从不建议在业务代码里用is去比较整数。is唯一推荐的用法就是判断Noneif value is None。hash(x)返回对象的哈希值。可变对象list、dict、set不可哈希强行调用会抛异常。这个函数平时用得少但理解它有助于理解dict和set的底层查找原理——它们都是先算哈希、再比较相等这才有了哈希查找近似O(1)的高性能。4. 序列处理类函数enumerate、zip、sorted、reversed的进阶用法4.1 enumerate带start参数从1开始编号的微妙差异enumerate(iterable, start0)在遍历时同时给你索引和元素这几乎是写Python循环的标配names [python, java, go] for index, name in enumerate(names, start1): print(index, name)start1在报表输出、序号展示时非常实用。很多人在循环里手动维护一个count变量我看了就忍不住想让他知道enumerate。手动count最大的问题是容易忘记在某个分支里自增而enumerate把索引逻辑封装好了不可能错。enumerate和zip还能组合出很优雅的循环for i, (key, value) in enumerate(zip(keys, values), start1): print(i, key, value)注意这里zip解包出来的两个变量要用括号包起来因为enumerate产生的是(index, (key, value))这种结构直接把i, key, value三个变量去接会报ValueError。4.2 zip的截断行为和zip_longestzip(*iterables)把多个可迭代对象按位置对齐打包成元组序列。最经典的用法是并行遍历两个列表、把两个列表转成字典。但zip有一个非常重要的行为它以最短的可迭代对象为准超出的元素直接丢弃。a [1, 2, 3] b [4, 5] print(list(zip(a, b))) # [(1, 4), (2, 5)]元素3被悄悄丢弃了这个行为有时候是你要的截断补齐有时候是个坑。比如你有两份学生名单一份10个一份9个zip完以后最后一个人对不上号数据就歪了。如果你希望能以最长的序列为准缺失部分用指定值填充用itertools.zip_longestfrom itertools import zip_longest a [1, 2, 3] b [4, 5] print(list(zip_longest(a, b, fillvalue0))) # [(1, 4), (2, 5), (3, 0)]另外zip也能做矩阵转置matrix [[1, 2, 3], [4, 5, 6]] transposed list(zip(*matrix)) print(transposed) # [(1, 4), (2, 5), (3, 6)]这里*matrix把matrix展开成两个列表参数传给zipzip内部按位置把两个列表的同位元素配对。这是一个很Pythonic的写法有时比嵌套列表推导式更直观。4.3 sorted的key参数从简单排序到复杂排序sorted(iterable, *, keyNone, reverseFalse)返回一个新列表本身不动原序列。list.sort()则是原地排序更省内存。选哪个取决于你是否还需要原顺序。key参数是整个函数的核心。它接收一个函数这个函数对每个元素调用一次返回一个用于比较的排序键。下面这些用法是我平时最常写的words [banana, apple, cherry, date] # 按字符串长度排序 print(sorted(words, keylen)) # 按最后一个字母排序 print(sorted(words, keylambda w: w[-1])) # 按字典的值排序 d {a: 3, b: 1, c: 2} print(sorted(d.items(), keylambda item: item[1]))如果比较逻辑比较复杂比如先按第一个条件升序、第二个条件降序我的习惯是使用operator模块里的itemgetter和attrgetter它们比lambda更快、更清晰from operator import itemgetter data [(zhang, 90), (li, 85), (wang, 95)] sorted(data, keyitemgetter(1), reverseTrue)说到多级排序注意reverseTrue是全局反转不能只反转某一个字段。要实现第一字段升序、第二字段降序有两个常见办法一是连续两次调用sorted因为Python的sorted是稳定排序后排序的优先级更高二是把key的某些字段取负值再统一reverse。我实践下来两次排序的思路最直观但要注意顺序写反。4.4 reversed与切片实现反转的差别reversed(seq)返回一个反向迭代器不会创建新序列适合只需要遍历一遍的场景nums [1, 2, 3, 4] for n in reversed(nums): print(n)而列表自身有reverse()方法做原地反转字符串反转则常用切片[::-1]s python print(s[::-1]) # nohtyp要注意reversed对象只能迭代一次如果想保留反转后的列表得list(reversed(nums))。还有一个不太常用但很值得知道的slice(start, stop, step)可以创建切片对象把切片逻辑保存成变量复用middle slice(1, 4) nums [0, 1, 2, 3, 4, 5] print(nums[middle]) # [1, 2, 3]如果你的代码里多次对序列做同样截取先定一个slice对象能把意图表达得更清楚也方便统一修改。5. 迭代与条件判断类函数map、filter、all、any、iter、next5.1 map和filter什么时候用它们什么时候改用生成器表达式map(func, iterable, ...)把func逐个应用到可迭代对象的每个元素上返回一个迭代器。filter(func, iterable)保留func返回真值的元素同样返回迭代器。map最擅长的事情是把已有函数套到每个元素上nums [1, 2, 3] print(list(map(int, nums))) # [1, 2, 3]这里int是现成的函数map一行搞定比列表推导式更简洁。如果传多个可迭代对象func需要接收对应数量的参数a [1, 2, 3] b [4, 5, 6] print(list(map(lambda x, y: x y, a, b))) # [5, 7, 9]filter有一个冷门但极其实用的写法filter(None, iterable)可以直接过滤掉所有假值0、、None、False、空列表data [0, 1, , hello, None, [], [1]] print(list(filter(None, data))) # [1, hello, [1]]这在清洗数据时很顺手。但要注意如果你需要保留0或者空字符串这些有业务意义的值就不要这么写。那map/filter和列表推导式怎么选我的判断标准是如果逻辑简单到能用一个现成函数表达map/filter可读性更好一旦出现稍微复杂的条件立刻改用列表推导式或生成器表达式。比如filter(lambda x: x % 2 0, nums)明显不如[x for x in nums if x % 2 0]直白。Python社区的整体偏好也是推荐后者因为推导式不需要额外记忆lambda的语义。不过map有一个推导式比不了的优势——惰性求值。map返回的是迭代器不会一次性把所有结果算出来在数据量巨大或只需要前几个结果时这能省不少内存。生成器表达式也同样惰性所以二者在这个场景下打了个平手。5.2 all、any的空序列返回值与短路行为all(iterable)在所有元素都为真时返回Trueany(iterable)只要有一个元素为真就返回True。这两个函数在写条件判断时非常常用但有两个细节值得注意。第一个是空序列的返回值。all([])返回Trueany([])返回False。这个反直觉的结果其实是逻辑学里的全称命题对空集为真约定但很多人第一次遇到都会愣一下。如果你要在代码里判断是否存在而不是是否全部建议显式判断序列长度避免空序列导致逻辑错误。第二个是短路行为。all遇到第一个假值就停止迭代any遇到第一个真值就停止data [1, 0, 2] print(all(x 0 for x in data)) # 第一次迭代遇到0就返回False后面的2不用看了因为短路配合生成器表达式在判断是否满足条件时性能会非常好。比如检查一个超长列表里是否存在某个值any(x target for x in huge_list)可能在第一个元素就结束了根本不需要遍历整个列表。5.3 iter的第二个参数处理流式数据的冷门技巧iter(object, sentinel)是绝大多数Python教程不会讲的用法。当object是一个可调用对象时iter会反复调用它直到返回值等于sentinel才停止。这个模式在处理未知长度的流式数据时非常有用。举个例子读取一个大文件的固定大小块直到读到空块with open(huge_file.bin, rb) as f: for chunk in iter(lambda: f.read(1024), b): process(chunk)如果没有iter的双参数形式你得手动写一个while True循环每次判断是不是空块然后break代码要啰嗦不少。这个用法还经常配合队列做消费者逻辑比如从队列里反复取任务取到暂停标记就退出。next(iterable, default)也有类似的省心效果从迭代器取下一个元素如果迭代器已经耗尽返回default而不是抛StopIteration。我经常用它来取第一个满足条件的元素first_even next((x for x in nums if x % 2 0), None)这种写法比for循环加break简洁得多而且None表示没找到语义清晰。6. 对象内省与反射类函数写框架和调试时的好帮手6.1 getattr/setattr/hasattr动态属性操作的三种姿势getattr(obj, name, default)按字符串名字获取对象的属性setattr(obj, name, value)动态设置属性hasattr(obj, name)判断对象是否有某个属性。这三个函数放在一起能实现根据运行时数据来操作对象的能力也就是所谓的反射。getattr最经典的场景是根据字符串分发逻辑。比如一个命令处理器不同命令对应不同方法用if/elif写会长出一串分支用getattr就清爽多了class CommandHandler: def start(self): print(start command) def stop(self): print(stop command) handler CommandHandler() command input(command: ) method getattr(handler, command, None) if callable(method): method() else: print(funknown command: {command})这里getattr的default参数用None配合callable判断就实现了安全分发。这种写法在插件系统、配置驱动代码、ORM动态查询里随处可见。setattr则常见于把字典转成对象属性一类的场景比如加载配置文件后动态生成配置类config {host: localhost, port: 8080} class Config: pass cfg Config() for key, value in config.items(): setattr(cfg, key, value)用hasattr时有一个隐蔽的坑hasattr内部实现是调用getattr并捕获AttributeError但如果属性本身是property且其getter内部抛了AttributeErrorhasattr也会返回False让你误以为属性不存在。我排查过类似的诡异bug最后发现是getter内部的异常被hasattr吞掉了。所以别完全信任hasattr的判定结果。6.2 callable、dir、vars几个查看对象底层信息的方法callable(obj)判断对象是否可以被调用。函数、类、实现了__call__的实例callable都返回True。我在写参数类型校验或装饰器工厂时常用它。dir(obj)返回对象能访问的所有属性名和方法名列表。调试时先dir一下看有哪些方法比猜名字快得多。vars(obj)则返回对象的__dict__也就是实例属性字典真正存的值。注意这两个的含义不同dir管有哪些名字vars管值是什么。还有一个细节带__slots__的类没有__dict__调用vars(instance)会抛TypeError。我在优化内存占用、给类加__slots__时遇到过一次。6.3 globals、locals命名空间的读取边界globals()返回全局命名空间的字典locals()返回当前局部命名空间的字典。在模块顶层两者内容基本一致但在函数内部差异就大了def func(): x 1 print(locals()) # {x: 1} print(globals()) # 模块级的全局名字 func()有人试图通过修改locals()返回的字典来改变局部变量我明确告诉你在函数内部这个操作无效locals()返回的只是一个快照真正的局部变量不会因此改变。globals()的字典在模块层级改起来倒是有效果但一般不太推荐这样做它会让代码的依赖关系变得隐晦。真正需要动态操作全局命名空间时用exec或setattr到模块对象上会更清晰。7. 动态执行函数eval、exec、compile的使用红线7.1 eval能做和不能做的事eval(expression, globalsNone, localsNone)接收一个字符串形式的表达式计算并返回结果。它只能执行表达式不能执行语句。for循环、变量赋值、函数定义这些在eval里都会报语法错误。合法的用法比如解析用户传来的一段简单数学公式前提是用户完全可信、代码完全由你控制expr 3 * 4 2 print(eval(expr)) # 14但eval有一个绝对不能触碰的红线永远不要对不可信的外部输入调用eval。很多人以为限制了globals就能防止危险比如eval(user_input, {builtins: {}})但实际上绕过的姿势非常多——只需要构造出类对象就能一路访问到object的子类再找到__subclasses__进而拿到可以执行系统命令的类。这是Python安全圈的经典攻击链。我见过很多把eval当JSON解析器用的代码后来都换掉了。如果你只是想把{a: 1}这种Python字面量字符串转成字典用ast.literal_eval如果你要解析JSON用json.loads。它们都远比eval安全。7.2 exec和compile的用途exec(code, globalsNone, localsNone)比eval更强大它执行的是完整代码块可以包含语句、赋值、函数定义。很多时候用它来实现动态加载配置、运行时生成代码。compile(code, filename, mode)则把字符串编译成代码对象。mode参数可以取exec、eval或single。编译后的代码对象可以被exec/eval反复执行避免每次执行都重新解析字符串性能好一些。一个典型场景是把用户上传的一段Python脚本编译后执行——但如果你真做了这个功能请务必确认这个用户绝对可信。7.3 为什么我建议把动态执行当最后手段动态执行最大的问题是让代码变得难以追踪、难以调试、难以静态分析。一个变量是怎么来的IDE点不进去代码搜索找不到出错了堆栈信息也糊成一团。维护成本很高。我自己的经验是90%的业务场景看起来非用eval/exec不可实际都有替代方案。比如需要按配置调用不同函数时用字典映射或getattr反射更安全需要解析结构化数据时用ast.literal_eval或json需要热加载代码时可以考虑importlib而不是exec来执行一个字符串。保持动态执行是最后手段这个意识能帮你少写很多让你半夜接电话的代码。8. 类机制辅助类函数理解Python面向对象设计的关键8.1 super与MROsuper()是Python面向对象里最容易用错也最重要的内置函数之一。它的作用是调用父类的实现但这里的父类并不是你想当然的继承关系而是由MROMethod Resolution Order方法解析顺序决定的。在单继承里super().method()很好理解。真正复杂的是多继承而Python保证继承顺序的算法会把所有父类线性化为一个顺序。出了钻石继承问题时显式调用某个父类的方法会导致同一个父类的初始化逻辑被执行多次而super()能保证MRO上的每个类只被调用一次。class A: def __init__(self): print(A init) class B(A): def __init__(self): super().__init__() print(B init) class C(A): def __init__(self): super().__init__() print(C init) class D(B, C): def __init__(self): super().__init__() print(D init) d D()执行顺序是D、B、C、A每个类的初始化代码都恰好执行一次。这就是super()的协作式设计。我在写mixins、多继承方案时几乎总是用super().init()而不是ClassName.init(self)因为前者能适应任意MRO后者会让代码在继承结构变化时悄悄出bug。8.2 staticmethod、classmethod、property的选择staticmethod和classmethod都是装饰器用来声明不需要实例也能调用的方法。区别在于staticmethod既不接收self也不接收cls就是一个普通函数只是恰好放在类里作为命名空间classmethod接收cls作为第一个参数能访问类本身。实际选择标准很简单方法逻辑跟类状态、实例状态都无关把它放类里只是图个组织方便用staticmethod。方法需要访问类属性或者需要根据类创建实例用classmethod。classmethod最经典的应用是替代构造函数class Date: def __init__(self, year, month, day): self.year year self.month month self.day day classmethod def from_string(cls, s): year, month, day map(int, s.split(-)) return cls(year, month, day) date Date.from_string(2025-01-15)这里from_string返回的是cls(year, month, day)而不是Date(...)这样写的好处是子类继承时cls会自动指向子类工厂方法仍然正确创建子类实例。标准库里的datetime.fromtimestamp就是这么实现的。property装饰器把方法变成属性访问用起来像字段背后是方法class Circle: def __init__(self, radius): self._radius radius property def area(self): return 3.14159 * self._radius ** 2 c Circle(2) print(c.area) # 12.56636注意area后面没有括号property的价值不只是替代getter/setter。更实际的好处是你可以先用普通属性写代码以后需要加校验、延迟计算、只读限制时把属性改成property而不影响调用方的代码。这是Python做渐进式设计的重要手段。8.3 issubclass、isinstance在框架代码里的应用issubclass(cls, classinfo)判断一个类是否是另一个类的子类。在写框架、插件、注册机制时它和isinstance配合能做非常严格且灵活的类型检查。比如实现一个类型分派注册表class BaseHandler: pass handlers {} def register(cls): if issubclass(cls, BaseHandler): handlers[cls.__name__] cls return cls这段代码的核心逻辑是只允许注册BaseHandler的子类防止别人把一个完全不相干的类塞进体系里。这种防御式检查在合作项目、框架开发中能提前暴露很多错误。为什么强调isinstance和issubclass而不是直接比较相等因为要留出扩展空间。一个插件系统里只要新的插件类继承自BaseHandler框架就能识别它。如果比较相等每加一个新类都要改框架代码系统就僵死了。9. 输入输出与文件操作类函数print、input、format、open的实用细节9.1 print的sep、end、file三个参数print(*objects, sep , end\n, filesys.stdout, flushFalse)可能是所有人用的第一个函数但大部分人只用到了它的前10%功能。sep参数指定多个输出值之间的分隔符默认是空格。比如打印CSV风格的自定义分隔数据print(a, b, c, sep,)输出a,b,c。end参数指定结尾字符串默认是换行。想在同一行连续输出进度信息时for i in range(5): print(., end, flushTrue)注意这里我加了flushTrue。因为stdout默认是带缓冲的如果后面没有换行符内容可能不会立即写到终端。flushTrue强制刷新缓冲区写进度条、实时日志时特别重要。file参数指定输出目标默认是sys.stdout。把日志直接输出到文件时with open(log.txt, a) as f: print(some log, filef)这比f.write(some log\n)少写一个换行符顺手很多。9.2 format的格式规格与f-string的取舍format(value, format_spec)是另一个被低估的函数。f-string虽然写起来方便但format的价值在于format_spec可以是一个运行时计算的字符串灵活性更强。常见的格式规格print(format(3.14159, .2f)) # 3.14 print(format(255, 08b)) # 11111111 print(format(255, x)) # ff print(format(abc, 10)) # abc右对齐宽度10 print(format(42, d)) # 42这里的格式规格语法跟f-string里的冒号后部分完全一致本质上f-string调用format是同一套机制。区别在于format的规格可以动态构造比如根据配置决定小数位数digits 3 value 3.14159265 print(format(value, f.{digits}f)) # 3.142如果格式简单且写死在代码里f-string可读性更好如果格式需要动态生成用format更自然。9.3 open的encoding参数Windows用户最容易踩的坑open(file, moder, encodingNone)是文件操作的核心入口。大部分教材讲open只讲mode参数很少讲encoding导致大量Windows用户在读取UTF-8文本时栽跟头with open(data.txt, r) as f: content f.read() # 报错 UnicodeDecodeError: gbk codec cant decode byte ...原因很简单Windows上的Python默认编码是本地编码通常是GBK而现代文本文件基本都是UTF-8。解决方式也很简单with open(data.txt, r, encodingutf-8) as f: content f.read()我建议在代码里打开任何文本文件时都显式指定encoding不要依赖系统默认值。因为不同的运行环境默认编码不同同样的代码在Windows正常、在Linux上可能乱码反之亦然。显式指定等于把不确定性消灭掉。另外读取大文件时别一次性read()进内存用for line in f逐行迭代with open(large.log, r, encodingutf-8) as f: for line in f: process(line)这里with语句的作用是上下文管理器不管处理过程是否抛异常文件都会在离开with块时被正常关闭。忘了with或用完没close文件句柄泄漏的问题在长驻服务里会累积成严重故障。10. 零散但救命ord、chr、len、hash等函数的实际应用10.1 ord和chr字符与码位之间的桥ord(char)返回字符的Unicode码位整数chr(code)反过来根据码位返回字符。写字符串处理、加密算法、进制转换时特别好用print(ord(A)) # 65 print(chr(65)) # A print(chr(20013)) # 中需要把一个字符串转成ASCII码列表或者按字符范围生成连续字母序列时这两个函数是标配。10.2 hash的实际应用场景hash(obj)返回对象的哈希值看起来冷门但理解它有助于理解Python字典的底层机制。字典之所以查找快不是因为它记住了位置而是通过哈希函数把键直接映射到存储位置再通过相等比较处理冲突。动手验证一下哈希的性质print(hash(python)) # 每次运行可能不同进程内稳定 print(hash(42)) # 42整数哈希等于它本身 print(hash((1, 2, 3))) # 元组可哈希 # print(hash([1, 2, 3])) # TypeError列表不可哈希注意字符串等对象的哈希值在Python进程间默认是随机化的PYTHONHASHSEED所以不要依赖哈希值的具体数值跨进程传递。哈希的实际应用更多体现在自定义类上想让对象能作为字典键或放进集合需要保证对象不可变且正确实现__hash__和__eq__。10.3 我对内置函数使用的个人习惯聊了这么多具体用法最后分享一个我自己的习惯写任何数据处理逻辑之前先问自己一句Python是不是已经有内置函数能解决这个问题。这个习惯帮我省掉了大量手写工具函数的时间和后续维护成本。比如要统计列表

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价