函数
终于讲到函数了,这是编程语言的灵魂所在——编程语言可以没有面向对象,没有语句,但是不能没有函数
函数是任何编程语言的核心部分,可以说它们形成了编程语言的基础。函数允许我们封装一段代码并给它一个名称,然后在我们需要的地方“调用”或执行该代码。
虽然前面一直在调用函数,但函数是怎么来的呢?
引
我们来看一个需求:
假设我们需要计算一个东西,输出它每一秒的位置,已知,在第 x 秒时,物体在 x2+3 的位置
数学上我们就会这样写:
设 $f(x)=x^{2}+3$
然后求出$f(1),f(2),f(3),f(4),...$
对应的程序也是非常类似的,对照起来应该很好理解:
def f(x):
return x**2+3
print(f(1))
print(f(2))
print(f(3))
print(f(4))
...
Python 用def
关键字来定义新的函数,定义内需要加一层缩进以和外面的代码区分,称为代码块,函数代码块内可以和外面一样写代码,用return
语句来结束函数运行,并返回值
这就是为什么 Python 对缩进很严格
假如在前面的需求中,对应的计算式突然变了,那我直接修改函数f
就行,而不用修改每一次调用
多次定义函数会覆盖先前的值,就和变量赋值一样
def f(x):
return x**2+3
def f(x):
return x
print(f(1)) # 打印 1
print(f(2)) # 打印 2
print(f(3)) # 打印 3
print(f(4)) # 打印 4
...
代码块
我也可以把上面的代码整个放进另一个函数里,这样我就能运行它好几遍了
def run():
def f(x):
return x**2+3
print(f(1))
print(f(2))
print(f(3))
print(f(4))
...
run()
run()
我们可以通过这个例子体会一下什么是代码块,以及 Python 是怎么通过缩进区分不同的区域的
这里由于run
函数并没有显式地返回值,故run()
的返回值是None
当run()
这个表达式被求值时,程序会跳转到run
定义的地方,运行函数的代码块,即调用函数
在运行结束,或者运行return
语句后,程序回到调用run()
的地方,返回对应的值,继续运行
参数变量
由于 Python 中可以传入传出函数,我甚至可以同时求出不同计算式的结果,而不用写两段代码
def f(x):
return x**2+3
def g(x):
return 3.14*x*x
def run(f):
print(f(1))
print(f(2))
print(f(3))
print(f(4))
...
run(f)
run(g)
注意,run
定义里面的f
和外面的f
是不同的,由于run
定义中的参数列表有f
,它会覆盖外面的同名变量,这是符合直觉的
函数调用时,会求出所有作为参数的表达式的值,并依次赋值给函数定义中的参数,如果不能对上则报错
def add(a, b):
return a + b
print(add(233)) # 报错,参数数量不对
print(add(1, 2, 3)) # 报错,参数数量不对
匿名函数
只有一个表达式的函数可以用匿名函数的形式来表达,这也是函数的字面量,我们在之前看过这个形式
f = lambda x: x**2+3
print(f(1))
print(f(2))
print(f(3))
print(f(4))
...
正常来说,匿名函数应该用在这种地方
def run(f):
print(f(1))
print(f(2))
print(f(3))
print(f(4))
...
run(lambda x: x**2+3)
run(lambda x: 3.14*x*x)
作用域
局部作用域与全局作用域
从参数得到的变量,和普通变量没有区别,也可以进行覆盖和删除,并且不能带到函数外面
def f(a, b):
print(a, b) # 打印 2 2
a = 3
print(a) # 打印 3
a = 1
f(2, 2)
print(a) # 打印 1
print(b) # 报错,变量 b 不存在
这显然有一个作用范围,变量出了代码块就没了,这称为函数的局部作用域,而最外层最大的作用域就称为全局作用域
静态作用域与动态作用域
复杂的问题出现在,参数之外的变量,与外部的变量重名时,算不算是同一个变量
首先,函数定义并不是表达式求值,这样的也是可以的
def f():
print(a) # 打印 1
a = 1
f()
这样似乎看不出明显的问题,但是如果我把定义和调用的作用域分开呢
def g(a):
def f():
print(a) # 打印 1 还是 2 ?
return f
def h(a):
g(1)()
h(2)
我们已经知道的是,函数参数的作用域是明显的,出了就没了
因此首先在函数g
里定义了函数f
,并且环境里有一个参数a
,这样当调用g(1)
时,就相当于是获取了一个,在a=1
的环境下定义的f
随后在另一个函数h
里调用g(1)
,并调用了g(1)
返回的函数f
,g(1)
不影响环境中的a
,因此当运行h(2)
时,是在a=2
的情况下调用了函数f
函数f
会打印出什么呢?
答案是 1
函数的生命周期分为定义
和调用
两个阶段
函数定义中,除参数外的变量,取决于函数定义时外部作用域的变量,而不是函数调用时外部作用域的变量,这被称之为静态作用域
反之则称为动态作用域,动态作用域多出现在早期的编程语言,而现代编程语言几乎都是静态作用域
Python 也是静态作用域的
大多数静态作用域的语言都遵循这样的原则
- 函数代码块中,可以使用定义时所在作用域中创建的变量
- 函数内部创建的变量不出函数的局部作用域,不应该影响到函数外面
- 如果创建了同名的变量,那么视为两个不同的变量,在各自作用域内互不干扰
动态作用域的问题在于这些
- 函数内部变量依赖于调用函数时的环境
- 调用函数可能会改变外部的变量,导致了不确定性
而静态作用域则消除了这些问题
- 函数内部变量仅依赖于定义函数时的环境,这使得可以隐藏函数定义时所使用的变量,而仅传出函数
- 调用函数不影响外部变量,让变量很容易跟踪
问题及其解决
看起来很美好,唯一的问题是, Python 没法确认变量算在哪里创建(也叫声明)的
在其它语言里声明和赋值是分开的,不声明就没法赋值,在哪里声明就算是在哪里的,但是 Python 里统一是 =
因此当出现同名变量时,Python 无法确认我们的意图,它究竟是对外界变量的使用,还是内部创建的临时变量
于是 Python 采取了简单粗暴的方法:对于函数内的变量,除了函数参数外,如果函数内部有这个变量的赋值语句,那么就认为这个变量是内部创建的临时变量,而不是外部变量,这也是初学者容易踩的坑
注意,这里的a += b
可以看作a = a + b
的简写:
def f(b):
x += b # 报错,变量 x 不存在
print(x)
x = 0
f(1)
f(1)
f(7)
但是有时我们就是想要使用怎么办
在局部作用域内,使用关键字global
来声明一个变量是全局的
def f(b):
global x
x += b
print(x)
x = 0
f(1)
f(1)
f(7)
如果是在另一个作用域内部,则改为使用nonlocal
关键字
def h():
x = 0
def f(b):
nonlocal x
x += b
print(x)
f(1)
f(1)
f(7)
h()
参数默认值和关键字调用
默认值
我们可以在定义函数时,为函数的参数提供默认值
如果我们在调用该函数时不提供该参数的值,那么将使用默认值作为参数
def greet(name="World"):
print("Hello, " + name)
greet() # 输出: Hello, World
greet("Alice") # 输出: Hello, Alice
关键字
我们可以在调用函数时,通过 “关键字=值” 的形式来传递参数
这种方式的优点是不需要记住参数的顺序,只需要知道它们的名字
可以和默认值混合使用
def describe_pet(animal_type='dog', pet_name):
print("My " + animal_type + "'s name is " + pet_name + ".")
describe_pet(pet_name='harry')
打印
My dog's name is harry.
默认值的坑
我们来看一个例子,这里用到了列表,不过按照直觉来理解就行了:
def add_to_list(value, lst=[]):
lst.append(value) # 往列表里添加 value
return lst
print(add_to_list(1)) # 输出: [1]
print(add_to_list(2)) # 输出: [1, 2]
直觉来看,我们想要让默认值在每次调用函数时被创建,然后赋值给参数
但是反直觉的地方在于, Python 只会在函数定义时创建一次默认值,之后每次赋值都是用的定义时创建的默认值
这意味着,如果默认值是那种可以存储值的可变类型,并且在调用的过程中会被改变,那么这个改动会影响后续所有使用默认值的函数调用
如果我们想要使用可变类型,必须得手动创建。。
def add_to_list(value, lst=None):
if lst is None:
lst = []
lst.append(value) # 往列表里添加 value
return lst
print(add_to_list(1)) # 输出: [1]
print(add_to_list(2)) # 输出: [2]
这里有另一个坑,a is b
判断的是a就是b,要求指向的就是同一个东西,如果不是同一个,那么值相等都不行
而a == b
只是判断值相等,并且==
运算符可以被重载(重新定义),因此没有is
那么靠谱
总结
使用函数的技巧是——你只需要知道怎么用它,不需要关心它是怎么来的
函数打包代码,让我们能够像拼积木一样,重复利用已有的甚至是未有的成果
这简化了我们大脑的负担,让我们能够站在更高的层级上,去完成更加复杂的任务
遇事不决可以先写出一堆函数调用,假装它们已经能用了,之后再去定义它们