西安科技大学

安全工程专业课程

安全仿真与模拟基础


金洪伟 & 闫振国 & 王延平

西安科技大学安全科学与工程学院


返回目录⇡

如何浏览?

  1. 从浏览器地址栏打开 https://zimo.net/aqmn/
  2. 点击章节列表中的任一链接,打开相应的演示文稿;
  3. 点击链接打开演示文稿,使用空格键或方向键导航;
  4. f键进入全屏播放,再按Esc键退出全屏;
  5. Alt键同时点击鼠标左键进行局部缩放;
  6. Esco键进入幻灯片浏览视图。

请使用最新版本浏览器访问此演示文稿以获得更好体验。

第 2 部分  Python 基础

第 5 章  函数

目 录

  1. 函数基础
  2. 参数传递
  3. 变量的作用域
  4. 匿名函数

1. 函数基础

函数是为了实现代码重用而提供的一种基本的程序结构,它由多个语句组成,负责完成某项特定任务,具有相对的独立性。函数能提高代码代码重用(同时减少代码冗余),同时也提供了一种将一个系统分割为定义完好的不同部分的工具。

我们在前面已经使用过 Python 标准库提供的各种函数了,如内置的 print()input()range()len()list(),等等,math 模块内的 sin()ceil() 等。

本章将介绍如何在 Python 中编写用户自定义函数。

1.1 函数定义

函数定义就是对用户自定义函数的定义。函数通过 def 语句定义,该语句创建一个函数对象并将其赋值给函数名(相当于一个变量),其形式为:


            def function_name(arg1, arg2,..., argN):
                ...
                return value
        

def 语句包含头部和一段紧随其后的代码块。其中头部定义了被赋值函数名 function_name,并在圆括号中包含了零个或更多的参数。头部后的代码块通常包含多个缩进的语句,即为函数体。函数体常常包含一个可选的 return 语句。函数定义并不会执行函数体。

每次调用函数的时候,括号中传入的对象将赋值给头部的参数,然后执行函数体。

1.2 示例

示例 1:打印欢迎信息

以下是一个非常简单的示例函数,它对传入的名称打印欢迎信息:


            def welcome(name):
                print('您好,' + name + '!')

            # 调用函数
            welcome('小明')  # 您好,小明!
            welcome('行者')  # 您好,行者!
        

1.2 示例

示例 2:计算两数之和

以下又是一个非常简单的示例函数,它计算并返回两数之和:


            def add(a, b):
                return a + b

            # 调用函数
            num = add(3, 6)
            print(num)  # 9
        

1.2 示例

示例 3:寻找序列的交集

以下定义的 intersect 函数可以计算两个序列的交集:


            def intersect(s1, s2):
                result = []
                for x in s1:
                    if x in s2:
                        result.append(x)
                return result
                # 以上 5 行也可以使用列表推导式简化为下面一行
                # return [x for x in s1 if x in s2]

            list1 = [2, 3, 4, 6, 9]
            list2 = [3, 4, 5, 6, 7]
            list3 = intersect(list1, list2)
            print(list3)  # [3, 4, 6]
        

1.3 Python 中的多态

在调用前面定义的 add()intersect() 函数时,我们可以向他们传入不同类型的参数。如下所示:


            print(add(3, 6))  # 9
            print(add(3, 6.2))  # 9.2
            print(add('Hello, ', 'world!'))  # Hello, world!
            print(add([4, 6], [True, 5]))  # [4, 6, True, 5]
            
            print(intersect((2, 1), (2,)))  # [2]
            print(intersect([2, 1], {2, 5, 1}))  # [2, 1]
            print(intersect('abcd', 'cow'))  # ['c']
        

这里 add(3, 6) 执行的是加法,而 add('Hello, ', 'world!') 执行的则是字符串拼接。这种依赖类型的行为称为多态,多态能根据被操作类型的不同执行不同的操作。

1.3 Python 中的多态

Python 作为一种动态类型的语言,对多态具有良好的支持,这极大地提高了程序的简洁性和灵活性。

如前面定义函数那样,并不用去声明参数的类型(静态类型语言必须显式声明参数的类型),这些函数会尝试对传递给他们的参数进行操作。但是,如果我们传入的参数类型不支持函数体所进行的操作,Python 会自动检测出不匹配,并抛出一个异常。


            print(intersect(5, [2, 4, 5]))
            # TypeError: 'int' object is not iterable
        

1.4 返回值

从前面的示例可知,在函数体内通过 return 语句为函数指定返回值。该语句用于退出函数;当其后有表达式时,将返回该表达式的值;当其后没有表达式时,默认返回 None。当没有 return 语句,函数执行完毕退出也返回 None

函数内可以同时包含多个 return 语句,但只有一个能被执行:


            def is_odd(num):
                if num % 2 != 0:
                    return True
                else:
                    return False
            
            print(is_odd(13))  # True
            print(is_odd(14))  # False
        

1.4 返回值

当需要函数有多个返回值时,可以将这些返回值打包为元组:


            def mid_point(a, b):
                x = (a[0] + b[0]) / 2.0
                y = (a[1] + b[1]) / 2.0
                return x, y
            
            x, y = mid_point((0, 0), (14, 16))  # 解包
            print((x, y))  # (7.0, 8.0)
        

2. 参数传递

函数参数的作用就是传递数据给函数用。在调用函数时,将函数外的数据传递给函数参数,其作用相当于为函数参数赋值。

2.1 形式参数和实际参数

为了便于叙述,通常把函数参数区分为形式参数和实际参数:

  • 形式参数(Parameters):在定义函数时,函数名后面括号中的参数,简称形参
  • 实际参数(Arguments):在调用函数时,函数名后面括号中的参数,简称实参

2.1 形式参数和实际参数


            def mid_point(p1, p2):  # 这里的 p1, p2 为形参
                x = (p1[0] + p2[0]) / 2.0
                y = (p1[1] + p2[1]) / 2.0
                return x, y

            a = (0, 0)
            b = (14, 16)
            x, y = mid_point(a, b)  # 这里的 a, b 为实参
            print((x, y))  # (7.0, 8.0)
        

2.2 传递可变对象与不可变对象

前面已经讲过,Python 中的对象分为不可变的和可变的。与其他语言不同,Python 的变量是没有类型的,而对象有类型。当将一个变量赋值给另外一个变量时,所传递的总是引用而非值。如下所示:


            a = 33
            b = a  # 传递引用,a 和 b 将指向相同的内存空间
            print(id(a))  # 4307682544
            print(id(b))  # 4307682544
        

函数调用就是将实参赋值给形参,也即将实参的引用传递给形参。当实参属于列表、字典、集合这些可变类型时,如果在函数中原位修改参数的值,将导致函数外的值也发生变化。但在函数内部为参数重新赋值并不会影响调用者。见下页示例。

2.2 传递可变对象与不可变对象


            def change(x, y):
                x = 1  # 使函数体内的局部变量 x 指向新的内存空间,对函数外的 a 并无影响
                y[0] = 33  # 原位修改列表 y,将影响函数外的 b
                y = [4, 5, 6]  # 使函数体内的局部变量 y 指向新的内存空间,对函数外的 b 并无影响
                y[0] = 44  # 此时对 y 的修改已经和函数外的 b 无关了
                print(y)  # [44, 5, 6]

            a = 6
            b = [1, 2, 3]
            change(a, b)
            print(a)  # 6
            print(b)  # [33, 2, 3]
        

尽管有时在函数中修改可变参数是有用的,但也常常会造成不可预料的后果,因此应尽量避免在函数中修改可变参数。有时,为了杜绝这个问题,可以向函数中传入不可变参数,如将列表转换为元组传递给函数。

2.3 参数匹配模式

Python 定义了多种实参和形参的匹配模式,使我们能更加灵活地定义或调用函数。见下页表。

我们在前面已经接触过很多这类函数,他们的签名如下:


            print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)
            sorted(iterable, /, *, key=None, reverse=False)
        

这里 print() 函数参数 object 前面的 * 表示可变长参数,可以为该参数提供以逗号分割的多个值构成的元组,余下的参数均是默认值参数,在函数调用时可以省略,省略时参数取等号后面的默认值。

sorted() 函数中的 / 表示其前方的 iterable 参数必须以常规的位置参数的形式传入,而不能以 name=value 形式的关键字参数传入;其中的 * 表示其后方若传入参数,则必须为关键字参数。

2.3 参数匹配模式

对可迭代对象操作常用的内置函数
语法 位置 解释
func(value) 调用者 常规参数,通过位置匹配
func(name=value) 调用者 关键字参数:通过名称匹配
func(*iterable) 调用者 iterable 中的所有对象作为独立的基于位置的参数传入
func(**dict) 调用者 dict 中的所有键/值对作为独立的关键字参数传入
def func(name) 函数 常规参数,通过位置或名称匹配
def func(name=value) 函数 默认值参数,如果没有在调用时传入的话
def func(*name) 函数 将剩下的基于位置的参数匹配并收集到一个元组中
def func(**name) 函数 将剩下的基于关键字参数匹配并收集到一个字典中
def func(*other, name) 函数 在调用中必须通过关键字传入 name 参数,仅限 3.x
def func(*, name=value) 函数 在调用中必须通过关键字传入 name 参数,仅限 3.x
def func(name, /) 函数 在调用中必须为 name 传入常规的位置参数,仅限 3.x

2.3 参数匹配模式

位置参数也称必备参数必需参数,是必须按照正确的顺序传到函数中,即调用时的数量和位置必须和定义时是一样的。否则将可能抛出异常,或产生与预期不符的结果。如下所示:


            def print_score(name, course, score):
                print(f'{name} 同学的 {course} 考了 {score:.1f} 分。')
            
            print_score('小明', '英语', 83.5)  # 小明 同学的 英语 考了 83.5 分。
            print_score('英语', '小明', 83.5)  # 英语 同学的 小明 考了 83.5 分。
            print_score('小明', 83.5, '英语')  # ValueError: Unknown format code 'f' for object of type 'str'
        

位置参数是最常用的方法,匹配顺序为从左到右。

2.3 参数匹配模式

关键字参数是指使用形式参数的名字来确定输入的参数值。即在调用函数时使用 name=value 的形式,来指定函数中哪个参数接受某个值。


            print_score('小明', score=83.5, course='英语')
            print_score(score=83.5, course='英语', name='小明')
            print_score(course='英语', name='小明', score=83.5)

            # 都将打印:
            # 小明 同学的 英语 考了 83.5 分。
        

上例中第一行混合使用了位置参数和关键字参数,这时位置参数首先按照从左到右的顺序匹配开头的参数。

2.3 参数匹配模式

函数定义中未使用 /* 时,在调用时参数可以按位置或关键字传递给函数。在 Python 3.x 中还可以使用仅限位置参数和仅限关键字参数。

仅限位置参数使用 / 标明其前方的形参必须使用位置参数,不能使用关键字参数的形式。/ 用于在逻辑上分割仅限位置形参与其它形参。

仅限关键字参数使用 * 标明其后方的形参必须是关键字参数,不能使用位置参数的形式。* 用于在逻辑上分割其他形参和仅限关键字形参。

2.3 参数匹配模式

示例:


            def f(a, b, /, c, d, *, e, f):
                print(a, b, c, d, e, f)
        

这里函数 f 的形参 ab 为仅限位置参数,cd 可以是位置参数或关键字参数,而 ef 为仅限关键字参数。以下是该函数的调用示例:


            f(4, 'hello', 'world', True, e=33, f=44)
            f(4, 'hello', c='world', d=True, e=33, f=44)
            # f(a=4, 'hello', 'world', True, e=33, f=44)  # SyntaxError: positional argument follows keyword argument
            # f(4, 'hello', c='world', True, e=33, f=44)  # SyntaxError: positional argument follows keyword argument
            # f(4, 'hello', 'world', True, 33, f=44)  # TypeError: f() takes 4 positional arguments but 5 positional arguments (and 1 keyword-only argument) were given
        

2.3 参数匹配模式

默认值参数为没有传入值的可选参数指定默认值。即在定义函数时使用 name=value 的形式,来指定参数的默认值。


            def mid_point(p1, p2=(0, 0)):
                x = (p1[0] + p2[0]) / 2.0
                y = (p1[1] + p2[1]) / 2.0
                return x, y

            a = (14, 16)
            x, y = mid_point(a)
            print((x, y))  # (7.0, 8.0)
            b = (2, 4)
            x, y = mid_point(a, b)
            print((x, y))  # (8.0, 10.0)
        

mid_point() 计算两点之间的中点坐标,如果没有传入第二点坐标,则默认其为坐标原点。

2.3 参数匹配模式

可变长参数又称不定长参数,即传入函数中的实际参数可以是任意多个。定义可变长参数函数时,主要有两种形式,一种是 *name,另一种是 **name

*name 接收任意多个实参并将其放到一个元组中:


            def total(*numbers):
                result = 0
                for x in numbers:
                    result += x
                return result
            
            num = total(1, 2, 3, 4)
            print(num)
        

2.3 参数匹配模式

**name 接收任意多个类似关键字参数一样显式赋值的实参,并将其放到一个字典中:


            def print_score(**scores):
                for name, score in scores.items():
                    print(f'{name} 的成绩为 {score:.1f}')
            
            print_score(张三=79, 李四=86, 王老五=92.5)
        

3. 变量的作用域

通常来说,一段程序代码中所用到的名字(变量名、函数名等)并不总是有效或可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域(scope)。

Python 中,源代码中变量名被赋值的位置决定了这个变量名的作用域。在被调用函数内赋值的变量,处于该函数的局部作用域;在所有函数之外赋值的变量,属于全局作用域。处于局部作用域的变量,被称为该函数的局部变量;处于全局作用域的变量,被称为该源文件的全局变量;函数可能有多层(函数内又定义函数),如果一个变量在一个外层的函数中定义,对于内层的函数来说,它是非局部变量

3. 变量的作用域

关于作用域,有如下规则:

  • 全局作用域中的代码不能使用任何局部变量;
  • 局部作用域可以访问全局变量;
  • 一个函数的局部作用域中的代码,不能使用其他局部作用域中的变量;
  • 在不同的作用域中,可以使用相同的名字命名不同的变量;
  • 当存在同名但不同作用域类型变量时,其优先访问顺序为:局部变量 > 非局部变量 > 全局变量 > 内置对象(builtins 模块导入的内置标识符);
  • 使用 globalnonlocal 语句声明的名称可将赋值的变量名分别映射到外围的模块和函数的作用域。

3. 变量的作用域

全局变量和局部变量的区别:


            name = '张三'  # name 为全局变量

            def welcome():
                name = '李四'  # name 为局部变量,它遮蔽了全局变量 name
                print(f'您好,{name}!')  # 此处

            welcome()  # 您好,李四!
            print(f'您好,{name}!')  # 您好,张三!
            # 上句中 name 为全局变量,此处函数内的局部变量 name 已超出作用域
        

3. 变量的作用域

在局部作用域中通过 global 语句声明全局变量:


            name = '张三'  # name 为全局变量

            def welcome():
                global name  # 在局部作用域中通过 global 声明全局变量
                name = '李四'  # name 为全局变量,它遮蔽了前面的全局变量 name
                print(f'您好,{name}!')  # 此处
            
            welcome()  # 您好,李四!
            print(f'您好,{name}!')  # 您好,李四!
        

3. 变量的作用域

在实际开发中,建议:

  • 尽量减少使用全局变量,函数应该依赖形参和返回值而不是全局变量;
  • 尽量不要使全局变量和局部变量重名,以免造成代码混乱;
  • 尽量避免跨文件(模块)直接修改另一个文件的变量。

4. 匿名函数

匿名函数是指没有名字的函数。有些时候,一些函数仅在特定环境下使用有限次,我们甚至懒得去给这种函数起一个名字,这时可使用匿名函数。在 Python 中,可使用 lambda 表达式创建小巧的匿名函数,其一般形式为:


            lambda arg1, arg2,..., argN: expression using arguments
        

lambda 表达式所返回的函数对象与由 def 创建并赋值的函数对象工作起来是完全一样的。不过他们也有不同之处:

  • lambda 表达式是一个表达式,不是语句,它能出现在不允许 def 语句出现的地方;
  • lambda 表达式的主体是一个单独的表达式,不是一个代码块,不能像 def 语句那样函数体内部可以包含多个语句,连简单的 if 语句也不能出现,它更像 return 语句后面所跟的表达式。

4. 匿名函数

以下的函数和 lambda 表达式的功能是相同的:


            def add1(a, b):
                return a + b

            add2 = lambda a, b: a + b

            print(add1(3, 6))  # 9
            print(add2(3, 6))  # 9
        

4. 匿名函数

又如,列表类型的 sort 方法可以对列表进行原地排序,其签名为:


            sort(*, key=None, reverse=False)
        

其中的 key 指定带有一个参数的函数,用于从每个列表元素中提取比较键(例如 key=str.lower)。对应于列表中每一项的键会被计算一次,然后在整个排序过程中使用。默认值 None 表示直接对列表项排序而不计算一个单独的键值。下面代码为 key 指定一个 lambda,实现按照列表中的成绩从高到低的顺序排序:


            scores = [('张三', 87.5), ('李四', 79.0), ('王老五', 84.0)]
            scores.sort(key=lambda score: score[1], reverse=True)
            print(scores)  # [('张三', 87.5), ('王老五', 84.0), ('李四', 79.0)]
        

作业

  1. 编写一个函数 coin_toss(),该函数模拟抛硬币行为,随机打印 '正面''反面' 字符串。(请在 random 模块中选择一个合适的函数来实现此功能)
  2. 编写一个函数 de_duplication(items),该函数接收一个列表 items,移除其中所有的重复项,并返回这个列表,同时保持原传入的列表 items 不变。

    统一用以下列表作为测试:

    
                        numbers = [45, 56, -34, 45, 67, 88, 90, -34, 66, 66, 70, 85, 100, 23, 54]
                    
  3. 编写一个函数 to_upper(s),该函数接受一个字符串 s,将其所有英文字母转换为大写并返回该大写字符串,如 University 被转换为 UNIVERSITY。要求不使用任何字符串方法(如不能使用 str.upper(),仅使用 Python 的内置函数 ordchr 完成。

作业

  1. 在数学上,斐波那契数(Fibonacci number)的定义如下:

    $$F_0=0\\F_1=1\\F_2=1\\F_n=F_{n-1}+F_{n-2}$$

    即斐波那契数由 0 和 1 开始,之后的每个数都是由之前的两数相加而得出。由斐波那契数形成的数列就是斐波那契数列

    请编写一个函数 fib(n),该函数能根据输入的整数 n 返回由前 n 个斐波那契数构成的序列。

    提示:我们没有讲函数的递归调用,有能力的同学可以自学,通过递归方式实现此函数。不过此函数不用递归也能书写。

作业

  1. 编写一个如下签名的函数:

    
                        length(*coordinates, dimension=3)
                    

    计算并返回由一个到多个坐标点 coordinates 构成的多段线的总长度,coordinates 中的每个元素是 (x,)(x, y)(x, y, z) 形式的数字元组。dimension 指定了坐标空间的维数,可选值为 1、2 或 3,总是计算该维数对应的直角坐标系内的线段长度,默认值 3 表示三维直角坐标系。

    要求充分考虑各种不满足计算要求的情况,这时在函数中打印相应的错误信息,并返回 None

作业

要求:

  1. 将本章全部作业放在一个 安模作业02-05-学号-姓名.py 的源文件中,通过电子邮件以附件形式发给任课教师。
  2. 在源文件中以注释的形式醒目地写明本次作业对应的章编号、各个作业题的编号,并按要求写出解题思路、代码注释。
  3. 以上各题不能只有文字说明,而应同时有可执行的示例代码。
  4. 邮件标题统一用 安模作业02-05-学号-姓名 的形式。
  5. 所有关于作业的回答都以代码注释的形式写在源文件中,不需要再额外附加其他文档或图片,请保证代码执行不会发生错误。
  6. 待本次作业批改后,请通过此链接下载本次作业的参考答案。

  谢谢!

返回目录
返回首页