2.4 函数与类
2.4.1 函数
1.什么是函数
2.3.1小节讲到了变量。每用一次变量,就可以少写几十“个”字符。在Python里面,还有一个“函数”,每用一次函数,就可以少写很多“行”代码。
所谓的函数,就是一套定义好的流程:输入数据,得到结果。在现实生活中,函数可以体现在方方面面。对厨师来讲,每一个菜谱都是函数;对农民来讲,每一种种菜的方法都是函数;对建筑工人来讲,每一个结构的修建都是函数;对司机来讲,在不同路线上的驾驶方式也是函数……
两个函数之间可能相互独立,也可能一个函数的输入是另一个函数的输出,也可能在一个函数内部调用另一个函数。
以做菜为例:输入蒜苗、五花肉、豆瓣、豆豉、油、盐、味精等原材料,输出回锅肉,这就是“做回锅肉”函数。做一盘回锅肉有很多的步骤,每次自己做一盘回锅肉都要花掉一小时的时间。现在有一个非常厉害的做菜机器人,无论什么菜,只要在它面前演示一遍,它就会做了。于是,你只需要最后花一小时在它面前演示怎么做回锅肉,之后如果想吃了,对它喊一声“做个回锅肉”,就可以躺在床上等菜做好了。
给机器人演示做菜,就是定义函数的过程;让机器人做菜,就是调用函数;原材料是这个函数的输入参数;回锅肉是这个函数的输出。
回锅肉=做回锅肉(蒜苗,五花肉,豆瓣,豆豉,……)
为什么程序里面需要用到函数呢?简单粗暴一点的答案是,因为使用函数可以少写代码。
2.函数的作用
例2-1:现在想得到两个房间在不同情况的温度的统计信息,包括这两个房间温度的和、差、积、商、平均数。
(1)不使用函数
print(’初始情况:') print(’温度和:{}'.format(A+B)) print(’温度差:{}'.format(A - B)) print(’温度积:{}'.format(A * B)) print(’温度商:{}'.format(A / B)) print(’温度平均值:{}'.format((A+B)/2)) print('A房间进入一个人,B房间留空以后的温度信息:') print(’温度和:{}'.format(A+B)) print(’温度差:{}'.format(A - B)) print(’温度积:{}'.format(A * B)) print(’温度商:{}'.format(A / B)) print(’温度平均值:{}'.format((A+B)/2)) print('A房间放入一块烧红的炭,B房间放入一只狗以后的温度信息:') print(’温度和:{}'.format(A+B)) print(’温度差:{}'.format(A - B)) print(’温度积:{}'.format(A * B)) print(’温度商:{}'.format(A / B)) print(’温度平均值:{}'.format((A+B)/2)) ……
像这样的写法,每增加一个对比情况,代码就要多增加5行。而且如果现在在统计信息里面再加一个平方和,那么就需要在代码里面每一个显示统计信息的地方都做修改。
(2)使用函数
def show_temp_stastic(A, B): print(’温度和:{}'.format(A+B)) print(’温度差:{}'.format(A - B)) print(’温度积:{}'.format(A * B)) print(’温度商:{}'.format(A / B)) print(’温度平均值:{}'.format((A+B)/2)) print(’初始情况:') show_temp_stastic(A, B)
print('A房间进入一个人,B房间留空以后的温度信息:') show_temp_stastic(A, B) print('A房间放入一块烧红的炭,B房间放入一只狗以后的温度信息:') show_temp_stastic(A, B)
直观上看,代码量少了很多。如果想增加更多的统计信息,那么只需要直接写在函数里面就可以了,调用函数的地方不需要做任何修改。
3.定义函数
在Python里面,可使用def这个关键字来定义一个函数。一个函数的结构一般如下:
def函数名(参数1, 参数2, 参数3):
函数体第一行
函数体第二行
函数体第三行
…
函数体第n行
return返回值
一个函数可以有参数,也可以没有参数。如果没有参数,函数名后面为一对空括号。如果函数有参数,参数可以有一个,也可以有很多个,参数可以是任何数据类型的。函数的参数甚至可以是另一个函数。
一个函数有至少一个返回值,可以人为指定返回任何类型的数据。如果没有人为指定,那么返回值为None,返回值的个数可以是一个,也可以是多个。函数的返回值可以是另一个函数。
一个函数可以没有return语句,可以有一个return语句,也可以有多个return语句。
下面3种情况是等价的。
(1)没有return。
(2)return(只有return,后面不跟任何变量)。
(3)return None。
在函数中,可以使用return将里面的结果返回出来。代码一旦运行到了return,那么函数就会结束,return后面的代码都不会被执行。
请注意这里“return后面的代码”的真正意思,如图2-31所示。
图2-31 return后面的代码
在图2-31所示的func_example_1()函数中:
b = 2+2 print(b)
这两行是return后面的代码,这两行代码是永远不会被执行的。
但是在图2-31所示的func_example_2(x)这个函数中:
elif 0 < x <= 1: return x * 10 else: return 100
虽然第10行有一个return,但是第11~14行并不属于“return后面的代码”,因为“if...elif...else...”形成了3条分支,只有每个分支内部的return后面的代码才不会被执行,但是各个分支之间的return是互不影响的。由于代码只能从上往下写,所以第12行虽然写在第10行后面,但是第12行在逻辑上其实是在第10行的旁边,因此第12行是可能被执行的。在逻辑上,每一个分支是并列的,如图2-32所示。
图2-32 分支在逻辑上是并列的
在一个Python工程中,应该保证每个函数的名字唯一。函数体就是这个函数需要执行的一系列操作。操作可能只有一行,也可能有很多行。
一个函数只做一件事情,Python编码规范建议一个函数的函数体不超过20行代码。如果超过了,说明这个函数做了不止一件事情,就应该把这个函数拆分为更小的函数。这也就暗示了在函数体里面也可以调用其他的函数。
4.调用函数
例2-2:接收由用户输入的通过逗号分隔的两个非零整数,计算这两个数字的和、差、积、商,并将结果返回给用户。
问题分析:这个问题其实涉及3个相对独立的过程。
① 得到用户输入的数据。
② 计算两个数字的和、差、积、商。
③ 将结果打印出来。
这3个过程可以定义成3个函数,分别为如下。
① get_input()。
② calc(a, b)。
③ output(result)。
(1)get_input()
这个函数没有参数,它负责接收用户的输入。这里用到了Python的input关键字,这个关键字可以接收用户输入的字符串,并将得到的字符串返回给一个变量。需要注意的是,input返回的一定是一个字符串,所以get_input()这个函数不仅需要接收输入,还需要将输入的形如’10,5’的字符串转换为两个整数:10和5。
完整的代码如下:
def get_input(): input_string = input(’请输入由逗号分隔的两个非零整数:') a_string, b_string = input_string.split(', ') #将字符串10,5变为列表['10', '5'],并分别赋值给a_string和b_string,使得a_string #的值为“10”, b_string的值为“5” return int(a_string), int(b_string)
(2)calc(a, b)
这个函数只负责计算。对它来说,a、b两个参数就是两个数字。它只需要计算这两个数字的和、差、积、商,并将结果保存为一个字典返回即可。
def calc(a, b): sum_a_b = a+b difference_a_b = a - b product_a_b = a * b quotient = a / b return {'sum': sum_a_b, 'diff': difference_a_b, 'pro': product_a_b, 'quo': quotient}
(3)output(result)
这个函数只负责输出,将result这个字典中的值打印到屏幕上。
def output(result): print(’两个数的和为: {}'.format(result['sum'])) print(’两个数的差为: {}'.format(result['diff'])) print(’两个数的积为: {}'.format(result['pro'])) print(’两个数的商为: {}'.format(result['quo']))
代码运行如图2-33所示。
图2-33 顺序执行函数,接收用户输入并计算和、差、积、商后输出
在图中的代码里面可以看到,3个函数是按顺序独立运行的,后一个函数的输入是前一个函数的输出。数据流将3个函数连起来了。再来看图2-34,运行结果和上面的是完全一样的,但是这里演示了在函数里面运行另一个函数的情况。
图2-34 函数中调用函数,接收用户输入,计算和、差、积、商后输出
通过以上示例说明:函数之间可以串行运行,数据先由一个函数处理,再由另一个函数处理;函数也可以嵌套运行,在一个函数里面调用另一个函数。当然,在函数里面还可以定义函数。这就属于比较高级的用法了,这里略去不讲,有兴趣的读者可以阅读Python的官方文档。
5.函数的默认参数
前面的例子中,函数可以有参数,也可以没有参数。如果有参数,定义函数的时候就需要把参数名都写好。有时候会有这样的情况:一个函数有很多的参数,假设有5个参数,其中4个参数在绝大多数情况下都是固定的4个值,只有极少数情况下需要手动修改,于是称这固定的4个值为这4个参数的默认值。如果每次调用这个函数都要把这些默认值带上,就显得非常麻烦。这种情况在Python开发中特别常见,尤其是在一些科学计算的第三方库中。
以前面的get_input()函数为例:
def get_input(): input_string = input(’请输入由逗号分隔的两个非零整数:') a_string, b_string = input_string.split(', ') #将字符串10,5变为列表['10', '5'], 并分别复制给a_string和#b_string,使得 a_string的值为“10”, b_string的值为“5” return int(a_string), int(b_string)
在这段代码中,两个整数是以英文逗号来分隔的,那么可不可以使用其他符号来分隔呢?来看一下下面这段代码:
def get_input(split_char): input_string = input(’请输入由{}分隔的两个非零整 数:'.format(split_char)) a_string, b_string = input_string.split(split_char) return int(a_string), int(b_string)
来运行一下这段代码,这一次使用#号来分隔,如图2-35所示。
图2-35 使用#号分隔两个数字
是否可以既能用英文逗号分隔,又可以用#号分隔,并且默认情况下使用英文逗号分隔呢?如果每次调用这个函数的时候都必须写成a, b = get_input(', '),真的很麻烦,而且如果一不小心漏掉了这个参数,还会导致程序报错,如图2-36所示。
图2-36 漏掉了函数参数导致报错
在Python里面,函数的参数可以有默认值。当调用函数的时候不写参数时,函数就会使用默认参数。请看下面的代码:
def get_input_with_default_para(split_char=', '): input_string = input(’请输入由{}分隔的两个非零整数:'.format(split_char)) a_string, b_string = input_string.split(split_char) return int(a_string), int(b_string)
运行效果如图2-37所示。
图2-37 调用带有默认参数的函数的运行结果
如果调用函数get_input_with_default_para时不写参数,就会使用默认的逗号;如果带上了参数,那么就会使用这个参数对应的符号来作为分隔符。
函数也可以有多个默认参数,例如如下的代码:
def print_x_y_z(x=100, y=0, z=50): print('x的值为{}, y的值为{}, z的值为{}'.format(x, y, z)) print_x_y_z(1, 2, 3) #直接写上3个参数,从左到右依次赋值 print_x_y_z(6) #只写一个值的时候,优先赋值给左边的参数 print_x_y_z(y=-8) #也可以指定参数的名字,将值直接赋给指定的参数 print_x_y_z(y=’哈哈’, x=’嘿嘿’) #如果指定了参数名,那么参数顺序就无关紧要
运行结果如图2-38所示。
图2-38 调用含有多个默认参数的函数的运行结果
在调用函数的时候,如果指定了参数名,就会把值赋给这个参数;如果没有指定参数名,就会从左到右依次赋值给各个参数。
6.Python函数的注意事项
(1)函数参数的类型决定了它的作用范围
函数外面的容器类作为参数传递到函数中以后,如果函数修改了这个容器里面的值,那么函数外面的容器也会受到影响。但是函数外面的普通变量作为参数传递到函数中,并且函数修改了这个参数的时候,外面的变量不受影响。
为了更好地理解这段话,请看图2-39的运行结果。在代码中演示的容器类为列表,对字典和集合同样适用。
图2-39 函数可以修改容器类的数据但不能修改普通变量
(2)默认参数陷阱
请看下面的代码,并猜测其输出:
def default_para_trap(para=[], value=0): para.append(value) return para print(’第一步’) print(’函数返回值:{}'.format(default_para_trap(value=100))) print(’第二步’) print(’函数返回值:{}'.format(default_para_trap(value=50)))
很多读者会认为结果应该是这样的:
第一步 函数返回值:[100] 第二步 函数返回值:[50]
但是实际情况如图2-40所示。
图2-40 实际运行效果
这个问题涉及Python底层的实现,这里不做深入讨论。为了避免这个问题的发生,对函数做以下修改:
def default_para_without_trap(para=[], value=0): if not para: para = [] para.append(value) return para
运行结果如图2-41所示。
图2-41 修改后的运行结果
这样写代码并运行表面上确实没有什么问题,但是逻辑上是有冗余的。因为调用default_para_without_trap函数的时候,其实是传了para这个参数的,但是传的就是一个空的列表。这个时候,在函数里面其实可以直接使用传进来的空列表,而不是再重新创建一个新的空列表。于是,代码可以进一步优化:
def default_para_without_trap(para=None, value=0): if para is None: para = [] para.append(value) return para
这里有一个知识点,就是当要判断一个变量里面的值是不是None的时候,可以使用“is”这个关键字,也可以使用“==”。一般建议使用“is”关键字,因为速度会比“==”稍微快一些。
2.4.2 类与面向对象编程
这一小节简要介绍一下面向对象编程。面向对象编程的内容繁多,多到可以专门写一本书来讲解,因此这一小节会略去大多数不必要的概念和深奥的应用,只通过几个实际的例子来讲解对象、类和类的结构,以及如何读懂和写出一个类,使读者至少达到可以看懂代码的程度。
在Python里面,一切都是对象。请看下面的代码:
a = 'abc, def' a_prefix, a_suffix = a.split(', ') b = [1, 2, 3] b.append(4) b.extend([5, 6, 7]) b.pop() c = {'x': 1, 'y': 2, 'z': 3} c.get('x')
在上面的代码中,出现了好几个“xxx.yyy('zzz')”形式的语句,其中的“split”“append”“extend”“pop”“get”在面向对象编程中叫作一个对象的“方法”。代码中的“a”“a_prefix”“a_suffix”都是字符串对象,“b”是列表对象,“c”是字典对象。
对象有“属性”和“方法”。“属性”就是描述这个对象的各种标签,“方法”就是这个对象可以做的动作。例如,现在看这本书的你,你就是一个对象,你的名字、身高、体重、胸围……都是你的属性;你可以读书,可以做饭,可以上厕所,可以走路等,这里的“读书”“做饭”“上厕所”“走路”都是你的方法。
对象可以只有属性没有方法,也可以只有方法没有属性。
你是一个对象,那什么是类呢?你是一个人,而人是一个类。“类”是一个泛指的概念,只能感受,但是看不到,也摸不到;而对象是具体的特定个体,看得到,也摸得到。你、你父亲、你母亲,都是对象。几乎每个男人都会成为父亲,“父亲”是一个类,但是具体到你自己的父亲,就是一个对象。
首先要有类,才能有对象。如果没有人类,怎么会有现在正在看这一行字的你?人类有眼睛,所以你才有眼睛;人类能行走,所以你才能行走。只要定义了人类可以做什么事情,那么也就定义了你能做什么事情。所以在Python以及其他支持面向对象的编程语言中,要创建每一个具体的对象,都需要先创建类。
1.如何定义一个类
在Python中使用关键字“class”来定义一个类。类一般由以下元素构成:
· 类名;
· 父类;
· 初始化方法(在有些编程语言中叫作构造函数);
· 属性;
· 方法。
先来看下面这一段代码:
class People(object): def __init__(self, name, age): self.name = name self.age = age self.jump()
def walk(self): print(’我的名字叫作:{},我正在走路’.format(self.name)) def eat(self): print(’我的名字叫作:{},我正在吃饭’.format(self.name)) def jump(self): print(’我的名字叫作:{},我跳了一下’.format(self.name)) xiaoer = People(’王小二’, 18) zhangsan = People(’张三’, 30) print('=============获取对象的属性=============') print(xiaoer.name) print(zhangsan.age) print('=============执行对象的方法==============') xiaoer.walk() zhangsan.eat()
运行结果如图2-42所示。
图2-42 People这个类和它生成的对象
在代码中,第1行定义了一个类,类名为“People”,这个类的父类为“object”。这种写法称为“新式类”,其实还有一种“经典类”的写法,但是那种写法已经不提倡了,所以不做单独讲解。object是Python内置的一个对象,开发者自己写的类需要继承于这个object或者继承自己写的其他类。
人是一个类,人的父类可以是“脊椎动物”,“脊椎动物”的父类可以是“动物”, “动物”的父类可以是“生物”。这样一层一层往上推,推到最上面推不动为止。在这里,Python初学者可以把object认为是这个最上面的角色。开发者自己创建的第1个类一般来说需要继承于这个object,第2个类可以继承于第1个类,也可以直接继承于object。
第2行称为构造函数。一旦类被初始化,就会自动执行。所以在第16行和17行初始化People并生成两个对象“xiaoer”和“zhangsan”的时候,构造函数里面的代码也就运行了。类可以不写构造函数。
“name”“age”是这个类的属性,“walk”“eat”“jump”都是这个类的“方法”。一般来说,属性是名词,方法是动词。在类的外面,把类初始化为一个对象以后,可以使用“对象.属性”的格式来获得这个对象的属性;可以使用“对象.方法名(参数)”的格式来执行对象的方法,这很像是调用一个函数。其实可以理解为,方法就是类里面的函数。
在类的内部,如果要运行它自己的方法,那么调用的时候需要使用“self.方法名(参数)”的形式。如果要让一个变量在这个类的所有方法里面都能直接使用,就需要把变量初始化为“self.变量名”。
2.如何读懂一个类
本书要求读者至少需要达到能读懂一个类的代码并明白这个类能做什么的程度。
首先需要明白一点,是否使用面向对象编程与代码能否正常运行没有任何关系。使用面向对象编程或者使用函数都可以实现相同的功能。区别在于写代码、读代码和改代码的“人”。面向对象编程的作用是方便代码的开发和维护。
那么如何阅读一个使用面向对象思想开发的程序呢?基本思路如下。
① 这个类有哪些属性(看外貌)。
② 这个类有哪些方法(能做什么)。
③ 这些方法在哪里被调用(做了什么)。
④ 这些方法的实现细节(怎么做的)。
例2-3:读懂一个机器人类。
class Robot(object): def __init__(self, name): self.name = name #名字 self.height=30 #身高30 厘米 self.weight=5 #体重5千克 self.left_foot_from_earth = 0 #左脚距离地面0厘米 self.right_foot_from_earth = 0 #右脚距离地面0厘米 self.left_hand_from_earth = 15 #左手距离地面15厘米 self.right_hand_from_earth = 15 #右手距离地面15厘米 def _adjust_movement(self, part, current_value, displacement): """ 脚不能插到地底下,也不能离地高于15厘米。 手不能低于身体的一半,也不能高于40厘米 :param part:foot或者hand :param displacement: int :return: int """ if part == 'foot': boundary = [0, 15] elif part == 'hand': boundary = [15, 40] else: print(’未知的身体部位!') return new_value = current_value+displacement
if new_value < boundary[0]: return boundary[0] elif new_value > boundary[1]: return boundary[1] else: return new_value def move_left_foot(self, displacement): left_foot_from_earth = self.left_foot_from_earth+displacement if left_foot_from_earth > 0 and self.right_foot_from_earth > 0: print(’不能双脚同时离地,放弃移动左脚!') return self.left_foot_from_earth = self._adjust_movement('foot', self.left_foot_from_earth, displacement) self.announce() def move_right_foot(self, displacement): right_foot_from_earth = self.right_foot_from_earth+displacement if right_foot_from_earth > 0 and self.left_foot_from_earth > 0: print(’不能双脚同时离地,放弃移动右脚!') else: self.right_foot_from_earth = self._adjust_movement('foot', self.right_foot_from_earth, displacement) self.announce() def move_left_hand(self, displacement): self.left_hand_from_earth = self._adjust_movement('hand', self.left_hand_from_earth, displacement) self.announce() def move_right_hand(self, displacement): self.right_hand_from_earth = self._adjust_movement('hand', self.right_hand_from_earth, displacement) self.announce() def announce(self): print('\n**************************') print(’左手距离地面:{}厘米’.format(self.left_hand_from_earth)) print(’右手距离地面:{}厘米’.format(self.right_hand_from_earth)) print(’左脚距离地面:{}厘米’.format(self.left_foot_from_earth)) print(’右脚距离地面:{}厘米’.format(self.right_foot_from_earth)) print('**************************\n') def dance(self): self.move_left_foot(14) self.move_right_foot(4) self.move_left_hand(20) self.move_right_hand(100) self.move_right_hand(-5) self.move_left_foot(-2) if __name__ == '__main__': robot = Robot(’瓦力’) robot.dance()
这一大段代码看起来非常多,但只要使用了分析类的方法,就会变得非常简单。先看__init__()构造函数中定义的各个属性,了解这个类的“外貌”。这个类的属性包括名字(name)、身高(height)、体重(weight)、左右脚到地面的距离(left_foot_from_earch、right_foot_from_earch)、左右手到地面的距离(left_hand_from_earch、right_hand_from_earch)。
再看这个类有哪些方法:移动左脚(move_left_foot)、移动右脚(move_right_foot)、移动左手(move_left_hand)、移动右手(move_right_hand)、跳舞(dance)、宣布(announce),还有一个前面加了下划线的调整移动(_adjust_movement)和构造函数(__init__)。这些方法就是这个机器人可以做的动作。
接着看这些方法在哪里被调用。首先,机器人这个类被初始化为一个对象,这个对象赋值给robot这个变量。在初始化的时候,构造函数自动执行,所以__init__里面的语句都会执行,将属性初始化。
初始化以后,调用robot.dance(),让机器人跳舞。
再看dance这个方法:
· 左脚向上移动14;
· 右脚向上移动4;
· 左手向上移动20;
· 右手向上移动100;
· 右手向下移动5;
· 左脚向下移动2。
看到名字就知道方法要做什么事情。此时即使不知道手脚是如何移动的,但是也已经对整个程序的功能了解得八九不离十了。
在对整体已经有了了解的情况下,再去看每个方法的具体实现细节。有时候即便某一行使用了一个特别生僻的用法,但是只要理解了其所在方法是用来做什么的,那就能理解这个生僻用法的原理。