【人生苦短,我学 Python】(12)函数(下)


Python 所有文章传送门
【Python】所有文章传送门

目录

  • [简述 / 前言](#简述 / 前言)
  • [1. 函数的返回值](#1. 函数的返回值)
  • [2. 变量](#2. 变量)
    • [2.1 局部变量](#2.1 局部变量)
    • [2.2. 全局变量](#2.2. 全局变量)
    • [2.3 局部与全局变量混合使用](#2.3 局部与全局变量混合使用)
  • [4. 匿名函数(Lamda表达式)](#4. 匿名函数(Lamda表达式))
  • [5. 函数装饰器](#5. 函数装饰器)
  • [6. 递归函数](#6. 递归函数)
  • [7. 函数注释(文档字符串)与注解](#7. 函数注释(文档字符串)与注解)
    • [7.1 函数注释(文档字符串)](#7.1 函数注释(文档字符串))
    • [7.2 函数注解](#7.2 函数注解)
  • 8. 编码风格(内容来源于 [Python官方](https://docs.python.org/zh-cn/3/tutorial/controlflow.html#intermezzo-coding-style))))

简述 / 前言

上篇文章主要介绍了函数的定义以及参数(形参、实参)的类型,接下来将会介绍函数的返回值 、与其息息相关的局部变量全局变量匿名函数函数装饰器 、在编程中时常会写的递归函数函数注释与注解 以及编码风格

1. 函数的返回值

在Python中,函数的返回值都是用 return 返回,但与平时写的C语言不同,像C语言只能返回一个值(在不用 引用传递参数 ) 的情况下),而python可以通过 return 返回多个值,但又不是显式的返回多个值,而是python会将这些返回值合并成一个元组进行返回,具体看下面的代码:

python 复制代码
def func():
    a, b, c = 1, (2, 3), {4, 5, 6}
    return a, b, c


if __name__ == "__main__":
    print(func())

其返回值如下:

python 复制代码
(1, (2, 3), {4, 5, 6})

那么我们可以通过解包操作来拿到这些值,即:

py 复制代码
def func():
    a, b, c = 1, (2, 3), {4, 5, 6}
    return a, b, c


if __name__ == "__main__":
    a, b, c = func()
    print(f'a = {a}\nb = {b}\nc = {c}')

其输出如下:

py 复制代码
a = 1
b = (2, 3)
c = {4, 5, 6}

除了在上述代码中一个函数只出现一次 return 的情况,一个函数还可以出现多个 return 语言,但一般都是配合条件判断一起出现,见如下代码:

py 复制代码
def func(num):
    if num >= 0:
        return True
    else:
        return False


if __name__ == "__main__":
    print(func(5))
    print(func(-5))

输出如下:

py 复制代码
True
False

2. 变量

2.1 局部变量

函数体内部 定义的变量就是局部变量,其作用域为函数体内定义的位置起,直到函数体结束位置。

比如下述代码中函数体 func()agehobby 都是局部变量。

py 复制代码
name = '小邓在森林'

def func(age):
    hobby = '篮球'
    return age, hobby

print(name)
print(func(18))

2.2. 全局变量

全局变量就是:在函数 定义之外 声明的变量。

全局变量的作用域为:从定义的位置起,直到文件结束位置。

像一般我们在代码文件开始定义的一些变量就是全局变量(不在类和函数中定义的变量),比如下面代码的变量 name 就是全局变量:

py 复制代码
name = '小邓在森林'

def func(age):
    hobby = '篮球'
    return age, hobby

print(name)
print(func(18))

要想在函数体中使用全局变量,需要用到 global 语句,详情看下面示例。

2.3 局部与全局变量混合使用

如果局部变量名和全局变量名重合,则在函数体内部定义的变量为局部变量,除非加上 global 语句才是全局变量。

函数体中若局部变量和全局变量名一样,则在函数体内的变量为局部变量,函数体外的变量为全局变量,比如下述代码中函数体 func() 的变量 name 就是局部变量:

py 复制代码
name = '小邓在森林'

def func(age):
    name = '我是局部变量'
    hobby = '篮球'
    return name, age, hobby

print(name)
print(func(18))

其输出如下:

py 复制代码
小邓在森林
('我是局部变量', 18, '篮球')

可以看见函数 func() 输出的name是局部变量,函数体外输出的变量name是全局变量。


如果你想在函数体内使用全局变量,则需要加上 global 语句,比如下述代码:

py 复制代码
name = '小邓在森林'

def func(age):
    global name
    name = '我是全局变量,此变量值已在函数体内被我修改!'
    hobby = '篮球'
    return name, age, hobby

print(f'没经过函数func前的name:{name}')
print(func(18))
print(f'经过函数func后的name(已被改变):{name}')

其输出如下:

py 复制代码
没经过函数func前的name:小邓在森林
('我是全局变量,此变量值已在函数体内被我修改!', 18, '篮球')
经过函数func后的name(已被改变):我是全局变量,此变量值已在函数体内被我修改!

需要注意的是,在函数体内使用 global 获取全局变量后,在函数体内修改这个值,函数体外的值也会被修改!


如果在函数体内再定义一个函数体,而希望内层函数体用到上一层的函数体变量,则需要使用语句 nonlocal,比如下述代码中函数体 func_in()age 是函数体 func_out() 中的 age

py 复制代码
name = '小邓在森林'

def func_out(age):
    hobby = '篮球'
    def func_in():
        nonlocal age
        age = 20	# 改变上层函数体中的局部变量 age
        print(f'函数体func_in中的age:{age}')
    func_in()
    return name, age, hobby

print(f'{name}')
print(func_out(18))

其输出为:

py 复制代码
小邓在森林
函数体func_in中的age:20
('小邓在森林', 20, '篮球')

可见改变 nonlocal 后的变量,其上层函数体的变量也会被改变!

4. 匿名函数(Lamda表达式)

Lamda表达式可以实现一行编写的简短函数,比如求a+b,用前面的方法写的函数是:

py 复制代码
def func(a, b):
    return a + b

print(func(1, 3))

而用Lamda表达式则可以写成:

py 复制代码
func = lambda a, b: a+b

print(func(1, 3))

这就是匿名函数,当要实现的函数非常简单时,可以采用这种方式!

5. 函数装饰器

装饰器其实就是一个用来包装函数的函数。比如下面实现一个在文本前后加上标识符形成一个html文本:

py 复制代码
def addb(f):
    def func(*s):
        return "<b>" + f(*s) + "</b>"
    return func

def addi(f):
    def func(*s):
        return "<i>" + f(*s) + "</i>"
    return func

@addb
@addi
def html(str):
    return str

print(html('小邓在森林'))

输出如下:

py 复制代码
<b><i>小邓在森林</i></b>

又或者在PyQt5中实现一个退出按钮,可以使用别人写好的装饰器 @pyqtSlot()btn_quit 是退出按钮的名称:

py 复制代码
# 退出界面
@pyqtSlot()
def on_btn_quit_clicked(self):
    self.close()

6. 递归函数

即自己调用自己的函数,这种函数清楚易懂,比如实现运算 n! = n*(n-1)*...*2*1,其代码如下:

py 复制代码
def func(n):
    if n <= 1:
        return 1
    else:
        return n * func(n-1)

print(f'5! = {func(5)}')

其输出为:

py 复制代码
5! = 120

当然这里没有考虑n小于0的情况,当出现负数或者非整数时需要额外考虑(没有阶乘)。


在写递归函数时,有两点是必须要考虑的:

  1. 终止条件必须要有):它是递归函数的结束出口,比如上述的阶乘例子,终止条件就是n<=1;如果一个递归函数没有终止条件 ,它就会一直不断循环计算下去,形成死循环
  2. 递归步骤 :它是要我们找到 nn-1 之间的关系。

还有一个经典的汉诺塔问题,基本上每本教材在讲到递归函数时,都会举这个例子,下面给出其相应的python递归函数代码,具体的汉诺塔问题是什么,感兴趣的同学可以自行搜索。

py 复制代码
def hanoi(n, a, b, c):
    if n == 1:
        print(a, '->', c)  # 只有一个圆盘,直接将圆盘从柱子a移动到柱子c上
    else:
        hanoi(n - 1, a, c, b)  # 先将n-1个圆盘通过柱子c从柱子a移动到柱子b上
        hanoi(1, a, b, c)  # 然后将最大的圆盘从柱子a移动到柱子c上
        hanoi(n - 1, b, a, c)  # 再将n-1个圆盘通过柱子a从柱子b移动到柱子c上

hanoi(3, 'A', 'B', 'C')

输出如下:

py 复制代码
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C

7. 函数注释(文档字符串)与注解

7.1 函数注释(文档字符串)

即我们可以给自己写的函数写一个文档字符串,用来说明我们的函数是干什么的,有什么参数,参数的含义又是什么,这样便于后续的代码维护。

我们一般会称在函数体内的开头部分以 """ """包裹的内容为函数注释,比如下述代码:

py 复制代码
def hanoi(n, a, b, c):
    """
    这个一个实现汉诺塔问题的函数
    :param n: 圆盘的个数
    :param a: 第一个柱子的名称
    :param b: 第二个柱子的名称
    :param c: 第三个柱子的名称
    :return: None
    """
    if n == 1:
        print(a, '->', c)  # 只有一个圆盘,直接将圆盘从柱子a移动到柱子c上
    else:
        hanoi(n - 1, a, c, b)  # 先将n-1个圆盘通过柱子c从柱子a移动到柱子b上
        hanoi(1, a, b, c)  # 然后将最大的圆盘从柱子a移动到柱子c上
        hanoi(n - 1, b, a, c)  # 再将n-1个圆盘通过柱子a从柱子b移动到柱子c上

# hanoi(3, 'A', 'B', 'C')
print(hanoi.__doc__)		# 输出函数注释

其输出如下:

py 复制代码
    这个一个实现汉诺塔问题的函数
    :param n: 圆盘的个数
    :param a: 第一个柱子的名称
    :param b: 第二个柱子的名称
    :param c: 第三个柱子的名称
    :return: None
    

7.2 函数注解

用过python的都知道,在python里面其实没有定义一个变量这么一说,也就是说一个变量的数据类型是不确定的,比如下面代码中第一个temp是str型变量,而第二个temp就变成int型变量了:

py 复制代码
temp = '小邓在森林'
print(type(temp))	# <class 'str'>
temp = 18
print(type(temp))	# <class 'int'>

其输出如下:

py 复制代码
<class 'str'>
<class 'int'>

那么为了让使用我们函数的用户知道这个函数要输入的是什么类型的变量,我们可以这么做:

形参标注的定义方式是在形参名后加冒号,后面跟一个会被求值为标注的值的表达式。 返回值标注的定义方式是加组合符号 ->,后面跟一个表达式,这样的校注位于形参列表和表示 def 语句结束的冒号。

比如下述代码,要求输出的name是str型,age是int型,而函数的输出是str型:

py 复制代码
def func(name: str, age: int = 18) -> str:
    return '\'' + name + '\'已经' + str(age) + '岁了'

print(func('小邓在森林', 18))
print("Annotations:", func.__annotations__)     # 输出函数注解

其输出如下:

py 复制代码
'小邓在森林'已经18岁了
Annotations: {'name': <class 'str'>, 'age': <class 'int'>, 'return': <class 'str'>}

当然这并不是就限制你输入的变量类型了,用户也可以不按照要求的数据类型输入,比如:

py 复制代码
def func(name: str, age: int = 18) -> str:
    return '\'' + name + '\'已经' + str(age) + '岁了'

print(func('小邓在森林', '我就是玩,我就不按照要求来^_^'))		# 不按照要求来不报错
print("Annotations:", func.__annotations__)     # 输出函数注解

其输出如下:

py 复制代码
'小邓在森林'已经我就是玩,我就不按照要求来^_^岁了
Annotations: {'name': <class 'str'>, 'age': <class 'int'>, 'return': <class 'str'>}

不过可以在Pycharm中看到,不按照要求的输入被标黄了,提示我们输入应该是int型:

8. 编码风格(内容来源于 Python官方

Python 项目大多都遵循 PEP 8 的风格指南;它推行的编码风格易于阅读、赏心悦目。Python 开发者均应抽时间悉心研读;以下是该提案中的核心要点:

  1. 缩进,用 4 个空格,不要用制表符。
  2. 4 个空格是小缩进(更深嵌套)和大缩进(更易阅读)之间的折中方案。制表符会引起混乱,最好别用。
  3. 换行,一行不超过 79 个字符。
  4. 这样换行的小屏阅读体验更好,还便于在大屏显示器上并排阅读多个代码文件。
  5. 用空行分隔函数和类,及函数内较大的代码块。
  6. 最好把注释放到单独一行。
  7. 使用文档字符串。
  8. 运算符前后、逗号后要用空格,但不要直接在括号内使用: a = f(1, 2) + g(3, 4)。
  9. 类和函数的命名要一致;按惯例,命名类用 UpperCamelCase,命名函数与方法用 lowercase_with_underscores。命名方法中第一个参数总是用 self (类和方法详见 初探类)。
  10. 编写用于国际多语环境的代码时,不要用生僻的编码。Python 默认的 UTF-8 或纯 ASCII 可以胜任各种情况。
  11. 同理,就算多语阅读、维护代码的可能再小,也不要在标识符中使用非 ASCII 字符。
相关推荐
CryptoPP2 分钟前
使用 KLineChart 这个轻量级的前端图表库
服务器·开发语言·前端·windows·后端·golang
18你磊哥7 分钟前
chromedriver.exe的使用和python基本处理
开发语言·python
小坏讲微服务21 分钟前
Spring Cloud Alibaba 整合 Scala 教程完整使用
java·开发语言·分布式·spring cloud·sentinel·scala·后端开发
Kiri霧22 分钟前
Scala 循环控制:掌握 while 和 for 循环
大数据·开发语言·scala
闲人编程33 分钟前
Python的抽象基类(ABC):定义接口契约的艺术
开发语言·python·接口·抽象类·基类·abc·codecapsule
qq_1728055933 分钟前
Go 语言结构型设计模式深度解析
开发语言·设计模式·golang
vx_dmxq21136 分钟前
【微信小程序学习交流平台】(免费领源码+演示录像)|可做计算机毕设Java、Python、PHP、小程序APP、C#、爬虫大数据、单片机、文案
java·spring boot·python·mysql·微信小程序·小程序·idea
无垠的广袤1 小时前
【工业树莓派 CM0 NANO 单板计算机】本地部署 EMQX
linux·python·嵌入式硬件·物联网·树莓派·emqx·工业物联网
lkbhua莱克瓦241 小时前
集合进阶8——Stream流
java·开发语言·笔记·github·stream流·学习方法·集合
20岁30年经验的码农1 小时前
Java Elasticsearch 实战指南
java·开发语言·elasticsearch