摘要:所谓“魔数”,是指直接写在代码中的硬编码数值。它们往往缺乏明确含义,阅读时难以理解其目的。比如,当你在代码里看到一个数字 50,你可能会疑惑:“这个 50 代表什么?为什么要用50?”如果以后需要改这个数字,你可能要到处找它,很容易漏掉或者出错。
所谓“魔数”,是指直接写在代码中的硬编码数值。它们往往缺乏明确含义,阅读时难以理解其目的。比如,当你在代码里看到一个数字 50,你可能会疑惑:“这个 50 代表什么?为什么要用50?”如果以后需要改这个数字,你可能要到处找它,很容易漏掉或者出错。
举个例子,假设在“您的订单”页面中,我们需要显示最近的 50 个订单。这里的“50”是一个魔数,因为它并不是基于某个标准或惯例确定的,而是根据某些业务需求随意设定的。
这种情况下,问题在于你可能会在多个地方重复使用这个数字,比如在 SQL 查询中:
SELECT TOP 50 * FROM orders。如果需要更改这个数字,你必须在代码的每个地方都找到并手动更改,容易出错且费时。因此,推荐的做法是将这个数字定义为一个常量:
# ❌不推荐的做法SELECT TOP 50 * FROM orders# ✅推荐的做法NUM_OF_ORDERS = 50SELECT TOP NUM_OF_ORDERS * FROM orders在编程中,循环、条件语句或函数调用的过度嵌套,会让代码变得臃肿和难以理解。每多一层嵌套,逻辑复杂度就随之增加,读者需要花费更多精力去追踪执行路径。这不仅降低了可读性,还提高了出错的可能性。
为了让代码更加简洁明了,应当尽量减少嵌套的层级。常见的做法包括:
使用早期返回(return 语句),避免继续进入不必要的逻辑分支;将复杂逻辑拆分为独立函数,把大块代码分解成小而清晰的单元;合并条件,避免层层嵌套的判断。这些方法都有助于让代码更加易读和易于维护。
# ❌不推荐的做法if x: if y: do_something # ✅推荐的做法if x and y: do_something在编写代码时,应尽量减少临时变量的使用,临时变量往往会使代码难以理解,因为它们增加了额外的复杂性和不必要的状态变化。
可以直接返回结果或通过将代码拆分为更小的函数来避免使用临时变量。这样既能减少不必要的中间状态,又能让代码更简洁、更易于理解。
# ❌不推荐的做法temp_result = calculate(x, y) final_result = temp_result * 2 # ✅推荐的做法final_result = calculate(x, y) * 2在命名变量、函数或类时,尽量使用清晰且具有描述性的名称,而非晦涩难懂的缩写。这样可以显著提升代码的可读性,使其他开发者(包括未来的自己)能够更轻松地理解代码的目的和逻辑。
# ❌不推荐的做法def calc ( x, y ): pass # ✅推荐的做法def calculate_total_price ( quantility, unit_price ): pass硬编码路径会给代码的可维护性带来问题。
想象你写了一个程序来打开文件,如果你把文件路径直接写在代码里(例如 /path/to/File.txt),如果别人想在自己的电脑上运行你的程序,文件路径可能就不一样了,就会出错。
所以,应当使用配置文件或环境变量来管理路径,这样就能避免环境差异带来的问题。
# ❌不推荐的做法file_path = "/path/to/file.txt" # ✅推荐的做法import os file_path = os.getenv( "FILE_PATH" )在编写代码时,错误处理是不可忽视的一部分。适当地使用 try-catch-finally 语句不仅能加快调试过程,还能提高代码的健壮性,使其更易于维护。
例如,在编写可能失败的操作(如 API 请求、文件读写、数据库访问等)时,就应该通过 try-catch 来捕获潜在的异常,并在 finally 中执行必要的清理工作:
try:file = open("data.txt", "r")data = file.readexcept FileNotFoundError:print("文件不存在,请检查路径!")finally:file.close # 确保文件总是被关闭但是,错误处理并不是万能的解决方案。在一些不会发生异常的简单逻辑(如基本的加减乘除运算)中,如果仍然使用 try-catch,反而会让代码显得冗余、复杂,增加不必要的维护负担。
当在代码中捕获到一个错误或异常时,如果缺乏上下文信息,只是简单地说“出错了”,别人很难知道问题出在哪里。
因此,在处理异常时,应该尽量提供有用的上下文信息,比如是“在读取文件时出错了”还是“输入的值不正确”。这样,别人就能更快地找到问题并解决它。
例如,在文件读取过程中可能出现错误:
# ❌不推荐的做法try:file = open("data.txt", "r")data = file.readexcept Exception as e:print("出错了")# ✅推荐的做法try:file = open("data.txt", "r")data = file.readexcept Exception as e:print(f"在读取文件时出错了:{e}")try:passexcept ValueError:passexcept TypeError:passexcept IndexError:passexcept KeyError:passexcept FileNotFoundError:pass表面上看,这种写法很“全面”,但实际上却会让代码变得冗长而难以维护。大多数情况下,并不需要为每一种错误分别写一个 except 块。
try:passexcept FileNotFoundError:print("文件不存在,请检查路径!")except Exception as e:print(f"发生了一个错误:{e}")在设计函数时,我们需要明确这个函数的目的:是修改传入的参数,还是需要返回某些结果。如果一个函数既修改参数,又返回值,就会让代码的意图变得模糊,调用者也难以理解它的真正用途。
如果函数的目的就是对传入的数据进行修改。那么它应仅执行这一任务,而不应该同时尝试做其他事情,如返回值。
“修改”意味着函数会改变传入参数的内容或数据类型,例如,下面的函数会直接在传入的列表中追加一个元素:
def changed(array):array.append('hello')调用这个函数后,外部的 array 对象会被改变,这类函数通常不会再返回额外的结果。
另一类函数不会改变传入参数,而是基于参数进行计算,并返回一个新的值。例如:
def calculate_distance(time, speed):distance = speed * timereturn distance在编写函数时,通常我们希望避免直接修改传入的参数(即避免变异),以减少潜在的副作用。副作用会让调用者很难预料函数的行为,从而增加代码的复杂度。
一种常见的做法是,在函数内部创建参数的副本,并在副本上进行修改,最终返回一个新对象,而不是改变原始参数:
def changed(array):array_copy = array[:]array_copy.append(4)return array_copy在这里,with_extra_element 这个函数没有直接修改传入的 array,而是生成了一个新的列表并返回。
类的设计原则与函数类似,也应当尽可能保持简洁。
但与函数不同,类的规模通常不是通过行数来衡量,而是取决于类中包含多少职责。如果一个类做了太多事情,即使代码行数不多,它依然是“庞大”的。
通常,类的名称能反映它的职责,但如果类名显得模糊或过于笼统,那么很有可能这个类承担了过多的职责。
这就引出了 SRP(单一责任原则):一个类应该只承担一个职责,或只有一个导致它发生变化的原因。
在类的设计中,实例变量用于存储对象的状态或属性。它们的存在本身没有问题,但数量过多时,往往意味着这个类承担了太多责任。
这种情况下,类中的某些方法可能会偏离其核心职责,只是为了处理那些不相关的变量。这会导致以下问题:
复杂性增加:过多的实例变量让类的状态管理变得复杂,每个方法都需要考虑更多的因素,增加了代码的复杂性。维护困难:在类中引入过多的实例变量可能会使得类变得臃肿,影响代码的可读性和可维护性。class Animal:def __init__(self, name):self.name = name #instance variable为了避免这些问题,尽量保持类的实例变量数量少而精。
想象你有一个工具箱,工具箱里有很多工具。如果工具箱里的工具都用来修车,比如扳手、螺丝刀和千斤顶,那么这个工具箱就很“专注”,因为所有工具都围绕同一个目标——修车。这就是“高凝聚力”。
但如果工具箱里既有修车的工具,又有做饭的工具(比如锅铲、勺子),甚至还有画画的工具(比如画笔、颜料),那么这个工具箱就显得很混乱,很难找到你需要的东西。这就是“低凝聚力”。
类的设计也是同样的道理,一个类里的方法和变量应该围绕同一个目标组织在一起,那么这个类就有高凝聚力。
高凝聚力意味着类中的属性和方法紧密相关、目标一致;而低凝聚力则意味着类承担了过多不相关的功能,最终变得难以理解和维护。
假设我们有一个表示矩形的类,它的职责是计算矩形的面积和周长。在这个类中,width和height是实例变量,calculate_area和calculate_perimeter是方法:
class Rectangle:def __init__(self, width, height):self.width = widthself.height = heightdef calculate_area(self):return self.width * self.heightdef calculate_perimeter(self):return 2 * (self.width + self.height)在这个例子中,width 和 height 是矩形的核心属性,而 calculate_area 和 calculate_perimeter 方法都依赖这两个属性,这样,所有的方法和属性都围绕着矩形的基本功能组织在一起,使得类具有高凝聚力。
在编程中,文件、数据库连接等资源必须在使用后被正确关闭或释放,否则可能会造成内存泄漏或资源占用问题。手动关闭资源容易出错,比如忘记写 close,或者在出错时提前中断,导致资源未被释放。
为了解决这个问题,可以使用 with 语句,它会在代码块结束后自动管理和释放资源,确保程序更加健壮。
# ❌不推荐的做法file = open("example.txt", "r")data = file.readfile.close# ✅推荐的做法with open("example.txt", "r") as file:data = file.read避免使用过于复杂的三元表达式;这样代码更清晰更易于理解。
# ❌不推荐的做法result = "even"if number % 2 == 0else"odd"if number % 3 == 0else"neither"# ✅推荐的做法if number % 2 == 0:result = "even"elif number % 3 == 0:result = "odd"else:result = "neither"在 Python 中,is 和 is not 用于检查两个变量是否引用同一个对象,即它们是否具有相同的内存地址。这与 == 操作符不同,后者用于检查两个对象的值是否相等。
大多数情况下,我们使用 == 来比较两个变量的值。例如,对于字符串和整数这类不可变数据类型,这种比较通常是有效的,因为具有相同值的不可变对象往往会被 Python 优化为共享同一个内存地址。从而使得 == 操作符和 is 操作符的结果一致。
但在处理 可变数据类型(如列表、字典或自定义对象)时,情况就不同了。即使两个可变对象的内容完全相同,它们的内存位置通常也不一样。这时使用 is 可以准确地判断它们是否引用的是同一个对象,而不仅仅是“长得一样”。
# 示例 2:检查两个列表是否引用同一个对象list1 = [1, 2, 3]list2 = [1, 2, 3]# ❌不推荐的做法 ==if list1 == list2:print("Lists are equal in value") # 会打印,因为它们的内容相同# ✅推荐的做法 isif list1 is list2:print("Lists are the same object") # 不会打印,因为它们是两个不同的对象# 注意:在这种情况下,list1 和 list2 是具有相同值的不同对象,# 因此使用 `is` 会给出与 `==` 不同的结果。依赖倒置原则(DIP)是面向对象设计中的一个重要原则。它指出高级模块不应该依赖于低级模块,但两者都应该依赖于抽象。换句话说,类应该依赖于接口或抽象类,而不是具体的实现。
想象你有一个遥控器(Calculator),用来控制电视(Logger)。如果遥控器直接依赖于某个特定品牌的电视(比如索尼电视),那么一旦换成了三星电视,它可能就无法工作了,因为不同电视的接口方式可能不一样。
而依赖倒置原则的核心思想是:遥控器(高级模块)不应该直接依赖某个品牌的电视(低级模块),而是应该依赖于一个统一的“电视接口”。这样,只要电视符合这个接口,遥控器就能正常工作,不受品牌变化的影响。
❌来看一个违反该原则的例子:
class Logger: def log(self, message):with open('log.txt', 'a') as f: f.write(message + '\n') class Calculator:def __init__(self):self.logger = Loggerdef add(self, x, y):result = x + y self.logger.log(f"Added {x} and {y}, result = {result}") return result在上面的例子中,我们定义了类Logger并直接在类中创建了它的新实例Calcuator。这意味着Calculator类直接依赖于 Logger 类,如果出于任何原因我们更改了Logger类,我们现在也必须修改Calculator类。因此这也不符合开放封闭原则(对扩展开放,对修改关闭)。
✅推荐的写法如下:
# 推荐from abc import ABC, abstractmethod class LoggerInterface(ABC):@abstractmethod def log(self, message):passclass Logger(LoggerInterface):def log(self, message):with open('log.txt', 'a') as f: f.write(message + '\n') class Calculator:def __init__(self, logger: LoggerInterface):self.logger = logger def add(self, x, y):result = x + y self.logger.log(f"Added {x} and {y}, result = {result}") return result这里,Calculator 不再直接依赖 Logger,而是依赖于一个抽象的接口(LoggerInterface)。这样,Calculator 只关心日志记录的功能,而不关心具体的实现:是写入文件、数据库,还是发送到远程服务。
这种做法带来了两个明显的好处:
模块化更强:只要接口保持一致,对某个组件的修改不会影响其他组件。更易扩展:以后如果需要新增不同的日志方式(比如 DatabaseLogger 或 ConsoleLogger),只要实现 LoggerInterface 即可,无需修改 Calculator。assert 是一种断言语句,用于检查某个条件是否为真。如果条件为真,程序继续运行;如果条件为假,程序会报错并停止运行。这种检查在开发和调试阶段很有用,但在生产环境中并不合适。因为 assert 在某些情况下可能被禁用,从而导致关键的验证逻辑失效。
# ❌不推荐的做法assert x > 0 , "x 应该为正数" # ✅推荐的做法if x在编程中,重复的代码是维护的大敌。DRY 原则要求我们尽量避免写重复的逻辑,而是将公共代码提取到函数、类或模块中,让它们在多个地方复用。
这样代码不仅更简洁,而且维护起来也更容易。因为如果需要更改或更新代码,只需要在一个地方修改它。
# ❌违反了 DRY 原则def calculate_book_price(quantity, price):return quantity * pricedef calculate_laptop_price(quantity, price):return quantity * price# ✅推荐的做法 def calculate_product_price(product_quantity, product_price):return product_quantity * product_price想象你在写作文,老师要求用统一的格式:标题要居中,段落要缩进,字迹要工整。如果每个人都随意写,老师批改起来会很困难,而且看起来也很乱。编程也是一样,每种语言都有自己的“作文格式”,在 Python 中,这个格式就是 PEP 8。遵循这些标准可以让代码更加整洁、易读,也更方便团队协作和后期维护。
以下是 Python 的 PEP 8 中一些常见的代码规范:
使用蛇形命名法:变量名和函数名用小写字母,单词之间用下划线分隔,比如 calculate_product_price。类名用驼峰命名法,比如 ProductCalculator。缩进使用 4 个空格而不是制表符(Tab),因为不同编辑器对制表符的显示宽度可能不一样,而空格是固定的。每行最多 79 个字符:如果一行代码太长,可以换行。这样可以让代码更易读,尤其是当多个文件并排显示时。二元运算符前换行:当代码很长时,运算符(比如 +、-)应该放在新的一行开头,而不是上一行的结尾。这能让代码更容易对齐和阅读。#✅推荐的做法# 遵守 PEP 8:使用空格缩进,变量名用蛇形命名法def calculate_product_price(product_quantity, product_price):return product_quantity * product_price#❌不推荐的做法:运算符放在上一行的末尾income = (gross_wages +taxable_interest +dividends - qualified_dividends -ira_deduction -student_loan_interest)# ✅推荐的做法:运算符放在新的一行开头income = (gross_wages+ taxable_interest+ dividends - qualified_dividends- ira_deduction- student_loan_interest)想象你正在参加一个派对。你当然认识你的朋友,但你未必认识你朋友的朋友。如果你需要和他们交谈,通常会通过你的朋友来介绍,而不是自己直接跑过去搭话。这就是迪米特法则的核心思想:“只和你的直接朋友交流,不要和朋友的朋友直接交流。”
在编程中,这条原则意味着:一个类(比如 Order)可以和它的直接邻居(比如 Customer)交互,但不应该深入到邻居的内部结构(比如直接访问 Customer 的 Profile)。这样可以避免类之间过度耦合,让代码更灵活、更容易维护。这里的“直接邻居”,指的就是可以直接调用的方法、函数或变量。
来看一个具体例子。假设我们正在设计一个订单系统,其中 Order 需要获取客户的姓名。
❌不推荐的做法:
class Order : def __init__ ( self, customer ): self.customer = customer def get_customer_name ( self ): # Order 对客户的结构了解太多return self.customer.get_profile.get_name在这个实现里,Order 先访问了 Customer,然后再去访问 Customer 的 Profile,最后才拿到 name,使得 Order 依赖于 Customer 的内部实现细节。这就相当于“直接去找朋友的朋友说话”,明显违反了迪米特法则。
✅推荐的做法
class Order:def __init__(self, customer):self.customer = customerdef get_customer_name(self):# 遵守:Order 只和它的直接邻居(Customer)交互return self.customer.get_name在改进后的写法中,Order 类只调用了 Customer 的公共方法,而不关心其内部实现细节,这样,Order 类和 Customer 类之间的耦合更松散,代码更灵活。
代码不仅需要能正确运行,还需要让人容易理解。计算机永远不会抱怨代码的可读性差,但其他开发人员包括未来的自己却必须阅读和维护它,特别是当我们与多人一起开发项目时。这就是为什么在软件开发中代码的可读性总是比其简洁性更重要。如果其他开发人员无法理解,那么编写简洁的代码也没有意义。
# ❌不推荐的做法:过于简洁,a 和 b 的含义不明确,其他开发者可能需要花时间理解。def add(a, b):return a + b# ✅推荐的做法:参数名和函数名都清楚地表达了它的目的def calculate_total(price, quantity):total = price * quantityreturn total在 Python 中,导入模块是常见的操作。但如果导入不加以规范,就会让代码变得杂乱,降低可读性和可维护性。为了保持代码整洁,建议只导入真正需要的模块或符号。
❌不推荐的做法:
from some_module import * # 导入所有内容这种方式会把模块中的所有变量、函数和类都导入进来,可能会导致以下问题:
✅推荐的做法:
from some_module import function1, function2, ClassA简约设计是一种软件设计原则,强调用最简洁、最高效的方式实现功能,同时确保代码的可维护性和可读性。以下是简约设计几个关键要点:
1.运行所有测试(Runs all the tests)
在软件开发中,光靠理论推导无法保证代码的正确性。必须通过测试来验证代码是否按预期工作。测试可以发现潜在的错误、边界情况和性能问题。
常见的测试方式包括:
单元测试:验证代码的每个独立部分是否能正确运行。集成测试:测试多个模块之间的交互,确保它们能够协同工作。自动化测试:使用自动化测试工具(如 pytest、unittest)来运行测试,确保测试的可重复性和效率。测试覆盖率:确保测试覆盖正常情况、异常情况和边界情况,提高代码的可靠性。2.不包含重复(Contains no duplication)在一个精心设计的系统中,重复代码是最大的敌人。它不仅会让代码库膨胀,还使得维护变得困难。如果逻辑需要修改,重复代码会导致需要在多个地方进行修改,容易遗漏并引入错误。
实现“去重复”的常见方式包括:
提取公共逻辑:将重复的代码提取到单独的函数或类中。重用代码:尽量调用已有的函数,而不是复制粘贴相同逻辑。重构:定期审查代码,移除重复的逻辑,简化代码结构。3.清晰表达程序员的意图(Runs all the tests)代码不仅要能运行,还要让其他开发者能够快速理解。这意味着代码应该具有自解释性:当别人读代码时,就能看出作者的意图,而无需反复揣测。
清晰的命名:使用有意义的变量名和函数名,避免模糊或缩写。简洁的注释:在必要时添加注释,解释复杂的逻辑或设计决策。一致的代码风格:遵循一致的代码风格和规范,提高代码的可读性。避免复杂的嵌套:使用清晰的逻辑结构,避免过多的嵌套和复杂的条件语句。4.最小化类和方法的数量
设计时应尽量减少不必要的类和方法。过多的类和方法会增加系统的复杂性,使得代码难以理解和维护。简约设计强调“够用就好”,而不是“越多越好”。
有以下几个原则需要注意:
必要性原则:只在确实需要时才创建类或方法,避免无意义的抽象。单一职责原则(SRP):每个类或方法只负责一个功能,避免过度复杂。定期审查:在迭代过程中持续检查代码,及时移除冗余或不再使用的类和方法。模块化设计:通过合理的模块划分来减少类与方法之间的依赖,让系统更清晰。避免过度嵌套 try-except 块,因为嵌套会增加代码的复杂性,降低可读性和可维护性。嵌套的 try-except 块会让错误处理逻辑变得难以理解和调试。
❌不推荐的做法
try:try:# Code that might raise errorspassexcept ValueError:# Handle ValueErrorpassexcept Exception as e:# Handle any other unexpected errorspass这个例子中,异常处理被嵌套在两层 try-except 中。如果 result = 10 / 0 引发了 ZeroDivisionError,它不会被任何 except 块捕获,因为 ZeroDivisionError 不是 ValueError 或 Exception 的子类。这会导致错误处理逻辑混乱。
✅推荐的做法
try:# Code that might raise errorspassexcept ValueError:# Handle ValueErrorpassexcept Exception as e:# Handle any other unexpected errorspass在改进后的写法中,所有可能引发的错误都被放在一个 try 块中,不同类型的异常则通过多个 except 分支分别处理。这种方式避免了嵌套,让错误处理逻辑更加清晰、直观,也更易于维护。
在实现并发功能时,往往很容易写出存在缺陷的代码,而这些问题可能在轻负载下不会暴露,直到系统承受较大压力时才会显现出来。
以下是其中一些常见的并发问题:
饥饿(Starvation):当线程或进程尝试访问共享资源但始终无法访问时,就会发生饥饿。就好像你在餐厅排队等座位,但总有新客人插队,你一直等不到座位。死锁:假设你有两个线程,线程A和线程B,它们分别需要两个锁(比如锁A和锁B)。线程A先获取了锁A,线程B先获取了锁B。然后线程A需要锁B,线程B需要锁A,结果两个线程都卡住了,这就是死锁。前面介绍的所有法则,涵盖了编写高质量代码的方方面面,作为开发者,我们应当在日常工作中自觉遵守这些原则。
当然,规则并不是一成不变的。随着经验和技能的增长,我们需要决定在什么场景下必须严格遵循某条规则,而在什么情况下可以灵活变通。这种把握需要实践和积累。
但如果你是新手,或者两年前才开始自己的职业生涯,那么最好遵循这些法则。
来源:心平氣和