python英文官方文档:https://docs.python.org/3.8/tutorial/index.html
比较不错的python中文文档:https://www.runoob.com/python3/python3-tutorial.html
1. 写在前面
这几周从实践角度又学习了一遍python,温故而知新,还是有蛮多心得的, 周末再看之前记的python笔记,总觉得零零散散, 未成体系,所以后面这段时间,陆续对之前的python笔记做一次整合, 使得内容更加清晰,成体系,做到简单可依赖,既是复习,也方便以后回看回练。希望能帮助到更多的伙伴啦。
这是第二篇文章,主要整理python函数相关的内容,函数初识到高级特性再到函数式编程,层层递进。既有基础,又有新知识,这样更有意思一些。
文章很长,内容很多,各取所需即可 😉
大纲如下:
- 1. 写在前面
- 2. 函数基础
-
- 2.1 函数初识
- 2.2 global和nonlocal
- 2.2 参数总结
-
- 2.2.1 默认参数
- 2.2.2 可变参数
- 2.2.3 关键字参数
- 2.2.4 命名关键字参数
- 2.2.5 使用总结
- 2.2.6 逆向参数收集
- 2.3 值传递和引用传递
- 2.4 高级特性
-
- 2.4.1 切片、迭代、列表生成
- 2.4.2 生成器
- 2.4.3 迭代器
- 2.5 给函数写说明文档
- 3. 函数式编程
-
- 3.1 高阶函数
-
- 3.1.1 map/reduce
- 3.1.2 filter
- 3.1.3 sorted
- 3.1.4 使用小总
- 3.2 函数作为返回值
-
- 3.2.1 函数懒加载
- 3.2.2 闭包
- 3.3 匿名函数
- 3.4 装饰器
- 3.5 偏函数
- 4. 常用内置函数
-
- 4.1 数学运算
- 4.2 逻辑运算
- 4.3 进制转化
- 4.4 类型相关
- 4.5 类相关
- 4.6 eval和exec
- 5. 小总
Ok, let’s go!
2. 函数基础
2.1 函数初识
函数,完成特定功能的代码封装, 一次编写多次调用,避免反复写代码。这里先整理一些基础操作。
# 1. 函数起别名: 函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量
a = abs # 变量a指向abs函数
a(-1) # 所以也可以通过a调用abs函数
1
# 这个在用Pytorch搭建神经网络的时候经常遇到这种起别名的操作
# 2. 空函数 Python中有个比较骚的操作就是可以定义一个函数,但是什么都不写
# # 那有什么用? 写程序的时候,我们往往喜欢先搭一个简单的框架出来, 把各种函数定义好放那,至于作用可能一时半会还没想好,这时候就可以放一个pass, 让代码运行起来再说
def nop():
pass
if age >= 18:
pass # 如果这时候缺少pass,就会报语法错误
# 3. Python貌似可以返回多个值呢? 但是真的是这样吗?
def move(x, y, step, angle=0):
nx = x + step * math.cos(angle)
ny = y - step * math.sin(angle)
return nx, ny
# 调用
a, b = move(x,y)
# 上面代码看起来,好像python的return可以同时返回多个值, 其实这是一种假象, python函数返回的仍然是单一值,只不过这个单一值是一个元组。 在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便
2.2 global和nonlocal
在Python中,global
和nonlocal
关键字用于在不同作用域内访问和修改变量。这两个关键字在处理嵌套函数或全局变量时特别有用
# 1. global关键字用于在局部作用域中声明全局变量。当你在一个函数内部想要修改外部定义的全局变量时,就需要使用global。
x = 5
def func():
global x # 声明x是全局变量
x = 10 # 修改全局变量x的值
func()
print(x) # 输出 10
# 2. nonlocal关键字用于在闭包或嵌套函数中声明一个变量指向非全局作用域,例如指向外层(但非全局)作用域的变量。它使得我们能够修改位于嵌套作用域中的变量。
def outer():
y = 5
def inner():
nonlocal y # 声明y不是局部变量,而是外层函数的变量
y = 10
inner()
print(y) # 输出 10
outer()
# 注意:
# 1. global关键字使得局部作用域可以修改全局作用域中的变量。
# 2. nonlocal关键字确保变量指向最近的外层作用域(非全局作用域),并且可以在那个作用域中修改它。
# 3. 不应当滥用global和nonlocal关键字,因为它们会使得代码变得难以理解和维护。在可能的情况下,最好使用函数参数和返回值来传递和接收数据,而非依赖外部状态。
# 4. nonlocal不能用于访问全局作用域的变量,它只适用于嵌套的局部作用域中。
如下,函数 f 里嵌套一个函数auto_increase。实现功能:不大于 10 时自增,否则置零后,再从零自增。
# 函数 f 里嵌套一个函数auto_increase。实现功能:不大于 10 时自增,否则置零后,再从零自增。
def f():
i = 0
def auto_increase():
nonlocal i # 使用 nonlocal 告诉编译器,i 不是局部变量 , 如果没有这句话, 函数会报错, 说i没有在函数里面声明赋值
if i >= 10:
i = 0
i += 1
ret = []
for _ in range(28):
auto_increase()
ret.append(i)
print(ret)
f() # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8]
2.2 参数总结
2.2.1 默认参数
python函数定义的时候可以给出默认参数, 这样简化了函数调用的难度, 无论是简单调用还是复杂调用, 函数只需要定义一个即可。 举个例子体会一下:
# 一个一年级小学生注册的函数, 需要传入name和gender两个参数:
def enroll(name, gender):
print(name)
print(gender)
# 这样调用的时候只需要传入两个参数
enroll('Sarah', 'F') # 就会输出名字和性别
但是如果我想传入年龄, 城市等信息怎么办, 上面那个就不能用了, 所以看看有默认参数的函数:
def enroll(name, gender, age=6, city='Beijing'):
print('name:', name)
print('gender:', gender)
print('age:', age)
print('city:', city)
这样, 大多数学生注册的时候不需要提供年龄和城市,只提供必须的两个参数性别和姓名。 依然可以:
enroll('Sarah', 'F')
# 但这个还可以更复杂的调用
enroll('Bob', 'M', 7)
enroll('Adam', 'M', city='Tianjin')
所以默认参数的使用还是非常方便的,在定义神经网络函数的时候,就通常会有默认的学习率, 迭代次数等参数。 但也有一个注意的点: 首先就是设置的时候, 必选参数要放在前面,默认参数要放后面, 否则解释器不知道你使用的是默认参数还是必选参数。 其次就是默认参数的顺序, 变化大的放前面, 变化小的放后面。 最后,有多个默认参数时, 调用的时候既可以按顺序提供默认参数,也可以不按顺序提供, 但此时要把默认参数的参数名加上。(上面那个例子)
关于默认参数的使用, 还会有坑:默认参数必须指向不变对象!, 这是啥意思, 下面演示一个例子:我定义了一个函数, 传入一个list
def add_end(L=[]):
L.append('END')
return L
# 我正常调用
add_end([1, 2, 3]) # [1, 2, 3, 'END']
add_end(['x', 'y', 'z']) # ['x', 'y', 'z', 'END']
# 我使用默认参数调用, 加了三次end
a = add_end()
b = add_end()
c = add_end()
# 你知道执行之后, a, b, c都是什么值了吗?
print(a) # ['END', 'END', 'END']
print(b) # ['END', 'END', 'END']
print(c) # ['END', 'END', 'END']
为什么默认参数是[]
, 但是函数似乎每次都“记住了”上次添加了’END’后的list呢? 这是因为Python函数在定义的时候, 默认参数L的值就被计算出来了,即[]
, 因为默认参数L也是一个变量, 它指向对象[]
, 每次调用该函数, 如果改变了L的内容, 则下次调用的时候, 默认参数的内容就变了,不再是函数定义时的[]
了。所以, 默认参数必须指向不变对象!。 要修改上面例子,我们可以使用不变对象None来实现:
def add_end(L=None):
if L is None:
L = []
L.append('END')
return L
a = add_end()
b = add_end()
c = add_end()
print(a) # ['END']
print(b) # ['END']
print(c) # ['END']
# 可以和上面进行一个对比
为什么要设计str、None
这样的不变对象呢? 因为不变对象一旦创建, 对象内部的数据就不能修改, 这样就减少了由于修改数据导致的错误。 此外,由于对象不变, 多任务环境下同时读取对象不需要加锁,同时读问题不会产生。 我们编写程序时,如果可以设计一个不变对象, 那就尽量设计成不变的对象。 如果想查看一个函数使用了什么默认值,可以用fun_name.__defaults__
查看。
2.2.2 可变参数
在Python函数中,还可以定义可变参数。顾名思义,可变参数就是传入的参数个数是可变的,可以是1个、2个到任意个,还可以是0个。 这个之前确实不知道是啥意思,所以在这里整理一下, 以一个数学题为例子:
给定一组数字a, b, c…, 计算 a 2 + b 2 + c 2 . . . . . a^2+b^2+c^2..... a2+b2+c2.....
要定义出这个函数,我们必须确定输入的参数。由于参数个数不确定,我们首先想到可以把a,b,c……作为一个list或tuple传进来
def calc(numbers):
sum = 0
for n in numbers:
sum += n * n
return sum
# 分分钟搞定, 然后我们调用, 得先组装成一个数组
calc([1, 2, 3]) # 14
calc((1, 3, 5, 7)) # 84
如果利用可变参数的话,我们就可以不用先组装成数组往里面传:
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
# 这时候调用
calc(1, 2, 3)
calc(1, 3, 5, 7)
# 甚至不传都可以
calc()
定义可变参数和定义一个list或者tuple参数相比,仅仅在参数前面加了一个*
(可千万别认为这成了指针了, python中就没有指针一说),在函数内部, 参数numbers接收的是一个tuple, 因此函数代码完全不变,调用该函数时,可以传入任意个参数, 包括0个参数。
如果此时有了一个list或者tuple, 要调用一个可变参数怎么办?
# 可以这样做
nums = [1, 2, 3]
calc(nums[0], nums[1], nums[2])
# 但上面这样更加繁琐,所以可以直接在nums前加*, 把list或tuple元素变成可变参数传入:
calc(*nums)
这种写法也是非常常见, *nums
表示把nums这个list的所有元素作为可变参数传进去。
2.2.3 关键字参数
可变参数允许传入0和或者任意个参数, 这些可变参数在调用时自动组装成一个tuple。 而关键字参数允许传入0个或者任意个含参数名的参数, 这些关键字参数在函数内部自动封装成一个dict, 就是带名字的参数,可以传入任意个, 看例子就明白了:
def person(name, age, **kw):
print(name, age, kw)
person('Michael', 30) # Michael 30 {}
person('Bob', 35, city='Beijing') # Bob 35 {'city':'Beijing'}
person('Adam', 45, gender='M', job='Engineer') # Adam 45 {'gender':'M', 'job':'Engineer'}
关键字参数有什么用呢? 它可以扩展函数的功能。比如,在person函数里,我们保证能接收到name和age这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:
extra = {
'city':'Beijing', 'job':'Engineer'}
person('Jack', 34, **extra) # name: Jack age: 34 other: {'city': 'Beijing', 'job': 'Engineer'}
**extra
表示把extra这个dict的所有key-value用关键字参数传入到函数的**kw
参数,kw将获得一个dict,注意kw
获得的dict是extra
的一份拷贝,对kw
的改动不会影响到函数外的extra
。
2.2.4 命名关键字参数
对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数,如果要限制关键字参数的名字,就可以用命名关键字参数:
def person(name, age, *, city, job):
print(name, age, city, job)
和关键字参数**kw
不同,命名关键字参数需要一个特殊分隔符*
,*
后面的参数被视为命名关键字参数。调用的时候,必须传入参数名, 否则,解释器会报错。
person('Jack', 24, city='Beijing', job='Engineer')
person('jack', 24, 'Beijing', 'Enginneer') # 这种会报错, 解释器会看成四个位置参数, 但其实person函数有两个位置参数, 后面的两个叫做命名关键字参数
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*
了:
def person(name, age, *args, city, job):
print(name, age, args, city, job)
使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个*
作为特殊分隔符。如果缺少*
,Python解释器将无法识别位置参数和命名关键字参数。
在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
def f1(a, b, c=0, *args, **kw):
print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)
def f2(a, b, c=0, *, d, **kw):
print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)
虽然可以组合多达5种参数,但不要同时使用太多的组合,否则函数接口的可理解性很差。 多了,自己都不知道咋调用了! 会出现这样的一些报错:
SyntaxError: positional argument follows keyword argument
,位置参数位于关键字参数后面TypeError: f() missing 1 required keyword-only argument: 'b'
,必须传递的关键字参数缺失SyntaxError: keyword argument repeated
,关键字参数重复TypeError: f() missing 1 required positional argument: 'b'
,必须传递的位置参数缺失TypeError: f() got an unexpected keyword argument 'a'
,没有这个关键字参数TypeError: f() takes 0 positional arguments but 1 was given
,不需要位置参数但却传递 1 个
2.2.5 使用总结
在实际编程的时候,最常用的是可变参数系列。可变参数可以让函数的参数统一起来, 使得代码更加美观, 我在编程的时候非常喜欢使用**kwargs
# *args: 可以处理任意数量的位置参数, args这个名字约定俗成
# 这里的args将会是一个元组(tuple),包含了所有传递给函数的位置参数
def print_args(*args):
for arg in args:
print(arg)
nums = [1, 'two', 3.0]
print_args(nums[0], nums[1], nums[2])
print_args(*nums) # *nums表示把nums这个list的所有元素作为可变参数传进去
# **kwargs: 可处理任意数量的关键字参数, kwargs这个名字约定俗成
# 这里的kwargs将会是一个字典,其中包含了所有传递给函数的关键字参数。
#
def print_kwargs(**kwargs):
for key, value in kwargs.items():
print(f"{
key} = {
value}")
print_kwargs(a=1, b='two', c=3.0)
test = {
"a": 1, "b": 'two', "c": 3.0}
print_kwargs(**test) # **test表示把test这个dict的所有key-value用关键字参数传入到函数的**kwargs参数
# 实际场景使用
# 有一个平台审批流程有3条, 每一条对应的审批表单有些额外的信息不同,但基础信息一样,这时候我就希望把基础的信息写成位置参数,额外的信息用可变关键字参数使得3条审批流代码统一
def create_test_artifact_process(app_token: str, artifact: artifact_dal.ArtifactModel, start_user: str, **kwargs) -> dict:
def create_sop_release_process(app_token: str, artifact: artifact_dal.ArtifactModel, start_user: str, **kwargs) -> dict:
def create_sop_father_process(app_token: str, artifact: artifact_dal.ArtifactModel, start_user: str, **kwargs) -> dict:
# 这时候,拿额外参数的时候,只需要kwargs.get("xxx") 去拿就好
2.2.6 逆向参数收集
在Python中,逆向参数收集是指过程中的相反操作:当你有一个序列(比如列表、元组)或字典,并希望将它们的元素作为单独的参数传递给函数时使用。这可以通过*
(对于序列)和**
(对于字典)操作符实现,它们在调用函数时使用,将序列或字典拆解成单独的参数
# 1. 使用*对序列进行逆向参数收集: 可以将序列中的每个元素都转化为位置参数传递给函数
# 以逆向参数收集的方式,还可以给拥有可变位置参数的函数传参, 见上面
def add(a, b, c):
return a + b + c
numbers = [1, 2, 3]
# 使用*操作符传递numbers列表中的元素作为单独的参数
result = add(*numbers)
print(result) # 输出6
# 2. 使用**对字典进行逆向参数收集: 可以将字典中的每个键值对转化为关键字参数和对应的值传递给函数。
# 以逆向参数收集的方式,还可以给拥有可变关键字参数的函数传参, 见上面
def introduce(first_name, last_name, age):
print(f"Hello, my name is {
first_name} {
last_name} and I am {
age} years old.")
person_info = {
"first_name":