函数
把函数视为 一等对象
函数
1. 概念
1.1 函数定义
- 函数,不应该理解为数学中的函数
!= function()
- 计算机的函数,则应该理解为
== subroutine
,这在 BASIC 中称为 子程序,在 Pascal 中叫做 过程(procedures) - 在 C / Python 中通常称为
function
函数 ,而 Java 中则叫method
方法
创建格式为
def function_name(): # 需根据功能来下划线形式的命名
pass
- 其中 def 表示 define(定义) ,在函数名称前声明,为 Python 的关键字
1.2 函数地址
函数是有地址的,可以直接打印出函数本身的地址
def func():
pass
print(func)
### 输出结果
# <function func at 0x0000025DD40B28B8>
提示
- 变量名 不能与函数名相同
- 函数还有返回的地址,当函数 赋值给变量 时,变量中存储的便是函数的返回地址
1.3 函数的作用
函数只要修改一处,所有引用该函数的地方,都会统一被修改,因此好处在于
- 减少重复代码
- 方便日后改写扩展
- 保持代码的一致性
2. 函数参数
2.1 参数类别
- 形参: 形式上的, 不占内存空间的 参数变量,用于接收值
- 实参: 实际上的, 有实际的地址|指针的 参数变量,占用内存的实际传入值
关键字参数
形如 key=value 的传参,表示使用 关键字 的形式来传参
def info(name, age):
print(f"age: {age}, name: {name}")
pass
info(age=18, name="Warira")
## 输出结果
# age: 18, name: Warira
- 若正常按顺序传参,
name, age
便是 位置参数 - 像
age=18 | name="Warira"
这种形式的传参,age=18 | name="Warira"
则为关键字参数,可无序传参,可以不用像 位置参数 那样,按位置传
默认参数
当一个函数值大多数参数相同,便可以设置一个 默认参数 进行优化
def info(name, age, sex="female"):
pass
- 这里的
sex
就是默认参数,必须 放在最后 ,
警告
1.6 亿操作! 千万别在 默认参数 中写 xx = list()
或者 xx = []
def foo(list1=[]):
list1.append("2")
print("list1", list1)
foo() # list1 ['2']
foo() # list1 ['2', '2']
foo() # list1 ['2', '2', '2']
若函数内部有
append()
操作,仅第一次(且只一次)的调用正常外,但凡再被调用,就会不断内存泄漏一旦 多次调用,且参数中 没有默认参数,函数则会 直接拿第一次调用的默认参数(因为地址相同,没有传入新地址)继续
append()
,业务量一上来,百分百炸裂因此以后需要 可变类型的默认参数 时,直接初始化 None,不会有地址问题
不定长参数
*args
def add(*args):
print(args, type(args))
add(1, 2, 3, 4, 5)
## 输出结果
# (1, 2, 3, 4, 5) <class 'tuple'>
*args
可以接收 不定长 的任意多个位置参数add(1, 2, 3, 4, 5)
这里形参接收实参的赋值后,会返回元组 tuple
def add(*args):
print(args, type(args))
list1 = list(*args)
print(list1)
add([1, 2, 3, 4])
## 输出结果
# ([1, 2, 3, 4],) <class 'tuple'> # 返回元组,但第一个值为传入的列表
# [1, 2, 3, 4]
*args
不能直接赋值,但若使用内置类型转换,如使用list()
则能原样取到传入的值
**kwargs
def info(*args, **kwargs):
print(args, kwargs, type(kwargs))
info("Shiki", age=18, sex="female")
## 输出结果
# ('Shiki',) {'age': 18, 'sex': 'female'} <class 'dict'>
**kwargs
可以接收 不定长 的任意多个 关键字参数- 返回字典 dict 类型
- 位置参数:
"Shiki"
是*args
接收的,关键字参数:age=18
和sex="female"
是**kwargs
接收的
提示
这里的
**kwargs
作为接收参数的默认值,仅接收 string 为键的值若有针对如上,需要不同类型,可通过如下方式转为 string, 转化后才能接受该参数
**{str(k): v for k, v in kwargs.items() if isinstance(k,str)}
过滤非字符串参数,生成新的字符串。直接暴力的将非字符串转化为字符串,存在覆盖(同字符串)键的风险
实际开发中,直接传字典 的形式十分常见
def info(**kwargs): print(kwargs, type(kwargs)) info(**{"key": "value"}) ## 输出结果 # {'key': 'value'} <class 'dict'>
def info(name, sex="female", *args, **kwargs):
pass
注
【优先级】: 位置参数 > 默认参数 > *args
> **kwargs
特殊参数
Python 通常能按 位置 或 关键字 形式给函数传参,但为了提高代码易读性,可以使用特殊标识来 限制参数传递
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
----------- ---------- ----------
| | |
| Positional or keyword |
| - Keyword only
-- Positional only
/
可选,用来 限制位置,意思是/
前面 的,只能用位置传参,禁用kwarg=value
这种关键字形式*
可选,用来限制关键字,意思是*
后面 的,只能用关键字传参,必须以kwarg=value
形式
3. 函数返回值(return)
Python 默认情况,无返回值时 会返回
None
Python 可返回多个对象,编译器不会报错,而是默认返回如
(1,"qw",3)
这种封装后的 tuple 对象def foo(): return 1, "qw", 3 a, b, _ = foo() print(a, b, _) # _ 是有意义的,不仅仅是占位 ## 输出结果 # 1 qw 3
用处:
- 结束函数,
return
之后便结束,后续代码无任何意义 - 返回对象
4. 函数的作用域
如下禁止
def f():
a = 10
f()
print(a)
### 输出结果
# Traceback (most recent call last):
# ...
# NameError: name 'a' is not defined
变量 a
,声明在函数 f()
的 作用域 里,一旦函数执行完后,就会销毁,故变量 a
只作用于函数内部,是无法在全局中引用的
4.1 作用域 LEGB
- L(local)局部作用域,即函数中定义的变量
- E(enclosing )嵌套的父级函数的局部作用域,即包含此函数的上级函数的局部作用域,但不是全局的
- G(global)全局变量,模块级别定义的全局变量
- B(bulid_in)系统固定模块里的变量, 比如
int
、bytearray
等
优先级顺序依次:
局部作用域 > 外层作用域 > 模块的全局变量 > Python 内置作用域变量,也就是 LEGB
x = int(2.9) # int built_in 最外一层
o_count = 0 # global 全局变量
def outer():
o_count = 1 # enclosing 父级函数的局部作用域
i_count = 8
def inner():
i_count = 2 # local 局部作用域
print(o_count)
print(i_count) # 有优先级 仅本地没有,才去找上层
inner()
outer()
## 输出结果
# 1
# 2
4.2 全局变量和局部变量
全局变量,在局部里,只读不可改
count = 10
def f():
print(count)
count = 5
f()
## 输出结果
# UnboundLocalError: local variable 'count' referenced before assignment
- 报错,赋值前引用了局部变量
- 执行时,会 先在局部中找 这个变量,若存在,便不再去全局查
- 找到后,又发现是使用在
print(count)
后,故报错
函数内部生成
函数一创建,便加载到内存,会在内存开辟了一段空间,把局部的内容存储
程序是顺序执行(不论全局还是局部),但先在局部函数地址域中执行变量检索
局部中没有,再去全局检索,若依然没有找到引用的变量 ,则报错
在局部变量里面修改全局变量
如果遇到需要改写全局变量的需求,需要如下方式
count = 10
def f():
global count
print(count)
count = 5
print(count)
f()
## 输出结果
# 10
# 5
global
可以在局部直接获取到全局变量count
的地址指针- global 是对解析器的指令,慎用
在局部变量里面修改嵌套作用域
def foo():
count = 10
def inner():
nonlocal count
count = 20
print(count)
print(count)
return inner
foo()()
## 输出结果
# 10
# 20
nonlocal
可以在局部获取到嵌套作用域变量count
的地址指针
4.3 命名空间
Python 的命名空间,通过 dict(字典)形式体现,内置函数 globals()
和 locals()
分别对应 全局命名空间、局部命名空间,经常在需要 动态进行变量赋值 时使用
globals()
返回 当前模块全局命名空间 的字典,在任何位置调用,返回都不变,可通过如下形式修改全局变量
a = 3
def foo():
globals()['a'] = 4
foo()
print(a)
## 输出结果
# 4
locals()
既然 globals()
可以改全局变量,那么 locals()
也理所当然的可以修改局部变量吗?
注意: 不要用 locals()
去修改变量,尽管 Python 高版本已经没有报错,建议 依然作为只读使用
def foo():
print(locals())
for i in ['a', 'b', 'c']:
locals()[i] = 1
print(locals())
foo()
## 输出结果: Python2 中报错
# {}
# {'i': 'c', 'a': 1, 'b': 1, 'c': 1}
5. 进阶
5.1 函数为参
函数名可作为 参数 和 返回值
def f():
print("ok")
def foo(a, b, func): # 接收函数参数
func()
print(f"a: {a}, b: {b}, func: {func} - id: {hex(id(func))}")
foo(1, 2, f) # 传入函数,后文回调思想
# ok
# a: 1, b: 2, func: <function f at 0x000001C0EA826550> - id: 0x1c0ea826550
- 函数地址(1928079019888)因此一定义,即使没调用,也会在内存开辟一块地址来初始化,并等待调用
- 在内存是以 str (字符串)形式存储的函数对象
- 函数名即是引用这个函数对象,因此存放函数对象的 地址指针
5.2 嵌套
函数内部可以 定义函数,还可以 返回内部函数
def foo():
def inner():
return 8
return inner # 返回函数对象的地址指针
print(foo()())
# 8
-------------------------------------
# 后文[闭包/装饰器]思想
# foo() == inner
# foo()() == inner()
5.3 递归
凡递归程序,都可改循环,建议不用递归,效率低、用不好就是灾难
特点:
递归函数 自己调自己
有出口,结束条件
def fibo(n):
if n <= 1:
return n
else:
return fibo(n-1) + fibo(n-2)
for i in range(10):
print(f"fibo({i}) = {fibo(i)}")
## 输出结果
# fibo(0) = 0
# fibo(1) = 1
# fibo(2) = 1
# fibo(3) = 2
# ...
# fibo(8) = 21
# fibo(9) = 34
注意:
递归层次数过多,会导致 栈溢出
- 计算机中,函数是通过 stack(栈)来实现的
- 函数调用,系统栈就会增加一层栈帧,函数返回,减少一层栈帧
- 由于栈的大小 不是无限的,递归调用次数过多会使栈溢出
递归 效率低,栈入栈出操作频繁,因此效率十分低下
- 每次都自调一层后 进栈,直到条件成立
- 直到退出条件成立,再层层
return
出栈
5.4 常用内置函数
Python 对常用函数进行了封装 build-in Functions
lambda
Python 支持 lambda 匿名函数
其扩展的 BNF 表示法是
lambda_expr ::= "lambda" [parameter_list] ":" expression
若翻译成函数,则是如下形式
def <lambda>(parameter_list): return expression
即
lambda 参数序列 : 表达式
优点:
- 单行简洁,不需要函数命名与换行缩进
缺点:
- 只支持单行表达式,不适合复杂逻辑
- 可读性差
示例
add = lambda a, b : a + b
add(1, 2)
# 3
(lambda a, b: print(a + b))(1, 2) # lambda的另一种执行方式,类似js的自执行函数(连开销都省了)
lambda
的:
左侧接收参数a
、b
,右侧则是执行的表达式后的返回值- 创建时不需要命名,故也称为 匿名函数
lambda 函数通常的用法是结合 map()
、reduce()
、filter()
、sorted()
等函数一起使用,这些函数的共性是:都可以接收其它函数作为参数
map(function, iterable, ...)
将 iterable(可迭代对象)的每一项,传入 function 执行,返回一个包含所有结果的 迭代器
def foo(x):
return x * x
ret = map(foo, [1, 2, 3, 4, 5, 6]) # => map(lambda x: x * x, [1, 2, 3, 4, 5, 6])
print([i for i in ret])
# [1, 4, 9, 16, 25, 36]
特殊用法
利用 map()
做类型转换
[i for i in map(int, "12345")]
# [1, 2, 3, 4, 5]
filter(function, iterable)
将 iterable(可迭代对象)的每一项,传入 function 执行,返回一个包含所有 真值 的 迭代器
# 当 function 不为 None 的时是 (item for item in iterable if function(item))
li = [1, 2, 3, 4, 5]
[i for i in filter(lambda x: x > 2, li)] # => [i for i in li if i > 2]
## [3, 4, 5]
# function 为 None 的时是 (item for item in iterable if item)
data = {"name": "zz", "age": 18}
[i for i in filter(None, data)] # => [i for i in data if i]
reduce
Python 3.4 中改到 functools 模块中实现
5.5 题外话
函数式编程
起源于 范畴论(Category Theory)的数学分支,认为世界上所有概念体系,都成抽象成一个个 范畴(category)
- 范畴: 使用箭头连接的物体,彼此存在某种关系
- 集合 + 函数: 所有成员是一个集合、变形关系是函数,因此通过函数,可以从范畴的一个成员,算出其他成员
因此可以简单理解为是 范畴类
Category
+ 值value
+ 函数function
这玩意前端大放异彩,包括一些合成、柯里化、函子啥的,但不怎么符合 Python 哲学