python代码运行的时候遇到函数时如何处理的:
从python解释器开始执行之后,就在内存中开辟了一个空间
每当遇到一个变量的时候,就把变量名和值之间的对应关系记录下来
但是,当遇到函数定义的时候解释器只是象征性的将函数名读入内存,表示知道这个函数的存在了,至于函数内部的变量和逻辑解释器根本不关心
等执行到函数调用的时候,python解释器会再开辟一块内存来存储这个函数里面的内容,这个时候,才关注函数里面有哪些变量,而函数中的变量会存储在新开辟出来的内存中,函数中的变量只有在函数的内部使用,并且会随着函数执行完毕,这块内存中的所有内容也会被清空。
我们给这个‘存放名字和值的关系’的空间叫做-------命名空间
代码在运行开始,创建的存储‘变量名与值的关系’的空间叫做全局命名空间,在函数的运行中开辟的临时空间叫做局部命名空间,存放内置的叫做内置命名空间
三种命名空间之间的加载和取值顺序:
加载顺序:内置命名空间(程序运行前加载)>全局命名空间(程序运行中:从上到下加载)>局部命名空间(程序运行中:调用时才加载)
取值:在局部调用:局部命名空间----->全局命名空间----->内置命名空间
在全局调用:全局命名空间----->内置命名空间
作用域
作用域就是作用范围,按照生效范围可以分为全局作用域和局部作用域
全局作用域:包含内置名称空间、全局名称空间,在整个文件的任意位置都能被引用、全局有效
局部作用域:局部名称空间,只能在局部范围内生效
global关键字:内部作用域想要对外部作用域的变量进行修改,global关键字在一个函数中来修改全局变量,在一个函数外使用global关键字没有效果,因为该变量本身就是全局变量
a = 10 def func(): global a a = 20 print(a) func() print(a)
10 20
global是对整个环境下的变量起作用,而不是对函数内的变量起作用
x = 0 def outer(): x = 1 def inner(): global x x = 2 print('inner:',x) inner() print('outer:',x) outer() print('global:',x)
inner: 2 outer: 1 global: 2
函数的嵌套定义
def f1(): print('in f1') def f2(): print('in f2') f2() f1()
in f1 in f2
函数的作用域链
def f1(): a = 1 def f2(): print(a) f2() f1()
1
def f1(): a = 1 def f2(): a = 2 f2() print('a in f1 : ',a) f1()
a in f1 : 1
nonlocal关键字
1)外部必须有这个变量
2)在内部函数声明nonlocal变量之前不能再出现同名变量
3)内部修改这个变量如果想在外部有这个变量的第一层函数中生效
x = 0 def outer(): x = 1 def inner(): nonlocal x x = 2 print('inner:',x) inner() print('outer:',x) outer() print('global:',x)
inner: 2 outer: 2 global: 0
当使用nonlocal时,就声明了该变量不止是嵌套函数inner()里面才有效,而是在整个大函数里面都有效,但是并未改变全局变量
def f1(): a = 1 def f2(): nonlocal a a = 2 f2() print('a in f1:',a) f1()
a in f1: 2
五、函数式编程
1.高阶函数
高阶函数英文叫Higher-order function
变量可以指向函数
可见,abs(-10)是函数调用,而abs是函数本身
f = abs
函数本身也可以赋值给变量,即:变量可以指向函数
f(-10)
说明变量f现在已经指向了abs函数本身,直接调用abs()函数和调用变量f()完全相同
函数名也是变量
abs=10
abs(-19) #不能执行了,因为abs = 10
把abs指向10后,就无法通过abs(-10)调用该函数了,因为abs这个变量已经不指向求绝对值函数而是指向一个整数10
当然实际代码绝对不能这么写,这里是为了说明函数名也是变量,要恢复abs函数,请重启python交互环境
传入函数
既然变量可以指向函数,函数的参数能接受变量,那么一个函数就可以接受另一个函数作为参数,这种函数就称之为高阶函数
一个函数可以接受另一个函数作为参数,这种函数为高阶函数
一个简单的高阶函数:
def add(x,y,f):
return f(x)+f(y)
add(1,-2,abs) #3
map/reduce
python 内建了 map()和reduce()函数
map()函数接受了两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为Iterator返回
要把函数作用在list:[1,2,3,4,5,6,7,8,9]上,就可以用map()实现
def f(x):
return x*x
r = map(f, [1,2,3,4,5,6,7,8,9])
list(r)
[1, 4, 9, 16, 25, 36, 49, 64, 81]
map()传入的第一个参数是f。即函数对象本身,由于结果r是一个Iterator,Iterator是惰性序列,因此,通过list()函数让他把整个序列都计算出来并返回一个list
def f(x):
return x*x
L = []
for n in [1,2,3,4,5,6,7,8,9]:
L.append(f(n))
print(L)
[1, 4, 9, 16, 25, 36, 49, 64, 81]
这样也可以,但是你能一眼看明白“把f(x)作用在list的每一个元素并把结果生成一个新的list吗”
所以,map()作为高阶函数,事实上他把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2方,还可以计算任意复杂的函数
比如,把这个list所有数字转为字符串,只需要一行代码,其中str是函数
list(map(str,[1,2,3,4,5,6,7,8,9]))
['1', '2', '3', '4', '5', '6', '7', '8', '9']
reduce
把一个函数作用在一个序列[x1,x2,x3,;;;],这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:
reduce(f,[x1,x2,x3,x4]) = f(f(f(x1,x2),x3),x4) #第一个和第二个作用,结果:a #结果a和第三个作用,结果:b #结果b和第四个作用,结果:c,,,以此类推
比方说对一个序列求和,就可以用reduce实现:
from functools import reduce #1+3 def add(x,y): #4(1+3)+5 return x+y #9(4+5)... reduce(add,[1,3,5,7,9]) #29
当然求和运算可以直接用python内建函数sum(),没必要动用reduce,但是如果要把序列[1,3,5,7,9]变换成整数13579,reduce就可以派上用场:
from functools import reduce #x=1,y=3 def fn(x,y): #x=13,y=5 return x*10 + y #x=135,y=7 reduce(fn,[1,3,5,7,9]) #x=1357,y=9 #结果:13579
这个例子本身没多大用处,但是,如果考虑到字符串str也是一个序列,对上面的例子稍加改动,配合map(),我们就可以写出把str转换成int的函数
from functools import reduce
def fn(x,y):
return x*10+y
def char2num(s):
digits = {'0':0,'1':1,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9}
return digits[s] a = map(char2num,'13579')#因为map()是惰性的,所以一个一个放进去 a #结果:<map at 0x5ddab90> list(a) #[1, 3, 5, 7, 9]
reduce(fn,list(map(char2num,'13579'))) #等价于reduce(fn,[1, 3, 5, 7, 9])
整理在一起
DIGITS = {'0':0,'1':1,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9}
def str2int(s):
def fn(x,y):
return x*10+y
def char2num(s):
return DIGITS[s] return reduce(fn,map(char2num,s))#可能是因为map是惰性的,所以运行不出来,另外为什么不加list,是因为reduce里面可以接受元组;map第二个数作为前面函数的参数 str2int('12345')
12345
filter
python内建的filter()函数用于过滤序列
filter()函数,是 Python 内置的另一个有用的高阶函数,filter()函数接收一个函数 f 和一个list,这个函数 f 的作用是对每个元素进行判断,返回 True或 False,filter()根据判断结果自动过滤掉不符合条件的元素,返回由符合条件元素组成的新list。
和map()类似,filter()也接受一个函数和一个序列、和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素
例如,一个list中,删掉偶数,只保留奇数,可以这么写:
def is_odd(n):
return n%2 == 1 # ==了就是True
list(filter(is_odd,[1,2,4,5,6,9,10,15]))
[1, 5, 9, 15]
把一个序列中的空字符串删掉,可以这么写:
def not_empty(s):
return s and s.strip() #strip()参数为空时为删除空白符;.strip()方法作用是把字符串头尾的指定字符去掉,比如“000xxx000”,s.strip()为“xxx”
#None类型没有strip方法,执行会报错。前面加s,当s为None时,执行到and前面已经可以判定为False,and后的部分就不会被执行了。
#变量A and 变量B 返回的一定是个bool值,如果 x 为 False,x and y 返回 False,否则它返回 y 的运算值。并且 None没有strip()方法。
list(filter(not_empty,['A','','B',None,'c','']))
可见用filter()这个高阶函数,关键在于正确实现一个’筛选‘函数
注意到filter()函数返回的是一个Iterator,也就是一个惰性序列,所以要强迫filter()完成计算结果,需要用list()函数获得所有结果并返回list
用filter求素数
'''计算素数的一个方法是埃氏筛法,它的算法理解起来非常简单:
首先,列出从2开始的所有自然数,构造一个序列:
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...
取序列的第一个数2,它一定是素数,然后用2把序列的2的倍数筛掉:取新序列的第一个数3,它一定是素数,然后用3把序列的3的倍数筛掉:新序列的第一个数5,然后用5把序列的5的倍数筛掉:不断筛下去,就可以得到所有的素数。
用python来实现这个算法,可以先构造一个从3开始的奇数序列:
def _odd_iter():
n = 1
while True:
n = n+2
yield n #注意这是一个生成器,并且是一个无限序列
#while True语句中一定要有结束该循环的break语句,否则会一直循环下去
然后定义一个筛选函数
def _not_divisible(n):
return lambda x:x%n>0
#最后,定义一个生成器,不断返回下一个素数
def primes():
yield2
it = _odd_iter()#初始序列
while True:
n = next(it) #返回序列的第一个数
yield n
it = filter(_not_divisible(n),it)#构造新序列,把不能被整除的挑出来
#由于primes()也是一个无限序列,所以调用时需要设置一个退出循环的条件:
#难道说filter以及map、reduce里面的第二部分可以是iterator吗??????????
#打印1000以内的素数
for n in primes():
if n<1000:
print(n)
else:
break
#注意到Iterator是惰性计算的序列,所以我们可以用python表示‘全体自然数’,‘全体素数’这样的序列,而代码非常简洁
sorted排序算法
如果是数字,可以直接比较,但是如果是字符串或者是两个dict呢,直接比较数字上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来
python内置的sorted()函数就可以对list进行排序
sorted([36,5,-12,9,-21])
[-21, -12, 5, 9, 36]
此外,sorted函数也是一个高阶函数,他还可以接受一个key函数来实现自定义的排序,例如按照sin函数大小排序
import math
sorted([36,5,-12,9,-21],key=math.sin)
[36, 5, -21, 9, -12]
key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序,对比原始的list和经过key=math.sin处理过的list
lis = [36,5,-12,9,-21] #这个是sorted([36,5,-12,9,-21])的结果 keys = [36,5,-21,9,-12] #这个是输出sorted([36,5,-12,9,-21],key=math.sin)的结果,返回的还是原始序列的数
我们在看一个字符串排序的例子
sorted(['bob','about','Zoo','Credit'])
['Credit', 'Zoo', 'about', 'bob']
默认情况下,对字符串排序,是按照ASCII的大小比较的,由于'Z'<'a',结果,大写字母Z会排在小写字母a的前面
注意是,大写排在小写的前面
反向排序
sorted(['bob','about','Zoo','Credit'],key = str.lower, reverse = True)
['Zoo', 'Credit', 'bob', 'about']
从上面例子可以看出,高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁
2.返回函数
函数作为返回值
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回
我们来实现一个可变参数的求和
def calc_sum(*args):
ax = 0
for n in args:
ax = ax+n
return ax
但是不需要立刻求和,而是在后面的代码中,根据需要在计算怎么办?可以不反悔求和的结果,而是返回求和的函数
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax return sum #sum是函数
当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数(可能是函数是sum(),返回的是sum吧)
f = lazy_sum(1,3,5,7,9)
f
<function __main__.lazy_sum.<locals>.sum()>
调用函数f时,才真正计算求和的结果
def lazy_sum(*args):
def sum(): #相当于在原来的部分上加了def sum(): return sum
ax = 0
for n in args:
ax = ax + n
return ax
return sum #sum是函数
f = lazy_sum(1,3,5,7,9) f()
25
在这个例子中,我们在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,
当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为‘闭包(closure)’的程序结构拥有极大的威力
请再注意一点,当我们调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数
f1 = lazy_sum(1,3,5,7,9)
f2 = lazy_sum(1,3,5,7,9)
f1 == f2
False
f1 和 f2的调用结果互不影响
闭包
def func(): name = 'eva' def inner(): print(name)
闭包函数:内部函数包含对外部作用域而非全局作用域名字的引用,该内部函数(函数内部定义的函数)称为闭包函数
我们知道函数内的变量要想在函数外部用,可以直接返回这个变量,那么如果我们想在函数外部调用函数内部的函数,自然是直接把这个函数名字返回就好了
def func(): name = 'eva' def inner(): print(name) #内部函数inner对外部name的引用 return inner f = func() f()
eva
判断闭包函数的方法
#输出的__colsure__有cell元素:是闭包函数 def func(): name = 'eva' def inner(): print(name) print(inner.__closure__) return inner f = func() f() print('-------------------------------------') #输出的__closure__为None:不是闭包函数 name = 'egon' def func2(): def inner(): print(name) print(inner.__closure__) return inner f2 = func2() f2()
(<cell at 0x0497D3F0: str object at 0x04938D60>,) eva ------------------------------------- None egon
闭包嵌套
def wrapper(): money = 1000 def func(): name = 'eva' def inner(): print(name,money) return inner return func f = wrapper() i = f() i()
eva 1000
命名空间:
一共有三种命名空间从大范围到小范围顺序:内置命名空间,全局命名空间,局部命名空间
作用域:小范围可以用大范围的,但是大范围不能用小范围的
注意到返回的函数在其定义内部引用了局部变量(在函数内定义的量)args,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单实现起来可不容易
另一个需要注意的问题是,返回的函数并没有立刻执行,而是直到调用f()才执行,我们来看一个例子:
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs f1, f2, f3 = count()
f1()
f2()
f3()
在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了
你可能认为调用f1(),f2()和f3()结果应该是1,4,9,但实际结果是都为9
9 9 9
返回的fs的列表为[f,f,f],当执行f1,f2,f3=count()的时候,f1=f,f2=f,f3=f,这个时候变量i的值已经变成了3,所以最后结果都为9
原因就是当count()函数返回了3个函数时,这3个函数所引用的变量 i 的值已经变成了3。由于f1、f2、f3并没有被调用,所以,此时他们并未计算 i*i
如果一定要引用循环变量怎么办,方法是在创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f())
return fs f1, f2, f3 = count()
或
def count():
def f(j):
def g():
return j*j
return g
fs = [] for i in range(1, 4): fs.append(f(i)) return fs f1, f2, f3 = count() f1() f2() f3()
1 4 9
3.匿名函数
当我们在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便
在python中,对匿名函数提供了有限支持,还是以map()函数为例,计算f(x)=x2时,除了定义一个f(x)外,还可以传入匿名函数
list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
[1, 4, 9, 16, 25, 36, 49, 64, 81]
通过对比可以看出,匿名函数lambda x:x * x实际上就是:
def f(x):
return x*x
list(map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
[1, 4, 9, 16, 25, 36, 49, 64, 81]
关键字lambda表示匿名函数,冒号前面的x表示函数参数
匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果
用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突,此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变函数变量,再利用变量来调用该函数
f = lambda x:x*x
f(5)
25
同样,也可以把匿名函数作为返回值返回,比如
def build(x,y):
return lambda :x*x +y*y
build(1,5)
<function __main__.build.<locals>.<lambda>()>
def build(x,y):
return lambda :x*x +y*y
build(1,5)()
26
4.装饰器
由于函数也是一个对象,而且函数对象可以赋值给变量,所以,通过变量也能调用该函数
def now():
print('2015-3-25')
f = now
f() #或f = now()
#2015-3-25
函数对象有一个__name__属性,可以拿到函数的名字:
def now():
print('2015-3-25')
f = now
f() #或f = now()
now.__name__
f.__name__
now now
现在,假设我们要增强now()函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()函数的定义,这种代码运行期间动态增加功能的方式,叫做‘装饰器(Decorator)
本质上,decorator就是一个返回函数的高阶函数,所以,我们要定义一个能打印日志的decorator,可以定义如下
def log(func):
def wrapper(*args,**kw):
print('call ^%s():' %func.__name__)
return func(*args,**kw)
return wrapper
观察上面的log,因为他是一个decorator,所以接受一个函数作为参数,我们要借助python的@语法,把decorator置于函数的定义处now
@log
def now():
print('2015-3-25')
调用now()函数,不仅会允许now()函数本身,还会在运行now()函数前打印一行日志
now()
call ^now(): 2015-3-25
把@log放在now()函数的定义处,相当于执行了语句
now = log(now)
由于log()是一个decorator,返回一个函数,所以,原来的now()函数仍然存在,只是现在同名的now变量指向了新的函数,于是调用now()将执行新函数,即在log()函数中返回的wrapper()函数
#wrapper()函数的参数定义是(*args,**kw),因此,wrapper函数可以接受任意参数的调用,在wrapper()函数内,首先打印日志,再紧接着调用原始函数
#如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂,比如,要自定义log的文本
#什么是装饰器?
#python装饰器(functional decorators)就是用于拓展原来函数功能的一种函数,目的是在不改变原函数名(或类名)的情况下,给函数增加新的功能
#这个函数的特殊之处在于它的返回值也是一个函数,这个函数是内嵌‘原’函数的函数
#一般而言,我们要想拓展原来函数代码,最直接的方法就是侵入代码里面修改,例如:
import time
def f():
print('hello')
time.sleep(1)#python time sleep()函数推迟调用线程的运行,可通过参数secs指秒数,表示进程挂起的时间
print('world')
f()#就是说先打印hello,过一秒打印world
#这是我们最原始的一个函数,然后我们试图记录下这个函数执行的总时间,最简单的做法就是改动原来的代码
import time
def f():
start_time = time.time()
print('hello')
time.sleep(1)
print('world')
end_time = time.time()
execution_time = (end_time - start_time)*1000
print('time is %d ms' %execution_time)
f()
#但是实际工作中,有些时候核心代码并不可以直接去改,所以在不该动原代码的情况下,我们可以再定义一个函数(但是生效需要再次执行函数)
import time
def deoo(funo):
start_time = time.time()
f()
end_time = time.time()
execution_time = (end_time - start_time)*1000
print('time is %d ms' %execution_time)
def f():
print('hello')
time.sleep(1)
print('world')
if __name__ == '__main__':
deoo(f)
print('f.__name__is',f.__name__)
#一个python文件有两种使用方法,第一是直接作为脚本执行,第二是import到其他的python脚本中被调用(模块重用)执行。因此if__name__=='main':的作用是控制这两种情况执行代码的过程
#在if__name__ == 'main':下的代码只有在第一种情况下(即文件作为脚本直接执行)
#简而言之,__name__就是当前模块名,当模块被直接运行时候模块名称为__main__,当模块被直接执行的时候,代码被执行,当模块被导入的时候,代码不运行
#举个例子:在test.py中写入如下代码:
print('i am the first')
if __name__ == '__main__':
print('i am the second')
#即,if__name__=='__main__':语句之前和之后的代码都被执行
#然后在用一文件夹新建名称为import_test.py的脚本,只输入如下代码
import test
#执行import_test.py脚本,输出结果如下:
print('i am the first')
if __name__ == '__main__':
print('i am the second')#虽然此处的输出结果两个都输出了,但是如果是在创建了test.py的话,第二个是不会执行的
5.偏函数
python中的functools模块提供了很多有用的功能,其中一个就是偏函数(Partial function),要注意,这里的偏函数和数学意义上的偏函数不一样
在介绍函数参数的时候,我们讲到,通过设定参数的默认值,可以降低函数调用的难度,而偏偏函数也可以做到这一点,举例如下:
int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换:
int('12345')
但int()函数还提供额外的base函数,默认值为10,如果传入base函数,就可以做N进制转换
int('12345',base=8) #5349 int('12345',base=16) #74565
假设要转换大量的二进制字符串,每次都传入int(x,base=2)非常麻烦,于是,我们想到,可以定义一个int2()的函数,默认把base=2传进去
def int2(x,base=2):
return int(x,base)
int2('1000000')
int2('1010101')
functools.partial就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2
import functools
int2 = functools.partial(int,base=2)
int2('1010101')
所以,简单总结functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数就会更简单
注意到上面的新的int2函数,仅仅是把base参数重写设定默认值为2,但也可以在函数调用时传入其他值
int2('1000000',base=10)
最后,创建偏函数时,实际上可以接受函数对象,#args和**kw这3个函数,当传入:int2 = functools.partial(int,base=2),实际上固定了int()函数的关键字参数base,也就是:
int2('10000')
相当于:
kw = {'base':2}
int('10010',**kw)
当传入:
max2 = functools.partial(max,10)
实际上会把10作为*args的一部分自动加到左边,也就是:max2(5,6,7)
相当于:
args = (10,5,6,7)
max2(*args)
总结:当函数的参数个数太多,需要简化时,使用functools.partial可以创建一个函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单