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语句把结果返回

注意:

  1. 函数在执行过程中只要遇到return语句,就会停止执行并返回结果,so 也可以理解为 return 语句代表着函数的结束
  2. 如果未在函数中指定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>

创建Lg的区别仅在于最外层的[]()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错误,返回值包含在StopIterationvalue中。(关于如何捕获错误,后面的错误处理还会详细讲解)

还可通过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循环的数据类型有以下几种:

  一类是集合数据类型,如listtupledictsetstr等;

  一类是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迭代器对象,但listdictstr虽然是Iterable可迭代对象,却不是Iterator迭代器

listdictstrIterable变成Iterator可以使用iter()函数:

>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True

那么,为什么listdictstr等数据类型不是Iterator

  这是因为Python的Iterator迭代器对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。

  Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。

小结:

  凡是可作用于for循环的对象都是Iterable类型;

  凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列;

  集合数据类型如listdictstr等是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

 

posted @ 2018-01-06 19:39  杨同学在这里  阅读(180)  评论(0编辑  收藏  举报