一.函数的定义调用
函数的使用必须遵循先定义,后调用的原则
函数 定义 阶段: 只检测函数体的语法,不执行函数体代码
函数 调用 阶段: 执行函数体代码
Ps:代码从上而下执行,遇到def语句,函数体代码不会执行.但在运行前会进行 预编译.
def 函数名(参数1,参数2,...):
"""
函数功能描述消息
:param 参数1:描述
:param 参数2:描述
:return: 返回值
"""
pass
函数返回值
函数中不写return或return不带任何返回值,该函数返回值都是None
不写return默认会在函数的最后一行添加 return None
二.函数参数传递
敲重点, 根据 python在作用域里对变量的赋值操作规则 , 函数的参数就是局部变量!!
既然是局部变量,就需要定义, 即函数的参数传递 可看作是一个赋值操作a = ?
重点看?
的指向是啥..
赋值操作
在理清函数参数传递之前,要先了解两个重要的概念!! 非常重要! 知其然要知其所以然..
1> python在作用域里对变量的赋值操作规则:
若这个变量在该作用域存在(已经定义), 则对其绑定新的对象; 若不存在,则将这次赋值视为对这个变量的定义..
2> python对象的赋值都是引用(内存地址)传递 被赋值 = (右侧的变量们)被引用
"="赋值操作右侧的变量需要在某一个作用域里被找到,不然会报该变量未定义..
Ps:变量的找寻遵循一个规则 依次从 局部 - 全局 - 内置 命名空间找.. 后面会有详细阐述..
在此处,直接理解成 此变量在函数里没有,就去函数外找..
在这里,简述 赋值即引用 带来的坑.. 需求:循环去除列表中的3
"""
分析: 问题出在,B操作会同步改变A操作循环的值,i的取值 num[0]、num[1]、num[2]..这样的顺序来的
当循环到num[2]时,去除了第一个3,此时列表为[1,2,3,3,4];
继续循环,轮到nums[3],可此时列表中nums[2]的值为3却被跳过了..
"""
nums = [1, 2, 3, 3, 3, 4]
for i in nums: # A
if i == 3:
nums.remove(i) # B
print(nums, len(nums)) # [1, 2, 3, 4] 4
# --- 正解
nums = [1, 2, 3, 3, 3, 4]
for i in nums[:]:
if i == 3:
nums.remove(i)
print(nums, len(nums)) # [1, 2, 4] 3
值传递
函数参数 值传递(拷贝) 针对不可变对象(tuple\str\bool\数字\不可变集合)
"""-- 分析
1> 参数a接收到的是值的类型是数字,数字是一个不可变对象
2> So,会将接收到的值进行一份拷贝! -- 值传递(拷贝)
因为func函数内不存在变量a,所以会在func函数里新定义了一个局部变量a,将拷贝的值赋值给局部变量a..
注意: func函数新定义的局部变量a与全局变量a无任何瓜葛!!
"""
def func(a):
a = a + '3'
return a
a = '5'
res = func(a)
print(a,res) # '5' '53'
引用传递
函数参数 引用传递(内存地址) 针对可变对象(list\dict\set)
"""-- 初步分析
1> 参数a接收到的是值的类型是列表,列表是一个不可变对象
2> So,会对接收到的值进行引用 -- 引用传递(内存地址)
因为func函数内不存在变量a,所以会在func函数里新定义了一个局部变量a,将接收到的引用给局部变量a..
注意: func函数新定义的局部变量a与全局变量a维护/指向的是同一个内存地址!!
"""
def func(a):
a += [4] # -- 列表+=操作原地改变 等同于 a.extend([4])
return a
a = [1,2,3]
res = func(a)
print(a,res) # [1, 2, 3, 4] [1, 2, 3, 4]
# --- --- ---
def func(a):
a = a + [4] # -- 在参数传递的时候,已经定义了局部变量a. 此时[1,2,3]的引用计数为2
# 根据python在作用域里对变量的赋值操作规则 该赋值操作是在对局部变量a重新赋值
# 重新赋值后,局部变量a重新绑定了一个新的对象[1,2,3,4]. 导致[1,2,3]的引用计数减1
# id(左边a) != id(右边a) 它俩不是同一个对象
return a
a = [1,2,3]
res = func(a)
print(a,res) # [1, 2, 3] [1, 2, 3, 4]
UnboundLocalError
官方解释: When a name is not found at all, a
NameError
exception is raised.
If the name refers to a local variable that has not been bound, aUnboundLocalError
exception is raised.UnboundLocalError
is a subclass ofNameError
.翻译成人话: 若引用了某个变量,此变量在各个作用域里都找不到(详见4.3.1LEGB),就会报错
NameError
;
若引用的变量是局部变量,但还未完成绑定,就会报错UnboundLocalError
..UnboundLocalError
是NameError
的一个子类..
"""
代码第二行报错: local variable 'a' referenced before assignment
翻译过来: 局部变量'a'在赋值前被引用
分析: func()函数没有参数,即不存在函数传递过程中定义局部变量
函数体内有对a的赋值操作,根据python在作用域里对变量的赋值操作规则,因为func函数内没有局部变量a,则该赋值操作是在定义局部变量a,而在赋值操作的右侧引用了还未被定义完成的局部变量a
Ps:有很长一段时间我是这样理解这个报错的.开始回溯我的理解.理解出错的地方在于(-- --)包含起来的这段文字.
(-- 解释器发现右侧的a变量在局部作用域里还未绑定好,就引用全局变量a-- ),而"="左侧的a是局部变量,在一个赋值操作中,某一变量不可能既充当局部变量,又充当全局变量.. 进而产生了冲突.报错提示不是这样的,显然这样理解有偏差!!
正确的过程应该是,解释器执行到a = a + 4,这个语句,发现它是赋值操作,准备将a变量名写入局部命名空间里,到这一步a变量已经定性为一个局部变量啦,但是此a变量还未完成绑定,正准备将"="右侧的内容与a变量进行绑定时,发现右侧的内容中又引用了还未绑定好的局部变量a..
Python解释器一看这骚操作,很生气,大骂一声搁这套娃呢?果断扔出一张红牌,直接报错..
"""
def func():
a += 4 # a+=[4]报错 a.extend([4])不会报错
return a
a = 1 # a=[] 当a是一个列表时
res = func()
print(a,res)
三.函数形参和实参
在上文我们知道 函数的参数传递 可看作是一个赋值操作
a = ?
定义的是函数的局部变量
具体一点, 参数分为形参和实参:
形参 -- 本质就是变量名; 实参 -- 本质就是变量的值
定义函数时 -- 行参
定义函数时 位置形参-默认形参-不定长位置形参-命名关键字形参-不定长关键字形参
形参 | 特性 |
---|---|
位置形参 | 必须被传值,多一个不行少一个也不行!! |
默认形参 | 意味着调用阶段可以不用为其赋值(可以不用指定对应的实参)!! |
不定长位置形参 |
*args 会将溢出的位置实参全部接收,然后以元祖的形式赋值给* 后面的变量args
|
命名关键字形参 |
* (* 或者*args ) 后面的形参对应的实参必须按照key=value的形式进行传值 |
不定长关键字形参 |
**kwargs 会将溢出的关键字实参全部接收,然后以字典的形式赋值给 ** 后面的变量kwargs
|
调用函数时 -- 实参
调用函数时 位置实参 关键字实参
1> 两种实参可以混用, 但位置实参必须在关键字实参前面!且不能对一个形参重复赋值!
2> 实参可以拆包!
实参中带*
,*
会将该变量的值循环取出,打散成位置实参.
即以后但凡碰到实参中带*
的,它就是位置实参,应该立刻打散成位置实参来看
实参中带**
,**
会将该变量的值循环取出,打散成关键字实参.
即以后但凡碰到实参中带**
的,它就是关键字实参,应该立刻打散成关键字实参去看
def func3(a1, a2, a3, a4=10, *args, a5=20, a6, **kwargs):
# 11 22 33 44 20 10 (55, 66, 77) {'a10': 123}
print(a1, a2, a3, a4, a5, a6, args, kwargs)
# func3(11, *[22, 33, 44, 55, 66, 77], **{'a6': 10, 'a10': 123})
func3(11, 22, 33, 44, 55, 66, 77, a6=10, a10=123)
def func(*p):
return sum(p)
func(1,2,3,4) # 10
def func(**p):
return ''.join(sorted(p))
func(x=1,z=2,y=3) # 'xyz'
默认形参陷阱
记住两点:
除非程序结束,否则该共享空间不会被释放; 类似于全局变量...
默认形参的值通常应该定义为不可变类型.
1> 若函数有默认形参,在函数初次调用时,会为其开辟一块共享空间;
2> 往后每一次调用此函数创建的空间都会共享这一块 “共享空间” 里的默认形参
3> 除非程序结束,否则该共享空间不会被释放!!!
Ps: 也有人会说, "默认形参的值只在定义阶段赋值一次,即默认参数的值在函数定义阶段就已经固定死了". 这种说法反复揣摩后,是有一定的道理的, 但不好理解.. 说法不够严谨,不必深究...
def func(a=[555]):
a.extend([1, 2, 3]) # -- 等同于 a+=[1,2,3]
return a,id(a)
# -- 函数第一次调用时共享空间为[555];第二次调用时没有用共享空间;第三次调用时共享空间为[555,1,2,3];
# 可以观察到,第一次id(a)和第三次id(a)是一样的..
# 证明第二次调用虽然没有用共享空间会导致[555]的引用计数为0,但该共享空间没有被释放!!
print(func())
print(func([]))
print(func())
"""结果
([555, 1, 2, 3], 140573741203776)
([1, 2, 3], 140692741938688)
([555, 1, 2, 3, 1, 2, 3], 140573741203776)
"""
# --- --- ---
# 依照python在作用域里对变量的赋值操作规则
# 注意哦,函数体内对此默认形参重新赋值,是为此变量绑定了一个新的对象!!!
def func(a=[555]):
print(a, id(a))
a = a + [1, 2, 3] # -- 局部变量a重新赋值,其id发生变化
return a,id(a)
# -- 可以观察到函数第一次和第二次调用的共享空间都为[555],其id也没有发生变化!
# 证明 a = a + [1, 2, 3] 该行代码虽然导致[555]的引用计数为0,但该共享空间没有被释放!
print(func()) # [555, 1, 2, 3]
print(func()) # [555, 1, 2, 3]
"""结果
[555] 140263832453056
([555, 1, 2, 3], 140263832467520)
[555] 140263832453056
([555, 1, 2, 3], 140263832428928)
"""
# --- --- --- 正解
def func0(a,b=None):
if b is None:
b = []
# --- --- --- 换个思考角度理解 默认形参陷阱
"""
默认形参的共享空间可以简单理解成 一个全局变量
"""
def func(a=[555]):pass func() func()
b = [555] def func(a=b):pass func() func()
b = [555] def func(a):pass func(b) func(b)
四.函数作用域
4.1 函数对象
4.1.1 函数是第一类对象
函数是第一类对象,可以当作数据进行传递!
1> 被引用;
2> 当作参数传递;
3> 返回值是函数;
4> 作为容器类型的元素
def foo():
print('foo')
def bar():
print('bar')
dic = {
'foo': foo,
'bar': bar,
}
while True:
choice = input('>>: ').strip()
if choice in dic:
dic[choice]()
4.1.2 函数嵌套定义
需求: 将圆相关的计算(面积、周长)集中在一起.
from math import pi
def circle(radius, action='area'):
def area():
return pi * (radius**2)
def perimeter():
return 2 * pi * radius
if action == 'area':
return area()
elif action == 'perimeter':
return perimeter()
print(circle(10, 'perimeter'))
# -- Ps: max(max(2,3),8)
4.1.3 打破层级限制
函数对象可以将定义在函数内的函数返回到全局中使用,从而打破函数的层级限制
def f1():
def inner():
print('from inner')
return inner
f = f1() # -- 拿到inner函数的内存地址.
4.2 命名空间/变量名
要改变惯性思维,从命名空间的角度去看待变量!!
4.2.1 命名空间概念
在一个复杂的程序中,会定义成千上万个变量(函数名、类名都是变量), 为了便于追踪这些变量,让它们互不干扰,命名空间就应运而生啦!!
命名空间 namespaces: 这几种说法都正确
存放名字与值绑定关系的地方, 一般简说为存放变量名的地方.
命名空间是键值对的集合! 变量名与值是一一对应的关系..
命名空间是变量名到对象内存地址的映射.
eg:x=1
开辟一块空间,1
存放在内存中; x:id(1)
放在命名空间中
从变量存储的角度, 在定义变量时, 变量名与值内存地址的关联关系 存放于栈区stack, 变量值 存放于堆区heap.
4.2.2 命名空间分类
各个命名空间是独立的, 没有任何关系的, 所以一个命名空间中不能有重名
但不同的命名空间是可以重名而没有任何影响
就好比一个文件夹中可以包含多个文件夹, 每个文件夹不能重名, 但不同文件夹中的文件可以重名...
print(len) # <built-in function len>
x=1
if 3>2:
z=5
def func():
y=2
func()
# -- 内置命名空间中的对象可以通过 dir(__builtins__) 命令查看.
类别 | 解释 |
---|---|
内置命名空间 built-in names | 存放python解释器自带的名字 len |
全局命名空间 global names | 存放模块中定义的名字(该模块中除开内置的和局部的,都是全局的) x func z |
局部命名空间 local names | 存放函数调用时函数中定义的名字 y |
Ps: 严谨一点, 解释器还为程序使用 import
语句加载的任何模块创建一个全局命名空间
注意: 全局和局部命名空间的实现是字典, 但内置命名空间不是!
4.2.3 命名空间生命周期
指的是命名空间中 变量名!! 不是内存中的变量值的生命周期!!
内置: 在解释器启动时直接创建加载,直到解释器关闭时失效.
全局: 在文件执行时生效,在文件执行完毕时失效.
局部: 在文件执行过程中,如果调用了某个函数才会临时生效,在函数执行完毕后失效.
局部命名空间的声明周期是自其建立开始,到它们各自的函数执行完毕终止.. 当这些命名空间的函数终止时, Python可能不会立即回收分配给这些命名空间的内存, 但是对其中对象的所有引用都将失效..
4.2.4 变量加载查询顺序
变量加载进命名空间的顺序: 内置 --> 全局 --> 局部
变量在命名空间里查找的顺序: 从当前位置的局部命名空间 --> 全局 --> 内置
通俗点解释下:
python是解释型语言,边翻译边执行,在代码从而下的执行过程中,不断有变量被加载进命名空间.. 值得一提的是,当遇到def语句,会先将函数名字加载进全局命名空间,但函数体代码不会执行,会暂时跳过; 函数内部的变量要等到函数调用的时候才会加载进局部命名空间...
变量在命名空间里查找的顺序(简单理解就是作用域).. 举个例子,函数里使用了x变量,但此变量不在该函数的局部命名空间里,就会去全局命名空间里找... 当然实际情况会更复杂.后文会详细阐述作用域.
"""变量加载进命名空间
第几行代码 全局 内置
1 func
4 func x:1
5 func x:1 y:2
6 func x:10 y:2
记住一句话,作用域关系是在函数定义阶段就固定死了,与函数的调用位置无关!!
"""
def func():
y = 2
print(x)
x = 1
func() # 1
x = 10
func() # 10 -- 在调用函数之前,全局命名空间里的x的值被改为10啦
# --- --- ---
x = 10
a = lambda y: x + y
x = 20
b = lambda y: x + y
print(a(10)) # 30
print(b(10)) # 30
4.3 作用域
说白了, 作用域就是变量在命名空间里查找的顺序.. 作用域定义了命名空间里的变量的在多大的范围起作用!!
作用域关系是在函数定义阶段就固定死了,与函数的调用位置无关!!
但凡调用函数都需要跑到定义阶段去找作用域关系!一层套一层的..在定义阶段就套好啦.
实则说的就是变量加载进命名空间以及变量在命名空间查询的顺序.. 不是什么新的知识,换了个说法而已.
小声提醒下: 只有模块module,类class以及函数def、lambda才会引入新的作用域(即开辟新的命名空间), 其它的代码块(如 if/elif/else/、try/except、for/while等)是不会引入新的作用域的..
4.3.1 LEGB
在表达式中引用变量时,Python解释器将按照如下顺序遍历各作用域,以解析该引用:
局部作用域/局部命名空间 -- 当前函数的范围 L Local
内嵌作用域/内嵌命名空间 -- 函数嵌套里外围函数的范围 E Enclosing
全局作用域/全局命名空间 -- 当前py模块的范围(不包含import的py模块) G Global
内置作用域/内置命名空间 -- 包含len及str等函数的那个scope B Built-in
若在这些地方都找不到名称相符的变量,就会抛出NameError异常..
Ps: LEGB规则在Python文献中会经常看到, 但Python官方文档中并没有实际出现这个术语..Hhh
def f1():
x = 1 # -- 提一嘴,f1函数结束后,x变量还能被函数嵌套中的内部函数inner访问到..Why?因为x是*变量!
# 后续的闭包部分会详细阐述!!
def inner():
print(x) # -- 作用域关系是在函数定义阶段就固定死了,与函数的调用位置无关!!
return inner
f = f1()
def bar():
x = 111
f() # -- 与调用位置无关
bar() # 1
4.3.2 globals()、locals()
python提供了两个内置函数globals()和locals(),前者返回全局命名空间的字典,后者返回局部命名空间字典..
>>> type(globals()),type(locals())
(<class 'dict'>, <class 'dict'>)
注意:globals()
返回全局命名空间的实际引用; 随便怎么折腾都是在操作全局命名空间, 类似于 a = b
"""
可以通过引用对象的变量名x,以常规的方式访问该对象..亦可以通过全局命名空间字典间接访问它
"""
>>> x = 'foo'
>>> 'x' in globals() # -- 注意,变量名是以字符串的形式作为键的
True
>>> x
'foo'
>>> globals()['x']
'foo'
>>> x is globals()['x'] # -- x的值与"x"键所对应vlaue值的内存地址相同
True
"""
可以使用globals()函数在全局命名空间中创建和修改全局变量
"""
>>> y
NameError: name 'z' is not defined # -- 全局命名空间中没有变量y
>>> glo = globals() # -- 注意:glo是全局命名空间的引用
>>> glo = 100 # -- 创建 等同于 globals()['y'] = 100
>>> y
100
>>> glo['y'] = 123 # -- 修改
>>> y
123
"""
globals结合format的应用
"""
>>> x = 1
>>> y = 2
>>> "{x},{y}".format(**globals())
'1,2'
locals()
返回的不是对局部命名空间的引用!! 那到底返回的是什么呢?
查阅资料,很多博客说是拷贝、副本.. 云里雾里的. 自己做了实验后, 发现这样的描述都不够严谨..
先说结论: locals()函数返回的是对局部命名空间的一个"拷贝" (打了引号哦) ,但此拷贝有点特殊,它具备浅拷贝的一些特点,同时当我们再次调用locals()函数时,它会 **同步更新 **"拷贝"的值..
"""
什么是拷贝、副本、视图?
参考链接: https://blog.csdn.net/Reborn214/article/details/124539097
简单来说,副本、视图是python numpy数组中的专业名词,但其具备的特性跟拷贝差不多.
numpy中引用 = python中引用; numpy中视图 = python中浅拷贝; numpy中副本 = python中深拷贝
值得一提的是,引用一般发生在赋值操作(python赋值都是引用"内存地址"传递)
还要注意一个坑!!
id():返回对象的“标识值”.该值是一个整数,在此对象的生命周期中保证是唯一且恒定的.
两个生命期不重叠的对象可能具有相同的id()值..
举个例子
分析 -- 没有将a[:]赋值给变量进行引用,当执行完a[:]后,这个对象就释放啦.但这块内存可能并没有来得及释放.
当我们新执行a[:]的时候,用的将会是同一个内存,没有申请新的内存.
这会让我们觉得两个a[:]与a[:]是同一个对象
>>> a = [1,2,3]
>>> id(a[:])
140417051683648
>>> id(a[:])
140417051684544
>>> id(a[:])
140417051684544
>>> id(a[:]) is id(a[:]) # -- 让其处于同一生命周期,每次浅拷贝都会生成不同的对象,结果肯定为False
False
"""
def func():
# -- 验证了id()的坑.
print(id(locals())) # 140204424022656
print(id(locals())) # 140204424022656
print(id(locals()) is id(locals())) # False
m = [1, 2, 3]
n = 66
loc = locals() # -- loc是局部命名空间的“拷贝” loc是对此"拷贝"的引用!!
print(loc) # {'m': [1, 2, 3], 'n': 66}
"""来,简化一下
import copy
scope = {'m': [1, 2, 3], 'n': 66} # -- 指代局部命名空间
loc = copy.copy(scope) # -- 指代局部命名空间的浅拷贝
loc['m'].append(4)
scope.append(5)
scope['n'] = 88
scope['x'] = 20
loc['n'] = 77
print(scope) # -- {'m': [1, 2, 3, 4, 5], 'n': 88, 'x': 20}
print(loc) # -- {'m': [1, 2, 3, 4, 5], 'n': 77}
"""
loc['m'].append(4) # -- 试图通过"拷贝"过来的字典修改局部命名空间里的可变类型的变量m 成功
m.append(5) # -- 在局部命令空间里修改了m变量对应的值
n = 88 # -- 在局部命令空间里修改了n变量对应的值
x = 20 # -- 在局部命名空间里添加了x变量
# -- 可以发现此"拷贝",具备浅拷贝的特性
print(loc) # {'m': [1, 2, 3, 4, 5], 'n': 66}
loc['n'] = 77 # -- 试图通过"拷贝"过来的字典修改局部命名空间里的可变类型的变量m 失败
print(locals()) # {'m': [1, 2, 3, 4, 5], 'n': 88, 'loc': {...}, 'x': 20}
# -- 再次调用locals()后,"拷贝"同步更新了.
print(loc) # {'m': [1, 2, 3, 4, 5], 'n': 88, 'loc': {...}, 'x': 20}
func()
4.3.3 global、nonlocal
回顾下Python在作用域里对变量的赋值操作规则:
若这个变量在该作用域存在(已经定义), 则对其绑定新的对象; 若不存在,则将这次赋值视为对这个变量的定义..
global -- 不管是在函数嵌套的哪一层,对x变量的赋值操作都是修改全局作用域里的那个x变量!!
nonlocal -- 如果在闭包内给x变量赋值,那么修改的其实是闭包外那个作用域里的x变量..
nonlocal的唯一限制在于,不能延伸到模块级别,这是为了防止它污染全局变量..
Ps: 提一嘴闭包,后续会详细阐述. 函数嵌套,内部函数引用外部函数的参数或变量,就构成了闭包..
"""
★ --global
在局部若想修改全局的不可变类型,需要借助global声明
在局部若想修改全局的可变类型,不需要借助任何声明,可以直接修改
若全局中没有x变量,global语句和赋值的组合(不管处于嵌套的哪一层)可以间接在局部中创建x全局变量
"""
x = []
def func():
global m
m = 10
globals()['n'] = 20 # -- n变量在全局中存在的话,此赋值操作就是在修改;通常不会这么做,完全没必要
x.append(1)
func()
func()
print(x) # [1,1]
print(m, n) # 10,20
"""
★ --nonlocal
"""
x = 1
def f1(): # E f1的参数对f2而言
x = 111 # E 此处的x对f2而言
def f2(): # E f2的参数对f3而言
x = 222 # E 此处的x对f3而言
def f3():
nonlocal x # -- 若f1函数嵌套里没有x 则报错找不到x变量
x = 333 # -- 改的是最近的x = 222的值
f3()
print(x) # 333
# {'f3': <function f1.<locals>.f2.<locals>.f3 at 0x7fc60c71c9d0>, 'x': 333}
print(locals()) # f2的局部作用域
f2()
print(x) # 111
# {'x': 111, 'f2': <function f1.<locals>.f2 at 0x7fc60c71c790>}
print(locals()) # f1的局部作用域
f1()
print(x) # 1