Python之路--Python基础3--函数
1、函数简介
函数是重(chong)用的程序段。它们允许你给一个语句块一个名称,然后你用这个名字可以在你的程序的任何地方,任意多次地运行这个语句块。这被称为调用函数。我们已经使用了许多内建的函数,比如 len 和 range 。函数用关键字 def 来定义。def 关键字后跟一个函数的标识符名称,然后跟一对圆括号。圆括号之中可以包括一些变量名,该行以冒号结尾。接下来是一块语句,它们是函数体。下面这个例子将说明这事实上是十分简单的:
def sayhi(name): #函数名(参数)
print("Hello,I'm %s" % name)
sayhi("YL") #调用函数
2、函数参数、局部变量、全局变量
形参变量只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。因此,形参只在函数内部有效。函数调用结束返回主调用函数后则不能再使用该形参变量
实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。因此应预先用赋值,输入等办法使参数获得确定值
默认参数
先看下面代码:
def stu_register(name,age,country,course):
print("----注册学生信息------")
print("姓名:",name)
print("age:",age)
print("国籍:",country)
print("课程:",course)
stu_register("Jack",22,"CN","python")
stu_register("Tom",21,"CN","linux")
stu_register("Alex",25,"JP","C++")
country 这个参数基本都是"CN", 就像我们在网站上注册用户,像国籍这种信息,你不填写,默认就会是“CN”,这就是通过默认参数实现的,把country变成默认参数非常简单:
def
stu_register(name,age,course,country
=
"CN"
):
这样,country这个参数在调用时不指定,那默认就是CN,指定了的话,就用你指定的值。注意:默认参数只能放到最后面 。
关键参数
正常情况下,给函数传参数要按顺序,不想按顺序就可以用关键参数,只需指定参数名即可,但记住一个要求就是,关键参数必须放在位置参数之后
stu_register(age
=
22
,name
=
'alex'
,course
=
"python"
)
非固定参数
若你的函数在定义时不确定用户想传入多少个参数,就可以使用非固定参数
def stu_register(name,age,*args): #*args 会把多传入的参数变成一个元组形式
print(name,age,args)
stu_register("Tom",22)
#输出
#Tom 22 () #后面这个()就是args,只是因为没传值,所以为空
stu_register("Jack",32,"CN","Python")
#输出
#Jack 32 ('CN', 'Python')
还可以有一个**kwargs
def stu_register(name,age,*args,**kwargs): #*kwargs 会把多传入的参数变成一个dict形式
print(name,age,args,kwargs)
stu_register("Tom",22)
#输出
#Tom 22 () {} #后面这个{}就是kwargs,只是因为没传值,所以为空
stu_register("Jack",32,"CN","Python",sex="Male",province="ShanDong")
#输出
# Jack 32 ('CN', 'Python') {'province': 'ShanDong', 'sex': 'Male'}
调用函数时,没有对应上的位置参数会传入*args,变成元组形式;关键参数会传入**kwargs,变成字典形式。
全局变量与局部变量
3、返回值
要想获取函数的执行结果,就可以用return语句把结果返回
注意:
- 函数在执行过程中只要遇到return语句,就会停止执行并返回结果,so 也可以理解为 return 语句代表着函数的结束
- 如果未在函数中指定return,那这个函数的返回值为None
4、函数嵌套与函数递归
啥都别说直接看函数嵌套的代码1:
name = "YL"
def change_name():
name = "YL2"
def change_name2():
name = "YL3"
print("第3层打印", name)
change_name2() #调用内层函数
print("第2层打印", name)
change_name()
print("最外层打印", name)
#输出:
第3层打印 YL3
第2层打印 YL2
最外层打印 YL
代码2:
#嵌套调用
def my_max4(a, b, c, d):
res1 = my_max2(a, b)
res2 = my_max2(res1, c)
res3 = my_max2(res2, d)
return res3
def my_max2(x, y):
if x > y:
return x
else:
return y
print(my_max4(11,35,34,-5)) #35
#嵌套定义(通常2到3层,多了不好看懂)
x = 3
def f1():
x = 1
def f2():
x = 2
print(x)
return f2
func = f1()
func() #2
递归:如果一个函数在内部调用自己,这个函数就是递归函数
def calc(n):
print(n)
if int(n/2) ==0:
return n
return calc(int(n/2))
calc(10)
输出:
10
5
2
1
递归特性:
1. 必须有一个明确的结束条件
2. 每次进入更深一层递归时,问题规模相比上次递归都应有所减少
3. 递归效率不高,递归层次过多会导致栈溢出(在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出
堆栈扫盲http://www.cnblogs.com/lln7777/archive/2012/03/14/2396164.html
递归函数的实际应用:二分查找
def binary_search(find_str, data_set, count): #参数说明:要找的数据,数据集合,查找计数器
mid = int(len(data_set)/2)
if mid == 0:
if data_set[mid] == find_str:
print("找到了->:", find_str, count)
else:
print("没找到:", find_str, count)
return
if data_set[mid] == find_str:
print("找到了:", data_set[mid], count)
elif data_set[mid] > find_str:
print("Going to search in left:", data_set[mid], data_set[0:mid])
binary_search(find_str, data_set[0:mid], count+1)
else:
print("Going to search in right:", data_set[mid], data_set[mid+1:])
binary_search(find_str, data_set[mid+1:], count+1)
binary_search(585, data, 0)
5、匿名函数
匿名函数就是不需要显式的指定函数
#这段代码
def calc(n):
return n**n
print(calc(10))
#换成匿名函数
calc = lambda n:n**n
print(calc(10))
这个看不出啥NB之处,那就看看下面的:
res = map(lambda x:x**2,[1,5,7,4,8])
for i in res:
print(i)
#输出:
1
25
49
16
64
#匿名函数最复杂的函数就是三元运算,不能再复杂了
calc2 = lambda x, y: x**y
print(calc2(10, 12))
for i in map(calc, [1, 2, 3]):
print(i)
for i in map(lambda x: x*x, [1, 2, 3]): #逼格高
print(i)
for i in map(lambda x: x*2 if x>5 else x-1, [1, 2, 3]):
print(i)
map()函数是Python内置函数,第一个参数接受一个函数名,后面的参数接受一个或多个可迭代的序列,返回的是一个集合。(后面的博客中会有详细介绍)
6、高阶函数
# 高阶函数:1.把一个函数的内存地址当做参数传给另一个函数
# 2.一个函数把另一个函数当做返回值 返回
def add(x, y, f):
return f(x) + f(y)
res = add(3, -6, abs) #abs绝对值函数
print(res)
7、闭包函数
如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包。
举个栗子:
def closure():
x = 5
def sub():
return x * x
return sub
如上,在内部函数sub
中包含了对函数closure
中局部变量x
的引用,这就是闭包。
闭包的意义:返回的函数对象,不仅仅是一个函数对象,在该函数外还包裹了一层作用域,这使得,该函数无论在何处调用,优先使用自己外层包裹的作用域
应用领域:延迟计算(原来我们是传参,现在我们是包起来)
再看一个nb一点的栗子:
from urllib.request import urlopen
def page(url):
#url = http//:www.baidu.com
def get():
return urlopen(url).read()
return get
baidu = page('http://www.baidu.com') #爬取百度页面
# python = page('http://www.python.org')
# print("python:", python())
print("baidu:", baidu().decode("utf-8")) #这里要转码
8、装饰器
装饰器就是闭包函数的一种应用场景
装饰器遵循开放封闭原则:对修改封闭,对扩展开放
装饰器本身可以是任意可调用对象,被装饰者也可以是任意可调用对象。
装饰器的原则:1、不修改被装饰对象的源代码
2、不修改被装饰对象的调用方式
装饰器的目标:在遵循以上两点原则的前提下,为被装饰对象添加上新功能
#装饰器语法
#被装饰函数的正上方,单独一行
@deco1
@deco2
@deco3
def foo():
pass
#此时调用foo就相当于---> foo=deco1(deco2(deco3(foo)))
举个栗子:
#无参装饰器
import time
def timer(func):
def wrapper(*arg, **kwargs): #任意参数传入
strat_time = time.time()
res = func(*arg, **kwargs)#运行最原始的index
stop_time = time.time()
print("run time is %s" % (stop_time-strat_time))
return res
return wrapper
@timer #index = timer(index)
def index(msg):
print("in the index:%s" % (msg))
@timer
def home(user, msg):
print("in the home:%s,%s" % (user, msg))
return "home return 1"
index("hello world")
print(home("jack", msg="123456"))
#输出-------------------
in the index:hello world
run time is 0.0
in the home:jack,123456
run time is 0.0
home return 1
下面是有参装饰器的栗子:
#有参装饰器
accounts = {}
current_logon_user = None
def auth(auth_type):
def auth_deco(func):
def wrapper(*args, **kwargs):
if current_logon_user not in accounts: #之前没有验证成功过
username = input("username:")
password = input("password:")
if auth_type == "file":
if username == "YL" and password == "123":
accounts[username] = True
global current_logon_user #修改全局变量
current_logon_user = username
return func(*args, **kwargs)
elif auth_type == "ldap":
print("----->ldap")
return func(*args, **kwargs)
else:
return func(*args, **kwargs) #如果验证成功了的直接返回执行最原始的函数
return wrapper
return auth_deco
@auth("file")
def index(msg):
print("in the index %s" %(msg))
@auth("ldap")
def home(msg):
print("in the home %s" %(msg))
index("hello") #只要第一次验证通过,后面就不需要验证了
home("hello")
9、生成器
现在有个需求,将列表[0,1,2,3,4,5,6,7,8,9]里的每一个值都加一,有下面三种方法:
#普通青年版--->使用enumerate函数
a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for index, i in enumerate(a): #将列表中的各项加一
#print(index, i) #打印下标和元素值
a[index] += 1
print(a)
a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
#文艺青年版--->使用匿名函数和map函数
a = map(lambda x: x+1, a)
for i in a:
print(i)
a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
#nb青年版----->使用列表生成
a = [i+1 for i in a]
print(a) #[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
#列表生成式还可以使用三元运算
#a = [i*i if i>5 else i-1 for i in a] #大于5的进行平方运算,小于5的进行减1运算
#print(a) #[-1, 0, 1, 2, 3, 4, 36, 49, 64, 81]
通过上面的列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,如果创建一个包含100万个元素的列表,就会占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。
要创建一个generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的[]
改成()
,就创建了一个generator:
L = [x * x for x in range(10)]
print(L) #[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
g = (x * x for x in range(10))
print(g) #<generator object <genexpr> at 0x00000133F4CE6FC0>
创建L
和g
的区别仅在于最外层的[]
和()
,L
是一个list,而g
是一个generator。
我们可以直接打印出list的每一个元素,但我们怎么打印出generator的每一个元素呢?如果要一个一个打印出来,可以通过next()
函数获得generator的下一个返回值:
g = (x * x for x in range(4))
print(g) #<generator object <genexpr> at 0x00000133F4CE6FC0>
print(next(g)) #0
print(g.__next__()) #1
print(g.__next__()) #4
print(next(g)) #9
#print(next(a)) #最后一个元素已经计算出了,再调用next(g)就会报错
generator保存的是算法,每次调用next(g)
,就计算出g
的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration
的错误。当然,上面这种不断调用next(g)
实在是太变态了,正确的方法是使用for
循环,因为generator也是可迭代对象:
for n in g:
print(n)
#输出:
0
1
4
9
所以,我们创建了一个generator后,基本上永远不会调用next()
,而是通过for
循环来迭代它,并且不需要关心StopIteration
的错误。generator非常强大。如果推算的算法比较复杂,用类似列表生成式的for
循环无法实现的时候,还可以用函数来实现。
比如,著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到:1, 1, 2, 3, 5, 8, 13, 21, 34, ...
斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
print(b)
a, b = b, a + b
n = n + 1
return 'done'
注意:
赋值语句:a, b = b, a + b
相当于: t = (b, a + b) # t是一个tuple
a = t[0]
b = t[1]
调用上面的函数可以输出斐波那契数列的前N个数:
>>> fib(10)
1
1
2
3
5
8
13
21
34
55
done
仔细观察,可以看出,fib
函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator。也就是说,上面的函数和generator仅一步之遥。要把fib
函数变成generator,只需要把print(b)
改为yield b
就可以了:
def generator(max):
n, a, b = 0, 0, 1
while n<max:
#print(b)
yield b #生成器yield 保存了函数的中断状态
a, b = b, a + b
n = n+1
return "done"
g = generator(5)
print(next(g))
print(g.__next__())
print("do something else")
print(g.__next__())
print(g.__next__())
print(next(g))
# 输出:
# 1
# 1
# do something else
# 2
# 3
# 5
这就是定义generator的另一种方法。如果一个函数定义中包含yield
关键字,那么这个函数就不再是一个普通函数,而是一个generator:
注意,最难理解的就是generator和函数的执行流程不一样。函数是顺序执行,遇到return
语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()
的时候执行,遇到yield
语句返回,再次执行时从上次返回的yield
语句处继续执行。
在上面
generator(max)的例子中,我们在循环过程中不断调用yield
,就会不断中断。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。
同样的,把函数改成generator后,我们基本上从来不会用next()
来获取下一个返回值,而是直接使用for
循环来迭代:
for n in generator(6):
print(n)
#输出
1
1
2
3
5
8
但是用for
循环调用generator时,发现拿不到generator的return
语句的返回值“done”。如果想要拿到返回值,必须捕获StopIteration
错误,返回值包含在StopIteration
的value
中。(关于如何捕获错误,后面的错误处理还会详细讲解)
还可通过yield实现在单线程的情况下实现并发运算的效果:
#吃包子
#通过生成器实现协程并行运算
import time
def consumer(name):
print("%s 准备吃包子啦!" % name)
while True: #死循环
baozi = yield #接收到producer send 过来的值
print("包子[%s]来了,被[%s]吃了!" % (baozi, name))
def producer():
c = consumer('A')
c2 = consumer('B')
c.__next__()
c2.__next__()
print("开始准备做包子啦!")
for i in range(10):
time.sleep(1)
print("做了2个包子!")
c.send(i) #调用next 并传了一个值给yield
c2.send(i)
producer()
10、迭代器
我们已经知道,可以直接作用于for
循环的数据类型有以下几种:
一类是集合数据类型,如list
、tuple
、dict
、set
、str
等;
一类是generator
,包括生成器和带yield
的generator function。
这些可以直接作用于for
循环的对象统称为可迭代对象:Iterable
。
可以使用isinstance()
判断一个对象是否是Iterable
对象:
>>> from collections import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance({}, Iterable)
True
>>> isinstance('abc', Iterable)
True
>>> isinstance((x for x in range(10)), Iterable)
True
>>> isinstance(100, Iterable)
False
而生成器不但可以作用于for
循环,还可以被next()
函数不断调用并返回下一个值,直到最后抛出StopIteration
错误表示无法继续返回下一个值了。
可以被next()
函数调用并不断返回下一个值的对象称为迭代器:Iterator
。
可以使用isinstance()
判断一个对象是否是Iterator
对象:
>>> from collections import Iterator
>>> isinstance((x for x in range(10)), Iterator)
True
>>> isinstance([], Iterator)
False
>>> isinstance({}, Iterator)
False
>>> isinstance('abc', Iterator)
False
生成器都是Iterator迭代器
对象,但list
、dict
、str
虽然是Iterable可迭代对象
,却不是Iterator迭代器
。
把list
、dict
、str
等Iterable
变成Iterator
可以使用iter()
函数:
>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True
那么,为什么list
、dict
、str
等数据类型不是Iterator
?
这是因为Python的Iterator迭代器
对象表示的是一个数据流,Iterator对象可以被next()
函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration
错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()
函数实现按需计算下一个数据,所以Iterator
的计算是惰性的,只有在需要返回下一个数据时它才会计算。
Iterator
甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。
小结:
凡是可作用于for
循环的对象都是Iterable
类型;
凡是可作用于next()
函数的对象都是Iterator
类型,它们表示一个惰性计算的序列;
集合数据类型如list
、dict
、str
等是Iterable
但不是Iterator
,不过可以通过iter()
函数获得一个Iterator
对象。
Python的for
循环本质上就是通过不断调用next()
函数实现的,例如:
for x in [1, 2, 3, 4, 5]: pass
等价于下面的代码:
# 首先获得Iterator对象: it = iter([1, 2, 3, 4, 5]) # 循环: while True: try: # 获得下一个值: x = next(it) except StopIteration: # 遇到StopIteration就退出循环 break