【Python】一文说清楚类与函数的选择
转载自https://zhuanlan.zhihu.com/p/108418193
前两天一位已经学习python一段时间的小伙伴问了这样一个问题:
虽然已经使用python一年多了,也用python写过很多脚本,代码量从几十行到上千行的也有,但是从未使用过类(class),似乎用函数(def)就能解决所有问题,使用类有什么好处?到底什么时候该用类呢?
关于这个问题,算是困惑了许多刚接触python的同学,那么本文就尝试从多个角度来解读这个问题。首先还是先来看看官方给出类与函数的解释。
类提供了一种组合数据和功能的方法。 创建一个新类意味着创建一个新的对象类型,从而允许创建一个该类型的新实例 。 每个类的实例可以拥有保存自己状态的属性。 一个类的实例也可以有改变自己状态的(定义在类中的)方法。函数的本质就是一段有特定功能、可以重复使用的代码,这段代码已经被提前编写好了,并且为其起一个“好听”的名字。在后续编写程序过程中,如果需要同样的功能,直接通过起好的名字就可以调用这段代码。很显然,这样的答案并没有让人搞明白类和函数到底不一样在哪里。但是里面提到了类是创建一个对象,所以类是面向对象程序设计(Object Oriented Programming)。也就是我们常说的OOP。而OOP高度关注的是代码的组织,可重用性和封装。
几个例子
上面的官方解释上去还是很抽象,那么我们开始说人话。简单来说当Python中没有可以完全表达我们要表示的内容的数据类型时,那么就需要使用一个类。来看下面的例子。
如果我正在计算某人的年龄,则只需使用int因为它可以满足我的需求。如果我们需要在游戏中表示像敌人之类的东西,则可以创建一个类则可以创建一个类Enemy,其中包含诸如health和armor的数据,并包含诸如fire_weapon射击时的功能。然后,我们还可以创建另一个类FlyingEnemy,Enemy该类从该类继承所有内容,但又具有一个fly方法,因此具有其他功能。
我们再来看一个例子。假设我们需要编写一个音乐播放器。在这个播放器中,我们有关于不同类型数据的信息,如歌曲、专辑、艺术家和播放列表。还有一些可以播放歌曲、播放专辑、播放艺术家或播放播放列表的功能。我们将每种数据存储在字典中,不同类型的数据有不同的字段名,因为每个play函数需要做不同的事情,所以我们就有四个不同的函数:
1 some_song = { 2 "title": "Yellow Submarine", 3 "artist": the_beatles, # 指向到包含该艺术家的词典 4 "album": yellow_submarine_album, # 指向包含此相册的dict的链接 5 "duration": insert_time_object_here, 6 "filepath": "path/to/file/on/disk" 7 } 8 9 # 其他数据类型的结构也类似 10 11 # 一些函数 12 def play_song(song): 13 # 获取歌的路径 14 path = song["filepath"] 15 # 播放路径 16 call_some_library_function(path) 17 18 def play_album(album): 19 # 找到专辑里所有的歌曲 20 # 分别调用play_song 21 22 def play_artist(artist): 23 # 找到这位艺术家所有的专辑 24 # 分别调用play_album 25 26 def play_playlist(playlist): 27 # 找到播放列表中的所有歌曲 28 # 分别调用play_song
这样写有什么不好?我们有四个非常相似的函数,每个函数都与特定类型的数据相关。你必须把它们叫做不同的东西,而不仅仅是play,你必须确保你把正确的数据传递给它们。虽然这四种不同的类型都可以“播放”,但是没有一种通用的方法可以在不知道它是什么的情况下播放任何东西。那么在OOP下,怎么实现呢:
1 class Song: 2 def __init__(self, title, artist, album, duration, filepath): 3 self.title = title 4 self.artist = artist 5 self.album = album 6 self.duration = duration 7 self.filepath = filepath 8 9 def play(self): 10 path = self.filepath 11 call_some_library_function(path)
这样就定义了如何创建一个新的Song对象。该方法将字段值作为参数,并将它们作为对象的属性赋值。self是一个特殊参数(名称不保留;它可以被称为任何东西),它是对对象本身的引用。是一种从同一对象的其他方法内部访问属性和方法的方法。当我们从对象外部访问它们时(要使用play方法时将执行此操作),则可以使用在该范围内为对象指定的任何名称。
那么在之前:
1 # some_song是上面定义的歌 2 play_song(some_song)
在使用class之后:
1 # self参数没有在这里传递;它会自动添加 2 some_song = Song("Yellow Submarine", 3 the_beatles, 4 yellow_submarine_album, 5 insert_time_object_here, 6 "path/to/file/on/disk" 7 ) 8 some_song.play()
为什么这样更好?如果我们有一个对象,则不必知道它是什么就可以播放,因为现在播放任何内容的语法都是相同的:anyobject.play()即对象“知道”如何使用“自己的”数据进行处理的设计思想。无需从外部检查对象是否具有某些字段并决定如何处理这些内部字段,而是调用play对象提供的方法,并在每个类内部定义该类型的对象应如何实现此功能。我们继续看下面两段代码来实现输出一些学生的成绩,首先是使用类:
1 class Student(object): 2 def __init__(self, name, age, gender, level, grades=None): 3 self.name = name 4 self.age = age 5 self.gender = gender 6 self.level = level 7 self.grades = grades or {} 8 9 def setGrade(self, course, grade): 10 self.grades[course] = grade 11 12 def getGrade(self, course): 13 return self.grades[course] 14 15 def getGPA(self): 16 return sum(self.grades.values())/len(self.grades) 17 18 # 定义一些学生 19 john = Student("John", 12, "male", 6, {"math":3.3}) 20 jane = Student("Jane", 12, "female", 6, {"math":3.5}) 21 22 # 现在我们可以很容易地得到分数 23 print(john.getGPA()) 24 print(jane.getGPA())
再来看看用函数怎么实现
1 def calculateGPA(gradeDict): 2 return sum(gradeDict.values())/len(gradeDict) 3 4 students = {} 5 name, age, gender, level, grades = "name", "age", "gender", "level", "grades" 6 john, jane = "john", "jane" 7 math = "math" 8 students[john] = {} 9 students[john][age] = 12 10 students[john][gender] = "male" 11 students[john][level] = 6 12 students[john][grades] = {math:3.3} 13 14 students[jane] = {} 15 students[jane][age] = 12 16 students[jane][gender] = "female" 17 students[jane][level] = 6 18 students[jane][grades] = {math:3.5} 19 20 print(calculateGPA(students[john][grades])) 21 print(calculateGPA(students[jane][grades]))
这两段代码都实现了输出学生的成绩,但是在使用函数的时候,我们需要记住学生是谁,成绩存储在哪里,似乎不是很困难(如果需要输出的学生更多呢),但是OOP避免了这一点。并且代码也更加pythonic。
结束语
最后,让我们回到刚开始的问题上来,上面说了这么多类的好处所以我们就应该更多的去使用类吗?并不是!
其实从某种意义上来说,类并不比函数更好。只是在某些情况下使用类能够更好的帮助我们写代码。所以如果发现自己使用各种数据集调用some_function(data),那么将其用类表示为data.some_function()可能提高我们的效率。至于到底在何时使用类,我们来看看其他程序员的理解
- 当我们拥有一堆共享状态的函数,或者将相同的参数传递给每个函数时,我们可以重新考虑代码使用类。
- 类的“可重用性”意味着我们可以在其他应用程序中重用之前的代码。如果我们在自己的文件中编写了类,则只需将其放在另一个项目中即可使其工作。
- 函数对于小型项目非常有用,但是一旦项目开始变大,仅使用函数就可能变得混乱。类是组织和简化代码的一种非常好的方法
- 通常,如果在函数内部找到自写函数,则应考虑编写类。如果我们在一个类中只有一个函数,那么请坚持只写一个函数。
- 如果需要在函数调用之间保留一些状态,那么最好使用带有该函数的类作为方法