当前位置: 首页 > Web前端 > HTML

PythonTinkerers:使用数字和字符串的技巧

时间:2023-03-29 12:09:38 HTML

前言这是“PythonTinkerers”系列的第三篇文章。数字是几乎所有编程语言中最基本的数据类型,是我们通过代码连接现实世界的基础。Python中共有三种数值类型:整数(int)、浮点数(float)和复数(complex)。在大多数情况下,我们只需要处理前两个。整数在Python中更省心,因为它们无符号且无符号且永远不会溢出。但是浮点数和大多数其他编程语言一样,仍然存在精度问题,这常常让很多刚进入编程世界的新人感到困惑:“为什么浮点数不准确?”。Python中的字符串比数字复杂得多。要想掌握它,就得搞清楚bytes和str的区别。如果更倒霉,你还是Python2用户,就够你喝几壶unicode和字符编码问题了(赶紧迁移到Python3,今天!)。不过,以上都不是本文的主题。如果你有兴趣,你可以在网上找到成堆的相关资料。在本文中,我们将讨论一些更微妙和不太常见的编程实践。帮助您编写更好的Python代码。内容目录最佳实践1少写字面数字,使用enum枚举类型改进代码2裸字符串处理不要走得太远3不需要预先计算字面表达式实用技巧1多行出现多行时级别缩进字符串2布尔值其实就是“数字”3提高超长字符串的可读性4不要忘记那些以“r”开头的内置字符串函数5使用“无限”的常见错误float("inf")1"value=1"不是线程安全的2字符串拼接并不慢最佳实践1.少写字面数字“整数字面值”是指直接出现在代码中的数字。它们分布在代码的各个角落,例如代码delusers[0]中的0就是一个数字字面量。它们简单、实用,而且每个人每天都在写作。但是,当您在代码中不断重复某些文字时,您的“代码质量警告灯”应该呈黄色亮起。比如你刚入职一个你崇拜已久的新公司,同事交给你的项目中有这样一个功能:defmark_trip_as_featured(trip):"""在推荐栏中添加行程"""iftrip.source==11:do_some_thing(trip)eliftrip.source==12:do_some_other_thing(trip)......return这个函数是做什么的?您正在尝试弄清楚,但是trip.source==11呢?那么==12呢?这两行代码很简单,没有使用任何神奇的功能。但是,如果您不熟悉这些代码,可能需要整个下午才能弄清楚它们的含义。问题在于那几个数字文字。最先写这个功能的人可能是公司成立之初加入的老程序员。而且他很清楚那些数字的含义。但如果您是这段代码的新手,那就完全是另外一回事了。使用枚举改进代码那么,如何改进这段代码呢?最直接的方式就是给这两个条件分支加上注释。但在这里,“添加注释”显然不是提高代码可读性的最佳方式(事实上,在其他大多数情况下并非如此)。我们需要用有意义的名称来替换这些字面量,而枚举类型(enum)最适合于此。enum是Python从3.4版本开始引入的内置模块。如果您使用的是早期版本,可以通过pipinstallenum34安装。这是使用枚举的示例代码:#-*-coding:utf-8-*-fromenumimportIntEnumclassTripSource(IntEnum):FROM_WEBSITE=11FROM_IOS_CLIENT=12defmark_trip_as_featured(trip):iftrip.source==TripSource.FROM_WEBSITE:do_some_thing(trip)eliftrip.source==TripSource.FROM_IOS_CLIENT:do_some_other_thing(trip)......return将重复的数字字面量定义为枚举类型,这样不仅提高了代码的可读性,同时也降低了出现bug的几率减少。试想一下,如果你在判断某一个分支的时候,把11误打成了111,会发生什么?我们总是犯这个错误,而且很难及早发现。把这些数字字面量都放到枚举类型中,可以更好的避免这种问题。同样,将字符串文字重写为枚举会产生相同的好处。使用枚举类型而不是文字的好处:提高代码可读性:没有人需要记住某个幻数代表什么提高代码正确性:减少由错误的数字或字母引起错误的可能性当然,你没有在all需要把代码中所有的字面量都改成枚举类型。可以使用代码中出现的文字,只要它们在放置它们的上下文中易于理解即可。比如那些经常作为数字下标出现的0和-1就完全没问题了,因为大家都知道是什么意思。2.原始字符串处理不要走得太远什么是“裸字符串处理”?在本文中,它指的是仅使用基本的加减乘除和循环,通过内置函数/方法来操作字符串以获得我们需要的结果。每个人都写过这样的代码。有时我们需要拼接一大段发送给用户的报警信息,有时我们需要构造一大段发送给数据库的SQL查询语句,像这样:deffetch_users(conn,min_level=None,gender=None,has_membership=False,sort_field="created"):"""获取用户列表:paramintmin_level:最低要求的用户级别,默认为所有级别:paramintgender:过滤用户性别,默认为所有性别:paraminthas_membership:filterall会员/非会员用户,默认为非会员:paramstrsort_field:排序字段,默认创建"用户创建日期":returns:list:[(UserID,UserName),...]"""#一种古老的SQL拼接技术,使用“WHERE1=1”简化字符串拼接操作#区分查询参数,避免SQL注入问题statement="SELECTid,nameFROMusersWHERE1=1"params=[]ifmin_levelisnot无:语句+="ANDlevel>=?"params.append(min_level)如果性别不是None:语句+="ANDgender>=?"params.append(gender)ifhas_membership:statement+="ANDhas_membership==true"else:statement+="ANDhas_mmembership==false"statement+="ORDERBY?"params.append(sort_field)returnlist(conn.execute(statement,params))我们之所以这样拼接出需要的字符串——这里是SQLstatement-这是因为简单、直接、直观,但是这样做最大的问题是随着功能逻辑越来越复杂,这种拼接代码会变得容易出错,难以扩展。其实就是上面的Demo代码也只是没有明显的bug(谁知道有没有其他隐藏的问题),其实对于SQL语句这样的结构化、正则化的字符串,最好还是采用面向对象的方式来构造和编辑的方式.下面这段代码使用了SQLAlchemy模块完成同样的功能:deffetch_users_v2(conn,min_level=None,gender=None,has_membership=False,sort_field="created"):"""获取用户列表"""query=select([users.c.id,users.c.name])如果min_level不是None:query=query.where(users.c.level>=min_level)如果性别不是无:query=query.where(users.c.gender==gender)query=query.where(users.c.has_membership==has_membership).order_by(users.c[sort_field])returnlist(conn.execute(query))上面的fetch_users_v2函数更短,也更易于维护,完全不用担心SQL注入问题。所以,当你的代码中出现复杂的裸字符串处理逻辑时,请尝试用下面的方式代替:问:目标/源字符串是否结构化并遵循一定的格式?是:找找有没有开源的对象模块来操作它们,或者自己写一个SQL:SQLAlchemyXML:lxmlJSON,YAML...否:尝试使用模板引擎,而不是复杂的字符串处理逻辑来达到目的Jinja2makoMustache3。计算字面量表达式偶尔会在我们的代码中出现一些复数,像这样deff1(delta_seconds):#如果超过11天,什么都不做ifdelta_seconds>950400:return...之前我说了,没有错上面的代码。首先,我们在一本小书上计算(当然像我这样聪明的人会用IPython):11天有多少秒?.然后把结果950400这个幻数填入我们的代码,最后心满意足的在上面加上一行注释:告诉大家这个幻数是怎么来的。我想问的是:“为什么我们不把代码写成好像delta_seconds<11243600:?”“性能”,答案一定是“性能”。我们都知道Python是一种(速度很差)解释型语言,所以预计算950400正是因为我们不想在每次调用函数f1时都承担这部分计算开销。但事实是:即使我们把代码改成ifdelta_seconds<11243600:,函数也不会有任何额外的开销。Python代码在执行时被解释器编译成字节码,字节码中隐藏着真相。我们用dis模块看看:deff1(delta_seconds):ifdelta_seconds<11*24*3600:returnimportdisdis.dis(f1)#dis执行结果50LOAD_FAST0(delta_seconds)2LOAD_CONST1(950400)4COMPARE_OP0(<)6POP_JUMP_IF_FALSE1268LOAD_CONST0(无)10RETURN_VALUE>>12LOAD_CONST0(无)14RETURN_VALU请参阅上面的2LOAD_CONST1(950400)?这意味着当Python解释器将源代码编译成字节码时,它会计算整数表达式11243600并将其替换为950400。因此,当我们的代码中需要有复杂的计算字面量时,请保留整个计算公式。它对性能没有影响,并且会增加代码的可读性。提示:除了预先计算数字文字表达式外,Python解释器还对字符串和列表执行类似的操作。一切都与性能有关。谁让你抱怨Python很慢?实用技巧1、布尔值其实就是“数字”。在大多数情况下,Python中的True和False这两个布尔值可以直接等价于两个整数1和0,就像这样:>>>True+12>>>1/FalseTraceback(最近调用last):File"",line1,inZeroDivisionError:divisionbyzero那么记住这个有什么用呢?首先,当需要计算总和时,可以配合sum函数简化操作:>>>l=[1,2,4,5,7]>>>sum(i%2==0foriinl)2另外,如果用布尔表达式作为列表的下标,可以达到类似三元表达式的目的:#类似三元表达式:"Javascript"if2>1else"Python">>>["Python","Javascript"][2>1]'Javascript'2.提高超长字符串的可读性单行代码的长度不要太长。比如PEP8建议每行字符数不要超过79,在现实世界中,大多数人遵循的单行最大字符数在79到119之间,如果只是代码,这样的要求比较容易满足,但是如果代码中需要出现一个超长的字符串怎么办?这时候,除了用斜杠\和加号+把长字符串分成几段,还有一个更简单的方法:用括号把长字符串包起来,然后就可以随意换行了:defmain():logger.info(("过程中确实发生了一些不好的事情。""请联系你的管理员。"))当日常编码中出现多行缩进时,还有另一种麻烦Case。有必要在已经具有缩进级别的代码中插入多行字符串文字。因为多行字符串不能包含当前的缩进空格,所以我们需要这样写代码:defmain():ifuser.is_active:message="""欢迎,今天的电影列表:-大白鲨(1975)-闪灵(1980)-Saw(2004)""但是这样写会破坏整个代码的缩进视觉效果,很突兀。有很多方法可以改进。比如我们可以把这个多行字符串提取为avariable到模块的最外层。但是,如果在你的代码逻辑中更适合使用字面量,你也可以使用标准库textwrap来解决这个问题:fromtextwrapimportdedentdefmain():ifuser.is_active:#dedent将缩进最左边的空字符串message=dedent("""\欢迎,今天的电影列表:-大白鲨(1975)-闪灵(1980)-电锯惊魂(2004)""")3.不要忘记以“r”开头的内置字符串函数。Python的字符串有很多有用的内置方法,最常用的有.strip()、.split()等。大部分都是e内置方法,处理顺序是从左到右。但它也包括一些从右到左以r开头的镜像方法。在处理具体的逻辑时,使用它们可以让你事半功倍。假设我们需要解析一些访问日志,日志格式为:"{user_agent}"{content_length}>>>log_line='"AppleWebKit/537.36(KHTML,likeGecko)Chrome/70.0.3538.77Safari/537.36"47632'if使用。split()将日志拆分成(user_agent,content_length),我们需要这样写:>>>l=log_line.split()>>>"".join(l[:-1]),l[-1]('"AppleWebKit/537.36(KHTML,likeGecko)Chrome/70.0.3538.77Safari/537.36"','47632')但是如果使用.rsplit(),处理逻辑更直接:>>>log_line.rsplit(None,1)['"AppleWebKit/537.36(KHTML,likeGecko)Chrome/70.0.3538.77Safari/537.36"','47632']4.如果有人问你,使用"infinity"float("inf"):"PythonWhat数字是最大/最小的?”。你应该怎么回答?这样的事情存在吗?答案是:“是的,它们是:float("inf")和float("-inf")"。它们分别对应数学世界中的正无穷大和负无穷大。当它们与任何值进行比较时,满足以下规则:float("-inf")<任何值>>users={"tom":19,"jenny":13,"jack":None,"andrew":43}>>>sorted(users.keys(),key=lambdauser:users.get(user)orfloat('inf'))['jenny','tom','andrew','jack']#B.作为循环的初始值,简化第一次判断的逻辑>>>max_num=float('-inf')>>>#寻找列表中最大的数>>>foriin[23,71,3,21,8]:...:ifi>max_num:...:max_num=i...:>>>max_num71常见误解1."value+=1"不是线程安全的当我们编写多线程程序时,我们经常需要处理复杂的共享变量和竞争条件。“线程安全”通常用来描述某种行为或某种类型的数据结构,可以在多线程环境中共享使用,并产生预期的结果。一个典型的满足“线程安全”的模块就是队列模块。而我们经常做的value+=1操作,很容易被想当然地认为是“线程安全的”。因为它看起来像一个原子操作(指的是最小的操作单元,在执行过程中不会插入其他操作)。然而,事实并非如此,虽然从Python代码的角度来看,value+=1的操作似乎是原子的。但是当它最终被Python解释器执行时,它就不再是“原子的”了。我们可以使用上面提到的dis模块来验证:defincr(value):value+=1#使用dis模块查看字节码importdisdis.dis(incr)0LOAD_FAST0(value)2LOAD_CONST1(1)4INPLACE_ADD6STORE_FAST0(value)8LOAD_CONST0(None)10RETURN_VALUE在上面的输出结果中可以看到,这条简单的累加语句会被编译成包括取值和存储在内的几个不同的步骤。在多线程环境中,任何其他线程都可能在其中一个步骤中插入,从而阻止您获得正确的结果。因此,请不要凭直觉来判断某个行为是否“线程安全”,否则当程序在高并发环境下出现奇怪的bug时,你会为自己的直觉付出惨重的代价。2、字符串拼接不慢刚接触Python的时候,在某网站看到这样的说法:“Python中的字符串是不可变的,所以每拼接一个字符串,就会生成一个新的对象。导致new内存分配,非常低效”。我对此毫不怀疑。所以,很长一段时间,我尽量避免使用+=来连接字符串,而是使用"".join(str_list)代替。然而一个偶然的机会,我对Python的字符串拼接做了一个简单的性能测试,发现:Python的字符串拼接一点都不慢!查阅了一些资料后,终于弄清了真相。Python的字符串连接在2.2和更早版本中确实很慢,这与我第一次看到的行为一致。但是因为这个操作太常用了,所以在后面的版本中专门针对它做了性能优化。大大提高了执行效率。现在用+=来拼接字符串,效率和".join(str_list)"很接近了。所以,该拼接的时候就拼接,不用担心任何性能问题。提示:如果想了解更详细的相关内容,可以阅读这篇文章:Python-EfficientStringConcatenationinPython(2016edition)-smcl结语以上是《Python工匠》系列文章的第三篇,下面是内容比较零散。由于篇幅原因,一些常用的字符串格式化等操作在文章中没有涉及。有机会我会再写的。最后总结一下要点:写代码的时候,请考虑读者的感受,不要出现太多神奇的文字。在操作结构化字符串时,使用对象模块比直接处理更有优势。请多多使用它来验证您的猜测。在多线程环境中编码是非常复杂的,所以要足够谨慎,不要相信你的直觉。Python语言的更新非常快,不要被别人的经验影响。看完文章,你有什么想法吗?吐槽?请发表评论或在项目GithubIssues中让我知道。往期Python工匠推荐:善用变量提升代码质量打造业界领先的一站式自动化运维平台。目前已经推出社区版和企业版,欢迎体验。请点击访问蓝鲸官网:http://bk.tencent.com/