第四章 函数编程

1.函数是什么?

函数是指将一组语句的集合通过一个名字(函数名)封装起来,要想执行这个函数,只需调用其函数名即可

特性:

  • 减少重复代码
  • 使程序变的可扩展
  • 使程序变得易维护

语法定义

def sayhi():#函数名   
   print("Hello World!")
sayhi() #调用函数

2.函数参数

形参变量

只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。因此,形参只在函数内部有效。函数调用结束返回主调用函数后则不能再使用该形参变量

实参

可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。因此应预先给实参赋值

在这里插入图片描述
默认参数

def stu_register(name,age,country,course):    
    print("----注册学生信息------")    
    print("姓名:",name)    
    print("age:",age)    
    print("国籍:",country)    
    print("课程:",course)
stu_register("王山炮",22,"CN","python_devops")
stu_register("张叫春",21,"CN","linux")
stu_register("刘老根",25,"CN","linux")

国籍这种信息,你不填写,默认就会是 中国, 这就是通过默认参数实现的,把country变成默认参数非常简单

def stu_register(name,age,course,country="CN"):

关键参数
正常情况下,给函数传参数要按顺序,不想按顺序就可以用关键参数,只需指定参数名即可(指定了参数名的参数就叫关键参数),但记住一个要求就是,关键参数必须放在位置参数(以位置顺序确定对应关系的参数)之后

调用可以这样:

stu_register("王山炮",course='PY', age=22,country='JP' )

但是绝对不能这样:

stu_register("王山炮",course='PY',22,country='JP' )

注意,参数优先级顺序是 位置参数>关键参数

非固定参数

若你的函数在定义时不确定用户想传入多少个参数,就可以使用非固定参数

*args

def stu_register(name,age,*args): # *args 会把多传入的参数变成一个元组形式    
   print(name,age,args)
stu_register("Alex",22)
#输出
#Alex 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("Alex",22)
#输出#Alex 22 () {}#后面这个{}就是kwargs,只是因为没传值,所以为空

stu_register("Jack",32,"CN","Python",sex="Male",province="ShanDong")
#输出
# Jack 32 ('CN', 'Python') {'province': 'ShanDong', 'sex': 'Male'}

3.嵌套函数

每个函数里的变量是互相独立的,变量的查找顺序也是从当前层依次往上层找。问个哲学问题,这东西有什么用呢?哈,现在没用,不解释,后面后学了装饰器就知道有啥用了。

4.匿名函数

匿名函数就是不需要显式的指定函数名

#这段代码
def calc(x,y):    
   return x**yprint(calc(2,5)
#换成匿名函数
calc = lambda x,y:x**y
print(calc(2,5))

你也许会说,用上这个东西没感觉有毛方便呀, 。。。。如果是这么用,确实没毛线改进,不过匿名函数主要是和其它函数搭配使用的呢,如下

res = map(lambda x:x**2,[1,5,7,4,8])
for i in res:   
   print(i)

输出:

1
25
49
16
64

5.高阶函数

变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。

def get_abs(n):   
   if n < 0 :       
      n = int(str(n).strip("-"))   
   return n
def add(x,y,f):   
   return f(x) + f(y)
res = add(3,-6,get_abs)

print(res)

只需满足以下任意一个条件,即是高阶函数

  • 接受一个或多个函数作为输入
  • return 返回另外一个函数

6.名称空间

顾名思义就是存放名字的地方,存什么名字呢?举例说明,若变量x=1,1存放于内存中,那名字x存放在哪里呢?名称空间正是存放名字x与1绑定关系的地方。

python里面有很多名字空间,每个地方都有自己的名字空间,互不干扰,不同空间中的两个相同名字的变量之间没有任何联系。名称空间有4种: 简称LEGB

locals:函数内部的名字空间,一般包括函数的局部变量以及形式参数

enclosing function:在嵌套函数中外部函数的名字空间, 若fun2嵌套在fun1里,对fun2来说, fun1的名字空间就是enclosing.

globals:当前的模块空间,模块就是一些py文件。也就是说,globals()类似全局变量。

builtins: 内置模块空间,也就是内置变量或者内置函数的名字空间,print(dir(builtins))可查看包含的值。

6.1 作用域查找顺序

当程序引用某个变量的名字时,就会从当前名字空间开始搜索。搜索顺序规则便是: LEGB。即locals -> enclosing function -> globals -> builtins。 一层一层的查找,找到了之后,便停止搜索,如果最后没有找到,则抛出在NameError的异常。

7.闭包

关于闭包,即函数定义和函数表达式位于另一个函数的函数体内(嵌套函数)。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。

也就是说,内部函数会在外部函数返回后被执行。而当这个内部函数执行时,它仍然必需访问其外部函数的局部变量、参数以及其他内部函数。这些局部变量、参数和函数声明(最初时)的值是外部函数返回时的值,但也会受到内部函数的影响。

def outer():   
  name = 'alex'    
  def inner():       
    print("在inner里打印外层函数的变量",name)   
   return inner # 注意这里只是返回inner的内存地址,并未执行
f = outer() # .inner at 0x1027621e0> 
f()  # 相当于执行的是inner()

注意此时outer已经执行完毕,正常情况下outer里的内存都已经释放了,但此时由于闭包的存在,我们却还可以调用inner, 并且inner内部还调用了上一层outer里的name变量。这种粘粘糊糊的现象就是闭包。

闭包的意义:返回的函数对象,不仅仅是一个函数对象,在该函数外还包裹了一层作用域,这使得,该函数无论在何处调用,优先使用自己外层包裹的作用域

7.1 闭包在哪用

装饰器

8.装饰器

项目案例:
你是一家视频网站的后端开发工程师,你们网站有以下几个版块。视频刚上线初期,为了吸引用户,你们采取了免费政策,所有视频免费观看,迅速吸引了一大批用户,免费一段时间后,每天巨大的带宽费用公司承受不了了,所以准备对比较受欢迎的几个版块收费。想收费得先让其进行用户认证,认证通过后,再判定这个用户是否是VIP付费会员就可以了,是VIP就让看,不是VIP就不让看就行了呗。

8.1 把认证功能提取出来单独写个模块,然后每个版块里调用

你觉得这个需求很是简单,因为要对多个版块进行认证,那应该把认证功能提取出来单独写个模块,然后每个版块里调用 就可以了,与是你轻轻的就实现了下面的功能 。

account = {    
  "is_authenticated":False,# 用户登录了就把这个改成True 
   "username":"alex", # 假装这是DB里存的用户信息   
   "password":"abc123" # 假装这是DB里存的用户信息
}

def login():   
   if account["is_authenticated"] is False:        
        username = input("user:")       
        password = input("pasword:")        
        if username == account["username"] and password == account["password"]:            
           print("welcome login....")
           account["is_authenticated"] = True       
        else:           
           print("wrong username or password!")    
    else:        
           print("用户已登录,验证通过...")
  def home():    
        print("---首页----")
  def america():    
        login()  # 执行前加上验证    
        print("----欧美专区----")
  def zongyi():    
        print("----综艺专区----")
  def henan():   
        login()  # 执行前加上验证    
        print("----河南专区----")
home()
america()
henan()

但是代码很快被打回了,你的代码虽然实现了功能,但是需要更改要加认证的各个模块的源代码,这直接违反了软件开发中的一个原则“开放-封闭”原则,简单来说,它规定已经实现的功能代码不应该被修改,但可以被扩展,即:

  • 封闭:已实现的功能代码块不应该被修改
  • 开放:对现有功能的扩展开放

8.2 把这个功能 的函数名当做一个参数 传给 我的验证模块

account = {   
     "is_authenticated":False,# 用户登录了就把这个改成True  
     "username":"alex", # 假装这是DB里存的用户信息 
     "password":"abc123" # 假装这是DB里存的用户信息
}

def login(func):
    if account["is_authenticated"] is False:        
         username = input("user:")        
         password = input("pasword:")        
         if username == account["username"] and password == account["password"]:            
              print("welcome login....")
              account["is_authenticated"] = True        
          else:           
               print("wrong username or password!")   
    if account["is_authenticated"] is True:  # 主要改了这       
         func() # 认证成功了就执行传入进来的函数
   
 def home():    
         print("---首页----")
 def america():    
         print("----欧美专区----")
 def zongyi():    
         print("----综艺专区----")
 def henan():    
         print("----河南专区----")
home()
login(america) # 需要验证就调用 login,把需要验证的功能 当做一个参数传给login
login(henan)

你功能 是实现了,但是你又犯了一个大忌,什么大忌? 你改变了调用方式呀, 想一想,现在没每个需要认证的模块,都必须调用你的login()方法,并把自己的函数名传给你,人家之前可不是这么调用 的, 试想,如果 有100个模块需要认证,那这100个模块都得更改调用方式,这么多模块肯定不止是一个人写的,让每个人再去修改调用方式 才能加上认证,你会被骂死的。。。。

8.3 如何即不改变原功能代码,又不改变原有调用方式,还能加上认证呢?

lambda与正常函数的区别是什么?

最直接的区别是,正常函数定义时需要写名字,但lambda不需要

lambda定好后,为了多次调用 ,可否也给它命个名?

可以呀,可以写成plus = lambda x:x+1类似这样,以后再调用plus就可以了,但这样不就失去了lambda的意义了,明明人家叫匿名函数呀,你起了名字有什么用呢?

def plus(n):    return n+1

plus2 = lambda x:x+1  #两种写法代表相同的意思

上面两种写法代表相同的意思,我给lambda x:x+1 起了个名字叫plus2,是不是相当于def plus2(x) ?

这说明什么?

给函数赋值变量名就像def func_name 是一样的效果,如下面的plus(n)函数,你调用时可以用plus名,还可以再起个其它名字,如

calc = plus

calc(n)

之前写的下面这段调用 认证的代码 之所以改变了调用方式,是因为用户每次调用时需要执行login(henan)

home()
login(america) #需要验证就调用 login,把需要验证的功能 当做一个参数传给login# home()# america()
login(henan)

稍一改就可以了,这样以后其它人调用henan时,其实相当于调用了login(henan), 通过login里的验证后,就会自动调用henan功能。

home()
america = login(america)
henan = login(henan)

不过,等等, 这样写了好,那用户调用时,应该是下面这个样子
问题在于,还不等用户调用 ,你的america = login(america)就会先自己把america执行了呀。。。。,你应该等我用户调用 的时候 再执行才对呀

home()
america = login(america) #你在这里相当于把america这个函数替换了
henan = login(henan)#那用户调用时依然写
america()

想实现一开始你写的america = login(america)不触发你真正的america函数的执行,只需要在这个login里面再定义一层函数,第一次调用america = login(america)只调用到外层login,这个login虽然会执行,但不会触发认证了,因为认证的所有代码被封装在login里层的新定义 的函数里了,login只返回 里层函数的函数名,这样下次再执行america()时, 就会调用里层函数啦。。。代码见下图
在这里插入图片描述
在这里插入图片描述

上面的写法,还可以更简单 可以把下面代码去掉

america = login(america) # 这次执行login返回的是inner的内存地址
henan = login(henan)  # .inner at 0x102562840>

只在你要装饰的函数上面加上下面代码

在这里插入图片描述
你开心的玩着玩着就手贱给你的“河南专区”版块 加了个参数,然后,结果 出错了。。
怎么传个参数就不行了呢?

调用henan时,其实是相当于调用的login,你的henan第一次调用时henan = login(henan), login就返回了inner的内存地址,第2次用户自己调用henan(5),实际上相当于调用的是inner,但你的inner定义时并没有设置参数,但你给他传了个参数,所以自然就报错了呀

在这里插入图片描述

8.4 装饰器完整代码

在这里插入图片描述
在这里插入图片描述

9.列表生成式

现在有个需求,现有列表a=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],要求你把列表里的每个值加1,你怎么实现?你可能会想到2种方式

二逼青年版:生成一个新列表b,遍历列表a,把每个值加1后存在b里,最后再把a=b, 这样二逼的原因不言而喻,生成了新列表,浪费了内存空间。

>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> b = []
>>> for i in a:b.append(i+1)... 
>>> b
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> a = b
>>> a
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

普通青年版: 毫无新意

a = [1,3,4,6,7,7,8,9,11]
for index,i in enumerate(a):   
   a[index] +=1
print(a)

列表生成式版

>>> a = [i+1 for i in range(10)]
>>> a
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

10.生成器

通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。比如我要循环100万次,按py的语法,for i in range(1000000)会先生成100万个值的列表。但是循环到第50次时,我就不想继续了,就退出了。但是90多万的列表元素就白为你提前生成了。

所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?像上面这个循环,每次循环只是+1而已,我们完全可以写一个算法,让他执行一次就自动+1,这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算后面元素的机制,称为生成器:generator。

第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个generator:

>>> [x * x for x in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> 
>>> (x * x for x in range(10))
 at 0x101ebc3b8>

(x*x for x in range(10))生成的就是一个生成器。我们可以直接打印出list的每一个元素,但我们怎么打印出generator的每一个元素呢?如果要一个一个打印出来,可以通过next()函数获得generator的下一个返回值:

>>> g = (x * x for x in range(10))
>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9

当然,上面这种不断调用next(g)实在是太变态了,正确的方法是使用for循环,因为generator也是可迭代(遍历)对象:

>>> g = (x * x for x in range(10))
>>> for n in g:     
      print(n)
 ...
0
1
4
9

10.1 函数生成器

generator非常强大。如果推算的算法比较复杂,用类似列表生成式的for循环无法实现的时候,还可以用函数来实现。比如,著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到:1, 1, 2, 3, 5, 8, 13, 21, 34, …实现100以内的斐波那契数代码:

a,b = 0,1
n = 0  # 斐波那契数
while n < 100:    
      n = a + b    
      a = b # 把b的旧值给到a    
      b = n # 新的b = a + b(旧b的值)    
      print(n)

改成函数形式

def fib(max):    
     a,b = 0,1    
     n = 0  # 斐波那契数    
     while n < max:        
          n = a + b        
          a = b # 把b的旧值给到a        
          b = n # 新的b = a + b(旧b的值) 
          print(n)
fib(100)

仔细观察,可以看出,fib函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator。也就是说,上面的函数和generator仅一步之遥。要把fib函数变成generator,只需要把print(b)改为yield b就可以了:

f fib(max):    
      a,b = 0,1   
      n = 0  # 斐波那契数    
      while n < max:        
           n = a + b        
           a = b # 把b的旧值给到a        
           b = n # 新的b = a + b(旧b的值)       
           #print(n)        
           yield n # 程序走到这,就会暂停下来,返回n到函数外面,直到被next方法调用时唤醒
f = fib(100) # 注意这句调用时,函数并不会执行,只有下一次调用next时,函数才会真正执行
print(f)
print(f.__next__())
print(f.__next__())
print(f.__next__())
print(f.__next__())

这就是定义generator的另一种方法。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator:

这里,最难理解的就是generator和函数的执行流程不一样。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句暂停并返回数据到函数外,再次被next()调用时从上次返回的yield语句处继续执行。
在这里插入图片描述
在上面fib的例子,我们在循环过程中不断调用yield,函数就会不断的中断(暂停)。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。同样的,把函数改成generator后,我们基本上从来不会用next()来获取下一个返回值,而是直接使用for循环来迭代:

f = fib(100) # 注意这句调用时,函数并不会执行,只有下一次调用next时,函数才会真正执行
for i in f:    
    print(i)

10.2 并发编程

通过yield, 我们可以实现单核下并发做多件事的效果。

在这里插入图片描述

11.迭代器

我们已经知道,可以直接作用于for循环的数据类型有以下几种:
1.一类是集合数据类型,如list、tuple、dict、set、str等;
2.一类是generator,包括生成器和带yield的generator function。

这些可以直接作用于for循环的对象统称为可迭代对象:Iterable,可迭代的意思就是可遍历、可循环。可以使用isinstance()判断一个对象是否是Iterable对象:

生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。把list、dict、str等Iterable变成Iterator可以使用iter()函数

为什么list、dict、str等数据类型不是Iterator?

这是因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。

11.1 迭代器总结

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

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

3.集合数据类型如list、dict、str等是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值