当前位置: 首页 > 科技观察

写Python代码的几个重要技巧

时间:2023-03-13 19:44:13 科技观察

程序设计的好与坏早在我们年轻的时候就已经接触到了,只是没想到这么重要。可以立即改进程序设计并编写出“好”代码的知识包括以下几点:?面向对象的五个基本原则;?三种通用架构;?绘画;?好名字;?优化嵌套ifelse代码;当然,其他技术知识的丰富程度也决定了程序设计的好坏。比如通过引入消息队列解决双端性能差异问题,通过增加缓存层提高查询效率等。下面我们来看看上面列出的知识点都包含哪些内容,这些内容如何帮助改进代码和程序设计。面向对象的五个基本原则本书的作者是一名2010级的学生,而面向对象是作者年轻时发展起来的一种编程范式。它的五个基本原则是:?单一职责原则;?开闭原则;?依赖倒置原则;?接口隔离原则;对代码质量的影响。立竿见影的单一职责原则是正确的,立竿见影的结果和优秀的结果。对于我们这些无师自通编程的人来说,实现功能就够了,没时间考虑代码优化和维护成本。随着时间的推移,我花了很长时间才意识到编程是如此重要。俗话说,只要代码够烂,改进就够明显。举个例子,从文件内容中匹配关键数据,根据匹配结果发送网络请求,看看大部分程序员是怎么写的:importreimportrequestsFILE="./information.fet"defextract(file):fil=open(file,"r")content=fil.read()fil.close()find_object=re.search(r"url=\d+",content)find=find_object.group(1)text=requests.get(find)returntextif__name__=="__main__":text=extract(FILE)print(text)要求已经完成,这是毫无疑问的,但是问题来了:?读取文件时出现异常怎么办??如果数据源发生变化怎么办??如果网络请求返回的数据不符合最终要求怎么办?如果你心里的第一反应是改代码,那你就要注意了。完成一个任务的过程中某个环节发生变化,不可避免地要更改代码,但是如果按照上面的写法,不仅代码会越来越乱,连逻辑也会越来越乱.单一职责原则表示一个函数应该尽可能只做一件事,不要在一个函数中混合做多件事。如果把上面的代码重新设计一下,我觉得至少应该是这样的:""发出网络请求"""returndeftrim(val):"""修剪数据"""returndefextract(file):"""提取目标数据"""source=get_source()content=extract_(source)text=trim(fetch(content))returntextif__name__=="__main__":text=extract(FILE)print(text)将一个函数中实现的多个步骤拆分成多个更小的函数,每个函数只做一件事。当数据源发生变化时,只需要更改get_source相关的代码即可;如果网络请求返回的数据不符合最终要求,我们可以在trim函数中对其进行裁剪。这样一来,代码响应变化的能力得到了极大的提升,整个过程也变得更加清晰易懂。变更前后的变化如下图所示:单一职责原则的核心是解耦和增强内聚。如果一个函数承担了太多的职责,就相当于将这些职责耦合在一起。这种耦合将导致脆弱的设计。进行更改时,原始设计会遭受意外中断。单一职责原则实际上是将一件事情拆分成多个步骤,代码修改的影响很小。开闭原则和依赖倒置原则让代码稳定性飙升。开闭原则中的开闭原则是指对扩展开放,封闭是指对修改关闭。需求总是在变化。如果业务方要求你这个月把数据存入MySQL数据库,下个月可能会让你导出到Excel表格。这时候就得改代码了。这种场景和上面的单一职责原则非常相似。它还面临代码更改。单一职责原则的例子主要表达了通过解耦来减少变更的影响。这里的主要表现是通过对扩展开放,对修改关闭,来提高程序对变化的响应能力。能力和提高程序的稳定性。稳定这个词怎么理解?较少的变化或没有变化被认为是稳定的。稳定性是指调用该对象的其他代码得到的结果是可确定的,整体是稳定的。按照一般程序员的写法,数据存储的代码大概是这样的:MySQLSave()毫无疑问,saver.insert()函数是可以实现的。让我们看看它如何响应变化。如果要更换存储,就意味着需要改代码。根据上面的代码示例,有两种选择:?重写一个存储在ExcelSave中的类;?更改MySQLSave类;无论您选择哪个,以上两个选项都会改变2个类。因为不仅存储类要改,调用处的代码也要改。因此,它们作为一个整体是不稳定的。如果改变实现方式,按照依赖倒置的设计指导,就可以轻松应对这个问题。边看代码边理解:importabcclassSave(metaclass=abc.ABCMeta):@abc.abstractmethoddefinsert(self):pass@abc.abstractmethoddefupdate(self):passclassMySQLSave(Save):def__init__(self):self.classify="mysql"passdefinsert(self):passdefupdate(self):passclassExcel(Save):def__init__(self):self.classify="excel"definsert(self):passdefupdate(self):passclassBusiness:def__init__(self,saver):self.saver=saverdefinsert(self):self.saver.insert()defupdate(self):self.saver.update()if__name__=="__main__":mysql_saver=MySQLSave()excel_saver=Excel()business=Business(mysql_saver)这里通过内置abc实现了一个抽象基类。这个基类的目的是强制子类实现需要的方法,以达到子类功能的统一。子类功能统一后,无论调用哪个子类都是稳定的,不会出现调用者需要修改方法名或修改传入参数的情况。依赖倒置的倒置是指依赖倒置。前面的代码是调用方Business依赖对象MySQLSave。一旦需要更换MySQLSave这个对象,就需要改变Business。DependencyInversion中的依赖指的是对象之间的依赖关系。以前,它依赖于实体。如果换成后一种依赖抽象的方式,情况就会反过来:EntityBusiness依赖抽象。有一个优点:抽象是稳定的。抽象比可变实体更稳定。代码改动前后的依赖关系发生了较大的变化。调用方Business之前直接依赖实体MySQLSave,依赖倒置后,Businesses、ExcelSave、MySQLSave都依赖抽象。这样做的好处是,如果需要更换存储,只需要新建一个存储实体,然后在调用业务的时候传入即可,这样就不需要改业务的代码,并且符合对修改封闭,对扩展开放的开闭原则;反转的具体实现使用了一种叫做依赖注入的方法。其实单纯使用依赖注入而不使用依赖倒置也可以满足开闭原则的要求。有兴趣的读者不妨试一试。Pickandchoose接口隔离原则接口隔离原则中的接口指的是Interface,并不是Web应用中的Restful接口,但在实际应用中可以抽象理解为同一个对象。在设计层面,接口隔离原则与单一职责原则的目的是一致的。接口隔离原则的指导思想是:?调用者不应该依赖它不需要的接口;?依赖关系应该建立在最小的接口上;这实际上告诉我们要在界面上瘦身,功能太多的界面可以通过拆分的方式来优化。例如,现在为库设计一个抽象类:.abstractmethoddefshelf_on(self):pass图可以购买、借用、下架、上架,貌似没问题。但是这样一来,这个抽象就只能被管理者使用了,用户操作的时候需要设置一个新的抽象类,因为用户不可能把书从一个架子上架到另一个架子上去操作。接口隔离原则推荐的方法是将图书上架下架和图书购买借阅分为两个抽象类。管理端图书类继承2个抽象类,用户端图书类继承1个抽象类。好像有点绕,别慌,我们看图理解一下:这样是不是很容易理解?这个指导思想非常重要。它不仅可以指导我们设计抽象接口,还可以指导我们设计Restful接口。它还可以帮助我们发现现有界面中存在的问题,从而设计出更合理的程序。合成重用原则的指导原则是:尽量使用对象组合而不是继承来达到重用的目的。合成复用的作用是减少对象之间的依赖,因为继承是一种强依赖关系,无论子类使用了父类的哪些属性,子类都需要完全拥有父类。组合通过另一种方式来实现对象之间的关联,减少依赖。为什么建议先使用组合复用,再考虑继承?由于继承的强依赖关系,一旦依赖对象(父类)发生变化,依赖对象(子类)也需要随之变化,组合重用可以避免这种情况。需要注意的是,建议先使用复用,但也不拒绝使用继承,该用的地方一定要用。下面以一段代码为例来说明合成复用和继承的区别::defmove(self):passdefengine(self):pass这里,Car是父类,有两个重要的属性:move和engine。这时候如果需要给汽车涂上颜色,就需要添加一个颜色属性,三个类都要添加。.如果使用复合复用的方法,可以这样写::passclass对象合成复用的具体操作是在一个类中实例化一个类对象,然后在需要的时候调用它。代码可能没有那么直观,我们看图:这个例子主要是为了说明继承和组合复用前后的具体实现和变化。没必要深究Car的继承,因为如果硬要讨论为什么右图中有2个Car不需要继承,那你就会陷入死胡同。这里合成复用的实现方法是在两个Car中实例化另外一个类Color。其实也可以通过依赖注入的方式在外部实例化Color,然后将实例对象传递给两个Car。常见的三种架构了解各种不同的架构可以拓宽我们的知识面,我们可以在面对一类问题时提出其他的解决方案。同时,了解多种架构可以让我们在设计阶段做好规划,避免后续频繁重构。三种常见的架构是:?单体架构;?分布式架构;?微服务架构;单体架构单体架构是我们平时接触比较多的一种架构,也是一种比较容易理解的架构。单体架构将所有功能聚合到一个应用程序中。我们可以简单地把这种架构看成:这种架构简单,易于部署和测试,大多数应用在初期都是采用单体架构。单体架构也有几个明显的缺点:?复杂度高,所有功能都集中在一个应用中,模块多,边界容易模糊,随着时间的推移和业务的发展,项目越来越大,代码越来越大。越多,整体服务效率会逐渐降低;?释放/部署频率低,一举两得。新功能或bug修复的发布上线需要多方协调,发布时间一拖再拖。项目大,施工时间长,施工失败的概率也会增加;?性能瓶颈明显。一头牛再强大,也比不上多头牛的效果。随着数据量和并发请求的增加,读取性能的不足是最差的暴露出来,然后你会发现其他方面跟不上;?影响技术创新:单体架构通常使用单一语言或框架开发。引入新技术或获取现代服务非常困难。难的;?可靠性低,一旦服务出现问题,影响巨大。分布式架构与单体架构相比,分布式架构通过拆分解决了单体架构面临的大部分问题,比如性能瓶颈。如果说单体架构是一头牛,那么分布式架构就是多牛:当单体架构出现性能瓶颈时,团队可以考虑将单体架构转为分布式架构,以提升服务能力。当然,分布式也不是万能的。解决了单一架构的性能瓶颈和可靠性低的问题,但复杂性、技术创新和发布频率低等问题依然存在。这时候可以考虑微服务。微服务架构微服务架构的关键词是拆解,将原本组合在一个应用中的多个功能拆分成多个小应用,这些小应用串联起来形成一个完整的应用,与之前的单体架构具有相同的功能。具体图示如下:每个微服务都可以独立运行,它们之间通过网络协议进行交互。每个微服务可以部署多个实例,使其具有与分布式架构相同的性能。单个服务的发布/部署对其他服务的影响很小,代码上没有关联,可以经常发布新版本。复杂的问题很容易解决。拆分后架构逻辑清晰,功能模块职责单一,功能和代码的增加不会影响整体效率。服务独立后,项目就变得与语言无关了。评估服务可以用Java语言或Golang语言实现。不再受语言或框架的限制,技术创新的问题得以缓解。这不是很像单一职责原则和接口隔离原则吗?分布式和微服务都不是灵丹妙药从上面的对比来看,分布式架构好于单体架构,微服务架构好于分布式架构。那么微服务架构>分布式架构>单体架构?这种理解是错误的。需要根据场景和需求选择架构。微服务架构和分布式架构看起来很美,但同时也带来了很多新的问题。以微服务架构为例:?运维成本高。在单体架构中,运维只需要保证一个应用的正常运行,关注点可能只是硬件资源消耗。但是如果换成微服务架构,应用的数量是几百甚至上千。当一个应用出现问题或者多个应用之间的协调不正常时,运维人员的头会变大;?分布式系统固有的复杂性、网络分区、分布式事务、流量均衡等对开发者和运维的冲击;?接口调整成本高,一个接口的调用者可能很多。如果设计不遵循开放封闭和接口隔离的原则,调整量会很大;?接口性能有限。本来交互是通过函数调用来完成的,但是在内存中很快就完成了。模块,但是如果不考虑语言,还要考虑性能(不做界面交互),需要自己实现一个功能相同的代码;使用哪种架构取决于具体场景。如果你的系统复杂度不是那么高,对性能的追求不是那么高,比如一个每天只有几万条数据的爬虫应用,单一架构就可以解决问题,没必要强制分布式或者微服务,因为这只会增加你的工作量。画好图在需要表达关系和逻辑排序的场景中,图永远比代码好。业内流行一句话,“程序开发,设计先行”,意思是在开发之前需要对程序进行构思和设计。试想,如果你连对象关系和逻辑都解释不清楚,写出来的代码是好代码吗?在构思项目时,可以使用用例图来挖掘需求和功能模块;协作图可用于架构设计时梳理模块关系;在设计接口或类对象时,可以使用类图来制定交互计划;设计功能时可以使用状态图帮助我们挖掘功能属性……了解绘图的重要性后,具体的绘图方法和技巧可以参考本书工程师绘图指南章节(♂)。选择一个好名字你还记得你曾经命名过的名字吗:?reversalList?get_translation?get_data?do_trim?CarAbstract一个好的、合适的名字可以让代码风格更统一,看起来更清晰。想出一个好名字不仅是单词语法的问题,也是文体选择和用法的问题。具体的命名方法和技巧,可以参考本书的命名选择与风格指南章节(《Python 编程参考》)进行学习。优化嵌套的ifelse代码写代码的时候用一些控制语句很正常,但是ifelse嵌套太多了,也是很头疼的,代码如下所示。这个结构的产生是因为使用了if语句来检查先决条件,如果条件满足,则转到下一行代码,如果不满足,则停止。在这种情况下,我们可以反转先决条件检查。代码改完之后是这样的:if"http"notinurl:returnif"www"notinurl:return这是我常用的优化方法,后来在张叶老师的付费栏目里,看到了这个方法的名字——韦句。当然,对于这种简单的逻辑处理和ifelse控制流程,使用guard语句是非常有效的,但是如果逻辑比较复杂,使用guard语句的效果就没那么好了。假设汽车4S店有折扣授权限制,普通销售有权对30万以下的汽车给予一定的折扣,精英销售则需要授权30万以上80万以下,以及更高的价格-定价车优惠需经店长授权。该函数可以概括为根据金额确定授权人,对应代码如下:defbuying_car(price):ifprice<300000:print("普通销售")elifprice<800000:print("精英销售")elifprice<1500000:print("StoreManager")代码思路清晰,但存在的问题也很明显。如果后面扩展价格和评级,就会增加更多的ifelse语句,代码会变得臃肿。控制语句的顺序在代码中是固定的,如果要调整顺序,只能修改控制语句。那么问题来了,有没有比ifelse更合适的方式呢?这时候可以考虑一种叫做责任链的设计模式。责任链设计模式的定义是:为了避免请求发送者与多个请求处理者耦合,所有的请求处理者记住他们的下一个对象的引用连接成一个链;当请求发生时,请求可以沿着这条链传递,直到一个对象处理它。好像有点绕。我们通过代码图来加深一下理解:在处理类执行之前,根据前提判断自己的handler是否可以处理。如果没有,则交给next_handler,也就是责任链中的下一个节点。上面的责任链实现为:classManager:def__init__(self,):self.obj=Nonedefnext_handler(self,obj):self.obj=objdefhandler(self,price):passclassGeneral(Manager):defhandler(self,price):ifprice<300000:print("{}CommonSales".format(price))else:self.obj.handler(price)classElite(Manager):defhandler(self,price):if300000<=price<800000:print("{}EliteSales".format(price))else:self.obj.handler(price)classBOSS(Manager):defhandler(self,price):ifprice>=800000:print("{}Manager".format(price))抽象类和具体处理类创建之后,还没有关联。我们需要将它们挂载在一起形成一条链:general=General()elite=Elite()boss=BOSS()general.next_handler(elite)elite.next_handler(boss)这里建立的责任链是General->Elite->BOSS,来电者只需要将价格传给General,如果没有折扣权就交给Elite,如果Elite没有折扣权就交给到BOSS。对应的代码如下:prices=[550000,220000,1500000,200000,330000]forpriceinprices:general.handler(price)这个和我们去4S店买车是一样的。作为客户,我们只需要确认我们想要购买的汽车。至于4S店如何申请优惠,谁授权与我无关,只要能拿到相应的优惠就行。至此,ifelse优化知识学习完毕。