设计一个程序:
期待结果:
*
***
**** *
** **** *
**** **** *
**** **** ***
**** **** *
** **** *
**** *
***
*
*
***
**** *
** **** *
**** **** *
**** **** ***
**** **** *
** **** *
**** *
***
*
如果没有函数,我们的实现方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
rows=6
i=j=k=1
for i in range (rows):
for j in range (rows-i):
print (" " ,end=" " )
j+=1
for k in range (2 *i-1 ):
print ("*" ,end=" " )
k+=1
print ("\n" )
for i in range (rows):
for j in range (i):
print (" " ,end=" " )
j+=1
for k in range (2 *(rows-i)-1 ):
print ("*" ,end=" " )
k+=1
print ("\n" )
相信大家一定看出来了,这种方式会出现大量重复代码,对于阅读和维护整个程序都会变得十分麻烦。
这时候,函数就出现了!
简单说,函数就是一段封装好的,可以重复使用的代码,它使得我们的程序更加模块化,避免大量重复的代码。
刚才的程序函数版本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def print_ling ():
rows=6
i=j=k=1
for i in range (rows):
for j in range (rows-i):
print (" " ,end=" " )
j+=1
for k in range (2 *i-1 ):
print ("*" ,end=" " )
k+=1
print ("\n" )
for i in range (rows):
for j in range (i):
print (" " ,end=" " )
j+=1
for k in range (2 *(rows-i)-1 ):
print ("*" ,end=" " )
k+=1
print ("\n" )
print_ling()
print_ling()
7.1、函数声明
声明一个函数,也就是创建一个函数,可以理解为将一段可以重复使用的代码通过关键字def包裹起来。具体的语法格式如下:
1
2
3
4
5
6
7
8
9
10
'''
def 函数名(参数列表):
'''
params:
return :
'''
# 实现特定功能的多行代码
[return [返回值]]
'''
其中,用 [] 括起来的为可选择部分,即可以使用,也可以省略。此格式中,各部分参数的含义如下:
函数名:一个符合 Python 语法的标识符,最好见名知意,多个单词可以使用_
表示,比如cal_sum
形参列表:设置该函数可以接收多少个参数,多个参数之间用逗号( , )分隔。
[return [返回值] ]:整体作为函数的可选参参数,用于设置该函数的返回值。
python的函数体是通过冒号+缩进声明的
1
2
def foo ():
print ("foo函数" )
7.2、函数调用
函数的声明并没有执行函数中的代码块,想要执行函数体,需要进行函数调用,一个函数可以调用多次。
函数调用语法:
1
2
3
4
def foo ():
print ("foo函数" )
foo()
debug模式运行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def bar ():
print ("bar1" )
print ("bar2" )
print ("bar3" )
def foo ():
print ("foo1" )
print ("foo2" )
print ("foo3" )
foo()
bar()
foo()
7.3、函数参数
像上面我们举的例子,我想打印一个八层菱形和六层菱形,怎么设计?
7.3.1、形参和实参
声明一个计算1-100和的函数
1
2
3
4
5
def cal_sum ():
ret = 0
for i in range (1 ,101 ):
ret+=i
print (ret)
但是问题来了,如果我想计算1-200的和怎么呢,再声明一个新的函数吗?明显我们会发现计算1-100和与计算1-200的和逻辑是相同的,只有一个动态变化值,所以我们引入了参数的概念,这样可以使函数的功能更加强大灵活:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def cal_sum (temp ):
ret = 0
for i in range (1 ,temp+1 ):
ret+=i
print (ret)
cal_sum(100 )
cal_sum(101 )
def add ():
x = 10
y = 20
print (x+y)
def add (x, y ):
print (x + y)
add(5 , 6 )
add(10 , 5 )
在函数的定义阶段 括号内写的变量名,叫做该函数的形式参数,简称形参。在函数的调用阶段,括号内实际传入的值,叫做实际参数,简称实参。该例中,temp就是的函数形式参数,而每次调用根据需要传入的值,比如100,101都是实参。
形参就相当于变量名,而实参就相当于变量的值,函数调用传参的过程 就是给形参变量名赋值的过程。
函数参数只有在函数调用阶段有效,函数运行结束,参数作为垃圾释放。
7.3.2、位置参数
位置参数,有时也称必备参数,指的是必须按照正确的顺序将实际参数传到函数中,换句话说,调用函数时传入实际参数的数量和位置都必须和定义函数时保持一致。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def add (x,y ):
print (x+y)
add(2 ,3 )
def add (x,y,z ):
print (x+y)
add(2 ,3 )
def add (x,y ):
print (x+y)
add(2 ,3 ,4 )
7.3.3、默认参数
Python 允许为参数设置默认值,即在定义函数时,直接给形式参数指定一个默认值。这样的话,即便调用函数时没有给拥有默认值的形参传递参数,该参数可以直接使用定义函数时设置的默认值。
1
2
3
4
5
6
7
def print_stu_info (name,age,gender="male" ):
print ("学员姓名:" ,name)
print ("学员年龄:" ,age)
print ("学员性别:" ,gender)
print_stu_info("张三" ,23 )
当定义一个有默认值参数的函数时,有默认值的参数必须位于所有没默认值参数的后面,否则报错!
7.3.4、关键字参数
关键字参数可以避免牢记参数位置的麻烦,令函数的调用和参数传递更加灵活方便。关键字参数是指使用形式参数的名字来确定输入的参数值。通过此方式指定函数实参时,不再需要与形参的位置完全一致,只要将参数名写正确即可。
1
2
3
4
5
6
7
8
9
10
11
12
def print_stu_info (name,age,height,weight,job ):
print ("学员姓名:" ,name)
print ("学员年龄:" ,age)
print ("学员身高:" ,height)
print ("学员体重:" ,weight)
print ("学员工作:" ,job)
print_stu_info("张三" ,23 ,"180cm" ,"80kg" ,"销售" )
print_stu_info(name="张三" ,height="180cm" ,weight="90kg" ,job="销售" ,age=23 )
print_stu_info("张三" ,height="180cm" ,weight="90kg" ,job="销售" ,age=23 )
使用位置参数和关键字参数混合传参的方式。但需要注意,混合传参时关键字参数必须位于所有的位置参数之后。
7.3.5、不定长参数
在函数定义中使用*args
和**kwargs
传递可变长参数。*args
用作传递非命名键值可变长参数列表(位置参数);**kwargs
用作传递键值可变长参数列表。*args
的参数会以元组(tuple)的形式导入,存放所有未命名的变量参数。**kwargs
的参数会以字典的形式导入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def add (*args ):
print (args)
print (type (args))
ret = 0
for i in args:
ret += i
print (ret)
add(12 ,23 ,45 )
def print_stu_info (**kwargs, ):
print (kwargs)
print_stu_info(name="张三" ,height=190 )
同时使用*args
和**kwargs
:
1
2
3
4
5
6
def print_stu_info (name, age=18 , *args, **kwargs ):
print (name, age)
print (args)
print (kwargs)
print_stu_info("yuan" , 20 , "China" , "Beijing" , height="188cm" , weight="60kg" )
注意点:
1、参数arg
、*args
、**kwargs
三个参数的位置必须是一定的。必须是(arg,*args,**kwargs)
这个顺序,否则程序会报错。
2、不定长参数的长度可以为零。
3、args
和 kwargs
其实只是编程人员约定的变量名字,args
是 arguments 的缩写,表示位置参数;kwargs
是 keyword arguments 的缩写,表示关键字参数。
7.4、函数返回值
到目前为止,我们创建的函数都只是对传入的数据进行了处理,处理完了就结束。但实际上,在更多场景中,我们还需函数将处理的结果反馈回来。通过关键字return语句可以返回任意类型的数值。
7.4.1、基本使用
1
2
3
4
5
def add (x,y ):
return x+y
ret = add(2 ,3 )
print (ret)
7.4.2、默认返回值
在 Python 中,有一个特殊的常量 None(N 必须大写)。和 False 不同,它不表示 0,也不表示空字符串,而表示没有值,也就是空值。None 是 NoneType
数据类型的唯一值(其他编程语言可能称这个值为 null、nil 或 undefined),也就是说,我们不能再创建其它 NoneType
类型的变量,但是可以将 None 赋值给任何变量。
Python一个函数中如果没有return语句或者return后没有具体值,都默认返回None,比如print()函数就没有返回。
7.4.3、返回多个值
return也可以返回多个值,python其实会将多个值放在一个元组中元组返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def login (user,pwd ):
flag = False
if user == 'yuan' and pwd == 123 :
flag = True
return flag,user
flag,user = login("yuan" ,123 )
if flag:
print ("{}登陆成功!" .format (user))
else :
print ("用户名或者密码错误!" )
7.5、函数嵌套
1
2
3
4
5
6
7
8
def foo ():
def bar ():
print ("bar功能" )
print ("foo功能" )
foo()
7.6、作用域
所谓作用域(Scope),就是变量的有效范围,就是变量可以在哪个范围以内使用。有些变量可以在整段代码的任意位置使用,有些变量只能在函数内部使用。
LEGB含义解释
字母 英语 释义 简称 作用空间
L
Local(function)
当前函数内的作用域
局部作用域
局部
E
Enclosing Functions Locals
外部嵌套函数的作用域
嵌套作用域
局部
G
Global(module)
函数外部所在的命名空间
全局作用域
全局
B
Built In(python)
Python内置模块的命名空间
内建作用域
内置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def foo ():
x = 10
foo()
print (x)
x = 100
def foo ():
x = 10
foo()
print (x)
x = 100
def foo ():
x = 10
print (x)
foo()
print (x)
x = 100
def foo ():
print (x)
foo()
x = 100
def foo ():
x = 12
def bar ():
x = 1
print (x)
bar()
foo()
7.7、匿名函数
lambda 表达式,又称匿名函数,常用来表示内部仅包含 1 行表达式的函数。如果一个函数的函数体仅有 1 行表达式,则该函数就可以用 lambda 表达式来代替。
lambda 表达式的语法格式如下:
其中,定义 lambda 表达式,必须使用 lambda 关键字;[list] 作为可选参数,等同于定义函数是指定的参数列表;value 为该表达式的名称。
1
2
3
4
5
6
def add (x, y ):
return x+ y
print (add(2 ,3 ))
(lambda x,y:x+y)(2 ,3 )
可以这样理解 lambda 表达式,其就是简单函数(函数体仅是单行的表达式)的简写版本。相比函数,lambda
表达式具有以下 2 个优势:
对于单行函数,使用 lambda 表达式可以省去定义函数的过程,让代码更加简洁;
对于不需要多次复用的函数,使用 lambda 表达式可以在用完之后立即释放,提高程序执行的性能。
7.8、高阶函数
7.8.1、高阶函数定义
一个高阶函数应该具备下面至少一个特点:
将一个或者多个函数作为形参
返回一个函数作为其结果
1
2
3
4
5
6
def foo ():
print ("foo" )
foo = 10
foo()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import time
def foo ():
print ("foo功能" )
time.sleep(2 )
def bar ():
print ("bar功能" )
time.sleep(3 )
def timer (func ):
start = time.time()
func()
end = time.time()
print ("时耗" ,end-start)
timer(foo)
timer(bar)
1
2
3
4
5
6
7
8
def foo ():
def bar ():
print ("bar功能!" )
return bar
func = foo()
func()
7.8.2、常见函数
常见内置函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
bool
bin
oct
hex
sum
max
min
abs
pow
divmod
round
chr
ord
list
dict
set
len
any
all
zip
sorted
stu_dict = {1001 : {"name" : "yuan" , "score" : {"chinese" : 100 , "math" : 90 , "english" : 80 , "average" : 90 }},
1002 : {"name" : "alvin" , "score" : {"chinese" : 100 , "math" : 100 , "english" : 100 , "average" : 100 }},
1003 : {"name" : "rain" , "score" : {"chinese" : 80 , "math" : 70 , "english" : 60 , "average" : 60 }}
}
stu_list = [v for k,v in stu_dict.items()]
ret = sorted (stu_list,key=lambda stu:stu["score" ]["average" ],reverse=True )
print (ret)
my_list = [1 , 2 , 3 ]
my_tuple = [4 , 5 , 6 ]
print (list (zip (my_list, my_tuple)))
常见高阶函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def foo (x ):
if x % 2 == 0 :
return True
l = [1 , 2 , 3 , 4 ]
print (list (filter (foo, l)))
print (list (filter (lambda x: x % 2 == 0 , l)))
def bar (x ):
return x * x
l = [1 , 2 , 3 , 4 ]
print (list (map (bar, l)))
print (list (map (lambda x: x * x, l)))
from functools import reduce
def bar (x, y ):
return x * y
l = [1 , 2 , 3 , 4 ]
print (reduce(bar, l))
print (reduce(lambda x, y: x * y, l))
练习题:
7.9、闭包
首先看一下维基上对闭包的解释:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。
简单来说就是一个函数定义中引用了函数外定义的变量,并且该函数可以在其定义环境外被执行。这样的一个函数我们称之为闭包。
闭包需要满足以下三个条件:
1、必须是一个嵌套函数
2、必须返回嵌套函数
3、嵌套函数必须引用外部非全局的局部自由变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def foo ():
x = 10
def inner ():
print (x)
print ("bar功能!" )
return inner
func = foo()
func()
def foo (x ):
def inner ():
print (x)
print ("bar功能!" )
return inner
func = foo(12 )
func()
价值:能够动态灵活的创建以及传递函数,体现出函数式编程的特点。所以在一些场合,我们就多了一种编码方式的选择,适当的使用闭包可以使得我们的代码简洁高效。
7.10、装饰器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import time
def foo ():
print ("foo功能" )
time.sleep(2 )
def bar ():
print ("bar功能" )
time.sleep(3 )
def timer (target_func ):
def wrapper ():
start = time.time()
target_func()
end = time.time()
print ("时耗" , end - start)
return wrapper
foo = timer(foo)
foo()
bar = timer(bar)
bar()
练习:设计一个装饰器,能够计算某个函数被调用次数!
7.11、迭代器
(1)可迭代对象和迭代器
在具体讲迭代器之前,先了解一个概念:可迭代对象(Iterable
)。之前在数据类型中介绍的容器对象(列表,元组,字典,集合等)都是可迭代对象;从语法形式上讲,能调用__iter__
方法的数据对象就是可迭代对象:
1
2
3
4
5
6
>>> [1 ,2 ,3 ].__iter__()
<listiterator object at 0x10221b150 >
>>> {'name' :'alvin' }.__iter__()
<dictionary-keyiterator object at 0x1022180a8 >
>>> {7 ,8 ,9 }.__iter__()
<setiterator object at 0x1021ff9b0 >
obj.__iter__()
方法调用后返回的就是一个迭代器对象(Iterator)。迭代器对象的特性就是能够调用__next__
方法依次计算出迭代器中的下一个值。基于此就可以实现无论是否数据为序列对象,都可以通过迭代取值的方式完成查询功能。
1
2
3
4
5
6
7
8
9
10
11
12
>>> s={1 ,2 ,3 }
>>> i=s.__iter__()
>>> i.__next__()
1
>>> i.__next__()
2
>>> i.__next__()
3
>>> i.__next__()
Traceback (most recent call last):
File "<stdin>" , line 1 , in <module>
StopIteration
1、针对一个没有索引的可迭代数据类型,我们执行s.__iter__()
方法便得到一个迭代器,每执行一次i.__next__()
就获取下一个值,待所有值全部取出后,就会抛出异常StopIteration
,不过这并不代表错误发生,而是一种迭代完成的标志。需要强调的是:此处我们迭代取值的过程,不再是通过索引而是通过__next__
方法。
2、可以用iter(s)
取代s.__iter__()
,其实iter(s)
本质就是在调用s.__iter__()
,这与len(s)
会调用s.__len__()
是一个原理,同理,可以用next(i)取代i.__next__()
。obj.__iter__()
方法的调用后返回的就是一个迭代器对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class ListIterator (object ):
def __init__ (self, l ):
self.l = l
self.index = 0
def __iter__ (self ):
return self
def __next__ (self ):
try :
ret = self.l[self.index]
self.index += 1
return ret
except IndexError:
raise StopIteration
iterator = ListIterator([100 , 101 , 102 ])
print (iterator.__next__())
print (iterator.__next__())
print (iterator.__next__())
for i in iterator:
print (i)
(2)你不了解的for循环
之前的学习只知道for循环是用来遍历某个数据对象的。但for循环内部到底是怎么工作的,关键字in后面可以放什么数据类型呢?让我们带着这些疑问一起去解析for循环的实现机制。
1
2
3
for val in obj:
print (val)
解析:关键字in后面数据对象必须是可迭代对象。for 循环首先会调用可迭代对象内的__iter__
方法返回一个迭代器,然后再调用这个迭代器的next方法将取到的值赋给val,即关键字for后的变量。循环一次,调用一次next方法,直到捕捉StopIteration
异常,结束迭代。解析:关键字in后面数据对象必须是可迭代对象。for 循环首先会调用可迭代对象内的__iter__
方法返回一个迭代器,然后再调用这个迭代器的next方法将取到的值赋给val,即关键字for后的变量。循环一次,调用一次next方法,直到捕捉StopIteration
异常,结束迭代。
1
2
3
4
5
6
7
8
9
10
11
l = [11 , 22 , 33 ]
for i in l:
print (i)
it = [1 , 2 , 3 , 4 , 5 ].__iter__()
for j in it:
print (j)
for j in it:
print (j)
(3)自定义迭代器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Fib (object ):
def __init__ (self, max ):
self.max = max
self.n, self.a, self.b = 0 , 0 , 1
def __iter__ (self ):
return self
def __next__ (self ):
if self.n < self.max :
r = self.b
self.a, self.b = self.b, self.a + self.b
self.n = self.n + 1
return r
raise StopIteration()
for i in Fib(10 ):
print (i)
迭代器协议要求迭代对象具有 __iter__()
和__next__()
两个方法,__next__
之前讲过,是用于计算下一个值的,而__iter__
则是返回迭代器本身,目的是使for循环可以遍历迭代器对象,for循环的本质是调用被迭代对象内部的__iter__
方法将其变成一个迭代器然后进行迭代取值的操作,如果对象没有__iter__
方法则会报错。所以可以说,迭代器对象都是可迭代对象就是因为其内部定义了__iter__
方法。
7.12、生成器
生成器算得上是Python语言中最吸引人的特性之一,生成器其实是一种特殊的迭代器,不过这种迭代器更加优雅。它不需要再像上面一样在类中定义__iter__()
和__next__()
方法了,只需要在函数中声明一个 yiled
关键字。 所以生成器是一种特殊的迭代器(反之不成立),因此任何生成器也是以一种懒加载的模式生成值。用生成器来实现斐波那契数列的例子是:
1
2
3
4
5
6
7
8
9
10
def fib (max ):
n, a, b = 0 , 0 , 1
while n < max :
yield b
a, b = b, a + b
n = n + 1
for i in fib(10 ):
print (i)
(1)生成器对象
简单说,生成器就是使用了yield关键字的函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def countdown (n ):
print ('countdown start' )
while n > 0 :
yield n
n -= 1
print ('Done!' )
print (countdown)
gen = countdown(5 )
print (gen)
for i in gen:
print (i)
解析:生成器函数调用时只会返回一个生成器对象。只有当生成器对象调用__next__
方法时才会触发函数体代码执行,直到遇到关键字yield停止,将yield后的值作为返回值返回,所以,yield类似于return的功能,但不同于return的是,return返回,函数结束;而yield将函数的状态挂起,等待生成器对象再次调用__next__
方法时,函数从挂起的位置后的第一条语句继续运行直到再遇见yield并返回其后的值;如果不断调用__next__
方法,最后一次进入函数体,待执行代码不再有yield此时报出迭代异常的错误。
另外,对比迭代器,生成器对象多几个方法:
(1)一个内置的close方法用来关闭自己
(2)一个内置的send方法,进入生成器,类似于next,但是多一个传值给yield变量的功能。
yield的功能总结:
(1)封装iter
和next
方法 (2)执行函数时遇到yield返回其后的值,不同于return,yiled
可以返回多次值 (3)挂起函数的状态,等待下一次调用next方法时找到对应的暂停位置继续执行。
(2)生成器表达式
创建一个生成器对象有两种方式,一是通过在函数中创建yield关键字来实现。另一种就是生成器表达式,这是一种类似于数据类型中学过的列表生成式的语法格式,只是将[]换成(),即:
1
(expression for item in iterable if condition)
不同于列表生成式最后返回一个列表结果,生成器表达式顾名思义会返回一个生成器对象,比如:
1
2
3
4
5
>>> [x*x for x in range (4 )]
[0 , 1 , 4 , 9 ]
>>> gen=(x*x for x in range (4 ))
>>> gen
<generator object <genexpr> at 0x101be0ba0 >
当需要用到其中的值时,再通过调用next方法或者for循环将值一个个地计算出来:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> next (gen)
0
>>> next (gen)
1
>>> next (gen)
4
>>> next (gen)
9
>>> next (gen)
Traceback (most recent call last):
File "<stdin>" , line 1 , in <module>
StopIteration
>>> gen=(x*x for x in range (4 ))
>>> for i in gen:
... print (i)
...
0
1
4
9
(3)可迭代对象、迭代器、生成器关系
(4)面试题
1
2
3
4
5
6
7
8
9
10
11
12
def add (s, x ):
return s + x
def gen ():
for i in range (4 ):
yield i
base = gen()
for n in [1 , 10 ]:
base = (add(i, n) for i in base)
print (list (base))
配合debug模式运行,其中gi_frame, gi_code, gi_running.gi_code是生成器的代码,根据这个对象我们可以获取到生成器的函数名、文件名。gi_frame相当于栈帧,记录了生成器执行的状态,根据这个对象我们可以获取到执行到的行号信息。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律