《Python参考手册》6 函数与函数式编程

文章目录

    • 6.1 函数
    • 6.2 参数传递与返回值
    • 6.3 作用域规则
    • 6.4 作为对象与闭包的函数
    • 6.5 装饰器
    • 6.6 生成器与yield
    • 6.7 协程与yield表达式
    • 6.8 使用生成器与协程
    • 6.9 列表推导
    • 6.10 生成器表达式
    • 6.11 lambda运算符
    • 6.12 递归
    • 6.13 文档字符串
    • 6.14 函数属性
    • 6.15 eval(),exec(),compile()

6.1 函数

函数调用的sum(x,y)形式,其中(x,y)代表的是一个元组。
如果使用了带有默认值的参数,那么该参数及其所有后续参数都是可选的,可选参数需要有默认值。

def sum(x,y=0,z): #报错,z没有默认值
    return x+y+z

#如果不想程序报错,可以使用z=None,代表没有z值

默认参数可能会保留前面调用时进行的修改(可变对象),为了防止这种情况可以使用None

def foo(x,items=[]):
    items.append(x)
    return items

foo(1) #[1]
foo(2) #[1,2]
foo(3) #[1,2,3]
def foo(x,items=None):
    if items is None:
        items=[]
    items.append(x)
    return items

foo(1) #[1]
foo(2) #[2]
foo(3) #[3]

如果参数名加上星号,则代表参数个数不固定.这种参数不需要设定默认值。

#z的类型是元组
def sum(x,y=0,* z):
    temp=x+y
    for i in z:
        temp+=i
    return temp

sum(1,2,3,4) # 10

关键字参数
这是提供函数参数的一种方式,显示地命名每个参数并为其指定一个值。不允许多次定义一个相同的参数。

def sum(x,y=0,z=0):
    temp=x+y
    for i in z:
        temp+=i
    return temp

sum(1,z=2,y=3) 

注意:位置参数关键字参数可以同时出现在一次调用中,这就要求位置参数先出现,关键字参数后出现。

**kwargs
它必须是函数定义的最后一个参数,它代表一个字典(名kwargs),所有额外的关键字参数都会放入其中。常见的用法是配置选项较多的函数。

def sum(x,y,z,**kwargs):
    abs_flag=kwargs.pop("abs",False)
    if abs_flag:
        return abs(x)+abs(y)+abs(z)
    else:
        return x+y+z

sum(-1,-2,3)     # 0
sum(-1,-2,3,abs=True) # 6

6.2 参数传递与返回值

python的参数传递的规则与Java类似,属于“引用传递”。引用传递会带来隐藏的错误,因此使用不可变对象当作参数是一个较好的方式。

6.3 作用域规则

作用域总共有三类:

  1. 局部命名空间
  2. 全局命名空间
  3. 内置命名空间

系统每次执行一个函数时,就会创建一个局部命名空间,其中包含:参数名称在函数体内赋值的变量名称。解析名称时,按照 1->2->3 的顺序查找,如果仍然找不到,则引发异常。

a = 10

def foo() :
    a = 3 #局部变量

def foo1():
    global a #明确a是全局变量
    a=3

foo()
print(a) # 10

foo1()
print(a) # 3

Python支持嵌套函数,例如

def countdown(start):
    n=start
    def display():
        print(n)  # 局部命名空间找不到,向外层命名空间查找,这种用法有病~
     
      
    while n>0:
        display()
        n=n-1


countdown(5) # 5 4 3 2 1

再看一例

def countdown(start):
    n=start
    def display():
        nonlocal n  # 声明n
        print(n)  # 局部命名空间找不到,向外层命名空间查找
     #  n-=1 # 报错
     #  n=3  #不报错
        n=n-1
    while n>0:
        display()
        n=n-1


countdown(5) # 5 4 3 2 1

解释:嵌套函数的变量是一种专门的作用域静态作用域(lexical scoping)限定,读写模式也遵循一般的局部变量规则,内部函数不能给定义在外部函数中的局部变量重新赋值(赋值无效,不是语法错误)。
要解决这个问题,使用nonlocal声明变量,动态作用域(dynamic scope)

再看一例:

i=0 
def foo():
    i=i+1 #报错,这里i被认为是一个局部变量,而foo没有初始化这个值
    # i=3 #不报错,这里认为是定义了一个局部变量
    print(i)

6.4 作为对象与闭包的函数

函数是Python中是第一类对象,是对象,它就可以当作参数传递,还可以当作函数的结果返回。
使用举例:

# foo.py
def count():
    print(1)
# goo.py
def callf(func):
    return func() #()是函数调用运算符

import foo.py as f
callf(f.count) #f.count是一个函数对象

>>
1

闭包
将组成函数的语句和这些语句的执行环境打包在一起时,得到的对象称为闭包。执行环境,例如goo.py和foo.py中有相同的变量,执行环境就涉及这种问题。
查看函数的执行环境:
f.count.__globals__

利用嵌套函数实现延迟初始化:

def lazyinit():
    def getlist():
        return []
    return getlist

l=lazyinit()

items=l() #这里才真正初始化

如果需要在系列函数调用中保持某个状态,使用闭包非常高效。保持状态一般的方法是使用(可变)对象。

6.5 装饰器

def trace(func):
    def callf(*args,**kwargs):
        print(args)
        r=func(*args,**kwargs) # 不能使用func(args,kwargs)
        return r
    return callf

@trace
def sum(x,y):
    return x+y

sum(1,2) #使用装饰器时,相当于trace(sum)(1,2)

如果使用多个装饰器那么顺序是:

@foo
@bar
@spam
def go():
    pass

相当于

def go():
    pass
go=foo(bar(spam(go)))

装饰器也支持参数传递:

def trace(msg):
    def wrapper(func):
        def callf(*args,**kwargs):
            print(args)
            r=func(*args,**kwargs)
            return r
        return callf
    print(msg)
    return wrapper

@trace("wrapper param")
def sum(x,y):
    return x+y

sum(1,2)
>>
wrapper param
(1,2)

# 以上代码等同于
wrapper=trace("wrapper msg") #wrapper
wrapper(sum)(1,22)

装饰器要支持参数传递,关键点就是有多层包装。

6.6 生成器与yield

yield可以生成 生成器对象。

def count_down(n):
    while n>0:
        yield n
        n=n-1

for i in count_down(5):
    print(i)
    
>>
5
4
3
2
1

Python3将生成器函数的next()改成__next__,这已经说明,在大部分情况下,都是使用循环遍历生成器对象。

6.7 协程与yield表达式

def reciever():
    print("start recieve...")
    while True:
        s=(yield )
        print("msg:"+s)

r=reciever()
r.__next__() #必不可少:执行yield之前的语句
r.send("1")
r.send("2")
r.send("3")

>>
start recieve...
msg:1
msg:2
msg:3

利用装饰器可以包装next方法的调用,使代码更紧凑。

下面的例子,请关注send的返回值.

def line_splitter(s=None):
    print("start...")
    result=None
    while True:
        line=(yield result)
        result=line.split(s)

ls=line_splitter(",")
ls.__next__()
r0=ls.send("a,b,c")
r1=ls.send("e,d,f")
ls.close()
>>
start...
['a', 'b', 'c']
['e', 'd', 'f']

6.8 使用生成器与协程

下面是一个在指定目录下,查找特定的文件,并在这些文件中查找特定的行,它非常类似于unix管道命令:
find | cat | grep

import os
import fnmatch
import sys

def find_files(dir,pattern):
    for path,dirname,filelist in os.walk(dir):
        for name in filelist:
            if fnmatch.fnmatch(name,pattern):
                yield os.path.join(path,name)


def opener(filenames):
    for name in filenames:
        f=open(name)
        yield f

def cat(filelist):
    for f in filelist:
        for line in f:
            yield line

def grep(pattern,lines):
    for line in lines:
        if pattern in line:
            yield line


# 这个程序的好处是全部是延迟加载,程序中不存在特别大的列表
dir="/Applications/MyEclipse 2017/MyEclipse 2017.app/Contents/logs"

paths=find_files(dir,'*log')

files=opener(paths)

lines=cat(files)

pylines=grep("startup",lines)

for line in pylines:
    sys.stdout.write(line)

# 也可以采取以下方式,代码更紧凑
#for line in grep("startup",cat(opener(find_files(dir,"*log")))):
#    sys.stdout.write(line)

上面的程序简化后存在一个缺陷,代码的顺序不是类似unix管道,而是反过来,可以利用协程改进。改进的思路如下:

  1. 目标:反转管道
  2. 最外层是find 最内层是grep
  3. 要先执行grep
  4. 要先执行grep,那么grep函数又缺少文件的行的内容,所以要使用(yield)代替,由上层send过来

6.9 列表推导

[expression for item1 in iter1 if condition1
           for item2 in iter2 if condition2
           ...
           for itemN in iterN if conditionN]


#等价于
s=[]
for item1 in iter1:
    if condition1:
        for item2 in iter2:
            if condition2:
                ...
                for itemN in iterN:
                    if conditionN:
                        s.append(expression)
                        
                        
                        
                  
s=[ (x,y) for x in range(20) if x%2 !=0
        for y in range(10) if y%2!=0]
print(s)

注意,如果要得到元组的列表,那么(x,y)的括号不能省略。
在Python3中迭代变量是私有变量,Python2中不是。

6.10 生成器表达式

生成器表达式是一个对象,它执行的计算与列表推导相同,不同点是:列表推导会创建一个包含结果的列表,而生成器对象并未生成数据,而只包含生成数据的规则,在需要的时候再创建结果数据。这是函数式编程的体现。能够极大地优化内存使用。

f = open('t_example2.py')

lines = (line.strip() for line in f.readlines() 
                        if line is not None and len(line) > 0 and line[0] == '#')
#ls=list(lines)
for line in lines:
    print(line)

请注意注释的那行代码:

  • 生成器对象可以转化成列表
  • 对于已使用的生成器对象,它无法再重新迭代了,即生成器对象的游标是有状态的(类似一个工作流)

列表推导和生成器表达式都属于声明式编程。

6.11 lambda运算符

使用lambda可以创建表达式形式的匿名函数:

lambda args:expression

s=(lambda x,y:x+y)(1,2)
print(s) #3

6.12 递归

Python对递归函数的深度做了限制,一般是1000.
sys.getrecursionlimit()
sys.setrecursionlimit()

import sys
print(sys.getrecursionlimit())

#code 1:深度遍历一个列表
def flattern(lists):
    for s in lists:
        if isinstance(s,list):
            flattern(s)
        else:
            print(s)

items=[[1,3,2],3,[4,[5,6]]]
flattern(items)

# code 2
def genflattern(lists):
    for s in lists:
        if isinstance(s,list):
            genflattern(s)
        else :
            yield s

gf=genflattern(items)
for i in gf:
    print(i)  #只打印3
    
# code 3 

def genflattern3(lists):
    for s in lists:
        if isinstance(s,list):
            for item in genflattern(s):
                yield item
        else :
            yield item

gf=genflattern3(items)
for i in gf:
    print(i)

请注意上述2,3两段代码的区别,第2段是无法正常工作的,因为每次递归都创建新的生成器,而不是预想中的迭代生成器。

6.13 文档字符串

在使用文档字符串时,需注意带有装饰器的函数,它返回的文档字符串返回的是装饰器函数的相关属性,所以在定义装饰器时,通常可以使用以下语句来复制文档字符串:

def warp(func):
    call(*args,**kwargs):
        return func(*args,**kwargs)
    call.__doc__=func.__doc__
    call.__name__=func.__name__
    return call

@warp
def foo():
    "foo test"
    pass

6.14 函数属性

def foo():
    pass
foo.x=1

函数也是一个对象,它有一个__dict__属性。它也需要注意使用装饰器时,装饰器和原函数的对象的__dict__并不是一个,通常装饰器函数需要进行如下操作:

call.__dict__.update(func.__dict__)

6.15 eval(),exec(),compile()

x=1
y=2
s=eval('(lambda a,b:a+b)(x,y)') # 3

exec("s=s+1")

print(s) # s=4

eval(str,globals,locals)
执行的是一个表达式,可以传入全局和局部命名空间来指定运行时的部分的环境

exec(str,globals,locals)
它等同于一条语句

由于eval,exec运行时十分消耗资源,因此可以先将字符串编译,保存起来,以优化性能.

s=1
src="s+=1"
bsrc=compile(src,'','exec')
exec(bsrc) 
exec(bsrc)
print(s)  # 3

你可能感兴趣的:(python)