51Testing软件测试论坛

标题: 写好 Python 代码的几条重要技巧 [打印本页]

作者: lsekfe    时间: 2021-6-29 09:49
标题: 写好 Python 代码的几条重要技巧
程序设计的好与坏,早在我们青葱岁月时就接触过了,只是那是并不知道这竟如此重要。能够立即改善程序设计、写出“好”代码的知识有以下几点:
  · 面向对象五个基本原则;
  · 常见的三种架构;
  · 绘图;
  · 起一个好名字;
  · 优化嵌套的 if else 代码;
  当然,其他技术知识的丰富程度也决定了程序设计的好坏。例如通过引入消息队列解决双端性能差异问题、通过增加缓存层提高查询效率等。下面我们一起来看看,上面列出的知识点包含哪些内容,这些内容对代码和程序设计的改善有何帮助。
  面向对象五个基本原则
  本书作者是 2010 级学生,面向对象是作者青葱时期发展火热的编程范式。它的五个基本原则是:
  · 单一职责原则;
  · 开放封闭原则;
  · 依赖倒置原则;
  · 接口隔离原则;
  · 合成复用原则;
  下面我们将通过对比和场景假设的方式了解五个基本原则对代码质量的影响。
  立竿见影的单一职责原则
  没错,立竿见影、效果卓越。对于我们这些自学编程无师自通的人来说,能把功能实现就可以了,根本没有时间考虑代码优化和维护成本问题。时光流逝,竟在接触编程很长一段时间后才发现它竟如此重要。
  俗话说只要代码写的够烂,提升就足够明显。以一个从文件内容中匹配关键数据并根据匹配结果发出网络请求的案例,看看大部分程序员的写法:
  1. import re  
  2.   import requests  
  3.   FILE = "./information.fet"  
  4.   def extract(file):
  5.        fil = open(file, "r")  
  6.       content = fil.read()  
  7.       fil.close()  
  8.       find_object = re.search(r"url=\d+", content)  
  9.       find = find_object.group(1)  
  10.       text = requests.get(find)  
  11.       return text  
  12.   if __name__ == "__main__":  
  13.       text = extract(FILE)  
  14.       print(text)
复制代码
 需求已经实现,这点毋庸置疑,但是问题来了:
  · 如果读取文件的时候发生异常了怎么办?
  · 如果数据源发生变化该如何处理?
  · 如果网络请求返回的数据不符合最终要求怎么办?
  如果你心里的第一个反应是改代码,那你就要注意了。完成一件事中间的某个环节发生变化,改代码是在所难免的,但是如果按照上面这种写法,不仅代码越改越乱,连逻辑也会越来越乱。单一职责原则表达的是让一个函数尽量只做一件事,不要将多件事混杂在一个函数中。
  上面的代码如果重新设计,我认为至少应该是这样的:
  1.  def get_source():  
  2.       """获取数据源"""  
  3.       return  
  4.   def extract_(val):  
  5.       """匹配关键数据"""  
  6.       return  
  7.   def fetch(val):  
  8.       """发出网络请求"""  
  9.       return  
  10.   def trim(val):  
  11.       """修剪数据"""  
  12.       return   
  13.   def extract(file):  
  14.       """提取目标数据"""  
  15.       source = get_source()  
  16.       content = extract_(source)  
  17.       text = trim(fetch(content))  
  18.       return text  
  19.   if __name__ == "__main__":  
  20.       text = extract(FILE)  
  21.       print(text)
复制代码
 把原来放在一个函数中实现的多个步骤拆分成为多个更小的函数,每个函数只做一件事。当数据源发生变化时,只需要改动 get_source 相关的代码即可;如果网络请求返回的数据不符合最终要求,我们可以在 trim 函数中对它进行修剪。这样一来,代码应对变化的能力提高了许多,整个流程也变得更清晰易懂。改动前后的变化如下图所示:

  单一职责原则的核心是解耦和增强内聚力,如果一个函数承担的职责过多,等于把这些职责耦合在一起,这种耦合会导致脆弱的设计。当发生变化时,原本的设计会遭受到意想不到的破坏。单一职责原则实际上是把一件事拆分成多个步骤,代码修改造成的影响范围很小。
  让代码稳定性飞升的开放封闭原则和依赖倒置原则
  开放封闭原则中的开放指的是对扩展开放,封闭指的是对修改封闭。需求总是变化的,业务方这个月让你把数据存储到 MySQL 数据库中,下个月就有可能让你导出到 Excel 表格里,这时候你就得改代码了。这个场景和上面的单一职责原则很相似,同样面临代码改动,单一职责原则示例主要表达的是通过解耦降低改动的影响,这里主要表达的是通过对扩展开放、对修改封闭提高程序应对变化的能力和提高程序稳定性。
  稳定这个词如何理解呢?
  较少的改动或者不改动即视为稳定,稳定意味着调用这个对象的其它代码拿到的结果是可以确定的,整体是稳定的。
  按照一般程序员的写法,数据存储的代码大概是这样的:
  1.  class MySQLSave:  
  2.       def __init__(self):  
  3.           pass  
  4.       def insert(self):  
  5.           pass  
  6.       def update(self):  
  7.           pass  
  8.   class Business:  
  9.       def __init__(self):  
  10.           pass   
  11.       def save(self):  
  12.           saver = MySQLSave()  
  13.           saver.insert()
复制代码
 功能是能够实现的,这点毋庸置疑。来看看它如何应对变化,如果要更换存储,那么就意味着需要改代码。按照上面的代码示例,有两个选择:
  · 重新写一个存储到 ExcelSave 的类;
  · 对 MySQLSave 类进行改动;
  上面的两种选择,无论怎么选都会改动 2 个类。因为不仅存储的类需要改动,调用处的代码也需要更改。这样一来,它们整体都是不稳定的。如果换一种实现方式,根据依赖倒置的设计指导可以轻松应对这个问题。边看代码边理解:
  1.  import abc  
  2.   class Save(metaclass=abc.ABCMeta):  
  3.       @abc.abstractmethod  
  4.       def insert(self):  
  5.           pass  
  6.       @abc.abstractmethod  
  7.       def update(self):  
  8.           pass  
  9.   class MySQLSave(Save):  
  10.       def __init__(self):  
  11.           self.classify = "mysql"  
  12.           pass  
  13.       def insert(self):  
  14.           pass  
  15.       def update(self):  
  16.           pass  
  17.   class Excel(Save):  
  18.       def __init__(self):  
  19.           self.classify = "excel"  
  20.       def insert(self):  
  21.           pass  
  22.       def update(self):  
  23.           pass  
  24.   class Business:  
  25.       def __init__(self, saver):  
  26.           self.saver = saver  
  27.       def insert(self):  
  28.           self.saver.insert()  
  29.       def update(self):  
  30.           self.saver.update()  
  31.   if __name__ == "__main__":  
  32.       mysql_saver = MySQLSave()  
  33.       excel_saver = Excel()  
  34.       business = Business(mysql_saver)
复制代码
这里通过内置的 abc 实现了一个抽象基类,这个基类的目的是强制子类实现要求的方法,以达到子类功能统一。子类功能统一后,无论调用它的哪个子类,都是稳定的,不会出现调用方还需要修改方法名或者修改传入参数的情况。
  依赖倒置中的倒置,指的是依赖关系的倒置。之前的代码是调用方 Business 依赖对象 MySQLSave,一旦对象 MySQLSave 需要被替换, Business 就需要改动。依赖倒置中的依赖指的是对象的依赖关系,之前依赖的是实体,如果改为后面这种依赖抽象的方式,情况就会扭转过来:

  实体 Business 依赖抽象有一个好处:抽象稳定。相对于多变的实体来说,抽象更稳定。代码改动前后的依赖关系发生了重大变化,之前调用方 Business 直接依赖于实体 MySQLSave,通过依赖倒置改造后 Busines 和 ExcelSave、 MySQLSave 全都依赖抽象。
  这样做的好处是如果需要更换存储,只需要创建一个新的存储实体,然后调用 Business 时传递进去即可,这样可以不用改动 Business 的代码,符合面向修改封闭、面向扩展开放的开放封闭原则;
  依赖倒置的具体实现方式使用了一种叫做依赖注入的手段,实际上单纯使用依赖注入、不使用依赖倒置也可以满足开闭原则要求,感兴趣的读者不妨试一试。
  挑肥拣瘦的接口隔离原则
  接口隔离原则中的接口指的是 Interface,而不是 Web 应用里面的 Restful 接口,但是在实际应用中可以将其抽象理解为相同的对象。接口隔离原则在设计层面看,跟单一职责原则的目的是一致的。接口隔离原则的指导思想是:
  · 调用方不应该依赖它不需要的接口;
  · 依赖关系应当建立在最小接口上;
  这实际上是告诉我们要给接口减肥,过多功能的接口可以选用拆分的方式优化。举个例子,现在为图书馆设计一个图书的抽象类:
  1. import abc  
  2.   class Book(metaclass=abc.ABCMeta):  
  3.       @abc.abstractmethod  
  4.       def buy(self):  
  5.           pass  
  6.       @abc.abstractmethod  
  7.       def borrow(self):  
  8.           pass  
  9.       @abc.abstractmethod  
  10.       def shelf_off(self):  
  11.           pass  
  12.       @abc.abstractmethod  
  13.       def shelf_on(self):  
  14.           pass
复制代码
 图可以被购买、可以被借阅、可以下架、可以上架,这看起来并没有什么问题。但这样一来这个抽象只能提供给管理人员使用,用户操作时需要再设定一个新的抽象类,因为你不可能让用户可以操纵图书上下架。接口隔离原则推荐的做法是把图书的上下架和图书购买、借阅分成 2 个抽象类,管理端的图书类继承 2 个抽象类,用户端的图书类继承 1 个抽象类。这么看起来是有点绕,不要慌,我们看图理解:

  这样是不是一下就看懂了。这个指导思想很重要,不仅能够指导我们设计抽象接口,也能够指导我们设计 Restful 接口,还能够帮助我们发现现有接口存在的问题,从而设计出更合理的程序。











欢迎光临 51Testing软件测试论坛 (http://bbs.51testing.com/) Powered by Discuz! X3.2