Python青少年简明教程:函数
在Python中,函数是一段可重用的代码块,用于执行特定的任务。函数能够接受输入参数,执行特定的操作,并返回结果。
Python提供了许多内置函数,如print()、len()、eval()等,可以直接使用。编程人员还可以自定义函数。
先看几个内置函数,然后重点介绍编程人员自定义函数。
内置函数
Python 提供了许多内置函数,这些函数可以在任何 Python 程序中直接使用,不需要额外导入。下面介绍几个常用的内置函数。
1) len()函数
语法(用法):
len(object)
其中,object 是你想要获取长度的对象。
用途:返回对象的长度(元素个数)
适用于:字符串、列表、元组、字典等
示例:
print(len("Hello")) # 输出:5
print(len([1, 2, 3])) # 输出:3
my_string = "Hello"
print(len(my_string)) # 输出 5
2) eval()函数
语法(用法):
eval(expression, globals=None, locals=None)
其中,expression 是一个字符串,必须是有效的 Python 表达式——能计算的字符串表达式。
globals(可选):一个字典,表示全局命名空间。
locals(可选):一个字典,表示局部命名空间。
示例:
x = 1
print(eval("x + 1")) # 输出:2
result = eval("3 + 5")
print(result) # 输出 8
# 使用字典作为命名空间的例子:
globals_dict = {"a": 5, "b": 10}
result = eval("a + b", globals_dict)
print(result) # 输出: 15
更多的情况可参见,内置函数官方文档内置函数 — Python 3.12.5 文档
下面介绍程序员自定义函数。
函数定义
编程人员还可以自定义函数。
使用 def 关键字定义函数,后跟函数名和括号(包含参数列表)。语法:
def函数名(参数列表):
# 函数体
return返回值
函数名:函数名可以是任何有效的标识符,但最好是有意义的名字,能反映出函数的功能。
参数列表:在圆括号中定义,用于接收传递给函数的值。多个参数用逗号分隔。参数是可选的,如果没有参数,圆括号内为空。
函数体:包含函数执行的操作或计算。
返回值:使用 return 语句返回结果给调用者。如果没有 return 语句,函数将返回 None。
函数调用
使用函数名后跟一对括号,括号中包含参数——如果有的话。
形参(形式参数,parameters)和实参(实际参数,arguments)
形参是指函数定义中使用的参数,它们相当于占位符,用于指定函数可以接受的输入。形参在函数定义时出现,用于接收函数调用时传入的实参。
实参是指在函数调用时传递给函数的具体值。实参在函数调用时出现,用于替换形参的值。
示例:
#定义add函数
def add(x, y):
return x + y
result = add(5, 3) # 调用函数,并接收返回值
print(result) # 输出:8
文档字符串
如果三重引号用放在函数、类或模块的开头,Python 会把它们当作文档字符串(docstring),这是一种特别的多行注释形式,用于生成文档,可以通过 __doc__ 属性 或 help(add) 函数查看。
def add(a, b):
"""返回两个数的和。
参数:
a -- 第一个数
b -- 第二个数
"""
return a + b
print(add.__doc__) # 打印函数的文档字符串
在这个例子中,你可以通过 add.__doc__ 或 help(add) 来查看这个文档字符串的内容。
参数传递的内部机制
官方术语,参数传递使用按值调用(call by value)的方式(其中的值始终是对象的引用,而不是对象的值)。实际上,Python函数参数传递的始终对象的引用,而不是对象的值。这方面的内容,因一些人和资料介绍的比较混乱,特地附注。
【附注(有关官方文档节选):
The actual parameters (arguments) to a function call are introduced in the local symbol table of the called function when it is called; thus, arguments are passed using call by value (where the value is always an object reference, not the value of the object). [1] When a function calls another function, or calls itself recursively, a new local symbol table is created for that call.
https://docs.python.org/3/tutorial/controlflow.html#defining-functions
在调用函数时会将实际参数(实参)引入到被调用函数的局部符号表中;因此,实参是使用 按值调用 来传递的(其中的 值 始终是对象的 引用 而不是对象的值)。 [1] 当一个函数调用另外一个函数时,会为该调用创建一个新的局部符号表。
https://docs.python.org/zh-cn/3/tutorial/controlflow.html#defining-functions
Remember that arguments are passed by assignment in Python. Since assignment just creates references to objects, there’s no alias between an argument name in the caller and callee, and so no call-by-reference per se.
请记住,Python 中的实参是通过赋值传递的。由于赋值只是创建了对象的引用,所以调用方和被调用方的参数名都不存在别名,本质上也就不存在按引用调用的方式。
这种机制有时也被一些人非正式称呼较多如“传递对象引用”(pass by object reference)、按对象引用调用(call by object reference),但机制本质不变。
对于不可变对象(如整数、字符串、元组),因为其不可变性,函数内对参数的任何修改不会影响到外部变量。对于可变对象(如列表、字典、集合),函数内对参数的修改会影响到外部变量。
a)如果传入的参数是不可变类型(如数字、字符串、元组),那么在函数体内修改参数的值,并不会影响到原来的变量。因为不可变类型的变量实际上是值的引用,当试图改变变量的值时,相当于是在创建新的对象。例如:
def change_number(num):
num = 100
x = 10
change_number(x)
print(x) # 输出:10
在上面的例子中,尽管在函数内部num的值被改变了,但是原变量x的值并没有改变。参见下图:
b)如果传入的参数是可变类型(如列表、字典),那么在函数体内修改参数的值,会影响到原来的变量。因为可变类型的变量存储的是一个地址,当试图改变变量的值时,实际上是在改变这个地址所指向的内容。例如:
def change_list(lst):
lst.append(100)
x = [1, 2, 3]
change_list(x)
print(x) # 输出:[1, 2, 3, 100]
在上面的例子中,函数内部对参数lst的修改影响到了原变量x的值。参见下图:
从内存管理的角度来看,Python中的变量和参数传递有一些特点:
☆ 变量是对象的引用:在Python中,变量实际上是对象的引用,而不是对象本身。当你给一个变量赋值时,实际上是将变量指向了一个对象。这意味着变量可以指向不同类型的对象,并且可以在程序中随时改变指向的对象。
☆ 引用计数:Python使用引用计数来管理内存。每个对象都有一个引用计数,表示有多少个变量引用了该对象。当引用计数为0时,对象将被自动回收。当一个变量不再引用一个对象时,引用计数会减少。当引用计数为0时,对象的内存将被释放。
☆ 对象的可变性:Python中的对象分为可变对象和不可变对象。可变对象(如列表、字典)的值可以被修改,而不可变对象(如整数、字符串、元组)的值不能被修改。这意味着如果你修改了一个可变对象,那么所有引用这个对象的变量都会受到影响。
☆ 参数传递方式:在Python中,函数的参数传递是按值调用(call by value) 来传递的(其中的 值 始终是对象的 引用 而不是对象的值)——实际上,按对象引用调用(call by object reference)调用这种说法更好。对于不可变对象(如整数、字符串、元组),由于它们的值不能被改变,所以函数内部对这些对象的修改实际上是创建了一个新的对象。因此,函数内部的修改不会影响到函数外部的实际参数。对于可变对象(如列表、字典),由于它们的值可以被改变,所以函数内部对这些对象的修改会直接改变原始对象的值。因此,函数内部的修改会影响到函数外部的实际参数。
【附、Python语言的变量和参数传递情况https://blog.csdn.net/cnds123/article/details/134159800
多种(C++、Java、JavaScript、Python)编程语言参数传递方式介绍https://blog.csdn.net/cnds123/article/details/132981086 】
参数情况说明
☆位置参数(Positional Arguments)
位置参数是最基本的参数传递方式,按照参数在函数定义中的位置传递。例如:
def add(a, b):
return a + b
result = add(3, 5) # 位置参数,3 传给 a,5 传给 b
print(result) # 输出 8
☆关键字参数(Keyword Arguments)
关键字参数通过参数名传递,可以不按照定义时的顺序传递。例如:
def greet(name, msg):
print(f"{msg}, {name}!")
greet(name="Alice", msg="Hello") # 关键字参数
greet(msg="Hi", name="Bob") # 顺序可以不同
☆默认参数(Default Arguments)
默认参数在函数定义时指定了默认值,如果调用函数时没有提供该参数,则使用默认值。例如:
def greet(name, msg="Hello"):
print(f"{msg}, {name}!")
greet("Alice") # 使用默认参数,输出 "Hello, Alice!"
greet("Bob", "Hi") # 覆盖默认参数,输出 "Hi, Bob!"
☆可变参数(Variable-length Arguments)
Python 支持可变数量的参数,使用 *args 和 **kwargs。
1) *args 用于传递任意数量的位置参数,接收的是一个元组。例如:
def print_numbers(*args):
for num in args:
print(num)
print_numbers(1, 2, 3, 4) # 输出 1 2 3 4
2)不定长关键字参数(**kwargs)
**kwargs 用于传递任意数量的关键字参数,接收的是一个字典。例如:
def print_info(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print_info(name="Alice", age=25) # 输出 "name: Alice" "age: 25"
☆在 Python 3.8 及以后的版本中,函数参数传递机制中引入了两个新的语法元素:/ 和 *,它们用来指定参数的类型,即位置参数、关键字参数以及两者混合的参数。
1)仅限位置参数(Positional-only Parameters)
使用 / 将参数标记为仅限位置参数。这意味着这些参数必须通过位置进行传递,不能使用关键字来传递。例如:
def func(a, b, /, c, d):
print(a, b, c, d)
func(1, 2, 3, 4) # 正确
func(1, 2, c=3, d=4) # 错误,会引发 TypeError
在这个例子中,a 和 b 必须通过位置传递,而 c 和 d 可以通过位置或关键字传递。
2)仅限关键字参数(Keyword-only Parameters)
使用 * 将参数标记为仅限关键字参数。这意味着这些参数必须通过关键字传递。例如:
def func(a, b, *, c, d):
print(a, b, c, d)
func(1, 2, c=3, d=4) # 正确
func(1, 2, 3, 4) # 错误,会引发 TypeError
在这个例子中,c 和 d 必须通过关键字传递,而 a 和 b 可以通过位置或关键字传递。
3)同时使用 / 和 *
在函数定义中可以同时使用 / 和 *,以指定不同类型的参数。例如:
def func(a, b, /, c, *, d):
print(a, b, c, d)
func(1, 2, 3, d=4) # 正确
func(1, 2, c=3, d=4) # 正确
func(1, 2, 3, 4) # 错误,会引发 TypeError
func(a=1, b=2, c=3, d=4) # 错误,会引发 TypeError
在这个例子中,a 和 b 必须通过位置传递,c 可以通过位置或关键字传递,d 必须通过关键字传递。
☆参数解包
Python 支持参数解包(unpacking),这是一种将容器(如列表、元组或字典)中的元素解包并传递给函数参数的方式。参数解包可以通过 * 和 ** 实现。
1)使用 * 解包列表或元组
当你有一个包含多个元素的列表或元组,并且你希望将这些元素作为单独的参数传递给函数时,可以使用 * 进行解包。例如:
def add(a, b, c):
return a + b + c
numbers = [1, 2, 3]
result = add(*numbers) # 等同于 add(1, 2, 3)
print(result) # 输出 6
values = (4, 5, 6)
result = add(*values) # 等同于 add(4, 5, 6)
print(result) # 输出 15
2) 使用 ** 解包字典
当你有一个字典,并且字典的键与函数参数名匹配时,可以使用 ** 进行解包,将字典中的键值对作为关键字参数传递给函数。例如:
def greet(name, greeting, punctuation):
print(f"{greeting}, {name}{punctuation}")
params = {'name': 'Alice', 'greeting': 'Hello', 'punctuation': '!'}
greet(**params) # 等同于 greet(name='Alice', greeting='Hello', punctuation='!')
变量作用域(scope)
Python 中的作用域(scope)是指变量的可访问范围。Python 主要有以下几种作用域:
☆全局作用域(Global)
全局变量在函数外部定义,全局作用域是在整个程序(模块或脚本文件)中都可以访问的变量。例如:
global_var = "I'm global"
def access_global():
print(global_var)
access_global() # 输出: I'm global
print(global_var) # 输出: I'm global
☆局部作用域(Local)
局部作用域指的是在函数内部定义的变量,只在函数内部可见。例如:
def local_scope_example():
x = "local"
print(x)
local_scope_example() # 输出: local
# print(x) # 这会引发 NameError,因为 x 只在函数内部定义
☆闭包函数外的函数中(Enclosing)
这指的是嵌套函数中外层函数的作用域。例如:
def outer_function():
x = "outer"
def inner_function():
print(x) # 可以访问外层函数的变量
inner_function()
outer_function() # 输出: outer
☆内置作用域(Built-in)
这是 Python 内置的名字空间,包含了内置函数和异常名称。例如:
print(len("Python")) # 使用内置函数 len(),输出: 6
变量查找顺序
Python 按照 LEGB规则顺序查找变量:Local → Enclosing → Global → Built-in。
例如:
x = "global"
def outer():
x = "outer"
def inner():
x = "inner"
print("inner:", x)
inner()
print("outer:", x)
outer()
print("global:", x)
# 输出:
# inner: inner
# outer: outer
# global: global
特别提示:
1) global 关键字
如果你想在函数内部修改全局变量,需要使用 global 关键字。例如:
count = 0
def increment():
global count
count += 1
print(count)
increment() # 输出: 1
increment() # 输出: 2
2) nonlocal 关键字
在嵌套函数中,如果要修改外层函数的变量,需要使用 nonlocal 关键字。例如:
def outer():
x = 0
def inner():
nonlocal x
x += 1
print(x)
return inner
counter = outer()
counter() # 输出: 1
counter() # 输出: 2
理解 Python 的作用域规则对于编写清晰、可维护的代码非常重要。它可以帮助你避免命名冲突,更好地组织代码结构,并理解变量在不同上下文中的行为。
递归函数
函数调用自身的函数。需要有基本情况来结束递归。
递归是一种强大的编程技术,它允许函数调用自身来解决问题。
递归的基本概念:
递归函数是在其定义中直接或间接调用自身的函数。它通常用于解决可以被分解成相似的子问题的问题。
递归函数的结构:
一个典型的递归函数包含两个主要部分:
基本情况(Base case):不再递归调用的条件,用于终止递归。
递归情况(Recursive case):函数调用自身的部分。
示例1 - 计算阶乘
阶乘是最经典的递归函数之一。数学上,n 的阶乘(n!)定义为:
0! = 1
n! = n * (n-1)!,当n > 0时
源码如下:
def factorial(n):
# 基准情况:当 n 为 0 时,返回 1
if n == 0:
return 1
# 递归情况:n * (n-1) 的阶乘
else:
return n * factorial(n - 1)
print(factorial(5)) # 输出: 120
解释:
factorial(0) 返回 1(基准情况)。
factorial(n) 对于 n > 0,会递归调用 factorial(n - 1)。
计算 factorial(5) 的过程如下:
factorial(5) = 5 * factorial(4)
factorial(4) = 4 * factorial(3)
factorial(3) = 3 * factorial(2)
factorial(2) = 2 * factorial(1)
factorial(1) = 1 * factorial(0)
factorial(0) = 1(基准情况)
递归调用结束并逐步回溯,最终计算出结果 5! = 120。
匿名函数(Lambda函数)
在 Python 中,匿名函数(Anonymous Function)指的是没有名字的函数,通常使用 lambda 关键字来定义。匿名函数通常用于需要一个简单函数且只使用一次的场景。
名函数的语法非常简洁,使用 lambda 关键字定义,语法格式如下:
lambda 参数1, 参数2, ... : 表达式
lambda 是关键字,表示这是一个匿名函数。
参数1, 参数2, ... 是函数的参数,多个参数用逗号分隔。
表达式 是函数体,只能是一个表达式,计算结果就是这个函数的返回值。匿名函数的结果会自动返回,无需使用 return 关键字。
简单的匿名函数示例
一个简单的匿名函数,计算两个数的和:
add = lambda x, y: x + y
这个 lambda 表达式定义了一个匿名函数,并将其赋值给 add 变量。add 现在可以像普通函数一样使用:
result = add(3, 5)
print(result) # 输出: 8
与普通函数的对比
匿名函数和普通函数(使用 def 关键字定义的函数)的主要区别在于匿名函数更适合定义简单的、一次性的函数。普通函数适用于更复杂的逻辑或者需要多次使用的情况下。
普通函数:
def add(x, y):
return x + y
result = add(3, 5)
print(result) # 输出: 8
匿名函数可写为:
add = lambda x, y: x + y
result = add(3, 5)
print(result) # 输出: 8
在这个简单的例子中,匿名函数和普通函数的效果是一样的,但匿名函数更加简洁。
在使用像 map()、filter()、sorted()、reduce() 等高阶函数时,经常会用到匿名函数。关于高阶函数就不多说了。