Python小课(5)基础语法④

1. 函数

1.1 函数是什么

编程中的函数和数学中的函数有一定的相似之处。

数学上的函数是一种映射关系,把参数x通过一系列运算映射到y。

编程中的函数,是一段 可以被重复使用的代码片段


【代码示例】求数列的和,不使用函数。

# 1. 求 1 - 100 的和
sum = 0
for i in range(1, 101):
    sum += i
print(sum)

# 2. 求 300 - 400 的和
sum = 0
for i in range(300, 401):
    sum += i
print(sum)

# 3. 求 1 - 1000 的和
sum = 0
for i in range(1, 1001):
    sum += i
print(sum)

可以发现,这几组代码基本是相似的,只有一点点差异,只是每次计算的数据范围不一样。

可以把重复代码提取出来,做成一个函数。



【代码示例】求 数列 的和,使用函数。

# 定义函数
def calcSum(beg, end):
    sum = 0
    for i in range(beg, end + 1):    # 左闭右开
        sum += i
    print(sum)                       # 同一缩进

# 调用函数
sum(1, 100)        # 求 1-100 的和
sum(300, 400)      # 求 300-400 的和
sum(1, 1000)       # 求 1-1000 的和

可以明显看到,重复的代码已经被消除了。

1.2 语法格式

(1)创建函数(定义函数)

def 函数名(形参列表):
    函数体
    return 返回值


(2)调用函数(使用函数)

  • 函数名(实参列表)                                 // 不考虑返回值
  • 返回值 = 函数名(实参列表)                  // 考虑返回值
【注意】函数定义并不会执行函数体内容,必须要调用才会执行,调用几次就会执行几次。
def test1():
    print('hello')

# 如果光是定义函数, 而不调用, 则不会执行.

函数必须先定义,再使用。

test3() # 还没有执行到定义, 就先执行调用了, 此时就会报错.

def test3():
    print('hello')



【代码警告的处理】

函数定义、类定义后面期望有两个空行。




pycharm中更建议使用蛇形命名法,而不是驼峰命名法。

实际工作中使用什么规范取决于具体的要求。

警告是可以设置为忽略的。


1.3 函数参数

  • 在函数定义的时候,可以在 ( ) 中指定 "形式参数"(简称 形参)
  • 在函数调用的时候,由调用者把 "实际参数"(简称 实参)传递进去。

这样就可以做到:一份函数,针对不同的数据进行计算处理。


考虑前面的代码案例:

def calcSum(beg, end):
    sum = 0
    for i in range(beg, end + 1):
        sum += i
    print(sum)

calcSum(1, 100)
calcSum(300, 400)
calcSum(1, 1000)

上面的代码中,beg, end 就是函数的形参,1, 100 / 300, 400 就是函数的实参。

实参和形参之间的关系,就像签合同一样。


def 签合同(甲方,乙方):
    合同内容...

签合同('张总','李总')
签合同('张总','赵总')
签合同('张总','钱总')

合同的内容:函数的定义

合同的甲方乙方:形参

签合同的过程:函数的调用

实际签合同的人:实参


【注意】

  • 一个函数可以有一个形参,也可以有多个形参,也可以没有形参。
  • 一个函数的形参有几个,那么传递实参的时候也得传几个,保证个数要匹配。
def test(a, b, c):
    print(a, b, c)

test(10)

缺失两个要求的位置参数——实参按照位置一一对应给形参赋值。


不光是顺序,C++ / Java还要求参数的类型一致。

python不需要,python的函数定义的时候,形参都只有参数名,没有设置参数类型。

  • 和 C++ / Java 不同,Python 是动态类型的编程语言,函数的形参不必指定参数类型。
  • 换句话说,一个函数可以支持多种不同类型的参数。


def test(a):
    print(a)

test(10)
test('hello')
test(True)


写一个函数,就可以支持各种不同类型的参数。

C++、Java中则需要借助“模版”、“泛型”这样的概念来实现。


但是python里面也不是什么类型都可以,需要支持函数体内对应的运算操作。

这种错误在运行时才能报出。


1.4 函数返回值

函数的参数可以视为是函数的 "输入",则函数的返回值,就可以视为是函数的 "输出"。



下列代码

# 求 beg, end 这个范围的整数之和
def calcSum(beg, end):
    sum = 0
    for i in range(beg, end + 1):
        sum += i
    print(sum)

calcSum(1, 100)

可以转换成

def calcSum(beg, end):
    sum = 0
    for i in range(beg, end + 1):
        sum += i
    return sum

result = calcSum(1, 100)
print(result)

这两个代码的区别就在于,前者直接在函数内部进行了打印,后者则使用 return 语句把结果返回给函数调用者,再由调用者负责打印。



  • 耦合低:一方发生变化,对另一方的影响小;
  • 耦合高:一方发生变化,对另一方的影响大;

提升代码的可维护性:未来需要修改交互逻辑,需要改动的地方就很少。


  • 一个函数中可以有多个 return 语句。


一般多个 return 语句是搭配 分支语句 / 循环语句 的。

# 判定是否是奇数
def isOdd(num):
    if num % 2 == 0:
        return False
    else:
        return True

result = isOdd(10)
print(result)


上述代码的else是冗余的。

  • 执行到 return 语句,函数就会立即执行结束,回到调用位置。
# 判定是否是奇数
def isOdd(num):
    if num % 2 == 0:
        return False
    return True

result = isOdd(10)
print(result)

如果 num 是偶数,则进入 if 之后,就会触发 return False,也就不会继续执行 return True。



c++、java里面,一次函数调用只能返回一个值,不能返回一个数组,只能返回一个数组指针。

或者一个vector对象。

  • 一个函数是可以一次返回多个返回值的,使用 , 来分割多个返回值。
# 写一个函数, 返回平面上的一个点
# 横坐标, 纵坐标
def getPoint():
    x = 10
    y = 20
    return x, y

a, b = getPoint()

  • 如果只想关注其中的部分返回值,可以使用 _ 来忽略不想要的返回值。(占位)
def getPoint():
    x = 10
    y = 20
    return x, y

_, b = getPoint()



1.5 变量作用域

观察以下代码

def getPoint():
    x = 10
    y = 20
    return x, y

x, y = getPoint()

在这个代码中,函数内部存在 x,y,函数外部也有 x,y。

但是这两组 x,y 不是相同的变量,而只是恰好有一样的名字。

变量只能在所在的函数内部生效。

在函数 getPoint() 内部定义的 x, y 只是在函数内部生效,一旦出了函数的范围,这两个变量就不再生效了。

def getPoint():
    x = 10
    y = 20
    return x, y

getPoint()
print(x, y)


在不同的作用域中,允许存在同名的变量。



虽然名字相同,实际上是不同的变量。

x = 20

def test():
    x = 10
    print(f'函数内部 x = {x}')

test()
print(f'函数外部 x = {x}')

函数外部想获取函数内部的变量值,需要通过返回值的方式,并且在外部用变量接收。


【注意】

  • 在函数内部的变量,也称为 "局部变量"。
  • 不在任何函数内部的变量,也称为 "全局变量"。

如果函数内部尝试访问的变量在局部不存在,就会尝试去全局作用域中查找。

x = 20

def test():
    print(f'x = {x}')

test()

如果是想在函数内部,修改全局变量的值,需要使用 global 关键字声明。

x = 20

def test():
    global x
    x = 10
    print(f'函数内部 x = {x}')

test()
print(f'函数外部 x = {x}')

因为python的(全局)变量赋值语句和(局部)变量定义语句完全一样,所以给全局变量赋值之前需要声明它是一个全局变量。


  • if / while / for 等语句块不会影响到变量作用域。

换而言之,在 if / while / for 中定义的变量,在语句外面也可以正常使用。

for i in range(1, 10):
    print(f'函数内部 i = {i}')

print(f'函数外部 i = {i}')

能影响变量作用域的,是函数里面或类里面定义的变量,只能在函数或类里面访问。





在局部域,读取全局变量可以直接读取。

在局部域,修改全局变量需要先声明。


1.6 函数执行过程

  • 调用函数才会执行函数体代码,不调用则不会执行。
  • 函数体执行结束(或者遇到 return 语句),则回到函数调用位置,继续往下执行。
def test():
    print("执行函数内部代码")
    print("执行函数内部代码")
    print("执行函数内部代码")

print("1111")
test()
print("2222")
test()
print("3333")



这个过程还可以使用 PyCharm 自带的调试器来观察。





以下就是新界面了,之前都是2024.1的版本,下面是2026.1的版本。

打断点的行会变红,执行到那一行停下来,那一行会变蓝。




函数的调用栈:描述了当前的代码是怎么跳转过去的。

当前所在栈帧位置:code05.py的第178行。(test的函数体内部)

上一层栈帧的位置:code05.py的第182行。(test的调用语句)


继续往下调试,回到上一层栈帧。



1.7 链式调用

前面的代码很多都是写作

# 判定是否是奇数
def isOdd(num):
    if num % 2 == 0:
        return False
    else:
        return True

result = isOdd(10)
print(result)

实际上也可以简化写作

print(isOdd(10))

把一个函数的返回值,作为另一个函数的参数,这种操作称为 链式调用


链式调用也可能不止一层



1.8 嵌套调用

函数内部还可以调用其他的函数,这个动作称为 "嵌套调用"

def test():
    print("执行函数内部代码")
    print("执行函数内部代码")
    print("执行函数内部代码")

test 函数内部调用了 print 函数,这里就属于嵌套调用。

一个函数里面可以嵌套调用任意多个函数。

函数嵌套的过程是非常灵活的。

def a():
    print("函数 a")

def b():
    print("函数 b")
    a()                # b调用a

def c():
    print("函数 c")
    b()                # c调用b

def d():
    print("函数 d")
    c()                # d调用c
d()

先执行本函数的代码,再嵌套调用。


如果把代码稍微调整,打印结果则可能发生很大变化。

先嵌套调用,再执行本函数的代码。

def a():
    print("函数 a")

def b():
    a()
    print("函数 b")

def c():
    b()
    print("函数 c")

def d():
    c()
    print("函数 d")

d()


注意体会上述代码的执行顺序,可以通过画图的方式来理解。


  • 函数之间的调用关系, Python 中会使用一个特定的数据结构来表示,称为函数调用栈
  • 每次函数调用,都会在调用栈里新增一个元素,称为 栈帧

每个函数的局部变量,都包含在自己的栈帧中。

def a():
    num1 = 10
    print("函数 a")

def b():
    num2 = 20
    a()
    print("函数 b")

def c():
    num3 = 30
    b()
    print("函数 c")

def d():
    num4 = 40
    c()
    print("函数 d")

d()

选择不同的栈帧,就可以看到各自栈帧中的局部变量。


【思考】上述代码,a, b, c, d 函数中的局部变量名各不相同,如果变量名是相同的,比如都是 num,那么这四个函数中的 num 是属于同一个变量,还是不同变量呢?


每个栈帧里面都有num,但是是代表了不同的内存空间。

上述代码中,每个栈帧里面的num值都不一样。


1.9 函数递归

递归:是 嵌套调用 中的一种特殊情况,即一个函数嵌套调用自己。



【代码示例1】递归计算 5! ,使用循环结构。

# 写一个函数, 来求 n 的阶乘(n 是正整数)
def factor(n):
    result = 1
    for i in range(1, n + 1):
        result *= i
    return result

result = factor(5)
print(result)

【代码示例2】递归计算 5! ,使用递归结构。

# 写一个函数, 来求 n 的阶乘(n 是正整数)
# 递推公式:n! => n * (n - 1)!
# 初始条件:1! => 1

def factor(n):
    if n == 1:
        return 1
    return n * factor(n - 1)

result = factor(5)
print(result)

上述代码中,就属于典型的递归操作,在 factor 函数内部,又调用了 factor 自身。

【注意】递归代码务必要保证

  • 存在递归结束条件。比如 if n == 1 就是结束条件,当 n 为 1 的时候,递归就结束了。
  • 每次递归的时候,要保证函数的实参是逐渐逼近结束条件的。


如果上述条件不能满足,就会出现 "无限递归",这是一种典型的代码错误。

def factor(n):
    return n * factor(n - 1)

result = factor(5)
print(result)

如前面所描述,函数调用时会在函数调用栈中记录每一层函数调用的信息。

但是函数调用栈的空间不是无限大的,如果调用层数太多,就会超出栈的最大范围,导致出现问题。



1.9.1 递归的优点

  • 递归类似于 "数学归纳法",明确初始条件,和递推公式,就可以解决一系列的问题。
  • 递归代码往往代码量非常少。

1.9.2 递归的缺点

  • 递归代码往往难以理解,很容易超出掌控范围。
  • 递归代码容易出现栈溢出的情况。
  • 递归代码往往可以转换成等价的循环代码,并且通常来说循环版本的代码执行效率要略高于递归版本。

【注意】实际开发的时候,使用递归要慎重!


例如:二叉树的相关问题本身就是通过递归的方式定义的,使用循环来写,代码会很复杂。


1.10 参数默认值

  • Python 中的函数,可以给形参指定默认值。
  • 带有默认值的参数,可以在调用的时候不传参。

【代码示例】计算两个数字的和。

在函数内部加上打印信息,方便我们进行调试——理解程序的运行过程。

但是,像这种调试信息,希望在正式发布的时候不要有,只是在调试阶段才有。

就可以通过一个(开关)参数去控制,本次函数调用是否要打印参数信息。

def add(x, y, debug):
    if debug:
        print(f'调试信息: x={x}, y={y}')
    return x + y

print(add(10, 20, False))
print(add(10, 20, True))

引入了一个开关参数,确实可以控制打印信息的有无,但是每一层函数调用都需要多传一个参数,让代码变麻烦了,还让add函数的调用语句看起来有点别扭。


所以这里可以给add指定一个默认参数,在不传第三个参数的时候,直接使用默认参数。

def add(x, y, debug=False):
    if debug:
        print(f'调试信息: x={x}, y={y}')
    return x + y

print(add(10, 20))
print(add(10, 20, True))

此处 debug=False 即为参数默认值,当我们不指定第三个参数的时候,默认 debug 的取值即为 False。


带有默认值的参数需要放到没有默认值的参数的后面。

def add(x, debug=False, y):
    if debug:
        print(f'调试信息: x={x}, y={y}')
    return x + y

print(add(10, 20))

错误:非默认参数跟在默认参数后面。


让函数设计更灵活:有些函数需要给用户提供很多不同的功能,就需要用户提供很多的参数来控制,但是参数越多,调用者的使用成本越高,调用函数的人就需要仔细研究每个参数都是干嘛的。

更良好的版本是:在提供很多参数的同时,把一些参数设定上默认值,这些默认值是大多数函数调用中都会使用的参量值:

  • 当调用者只是入门简单使用一下,默认值完全可以覆盖大多数使用场景,很多参数就不用去关注;
  • 当调用者想更深入地控制函数的行为,就可以传入更多的参数去进行更精细的控制。

但是参数默认值的语法是存在争议的。但是在C++、Python等语言的标准库中是广泛使用的。


1.11 关键字参数

在调用函数的时候,需要给函数指定实参。

  • 一般默认情况下,是按照形参的顺序,来依次传递实参的。
  • 但是我们也可以通过 关键字参数 ,来调整这里的传参顺序,显式指定当前实参传递给哪个形参。
def test(x, y):
    print(f'x = {x}')
    print(f'y = {y}')

test(x=10, y=20)
test(y=100, x=200)

形如上述 test(x=10, y=20) 这样的操作,即为 关键字参数


一般的编程语言中,形参的名字是没有用的,甚至在编译的过程中,把形参的名字直接丢了。

或者在编写代码的时候,就不需要形参名,只需要形参类型。

python就很好地把形参名利用起来了。



1.12 小结

函数是编程语言中的一个核心语法机制,Python 中的函数和大部分编程语言中的函数功能都是基本类似的。

我们当下最关键要理解的主要就是四个点:

  • 函数的定义
  • 函数的调用
  • 函数的参数传递
  • 函数的执行流程(特别是递归调用时,结合画图、调试器来理解)

我们在后续的编程中,会广泛的使用到函数。大家在练习的过程中再反复加深对于函数的理解。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值