python函数的定义与调用

在python中 ,函数是一个组织好的 ,可以重复使用的代码段 ,函数可以提高代码的重复利用率 ,原则上一个函数只实现一个单一的功能 ,这样能增强程序的模块性, python有许多的内置函数可供你使用, 你也可以自己定义函数,这通常被称之为自定义函数

1. 函数的定义

  1. def 函数名(参数列表):
  2. 函数体

先看几个示例

  1. def hello_word():
  2. pass
  3. def print_diamond(count):
  4. pass
  5. def get_max(a, b, c):
  6. pass

关于上面这段代码,你要记住下面3个结论

  1. 函数可以没有参数
  2. 函数的参数可以是一个,也可以是多个
  3. pass的作用相当于占位符,没有实际作用

    2. 函数的调用

    示例1, 定义并调用没有参数的函数
    1. def hello_word():
    2. print('hello world')
    3. hello_word()
    示例2, 定义并调用有一个参数的函数
    1. def print_diamond(count):
    2. """
    3. 输出菱形
    4. """
    5. for i in range(count):
    6. if i <= count//2:
    7. print(" "*(count//2-i) + "*"*(2*i + 1))
    8. else:
    9. print(" "*(i - count//2) + "*"*((count-i)*2-1))
    10. print_diamond(11)
    示例3, 定义并调用有三个参数的函数
    1. def get_max(a, b, c):
    2. max_number = a
    3. if b > max_number:
    4. max_number = b
    5. if c > max_number:
    6. max_number = c
    7. return max_number
    8. max_num = get_max(22, 23, 21)
    9. print(max_num)
    当你使用def 定义了一个函数后,只有调用这个函数才能让这个函数运行。

    3. 函数的返回值

    在第2小节中的示例3中,这个函数的功能是找出a, b, c三个数据中的最大值,获得最大值后,使用return 语句将max_number返回,max_number就是函数get_max的返回值,这个返回值最终赋值给变量max_num。
    return的作用是退出函数并返回函数的返回值,任何时候,只要执行了return语句就一定会退出函数。你可能已经注意到第2小结中的示例1和示例2的函数里并没有return语句,那么这样的函数有返回值么?我们来做一下实验
    1. def hello_word():
    2. print('hello world')
    3. res = hello_word()
    4. print(res)
    尽管函数hello_word没有使用return语句,但仍然会有返回值,这种情况下,函数默认返回None, 关于None,我会专门写一篇教程。
    python的函数允许你一次返回多个结果
    1. def return_tuple():
    2. return 1, 2
    3. res = return_tuple()
    4. print(res, type(res))
    程序输出结果
    1. (1, 2) <class 'tuple'>
    函数一次返回多个结果时,是以元组的形式返回的。
    如果函数里没有任何数据需要返回,但需要提前结束,也可以使用return,这种用法我会在递归函数中做讲解。

python中的None

1. None的类型

None在python中是一个特殊的对象,它表示空值,其类型为NoneType

  1. >>> type(None)
  2. <class 'NoneType'>

2. 只存在一个None

None只存在一个,python解释器启动时创建,解释器退出时销毁

  1. >>> a = None
  2. >>> b = None
  3. >>> a == b
  4. True
  5. >>> a is b
  6. True

由于内存None只有一个,所以a is b的结果为True

3. None 的运算

None不支持任何运算,也没有内建方法,除了表示空以外,什么都做不了。
如果要判断一个对象是否为None,使用is身份运算符

  1. >>> a = None
  2. >>> a is None
  3. True

4. None的使用

如果一个函数,没有显式return任何数据,则默认返回None。
在判断语句中,None等价于False

  1. >>> a = None
  2. >>> not a
  3. True

函数的参数

函数参数专门用一篇教程来讲解,可见这部分内容的复杂与重要。

1. 形参和实参

所谓形参,就是函数定义中的参数,形参在函数体内可以使用
而实参,则出现在调用过程中,这两个概念不必刻意去记忆

  1. def print_diamond(count):
  2. for i in range(count):
  3. if i <= count//2:
  4. print(" "*(count//2-i) + "*"*(2*i + 1))
  5. else:
  6. print(" "*(i - count//2) + "*"*((count-i)*2-1))
  7. print_diamond(11)

上面的代码中,函数定义中的count就是形参,而最后一行的代码中,11就是实参,形参代表一种形式,实参,是实际的具体的起作用的数据。

2. 必传参数(位置参数)

  1. def my_print(content, count):
  2. for i in range(count):
  3. print(content)
  4. my_print('ok', 2)

上面定义的my_print函数里有两个参数,content 和count,他们都是必传参数,在调用函数时,你必须传入两个值,否则就会报错,下面演示一种错误的调用方法

  1. def my_print(content, count):
  2. for i in range(count):
  3. print(content)
  4. my_print('ok')

报错内容为

  1. Traceback (most recent call last):
  2. File "/Users/kwsy/kwsy/coolpython/test.py", line 5, in <module>
  3. my_print('ok')
  4. TypeError: my_print() missing 1 required positional argument: 'count'

从最后的TypeError的内容来看,缺失了一个位置参数(positional argument),我想,必传参数更能准确的描述这个参数的性质。 在函数my_print内部,用到了count这个形参,可是调用函数时,只传了content, 却没有传count,最终报错。

3. 默认参数

my_print函数根据count参数输出指定次数的content,我希望在两次输出的空档可以暂停一段时间,因此增加一个sleep参数

  1. import time
  2. def my_print(content, count, sleep=1):
  3. for i in range(count):
  4. print(content)
  5. time.sleep(1)
  6. my_print('ok', 2)

在调用函数时,我并没有传sleep这个形参,程序并没有报错,这是因为在定义函数的时候,给了sleep形参默认值1,这意味着,如果调用函数时没有传这个参数,python会用默认值来执行函数。默认值参数非常用有用,假设你在系统里的很多地方都使用最开始定义的my_print函数

  1. def my_print(content, count):
  2. pass

开始某一天,你需要增加一个参数sleep, 如果你将sleep定义为必传参数,那么所有用到函数my_print的地方都必须修改调用时的代码来增加这个入参,但如果你把sleep定义为默认参数,sleep已经默认等于1,除非必须传入其他的数值,否则就不需要这么做,之前的代码仍然可以使用。
定义函数是时,如果有多个默认参数,他们必须放置在参数列表的最后,不允许在中间放置一个必传参数。

4. 关键字参数

关键字参数不是一个出现在函数定义时的概念,而是一个出现在函数调用时的概念。

  1. import time
  2. def my_print(content, count, sleep=1):
  3. for i in range(count):
  4. print(content)
  5. time.sleep(1)
  6. my_print(count=2, content='关键字参数', sleep=2)

以第3小节中定义的函数my_print为例,在调用函数时,我使用了key=value的形式来传递参数,不仅如此,还打乱了顺序,先传入了count,后传入了content,关键字参数允许你以任何顺序传递参数,只要必传参数都以key=value的形式传值即可。现在,你应该明白我前面所讲的,关键字参数是出现在函数调用时的概念。
使用关键字参数,可以让参数传递更加明确,让调用方清楚的知道每个参数的传值情况。

5. 可变参数

可变参数分为两种:

  1. *args 接受任意多个实际参数
  2. **kwargs接收任意多个以关键字参数赋值的实际参数

    5.1 *args

    在定义函数时,有时候你并不希望参数的个数是固定的,这种设计在实际工作中很常见。
    1. def func(*args):
    2. print(args, type(args))
    3. sum_res = 0
    4. for item in args:
    5. sum_res += item
    6. return sum_res
    7. print(func(2))
    8. print(func(2, 3, 4))
    你可以看到,我在定义func时,使用*args, args只是个名字,你可以随意修改,关键是前面有一个星。有了这个星,函数调用时传递多少个参数就变成了一个很随意的事情,所有传入的参数将被放入到一个元组中,因此args的类型是tuple元组。

    5.2 **kwargs

    1. def print_score(**kwargs):
    2. print(kwargs, type(kwargs))
    3. for course, score in kwargs.items():
    4. print(course, score)
    5. print_score(yuwen=89, shuxue=94)
    在调用函数时,以关键字参数的形式进行参数传递,最终这些key-value对将组装成字典,kwargs的类型是dict。个人理解,kwargs就是一种为了偷懒而做的设计,当函数需要传入很多参数(多达10几个)时,使用kwargs定义函数是非常方便的。

递归函数

1. 什么是递归函数

递归函数是一种特殊的函数,函数内部会调用函数自身,现在,我将《函数的参数》这篇教程中的一个函数例子改造成递归函数,你可以对比他们之间的区别。

  1. def my_print(content, count):
  2. for i in range(count):
  3. print(content)
  4. my_print('ok', 2)

改造成递归函数后

  1. def my_print(content, count):
  2. print(content)
  3. if count == 1:
  4. return
  5. my_print(content, count-1)
  6. my_print('ok', 2)

在改造后的递归函数中,会再次调用my_print函数自身。你不禁会担忧,这样自身调用自身,岂不是要形成死循环?没错,会形成死循环,因此递归函数一定要有一个递归终止的条件,在本示例中,递归终止的条件是count=1,当这个条件满足时,就会执行return 语句,函数退出。

2. 4个步骤,写出一个递归函数

递归函数对初学者来说,是无法回避的噩梦。刚刚对程序顺序执行有了一定理解后,突然间告诉你函数可以调用函数自身,近似无限循环的递归调用让你深陷其中无法自拔。
编程,既是一门知识,也是一门手艺,你可以自学知识,但手艺很难自学,没有长时间的经验积累,没有一次次的试错和总结,你的技能就无法得到提升,所以,手艺,需要有人传帮带。那么今天,就让专业的python工程师教你如何用4个步骤写出一个递归函数。
咱们以最简单的计算阶乘为例,将完成递归函数归纳为4个步骤

  1. 定义函数功能
  2. 找到终止条件
  3. 甩锅
  4. 反甩锅

    2.1 第一步,定义函数功能

    不管最终能不能写出完整的递归函数,你至少应该有能力完成函数定义啊,这就好比数学考试,题不会做,”解” 字你总会写吧
    1. def recursion(number):
    2. """
    3. 计算number的阶乘
    4. :param number:
    5. :return:
    6. """

    2.2 第二步,找到终止条件

    所有的递归函数,一定会有终止条件,如果不符合这个要求,那么就会进入死循环。函数的功能是计算阶乘,阶乘的终止条件是什么呢? 是1啊,1的阶乘是1,你不能计算0的阶乘,1是最小的可以求阶乘的整数,现在,假设调用函数传参number为1,代码该怎么写呢?
    1. def recursion(number):
    2. """
    3. 计算number的阶乘
    4. :param number:
    5. :return:
    6. """
    7. if number == 1:
    8. return 1
    函数的功能是计算number的阶乘,如果number等于1,直接返回1就好了

    2.3 第三步,甩锅

    如果number不是1,该怎么办呢?答案很简单,甩锅,这就是说,现在你搞不定这个事情了,那你可以把锅帅给别人啊,你找到小明,告诉他: 老师让我计算number的阶乘,你现在计算number-1的阶乘,把结果告诉我,要快,不然老师生气了。
    你看,把锅甩给了小明,小明如果能算出来number-1的阶乘,你把他的答案乘以number,不就是number的阶乘了么,如果小明算不出来,你对老师也能有交代,是小明的过错,这就叫甩锅,这就是函数的递归调用
    1. def recursion(number):
    2. """
    3. 计算number的阶乘
    4. :param number:
    5. :return:
    6. """
    7. if number == 1:
    8. return 1
    9. next_recursion = recursion(number-1) # 等待小明的结果

    2.4 第四步,反甩锅

    你把锅甩给小明,小明也不傻,他又把锅甩给了小刚,小刚表示很无辜,但事情还是要做,于是又甩给了小红,就这样,一个接着一个的甩,但是要注意,总会有甩不出去的时候,最后,锅甩到了小芳这里,小芳是个好姑娘,聪明乖巧,到她这里,需要计算1的阶乘,小芳心想,甩不出去了,而且也不用甩啊,1的阶乘就是1啊,多简单。
    于是,开始了第四个步骤,反甩锅,1的阶层等于1,小芳心想,我已经告诉你答案了,剩下的事情你们处理吧,这口锅沿着一开始的路线反向的甩了回来,每个人都得到了之前自己甩锅的人的答案,现在小明把number-1的阶乘告诉给你,你应该怎么办呢,你应该把结果乘以number,然后return这个结果,最终完成了老师的要求
    1. def recursion(number):
    2. """
    3. 计算number的阶乘
    4. :param number:
    5. :return:
    6. """
    7. if number == 1:
    8. return 1
    9. next_recursion = recursion(number-1)
    10. return next_recursion*number
    11. if __name__ == '__main__':
    12. print(recursion(4))
    上面的代码,没有一个字符超出你的学习范围,就是知识;以怎样的逻辑组织代码,理解代码结构,这就是手艺

函数存在的意义

花草鱼鸟,都是自然造物,很难说哪一个事物的存在是必须的,哪一个事物的存在是没有必要的。而对于人造物,比如一门编程语言,它的语法和设计都是必须的,对于没有存在意义的设计,一定会被剔除掉。所有的主流编程语言,都有函数,这说明函数的存在是不可缺少的。

1. 代码重用

函数存在的一个非常明显的作用和意义就是代码重用。没有代码重用,编程人员就会被活活累死,费尽千辛万苦写出来的代码只能使用一次,有类似的功能需要完成时,不得不重头开始写起。下面就用一个特别简单的例子来向你阐明代码重用的重要性。

  1. import os
  2. file_lst = os.listdir('/Users/kwsy/kwsy/coolpython')
  3. py_file_lst = []
  4. for file_name in file_lst:
  5. if file_name.endswith('.py'):
  6. py_file_lst.append(file_name)
  7. print(py_file_lst)

上面这段代码可以获取指定文件夹下的所有文件名称,我使用一个for循环挑选出所有以.py结尾的文件,这样就获取到了指定目录下的所有python脚本。仅从功能实现上来看,代码没有任何问题,问题出在这段代码无法被重用。如果下一次你希望获取另一个文件夹下的python脚本,该怎么办呢,修改这段代码? 如果下一次你想获取指定目录下的所有word文档,又该怎么办呢?
你应该注意到,至少有两个维度在发生变化时会导致这段代码无法被重用,这时,你应该想到函数

  1. import os
  2. def get_file(file_path, suffix):
  3. file_lst = os.listdir(file_path)
  4. target_file_lst = []
  5. for file_name in file_lst:
  6. if file_name.endswith(suffix):
  7. target_file_lst.append(file_name)
  8. return target_file_lst
  9. py_file_lst = get_file('/Users/kwsy/kwsy/coolpython', '.py')
  10. print(py_file_lst)

一旦使用了函数,上面的问题就迎刃而解了,不管是文件夹发生了变化,还是想要获取的文件类型发生了变化,函数体里的代码都不需要修改,只需要在调用函数时传入你期望的参数就可以了。你写好了这段代码,其他人就可以通过引入模块的方式来调用你的代码。

2. 有助于我们思考

在函数设计上有一个原则,叫做单一职能原则,意思是说,一个函数只完成一个特定的功能。我以冒泡排序来向你解释什么叫做到单一职能原则,并向你展示函数是如何帮助我们思考问题的。

2.1 冒泡排序算法

冒泡排序是最简单的排序算法,它可以让一个无序的列表变得有序。
冒泡排序的核心思想是相邻的两个数据进行比较,假设数列A有n个数据,先比较第1个和第2个数据,如果A1 > A2,则交换他们的位置,确保较大的那个数在右侧。
接下来比较A2和A3,采用相同的规则,较大的数向右移动,最后会比较An-1 和An的大小,如果An-1 > An,那么交换他们的位置,这时,An就是数列中的最大值。

2.2 在指定范围内移动

你应该已经发现,经过一轮比较后,数列仍然是无序的,但是没有关系,你已经找到了最大值An,而且它在列表的末尾,我们定义个简单的函数

  1. def move_max(lst, max_index):
  2. """
  3. 将索引0到max_index这个范围内的最大值移动到max_index位置上
  4. :param lst:
  5. :param max_index:
  6. :return:
  7. """
  8. for i in range(max_index):
  9. if lst[i] > lst[i+1]:
  10. lst[i], lst[i+1] = lst[i+1], lst[i]

在0到max_index这个范围内,函数move_max实现了冒泡排序的基本操作,到目前为止,我们还没有完整的实现冒泡排序,但是我们实现了冒泡排序的一个基本操作。这个函数的功能非常单一,单一的你都不知道这个函数究竟和冒泡排序有什么关系,别着急,继续往下看。

2.3 重复简单

在2.2 中,我们已经实现了一个简单的函数,咱们用它来做一个小实验

  1. lst = [4, 2, 1, 6, 3]

这有一个列表,长度是5,最大索引是4,执行一次move_max(lst, 4)后,列表的内容发生了变化

  1. lst = [2, 1, 4, 3, 6]

列表的最大值被移动到了最大索引4的位置上,OK,最关键,最核心的地方到了,现在,让我们忘记6,忘记lst[4], 执行函数调用move_max(lst, 3), lst[4]已经是列表里的最大值了,如果此时我调用执行move_max(lst, 3),那么lst[3]不就变成了列表里第2大的数了么,沿着这个思路继续向,move_max(lst, 2), move_max(lst, 1),经过这一番操作,不就让列表变得有序了么?

  1. def pop_sort(lst):
  2. """
  3. 实现冒泡排序
  4. :param lst:
  5. :return:
  6. """
  7. for i in range(len(lst)-1, 1, -1):
  8. move_max(lst, i)
  9. def move_max(lst, max_index):
  10. """
  11. 将索引0到max_index这个范围内的最大值移动到max_index位置上
  12. :param lst:
  13. :param max_index:
  14. :return:
  15. """
  16. for i in range(max_index):
  17. if lst[i] > lst[i+1]:
  18. lst[i], lst[i+1] = lst[i+1], lst[i]
  19. if __name__ == '__main__':
  20. lst = [4, 2, 1, 6, 3]
  21. pop_sort(lst)
  22. print(lst)

函数的单一职能原则有助于我们抽丝剥茧,从混乱复杂的事物中找到解决问题的一小步,然后重复这一小步;或者找到多个一小步,之后组合这些小的步骤构建出一大步来解决更大的问题。

函数学习要点

函数是python的一等公民,是掌握python这门编程语言的关键,如何准确无误的理解其概念,如何得心应手的灵活运用,本篇教程尝试带给你一些启发。

1. 把函数定义与函数调用分开理解

1.1 函数定义

函数的定义由4部分组成

  1. def 关键字
  2. 函数名称
  3. 函数参数
  4. 函数体

    1. def add(a, b):
    2. return a + b

    函数体是指函数内部的代码,在上面的示例中,函数体内只有一行代码 return a + b

    1.2 函数执行

    使用def定义函数后,我们仅仅是定义了一个函数,这段代码什么也做不了,函数只有被执行,被调用才能运行函数体里的代码。而被执行,被调用的方式就是在函数名称后面加上一对小括号并传入相应的参数。

    1. add(1, 4)

    理解这段代码,要明确以下几点:

  5. add 是我们刚刚定义的函数

  6. add 后面加一对小括号,是在调用执行函数add,你可以引申为执行函数add函数体里面的代码
  7. 函数add 在定义时,定义了参数 a, b, 因此,在调用执行时必须明确告知a,b分别是什么
  8. a = 1, b = 4, a + b = 5, return将这个结果返回

执行这段代码,你在编辑器里什么也看不到,那是因为你并没有执行任何输出语句,所以程序不会输出任何东西,想要看到结果,可以这样操作

  1. print(add(1, 4))

定义只需一次,调用执行可以有多次, 这就好比你自己打造了一把斧头,这把斧头你只打造了一次,但可以用它来砍树很多次。

2. 正确理解返回值

我们先说结论,任何函数都有返回值。很多人不能理解返回值,究其根本,是因为还不能理解为什么要定义一个函数,不理解函数存在的意义。
以前面所定义的函数 add为例,这个函数的功能是计算两个数的和,当我执行代码 add(1, 4)时就是在计算1 + 4的和, 这个和等于5,函数的return语句把结果5返回

  1. num = add(1, 4)
  2. print(num)

在这个示例中,add函数的返回值是5, 并最终赋值给变量num, 如果函数没有返回值,那么我们怎么知道add(1, 4)的结果呢?

3. 正确理解return语句

一旦遇到return语句,函数就结束了,这一点是很多人都忽略的一个事情,在你还不能完全理解掌握函数之前,你不妨先死记硬背,return的作用就是结束函数的执行并且返回结果。
如果函数里没有return语句,或者有return语句但执行过程中所走的逻辑分支里没有遇到return, 函数最终会默认返回None作为函数的返回值,因此我讲,任何函数都有返回值。

4. 理解参数的意义

当我们执行函数的时候,本质上是在执行函数体里面的代码,对于函数add来说,就是执行return a + b, 计算a + b的值,然后使用return语句将其作为函数的返回值返回。那么问题来了,a 是什么, b是什么?
我们在调用函数时,传入参数,就是告知程序,a 是什么,b是什么

  1. def add(a, b):
  2. return a + b
  3. print(add(1, 4)) # a=1, b=4
  4. print(add(5, 4)) # a=5, b=4

每一次调用,都必须告知参数的具体值