包含编程资料、学习路线图、源代码、软件安装包等!【[点击这里] 】!
is和==的误用
is 比较对象 ID(内存地址,通过 id() 函数获取), == 比较对象值小整数和短 字符串可能被驻留优化,导致 is 误判。
如下所示的代码中,变量 a 和 b 其实是两个完全不同的对象,但是通过 is 判断时,结果都是 True,显然不是程序开发者预期的结果。
def main ( ) :
a = 256
b = 256
print ( id ( a) , id ( b) )
print ( a is b)
a = 1_000_000
b = 1_000_000
print ( id ( a) , id ( b) )
print ( a is b)
s1 = "hello!"
s2 = "hello!"
print ( id ( a) , id ( b) )
print ( s1 is s2)
最佳实践:始终用 == 比较值,is 仅用于 None、True、False 三种类型比较。
字符串结尾反斜杠
字符串不能以单个 反斜杠 结尾,否则会导致语法错误。 原因: 因为最后一个反斜杠,转义了结尾的字符串引号。
def main ( ) :
path1 = r"C: \new_folder\"
path2 = r"\"
修正方案当然也很简单,直接再对末尾的反斜杠进行二次转义即可。
def main ( ) :
path1 = r"C:\new_folder\\"
path2 = r"\\"
zap截取到最短匹配
zip() 函数在参数中,最短的迭代器结束时停止,不会为其他剩余元素配对。
def main ( ) :
names = [ "Alice" , "Bob" , "Carol" ]
scores = [ 90 , 85 ]
print ( list ( zip ( names, scores) ) )
如果需要填充缺失的值,可以使用 itertools.zip_longest(…) 或者指定参数 strict=True 强制进行剩余元素匹配,但是这样当迭代器元素数量不同时,就会出现报错
zip ( ) argument 2 is shorter than argument 1 . . .
列表的浅拷贝初始化
下面的二维数组初始化之后,结果并未像预期一样,只有第一个元素 grid[0][0] 的值是 1。
def main ( ) :
grid = [ [ 0 ] * 3 ] * 3
grid[ 0 ] [ 0 ] = 1
print ( grid)
问题的原因在于:* 操作符对于列表只复制引用,因为上面的代码中是一个二维数组,每一行都指向一个子数组/列表,所以最终每个子数组的第一个元素都是 1 (因为 3 个子数组/列表指向的是同一个引用)。
解决方案也很简单,就是为每个子数组/列表创建一个全新的值。
def main ( ) :
grid = [ [ 0 ] * 3 for _ in range ( 3 ) ]
grid[ 0 ] [ 0 ] = 1
print ( grid)
可变类型作为函数参数
Python 函数的参数默认值,如果是可变数据类型 (列表/字典/集合),那么默认参数只在函数定义时创建一次,后续的函数调用拿到的默认值都是同一个对象。 也就是说,函数每次调用完成后,默认值都会发生变化 (相当于变成全局变量参数的了)。
def my_append ( val, vals = [ ] ) :
vals. append( val)
return vals
def main ( ) :
print ( my_append( 100 ) )
print ( my_append( 100 ) )
print ( my_append( 100 , [ ] ) )
针对上面的问题,更加约定俗成的解决方法是采用 None 作为默认值,这样默认参数在每次调用时都会创建一个新的值。
def my_append ( val, vals = None ) :
if vals is None :
vals = [ ]
vals. append( val)
return vals
def main ( ) :
print ( my_append( 100 ) )
print ( my_append( 100 ) )
print ( my_append( 100 ) )
print ( my_append( 100 ) )
额外的思考
但是反过来说,我们又可以通过 Python 提供的这个默认值可变机制,来实现一些自定义数据结构,并对外开放非常简单友好的 API。 下面是一个模拟向数据库/数据表插入数据的示例代码 (仅限示例,请勿在生产环境中使用)。
def insert_record ( item, items = [ ] ) :
items. append( item)
return items
def main ( ) :
insert_record( 100 )
insert_record( 200 )
rows = insert_record( 300 )
print ( rows)
闭包和变量的延迟绑定
在 Python 中,闭包捕获的是变量本身而非调用闭包函数当时的值,导致在循环中创建的多个函数,最终都引用同一个循环变量的最后值,这就是所谓的 延迟绑定 问题。
def make_multipliers ( ) :
return [ lambda x: i * x for i in range ( 5 ) ]
def main ( ) :
funcs = make_multipliers( )
print ( [ f( 2 ) for f in funcs] )
解决方案: 利用默认参数在定义时绑定当前值。
def make_multipliers ( ) :
return [ lambda x, i= i: i * x for i in range ( 5 ) ]
def main ( ) :
funcs = make_multipliers( )
print ( [ f( 2 ) for f in funcs] )
生成器的遍历规则
不同于列表/字典/集合等基础数据类型,生成器 (数据类型) 有一个非常重要的特点:一次性消耗。 也就是说,生成器的所有元素只会被遍历一次,当所有的元素被遍历完成后,该生成器就会抛出异常 StopIteration。即使捕获并处理了异常,再次遍历时也无法再次访问到任何元素。
def main ( ) :
primes = [ 2 , 3 , 5 , 7 , 11 ]
numbers = ( x * 2for x in primes)
print ( type ( numbers) )
print ( 4in numbers)
print ( next ( numbers) )
print ( 100in numbers)
最简单直接的方案是将生成器的结果,一次性地放入到一个列表中,但是这样有一个潜在的问题:当生成器的元素很多时,可能出现性能问题甚至内存溢出 (OOM)。
为了解决潜在的内存性能问题,可以通过将生成器本身封装到一个专门的工厂函数中,这样每次调用时,都会返回一个新的生成器函数,避免对同一生成器返回调用时出现的问题。
def make_gen ( n) :
for i inrange( n) :
yield i
def main ( ) :
for x in make_gen( 5 ) :
print ( x)
for x in make_gen( 5 ) :
print ( x)
如果我们每次访问的都是同一个生成器的元素,并且可以在生成器 一次性消耗 之后,可以从零开始接着访问,然后无限循环这个访问过程,类似于数据结构中的环形队列。
这时就可以通过创建专属的自定义生成器类,然后重写迭代相关的魔术方法,最终实现 “循环利用” 生成器。
class Regenerator :
def __init__ ( self, factory, * args, ** kwargs) :
self. factory = factory
self. args = args
self. kwargs = kwargs
self. _gen = factory( * args, ** kwargs)
def __iter__ ( self) :
returnself
def __next__ ( self) :
try :
returnnext( self. _gen)
except StopIteration:
self. _gen = self. factory( * self. args, ** self. kwargs)
returnnext( self. _gen)
def make_gen ( n) :
for i inrange( n) :
yield i
def main ( ) :
reg = Regenerator( make_gen, 5 )
for _ inrange( 10 ) :
print ( next ( reg) )
生成器中忽略资源释放
生成器为惰性求值和节省内存提供了强大支持,最经典的应用场景中的大文件读取、惰性计算求值,但是稍不留神,就可能引发资源泄漏的问题。 下面的代码中,即使读取文件的生成器提前退出,已经打开的文件句柄也不会立即自动关闭,造成文件句柄资源闲置浪费,直到最终被 GC 回收。
def read_file ( path) :
f = open ( path)
for line in f:
iflen( line) == 0 :
return
yield line. rstrip( '\n' )
def main ( ) :
for row in read_file( '/var/log/nginx/access.log' ) :
. . .
解决方案也很简单,直接使用上下文管理器来负责文件句柄的生命周期,修改后的代码如下所示。
def read_file ( path) :
with open ( path, 'r' ) as f:
for line in f:
if len ( line) == 0 :
return
yield line. rstrip( '\n' )
cache装饰器解决递归问题
如果使用 @cache 或者 @lru_cache 缓存装饰器,来解决递归过程中的重复子任务计算问题,那么这个设计本身就存在缺陷。 递归需要考虑到 Python 语言本身对于递归深度的执行限制。比如当需要计算的数字很大时,递归函数在执行时会形成一个非常深的嵌套调用栈,当深度超过一定限制后,就会抛出 RecursionError 异常。
总结
最后希望你编程学习上不急不躁,按照计划有条不紊推进,把任何一件事做到极致,都是不容易的,加油,努力!相信自己!
文末福利
最后这里免费分享给大家一份Python全套学习资料,希望能帮到那些不满现状,想提升自己却又没有方向的朋友,也可以和我一起来学习交流呀。
包含编程资料、学习路线图、源代码、软件安装包等!【[点击这里] 】领取!
① Python所有方向的学习路线图,清楚各个方向要学什么东西 ② 100多节Python课程视频,涵盖必备基础、爬虫和数据分析 ③ 100多个Python实战案例,学习不再是只会理论 ④ 华为出品独家Python漫画教程,手机也能学习
可以扫描下方二维码领取【保证100%免费 】