我不是一个有条理的程序员,或者说,我不擅长组织整理代码。这也不是什么新鲜事,这种情况已经持续很长时间了......
许多年前,我制作了一个基于PHP的图片CMS——几千行代码在一个文件里,大部分功能都在一个巨大的循环内,还些多层嵌套循环,没有函数,没有注释,随意地到处添加变量...... 这一切太糟糕了,我阅读代码的时间比添加功能的时间还长。无奈之下,我把整个东西打印了出来,带到酒吧,醉醺醺地想把它弄明白。然而,我并没有搞明白,在某一时刻我放弃了。
从那时起,我认为应当努力整理代码,主要是为了避免错误,而我也尝试着这样做,这使你自己以及最终阅读和维护你的代码的其他人更加轻松。让我们在此探讨一些解决方案;这只是一个简单的、对初学者友好的概述,而不是一个确切的办法,因为正如你所见,这个主题可以变得相当复杂。
先来点面条式代码作开胃菜
for i in [1,2,3]:
def printMa():
print ('Ma')
x = True
if x == True:
printMa()
y = False
if y == True:
printMa()
else:
print("Ma")
y = True
if x and y == True:
if i == 3:
print('Mia let me GO !')
else:
print ('Mia')
# OUTPUT:
# Ma Ma mia Ma Ma mia Ma Ma mia let me GO !
你正在处理面条式代码(spaghetti code)的明显标志是难以读懂——你不知道它的功能以及工作方式。“面条式代码”这个名字源于需要来回引用变量和函数,最终就像意大利面条一样,但这里还有更混乱的情况:
我们可以把程序或脚本定义为一组指令,通常与我们提供给计算机的一些数据一起执行我们期望的事情,比如,唱一首歌。只要不干扰某些内部规则(语言语法),计算机就会很乐意执行我们告诉它的任何事情。那么,整理是我们为了解决作为人类的局限性和缺点而做的事情,而且由于我们各有不同,因此对大多数人有效的东西可能对你无效和/或没有意义,反之亦然;这通常是可以的,只要你明白在团队环境中写代码时,整理是需要达成共识的。
大家可能会问的第一个问题是:为什么需要整理代码?毕竟,面条式代码也可以工作,即便它混乱、难以阅读、难以维护;此外,当你刚开始作为一门技巧或爱好使用一种新的语言或编程方式时,你可能确实不知道如何正确地整理代码;如果我们再加上内部要求,如编程风格规范、可测试性、截止日期、文档和源代码控制,就不难理解为什么不整理代码具有一定的吸引力。这是一种折中行为,我认为可以总结如下:
在此需要真正的权衡,而这常与技术债务密切相关,你了解的越多,整理代码就越容易。请注意,了解并不局限于你选择的语言——在本示例中为python,还包括业务领域——你正在尝试创造的东西,以及代码库本身——对于置身于现有代码库的新开发者而言尤为如此。
如果涉及到时间和精力,那么我要向你推销整理代码的好处;这就是我将尝试的,如果你也愿意的话,这里有关于整理的几个层级,一些基本示例,以及从中获得的益处。
函数和类都是天然的聚合器:函数是语句的聚合(类比动作/动词和句子的关系),而类是处理对象(如东西的类,或名词和形容词)。从编程上来说,它们本身就是基本主体,也常是语言的基石,尽管你本来就可以让一个复杂的脚本或程序在不使用它们的情况下运行,那为什么还要使用它们呢?
精心设计的函数可以节省空间并且用作句子、组件或逻辑单元,精心设计的类可以极大地扩展项目里的词汇量,这两者的结合可以让你用段落来表述,而不是胡乱地叫喊命令。例如,用Functions重写前面的示例:
def sing(line):
print ('Ma Ma')
if line == 1:
print('Mia')
else:
print ('Mia let me GO !')
sing(1)
sing(1)
sing(2)
# OUTPUT:
# Ma Ma Mia Ma Ma Mia Ma Ma Mia let me GO !
Classes(用docs的话来说)将数据和功能捆绑在一起,而通过它你可以开始方便地考虑更复杂的事情。例如,在此我们可以创建一个代表chorusSinger的类,当然还有更多的代码要处理,但我们现在可以创建无限的合唱歌手,并要求TA们唱出相应的行。
class chorusSinger:
"""This class creates a chorus singer that
can sing one of two lines."""
line1 = "Ma Ma Mia"
line2 = "let me GO !"
def sing(self, line):
"""This function sings one of 2 lines"""
if line == 1:
print(self.line1)
else:
print(self.line2)
# Create chorus singers:
chorusSinger1 = chorusSinger()
chorusSinger2 = chorusSinger()
# Let them sing !:
chorusSinger1.sing(1)
chorusSinger2.sing(1)
chorusSinger1.sing(1)
chorusSinger2.sing(2)
Output:
Singer 1: Ma Ma Mia
Singer 2: Ma Ma Mia
Singer 1: Ma Ma Mia
Singer 2: let me GO !
请注意,我们还添加了""docstrings""形式的注释,它以后可用于记录标注你的代码功能;以及单行注释(inline comments),有助于你重新阅读自己的代码)。
我们还没有彻底完成对函数和类的处理。执行中如果你注意到前面那个示例最后被晾在那里,如果你想添加小节或其他歌手,则可能会变成乱七八糟的面条式代码,因此,我们可以通过添加一个执行该操作的函数来进一步整理,再一次我们获得的是灵活性和可读性:
class chorusSinger:
"""This class creates a chorus singer that
can sing one of two lines."""
line1 = "Ma Ma Mia"
line2 = "let me GO !"
def sing(self, line):
"""This function sings one of 2 lines"""
if line == 1:
print(self.line1)
else:
print(self.line2)
def singPart():
"""Creates Singers and tells them to sing"""
chorusSinger1 = chorusSinger()
chorusSinger2 = chorusSinger()
chorusSinger1.sing(1)
chorusSinger2.sing(1)
chorusSinger1.sing(1)
chorusSinger2.sing(2)
def main():
"""Plays parts"""
singPart()
singPart()
# Runs the program
main()
# OUTPUT:
Singer 1: Ma Ma Mia
Singer 2: Ma Ma Mia
Singer 1: Ma Ma Mia
Singer 2: let me GO !
Singer 1: Ma Ma Mia
Singer 2: Ma Ma Mia
Singer 1: Ma Ma Mia
Singer 2: let me GO !
这种简单的结构相当常见,可以作为许多短程序或脚本的基础,并且为较复杂的程序打开了方便之门。下面是这种模式的概述:
这种方案很容易遇到一个问题,特别是如果你刚开始使用OOP,那就是如何拥有一个可以从函数中访问的全局变量或状态;第二个密切相关的问题是如何处理多个对象,如对它们进行实例化、命名、方法调用等。如果你被这两个问题卡住了,可以查看另一篇关于全局变量替代方案的文章。当然,下一节内容也有助于阐明这些问题。
大部分人一开始写的大多数甚至所有的代码通常都是单个文件,甚至具有很多功能的高级脚本也可以整齐地放入一个文件中。关于如何整理单个文件脚本,没有固定的规则,但似乎确实存在一定的惯例。在此,我们对先前的概念进行了扩展,并包括了导入和变量等内容。
⚠️ 在下一个示例中,我将使用称为pyttsx3的文字转语音的库。
我们继续:
"""
Sings 2 lines in different voices
"""
import pyttsx3
TTS = pyttsx3.init()
VOICES = TTS.getProperty('voices')
LINE1 = 'Ma Ma Mia'
LINE2 = 'Let me GO!'
def sing(gender, PART):
"""Sings a given line in a given voice"""
if gender == 'male':
TTS.setProperty('voice', VOICES[0].id)
else:
TTS.setProperty('voice', VOICES[1].id)
TTS.say(PART)
def main():
sing('male', LINE1)
sing('female', LINE1)
sing('male', LINE1)
sing('female', LINE1 + LINE2)
TTS.runAndWait()
main()
OUTPUT:
# You should hear 2 different voices singing 2 different parts
除了不相关地升级到机器人语音之外,这里的新内容是我们首先具有模块导入和一些数据,如常量、全局变量。接着是函数——为了简洁起见,我省略了类,但你可以在函数之前添加类。最后是执行,这与前一级的结构非常相似:
在这种模式中,有几点值得注意:如果你使用类作为对象,则需要持有一个全局引用,以便在函数和主函数中与它们进行交互;列表List是一种流行的引用方式,这应该可以解决前面提到的问题;其次,大多数游戏和图形引擎只有一个事件循环,它通常在主函数中进行,对于更为复杂的结构,如具有多线程的GUI,你必须依靠我们即将介绍的模式。
当通过像我们刚才那样导入模块或包来编写单个文件脚本时,最有可能遇到的就是包和模块。
首先定义:一个python模块只是你导入的一个文件,包是具有一定层次结构的文件或模块的集合。
详细说明请参阅文档:https://docs.python.org/3/tutorial/modules.html#packages
从根本上说,当把代码整理成模块和包时,将获得明确的层次结构以及可读性;从系统设计角度来说,层次结构通过让各个部分专注于一项或几项任务,最终服务于底层活动(不管你的代码试图完成什么)。换句话说,除了一个或少数几个模块外,所有的模块都应该是专一职责化的。
假设我们想制作一个别人可以使用的带歌词的唱歌程序,我们可以把它全部填入一个文件中,或者如果我们使用的是模块和包,则要有一定的层次结构:
一旦有了这种或其他类型的层次结构,就可以使用主文件的import语句来访问子包:
FILE: lyrics_1.py
LYRICS = 'MA MA MIA'
FILE: singer_1.py
def SING(LYRICS):
print (LYRICS)
FILE: main.py
import LYRICS.lyrics_1
import SINGERS.singer_1
SINGERS.singer_1.SING(LYRICS.lyrics_1.LYRICS)
OUTPUT: "MA MA MIA"
这又是一个简单示例,仅用于演示目的;文件目录和结构通常更复杂,文件具有初始化脚本,并且导入可以具有额外的属性以简化工作,例如导入LYRICS.lyrics_1作为LYRICS_1可以节省一些录入时间,但是分离各个文件夹的数据和函数是这种整理的一个良好开始。
优势就是,你只需查看文件结构即可轻松地指导其他人或未来的自己添加歌词或歌手。虽然这次节省下来的重读代码的时间很遗憾地花在了第一次整理代码上,但是如果你想让更多的人参与到你的代码中来,这是至关重要的。
注意:最后这两级建立在大量信息的基础上,因此可以视为是高级篇,它们也用于专业工作和开源工作,并不是说你不应该尝试,只是说确实需要一些时间来理解,所以如果一开始你不理解或者觉得不适应,也不要灰心。
Pattern和Recipe试图解决的问题如下:你正在编写一些代码;与其重新发明轮子,你更愿意使用别人的解决方案;也许这个解决方案非常流行,很多人都在使用;这样一来,可以一次性解决你的业务问题和代码组织整理的问题。
Patterns通常处理数据结构和信息流,而Recipes处理更细化的问题。
这似乎一个完美的解决方案,但是还有一些缺点:
那么模式是什么样的呢?
下面这种称为单例(Singleton)模式——当需要某个对象的一个且仅仅一个实例时,就会用到这种模式。
class Singleton:
__instance = None
@staticmethod
def getInstance():
""" Static access method. """
if Singleton.__instance == None:
Singleton()
return Singleton.__instance
def __init__(self):
""" Virtually private constructor. """
if Singleton.__instance != None:
raise Exception("This class is a singleton!")
else:
Singleton.__instance = self
X = Singleton()
print (X)
Y = Singleton.getInstance()
print (Y)
Z = Singleton.getInstance()
print (Z)
OUTPUT:
<__main__.Singleton object at 0x1030db748>
<__main__.Singleton object at 0x1030db748>
<__main__.Singleton object at 0x1030db748>
单例对于代码组织很方便,因为它通常可以像国王或CEO那样处于层次结构的顶端(因为只能有一个)。你可能不会立即需要(或永远不需要)这种模式,但如果遇到这种模式,你将会对正在处理的程序有很多了解。
哪里可以找到模式和食谱?
通常在网络上的各种教程中都可以找到,类似于这篇文章和某些书籍。以下是一些入门资源:
Patterns: Python Design Patterns - Singleton, The Pattern Concept - Python 3 Patterns, Recipes and Idioms
Recipes: Python Cookbook, Stack Overflow
在自然场景和有时工作中遇到的大多数或甚至所有代码在一定程度上是整理好的,这种整理就像模式一样是别人的想法,它是如何实现的有时是一个谜,你只需要了解代码和文件结构,以及项目风格即可。
Templates和Boilerplates是受社区或行业驱动的解决方案。当你想开始一个项目并需要一些基础布局(scaffolding)时,比如说当你想创建一个Flask(一种python网络框架)项目时,你可以自己完成,或者搜索一个flask样板,如下:
https://github.com/realpython/flask-boilerplate
在运行Repo之后,你将拥有以下文件结构,以及一个功能完备的骨架应用,这对于几分钟的工作来说是难能可贵的。
├── Procfile
├── Procfile.dev
├── README.md
├── app.py
├── config.py
├── error.log
├── forms.py
├── models.py
├── requirements.txt
├── static
│ ├── css
│ │ ├── bootstrap-3.0.0.min.css
│ │ ├── bootstrap-theme-3.0.0.css
│ │ ├── bootstrap-theme-3.0.0.min.css
│ │ ├── font-awesome-3.2.1.min.css
│ │ ├── layout.forms.css
│ │ ├── layout.main.css
│ │ ├── main.css
│ │ ├── main.quickfix.css
│ │ └── main.responsive.css
│ ├── font
│ │ ├── FontAwesome.otf
│ │ ├── fontawesome-webfont.eot
│ │ ├── fontawesome-webfont.svg
│ │ ├── fontawesome-webfont.ttf
│ │ └── fontawesome-webfont.woff
│ ├── ico
│ │ ├── apple-touch-icon-114-precomposed.png
│ │ ├── apple-touch-icon-144-precomposed.png
│ │ ├── apple-touch-icon-57-precomposed.png
│ │ ├── apple-touch-icon-72-precomposed.png
│ │ └── favicon.png
│ ├── img
│ └── js
│ ├── libs
│ │ ├── bootstrap-3.0.0.min.js
│ │ ├── jquery-1.10.2.min.js
│ │ ├── modernizr-2.6.2.min.js
│ │ └── respond-1.3.0.min.js
│ ├── plugins.js
│ └── script.js
└── templates
├── errors
│ ├── 404.html
│ └── 500.html
├── forms
│ ├── forgot.html
│ ├── login.html
│ └── register.html
├── layouts
│ ├── form.html
│ └── main.html
└── pages
├── placeholder.about.html
└── placeholder.home.html
甚至还有一系列Template,可用于不同的python项目,甚至是用于制作Template的Template:
https://github.com/cookiecutter/cookiecutter
当然也有一些注意事项和Template的劣势:
那么我用什么呢?视情况而定,在工作中,我通常会使用Template/Boilerplate;对于我自己的项目,我通常从单个文件上的类和函数开始,随着项目的进展,我通常开始制作单个文件结构,如果项目扩大,它最终会变成一个文件层次结构,我一直在使用常见的Patterns,但很快就会忘记我正在使用它们,特别是对于像MVC这样常见的。
整理python代码可能很难,但是不整理的话就会更糟糕。当然,这的确很困难,因为它要求你了解python和你的项目,但是如果你愿意从小处着手并投入时间,那么你最终会变得更快,整理代码将几乎毫不费力。
在这里,我试图给你提供关于python(但也可以适用于其他语言)的渐进式整理技能的基本路线图,希望你觉得这个过程不那么可怕了。有了强大的整理能力,也就有了强大的学习能力。
本文系外文翻译,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系外文翻译,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。