master
/ 4.1 函数的定义、调用与返回值.ipynb

4.1 函数的定义、调用与返回值.ipynb @masterview markup · raw · history · blame

Notebook

4.2 函数的定义、调用和返回值

在前面的学习中,我们经常用到一些__系统内置函数__,如input()、print()、abs()等,它们把输入、输出和求绝对值等的语句封装起来,以函数的形式提供给用户使用。用户直接__调用函数__实现相应功能,而不需要再重复编写代码。

下面的这段代码导入了__math__库,使用__math.sqrt()__函数来求P点到坐标原点的距离,而P点的坐标值x、y都通过__input()__函数读入并使用__float()__函数转换为浮点值的。

In [ ]:
import math          #导入math库,后面要使用math.sqrt()函数


x = float(input())   #输入P(x,y)点的横坐标
y = float(input())   #输入P(x,y)点的纵坐标
dist = math.sqrt(x ** 2 + y ** 2)  #求P点到坐标原点的距离
print(f'{dist:.2f}')               #保留两位小数并输出

系统内置函数是系统预先定义好的函数,除了内置函数外,我们还能__自定义函数数__

在实际的程序设计过程中,有很多操作都是相同或相似的,我们可以将执行相似操作的__代码封装__成函数,通过函数调用来实现特定的功能。

将语句封装成函数就是__定义函数__,函数一经定义就可以多次调用,这个也叫__代码复用__

在程序设计中,__函数__是指用于进行某种计算或具有某种功能的一系列语句的有名称的组合,这个名称就是__函数名__

使用函数不仅能减少代码量、让代码更为简洁,而且保证了代码的一致性、降低了维护成本。

Python程序中函数的使用要遵循__先定义后调用__的规则。也就是说函数的调用必须位于函数定义之后,一般的作法是将函数的定义放在程序的开头部分,每个函数之间、函数与主程序之间各留一个空行。

4.2.1 函数的定义

Python中的函数定义格式如下:

In [ ]:
def 函数名([参数列表]):
    """文档字符串"""
    函数体
    return 返回值

__def__是用于函数定义的关键字,def后的空格接函数名,括号()里的是参数列表,参数可以是0个或多个,该行语句必须以半角冒号:结束。

__函数名__必须由字母、下划线、数字组成,__不能是关键字__,也__不能以数字开头__,一般建议函数名要有一定的意义,能够简单说明函数的功能。 函数名一般为小写字母,需要用多个单词时,使用下划线以增加可读性,如:add_number,find_number等。

In [2]:
def say_hi():
    print("Hi")


say_hi()    
Hi

上面的代码定义了一个名为__say_hi__的函数,函数名后的小括号里没有参数,这表示say_hi()是一个__无参函数__,但是小括号不可少。 下面的代码调用函数来打印问候语。

In [ ]:
say_hi()         #调用函数

函数体内尽量写上__文档注释字符串(文档注释)__,以方便查看代码功能。

文档注释是Python独有的注释方式,用三对双引号引起来的注释语句作为函数里的第一个语句,注释内容可以通过对象的doc成员被自动提取,并且被pydoc所用。

文档注释的内容主要包括该函数的功能、可接受参数的个数和数据类型、返回值的个数和类型等。

文档注释不是必须,但在函数的定义时加上一段用三对双引号引起来的注释,可以为用户提供更友好的提示和使用帮助。

以下是文档字符串内容和格式的约定。

第一行应为对象用途的简短摘要。为保持简洁,不要在这里显式说明对象名或类型,因为可通过其他方式获取这些信息(除非该名称碰巧是描述函数操作的动词)。这一行应以大写字母开头,以句点结尾。

文档字符串为多行时,第二行应为空白行,在视觉上将摘要与其余描述分开。后面的行可包含若干段落,描述对象的调用约定、副作用等。

Python 解析器不会删除 Python 中多行字符串字面值的缩进,因此,文档处理工具应在必要时删除缩进。这项操作遵循以下约定:文档字符串第一行 之后 的第一个非空行决定了整个文档字符串的缩进量(第一行通常与字符串开头的引号相邻,其缩进在字符串中并不明显,因此,不能用第一行的缩进),然后,删除字符串中所有行开头处与此缩进“等价”的空白符。不能有比此缩进更少的行,但如果出现了缩进更少的行,应删除这些行的所有前导空白符。转化制表符后(通常为 8 个空格),应测试空白符的等效性。

In [ ]:
def fact(n):
    """ 计算阶乘函数。
    
    接收一个非负整数n,计算返回n的阶乘,返回值为整型。
    """
    result = 1

    for i in range(1,n+1):
        result = result * i
    return result

print(fact(9))

上面的代码定义了fact()函数,并给出了文档注释。

下面的gif动画演示了在IDLE里调用fact时,__输入左侧括号后解释器会显示文档注释__,提示用户该如何使用fact()函数。

__函数体__是程序语句,完成函数要实现的功能,相对于 def 关键字必须要__保持一定数量的空格缩进__

4.2.2 函数的返回值

可以将函数的处理结果__返回__给调用处以进行后续处理,例如fact函数希望返回计算得到的阶乘值,此时可以使用__return语句__

函数的返回值语句由 return 关键词开头,返回值没有类型限制,也没有个数限制,当函数返回值为多个时,以元组形式返回。当没有renturn语句时,表示没有返回值,此时返回__None__

In [4]:
#下面定义了一个无返回值的函数say_hi_to_you
def say_hi_to_you(your_name):
    print('你好,' , your_name)    #如果your_name里存储的是周小丫,则打印 你好,周小丫
    
say_hi_to_you('周小丫')    #say_hi_to_you()函数没有返回值,其处理结果直接被打印出来
你好, 周小丫

return 是函数在调用过程中执行的最后一个语句,函数可以有多个 return 语句,但在执行过程中,__只能有一个__被执行到。

一旦某个 return 语句被执行,函数调用即__结束__,返回值将被返回给函数被调用的位置。

In [5]:
def is_odd(n):
    """输入整数n,如果是奇数则返回True,否则返回False"""
    if n % 2 == 1:
        return True    #当n为奇数时,返回True,函数结束,下面的语句不会被执行
    else:
        return False   #当n为偶数时,返回False

print(is_odd(233)) #233为奇数,打印True
print(is_odd(320)) #320为偶数,打印False
True
False

上面代码中定义了函数is_odd(n),__函数体中有两处__return语句,但实际执行时__只有一处__会被执行,函数一旦return则函数执行结束。

Python的函数可以使用一个return语句__同时返回多个值__。 下面的代码里定义了函数f(),其中return语句同时返回a和b,这两个值形成元组(元组的概念后续章节会有介绍)。

In [ ]:
def f(x,y):
    a = x // y   #计算x除以y的商
    b = x % y    #计算x除以y的余数
    return a,b

c,d = f(13,3)    #以13和3做参数调用f,两个返回值按顺序依次赋值给c和d
print(c,d)       #依次打印c和d的值
print(f(13,3))   #输出函数的返回值,可以看到是元组 (4,1)

4.2.3 函数的调用

函数只有被调用才会运行,定义好的函数__通过函数名__来调用。

函数被调用时,括号里要给出和函数定义里__同样多的__的参数。 函数定义时括号里的参数被称为__形式参数__,函数调用时括号里的参数被称为__实际参数__。 实际参数必须有确定的值,这些值被传递给形式参数,这相当于一个__赋值__过程。

实例4.1 阶乘函数

定义一个计算整数n的阶乘的函数fact(n),接受一个非负整数n作为参数,返回其阶乘值。

调用fact()函数的实际参数来源于用户的输入,fact()函数的返回值即是阶乘值,打印输出该阶乘值。

In [6]:
#下面定义计算n的阶乘的函数
def fact(n):
    """接受一个非负整数n,返回n的阶乘"""
    result = 1          #保存n的阶乘
    for i in range(1,n+1):
        result = result * i     # 1 ~ n 累乘
    return result       #返回

#下面读入num后调用fact(),并输出阶乘
num = int(input())    #输入一个正整数num
print(fact(num))      #调用fact()函数,其返回值即是num的阶乘,打印输出该阶乘值
479001600

上面的代码里输入num和打印阶乘值的语句都写在函数体之外,和函数定义处于__同一层次__

如下修改一下fact()函数的定义,函数体内直接输出n的阶乘,函数__没有返回值__

In [7]:
#下面定义计算n的阶乘的函数
def fact(n):
    """接受一个非负整数n,直接输出n的阶乘,无返回值"""
    result = 1          #保存n的阶乘
    for i in range(1,n+1):
        result = result * i     # 1 ~ n 累乘
    print(result)       #直接输出计算结果

#下面读入num后调用fact(),fact()函数里直接输出阶乘值
num = int(input())      #输入一个正整数num
fact(num)               #调用函数fact(),函数里直接输出结果

print('=============')  #打印分隔
#下面语句试图打印fact()函数的返回值
print(fact(num))       #fact()被调用,打印出num的阶乘,但是fact()本身没有返回值,print()会打印出None
479001600
=============
479001600
None

另一种更常用的函数定义方法是将输入输出和函数调用语句写在 if name == 'main': 下面。

注意,上面语句里的四处下划线都是__双下划线__

In [ ]:
def fact(n):
    """接受一个非负整数n,直接输出n的阶乘,无返回值"""
    result = 1          #保存n的阶乘
    for i in range(1,n+1):
        result = result * i     # 1 ~ n 累乘
    return result       #返回计算结果

if __name__ == '__main__':
    num = int(input())    #输入一个正整数num
    print(fact(num))      #调用fact()函数,,打印输出该阶乘值

if __name__ == '__main__': 语句表明,其后的代码块只有在__本文件作为脚本直接执行时__才会被执行到。当本文件被其它程序用__import导入__时,其后的代码块不会被执行,即屏蔽了输入输出函数调用等语句,只使用其中定义的函数。

当代码中函数和方法可能会被其它程序import并调用时,一般建议使用这种模式,此时代码既可以作为单独的程序使用,又可以作为模块被导入到其它程序使用。

可以发现定义为函数的代码比之前的版本更加简单优雅。更为重要的是,定义的求阶乘函数fact还可以在其他需要求阶乘的代码中重复使用。所以,使用函数可以帮助我们将功能上相对独立且会被重复使用的代码封装起来,当我们需要这些的代码,不是把重复的代码再编写一遍,而是通过调用函数实现对既有代码的复用。
事实上,Python 标准库的math模块中,已经有一个名为factorial()的函数实现了求阶乘的功能,我们可以直接用import math导入math模块,然后使用math.factorial来调用求阶乘的函数;我们也可以通过from math import factorial直接导入factorial函数来使用它,代码如下所示。

In [ ]:
from math import factorial

m = 10
n = 6
print(factorial(m) // factorial(n) // factorial(m - n))

将来我们使用的函数,要么是自定义的函数,要么是 Python 标准库或者三方库中提供的函数,如果已经有现成的可用的函数,我们就没有必要自己去定义,重复性的工作是没有意义的。如果觉得上面代码中factorial这个名字太长,书写代码的时候不是特别方便,在导入函数的时候还可以通过as关键字为其命别名。在调用函数的时候,我们可以使用函数的别名。
需要注意的是,过于简洁的别名会失去函数名原有的语义,使代码的可读性变差。

In [ ]:
from math import factorial as f

m = 10
n = 6

print(f(m) // f(n) // f(m - n))

实例4.2 素数函数

定义一个函数,接受一个正整数作为输入,判断其是否为素数。

素数又称__质数__。一个大于1的自然数,除了1和它自身以外,不能被其它自然数整除的数被称为素数。 2是最小的素数。

素数的应用非常广泛,分解质因数、反素数、回文素数、哥德巴赫猜想以及密码学等很多问题都会用到素数的判定。

对于这种广泛应用的程序,可以将其设计成一个函数,实现__代码复用__的目的。

In [ ]:
def is_prime(n):
    """接收非负整数n作为参数,判定n是否为素数,是素数则返回True,否则返回False"""
    if n < 2:             # 0和1以及负数都不是素数
        return False      # False为假,代表不是素数,此值返回给调用函数之处
    for i in range(2, n): #遍历2~n-1范围内的数
        if n % i == 0:    # 当存在n的因子时,n不是素数
            return False  # False为假,代表不是素数,此值返回给调用函数之处
    else:                 # else 子句与for 子句匹配,i == 2时也进入此语句块
        return True       # True为真,代表是素数,此值返回给调用函数之处


# 函数与主程序语句之间一般用1 到 2 个空行分隔
if __name__ == '__main__':
    num = int(input())     # 输入一个正整数
    print(is_prime(num))   # 输出素数函数的判定结果

大素数的计算量还是比较大的,在应用时尽可能编写高效算法以减少运算时间。

我们知道,约数是成对出现的。比如48,你找到约数6,那么一定有个约数8,这对约数里必然有一个小于等于$\sqrt{n}$,另一个大于等于$\sqrt{n}$。所以,在$\sqrt{n}$之前找不到约数,在$\sqrt{n}$之后也不会有。

利用这个原理,可以__缩小遍历范围__为range(2, int(n ** 0.5) + 1),以提高算法效率。上述函数可以改写如下:

In [ ]:
def is_prime(n):
    """接收非负整数n作为参数,判定n是否为素数,是素数则返回True,否则返回False"""
    if n < 2:             # 0和1以及负数都不是素数
        return False      # False为假,代表不是素数,此值返回给调用函数之处
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:    # 当存在能被整除的数时,不是素数
            return False  # False为假,代表不是素数,此值返回给调用函数之处
    else:                 # else 子句与for 子句匹配,i == 2时也进入此语句块
        return True       # True为真,代表是素数,此值返回给调用函数之处


if __name__ == '__main__':
    num = int(input())     # 输入一个正整数
    print(is_prime(num))   # 输出素数函数判定结果

实例4.3 幂函数

定义一个函数,用于计算x的n次幂。

进行函数定义时,可以定义__2个或多个__形式参数,函数调用时使用__数量相同__的实际参数。

In [ ]:
# 定义一个函数用于计算 x 的 n 次幂 
def power(x, n):
    """接收两个整数参数x和n,其中n为非负整数,计算并返回x的n次幂"""
    result = 1
    for i in range(n):           # 循环n次         
        result = result * x     
    return result                # 返回计算结果 


if __name__ == '__main__':
    a, m = map(int,input().split())  # 输入用空格分隔的两个整数,切分并映射为整数后赋值给a和m
    print(power(a, m))               # 调用函数计算a的m次幂 

power()函数定义时要求2个参数,那么调用这个函数时也要给出2个值a和m,按顺序分别传递给x和n。

在函数调用时,由用户输入x和n的值,那么这个函数就可以计算任意整数的非负整数次幂了。

类型提示

typing 是python3.5中开始新增的专用于类型注解(type hints)的模块,为python程序提供静态类型检查,如下面的greeting函数规定了参数name的类型是str,返回值的类型也是str。

In [ ]:
def greeting(name: str) -> str:
    """接收表示名字的字符串为参数,返回一句问候语,字符串类型"""
    return 'Hello ' + name


if __name__ == '__main__':
    print(greeting(input()))  # 以输入的名字为参数,输出问候语

PEP 484 中建议每个函数的参数和返回值都加上类型提示。
参数的类型提示是在参数后加半角逗号和类型;
返回值的类型提示是在函数定义的冒号前加“->”和类型名,无返回值函数的返回值类型为“None”

In [ ]:
def is_prime(n: int)->bool:
    """接收非负整数n作为参数,判定n是否为素数,返回布尔值。是素数则返回True,否则返回False"""
    if n < 2:             # 0和1以及负数都不是素数
        return False      # False为假,代表不是素数,此值返回给调用函数之处
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:    # 当存在能被整除的数时,不是素数
            return False  # False为假,代表不是素数,此值返回给调用函数之处
    else:                 # else 子句与for 子句匹配,i == 2时也进入此语句块
        return True       # True为真,代表是素数,此值返回给调用函数之处


if __name__ == '__main__':
    num = int(input())     # 输入一个正整数
    print(is_prime(num))   # 输出素数函数判定结果

温度转换、判定是否发烧、是否结冰

In [ ]:
def fahrenheit_to_celsius(num: str) -> float:
    """接受一个带单位的华氏温度,将其转为摄氏温度返回"""
    temp = (float(num[:-1]) - 32) / 1.8
    print(f'华氏{num[:-1]}℉为摄氏{temp:.2f}℃')
    return temp


def celsius_to_fahrenheit(num: str) -> float:
    """接受一个带单位的摄氏温度,将其转为华氏温度返回"""
    temp = float(num[:-1]) * 1.8 + 32
    print(f'摄氏{num[:-1]}℃为华氏{temp:.2f}℉')
    return temp


def have_a_fever(temp: str) -> bool:
    """接受一个带单位的温度,判定是否结冰(体温≥37.3℃),返回布尔值"""
    if temp[-1] in 'Ff':
        temp_c = fahrenheit_to_celsius(temp)
    else:
        temp_c = float(temp[:-1])
    return temp_c >= 37.3


def freeze(temp: str) -> bool:
    """接受一个带单位的温度,判定是否结冰(温度<0℃),返回布尔值"""
    if temp[-1] in 'Ff':
        temp_c = fahrenheit_to_celsius(temp)
    else:
        temp_c = float(temp[:-1])
    return temp_c < 0

if __name__ == '__main__':
    temperature = input()
    if temperature[-1] in 'FfCc':
        print(have_a_fever(temperature))
    else:
        print('data error')

typing 模块定义了一些常用的注解类型,如 List、Tuple、Dict、Sequence、TextIO 等等,了解了每个类型的具体使用方法,我们可以得心应手的对任何变量进行声明了,在引入的时候就直接通过 typing 模块引入就好了。

下面以List为例简单介绍,List、列表,是 list 的泛型,基本等同于 list,其后紧跟一个方括号,里面代表了构成这个列表的元素类型,如由字符串构成的列表可以声明为:

In [ ]:
from typing import List, TextIO

def csv_to_list(csv_file: TextIO) -> List[str]:
    """返回由字符串构成的列表,检查返回值类型如['2', '3.14', 'whut']"""
    pass
In [ ]:
from typing import List, TextIO

def csv_to_list(csv_file: TextIO) -> List[int or float]:
    """返回由整数或浮点数构成的列表,检查返回值类型如[2, 3.14]"""
    pass
In [ ]:
from typing import List, TextIO

def csv_to_list(csv_file: TextIO) -> List[List[int]]:
    """返回由元素为整数的子列表构成的列表,检查返回值类型如[[1, 2], [3, 4]]"""
    pass
In [ ]:
from typing import List, TextIO

def csv_to_list(csv_file: TextIO) -> List[List[str]]:
    """返回由元素为字符串的子列表构成的列表,
    检查返回值类型如:
    [['7090', 'Danforth Ave / Lamb Ave', '43.681991', '-79.329455', '15', '4', '10'],
    ['7486', 'Gerrard St E / Ted Reeve Dr', '43.684261', '-79.299332', '24', '5', '19'],
    ['7571', 'Highfield Rd / Gerrard St E - SMART', '43.671685', '-79.325176', '19', '14','5'],
    ......
    ]
"""
    pass
    

应用实例

读示例数据文件中的数据到列表中:
stations.csv

In [ ]:
def csv_to_list(csv_file: TextIO) -> List[List[str]]:
    """读取csv文件中的数据为元素为列表的列表,每个子列表容纳一行数据,每个数据项转为子列表的一个字符串元素
    """

    # 读取并忽略标题行
    csv_file.readline()    # 读文件的一行,未定义变量名,忽略不用

    data = []              # 创建空列表
    for line in csv_file:  # 遍历文件对象
        data.append(line.strip().split(','))  # 每一行字符串去掉换行符根据逗号切分为列表增加到列表中
    return data                               # 返回子列表元素为字符串的列表


if __name__ == '__main__':
    bike_io = open('/data/bigfiles/stations.csv')  # 打开文件,创建TextIO对象
    print(csv_to_list(bike_io))
In [ ]:
def csv_to_list(csv_file: TextIO) -> List[List[str]]:
    """读取csv文件中的数据为元素为列表的列表,每个子列表容纳一行数据,每个数据项转为子列表的一个字符串元素"""
    data = [line.strip().split(',') for line in csv_file]   # 列表推导式上述功能实现
    return data[1:]                         # 返回子列表元素为字符串的列表,去掉标题行


if __name__ == '__main__':
    bike_io = open('/data/bigfiles/stations.csv')  # 打开文件,创建TextIO对象
    print(csv_to_list(bike_io))

文档测试

在函数的文档字符串中用引导符>>> 加函数调用,下一行写函数预期返回值,可以进行函数文档测试,这样运行函数时可以尽早发现问题。

In [ ]:
def leap(year):
    """接收一个表示年份的整数,判定是否是闰年,返回布尔值
    >>> leap(1900)
    False
    >>> leap(2000)
    True
    >>> leap(2002)
    False
    """
    return year % 400 == 0 or (year % 4 == 0 and year % 100 != 0)


if __name__ == '__main__':
    test_year = int(input())
    if leap(test_year):  # 以输入的名字为参数,输出问候语
        print(f'{test_year}年是闰年')
    else:
        print(f'{test_year}年不是闰年')

在写代码尤其是开发商业项目的时候,一定要有意识的将相对独立且重复使用的功能封装成函数,这样不管是自己还是团队的其他成员都可以通过调用函数的方式来使用这些功能,减少工作中那些重复且乏味的劳动。

In [ ]: