文章是《流畅的Python》第五章节:一等函数的学习笔记和个人理解。
文章目录
1.0 前言
在Python中记住一句真理:一切皆对象
。包括函数也是对象,函数可以作为参数传递给另一个函数,也可以作为另一个函数的返回结果。
接受函数为参数,或者把函数作为结果返回的函数是高阶函数 ,比如说map
、filter
、reduce
、sorted
等。
1.1 匿名函数
当一个函数体比较简单,又不想去想函数体的命名时就可以使用匿名函数:
Python中使用lambda
关键字来构造匿名函数。
例如,使用lambda表达式反转拼写,然后依此给单词列表排序:
>>> fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
>>> sorted(fruits, key=lambda word: word[::-1])
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
>>>
优点:当函数体特别简单时使用可以显得优雅。
缺点:不易看懂,所以在写lambda函数时,最好编写一个注释说明,关于lambda的使用在Python中还是存在争议的。
1.2 可调用对象
在Python中一个可调用的对象,内部一定实现了callable()
函数。Python中有 7 种可调用对象:
>>> abs, str, 13
(<built-in function abs>, <class 'str'>, 13)
>>> [callable(obj) for obj in (abs, str, 13)]
[True, True, False]
用户自定义的函数
使用 def
语句或lambd
表达式创建。
内置函数
使用C语言(CPython)实现的函数,如len
或time.strftime
内置方法
使用C语言实现的方法,如dict.get
方法
在类的定义体中定义的函数。
类
调用类时会运行类的 __new__
方法创建一个实例,然后运行__init__
方法,初始化实例,最后把实例返回给调用方,所以调用类相当于调用函数。
类的实例
如果类定义了__call__
方法,那么它的实例可以作为函数调用。
生成器函数
使用yield关键字的函数或方法。调用生成器函数返回的是生成器对象。
1.2.1 自定义可调用类型
在自定义类型中只要实现__call__
方法就可以表现的像函数。
例如: 调用 BingoCage 实例,从打乱的列表中取出一个元素 。
import random
class BingoCage:
# 初始化一个打乱的列表
def __init__(self, items):
self._items = list(items)
random.shuffle(self._items)
# 从打乱的列表中返回一个元素,若列表为空则抛出异常
def pick(self):
try:
return self._items.pop()
except IndexError:
raise LookupError('pick from empty BingoCage')
# 使自定义类可以像方法一样使用
def __call__(self):
return self.pick()
>>> bingo = BingoCage(range(3))
>>> bingo.pick()
1
>>> bingo()
0
>>> callable(bingo)
True
1.3 函数内省
使用dir
函数可以查看一个对象的属性。
>>> dir(factorial)
['__annotations__', '__call__', '__class__', '__closure__', '__code__',
'__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__get__', '__getattribute__', '__globals__',
'__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__',
'__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__','__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__','__subclasshook__']
>>>
Python是一门动态语言,动态语言的强大之处就在于可以动态的给对象赋予属性。
例如Django管理后台的一段源代码,动态的给方法upper_case_name
赋予属性:
def upper_case_name(obj):
return ("%s %s" % (obj.first_name, obj.last_name)).upper()
upper_case_name.short_description = 'Customer name'
函数使用__dict__
属性存储赋予它的用户属性。
1.4 函数的几种传参方式
Python3中的函数可以有四种接收参数的方式:位置参数、可变参数、命名参数、命名关键字参数。
一般传递参数和接收参数的顺序最好符合上边的顺序。
通过生成标签的一段代码来理解几种参数的使用方式:
name
该位置参数是标签的名称*content
该可变参数用于接收标签的内容cls=None
该命名参数用于给标签生成class名**attrs
该命名关键字参数用于给标签生成属性
理解了这几个参数的作用,函数内的代码可以不用看。
def tag(name, *content, cls=None, **attrs):
"""生成一个或多个HTML标签"""
if cls is not None:
attrs['class'] = cls
if attrs:
attr_str = ''.join(' %s="%s"' % (attr, value)
for attr, value
in sorted(attrs.items()))
else:
attr_str = ''
if content:
return '\n'.join('<%s%s>%s</%s>' %
(name, attr_str, c, name) for c in content)
else:
return '<%s%s />' % (name, attr_str)
使用案例:
-
只传入标签名,函数使用位置参数接收:
>>> tag('br') '<br />'
-
传入标签名和标签内容,函数使用位置参数来接收标签名,使用可变参数来接收标签内容:
>>> tag('p', 'hello') '<p>hello</p>' >>> print(tag('p', 'hello', 'world')) <p>hello</p> <p>world</p>
-
传入标签名和标签内容以及id名,其中传入id名使用的是键值对的方式:
>>> tag('p', 'hello', id=33) '<p id="33">hello</p>'
其中
id=33
函数是以命名关键字参数**attrs
接收的,因为只有当键值对的键名和函数的命名参数的键名一样时,命名参数才能接收该参数。>>> print(tag('p', 'hello', 'world', cls='sidebar')) <p class="sidebar">hello</p> <p class="sidebar">world</p>
-
传入标签名和属性,有时候我们不确定函数的位置参数的位置是什么样的,我们可以以命名参数的方式传入,但是传递的参数的键必须与位置参数一样。
>>> tag(content='testing', name="img") '<img content="testing" />
上边的
content='testing'
虽然放在第一个位置,但是它是命名关键字参数。注:但是并不提倡这种写法,最好按照函数定义的参数位置来传入参数
-
传入标签名,标签内容,属性。不管是传递什么参数我们都可以使用键值对的方式传入:
>>> my_tag = {'name': 'img', 'title': 'Sunset Boulevard', ... 'src': 'sunset.jpg', 'cls': 'framed'} >>> tag(**my_tag) '<img class="framed" src="sunset.jpg" title="Sunset Boulevard" />'
只要在字典前加上
**
,字典中的所有元素作为单个参数传入,同名键会绑定到对应的命名参数和位置参数上,余下的则被**attrs
接收。这其实就是Python经典的打包和解包的过程。
1.5 获取关于参数的信息
在Bobo框架(Python一个轻量级web框架)中使用装饰器来映射url, 但是却不需要指定传递的参数:
import bobo
# hello 函数需要一个person作为参数,但是装饰器却并没有传递
@bobo.query('/')
def hello(person):
return 'Hello %s!' % person
那这个装饰器是如何知道需不需要传递参数,传递的参数又是什么类型呢。
在Python中函数对象有几个属性用来保存参数和变量的信息:
__defaults__
:值是一个元组,里面保存着位置参数和命名参数(关键字参数)的默认值。__kwdefaults__
:保存着命名关键字参数的默认值。__code__
:值是一个code对象引用, 自身也有很多属性,参数的名称保存在这里。
通过一个示例来讲解:在指定长度附近截断字符串的函数:
def clip(text, max_len=80):
"""在max_len前面或后面的第一个空格处截断文本
"""
end = None
if len(text) > max_len:
space_before = text.rfind(' ', 0, max_len)
if space_before >= 0:
end = space_before
else:
space_after = text.rfind(' ', max_len)
if space_after >= 0:
end = space_after
if end is None: # 没找到空格
end = len(text)
return text[:end].rstrip()
提取关于clip
函数的参数信息:
>>> from clip import clip
>>> clip.__defaults__
(80,)
>>> clip.__code__ # doctest: +ELLIPSIS
<code object clip at 0x...>
>>> clip.__code__.co_varnames
('text', 'max_len', 'end', 'space_before', 'space_after')
>>> clip.__code__.co_argcount
2
__code__.co_varnames
:里面保存着参数名称和局部变量的名称,参数名称保存在前N个位置。__code__.co_argcount
:确定__code__.co_varnames
中参数的前N个位置,需要注意的是这里不包含前缀为*
或**
的变长参数。- 确定了位置数量后就可以在
__default__
中取对应的参数的默认值,需要注意的是默认值的顺序与__code__.co_varnames
参数的名称的顺序是相反的。
1.5.1 使用inspect 模块获取函数信息
前边那种获取参数的方式比较复杂,使用inspect
模块中的signature
方法可以直接获取参数信息:
>>> from clip import clip
>>> from inspect import signature
>>> sig = signature(clip)
# inspect.signature 函数返回一个 inspect.Signature 对象
>>> sig # doctest: +ELLIPSIS
<inspect.Signature object at 0x...>
>>> str(sig)
'(text, max_len=80)'
# 该对象有一个parameters属性是一个有序映射
# 将参数名和inspect.Parameter对应起来
# 其中inspect.Parameter有自己的属性:
# name、default和kind
>>> for name, param in sig.parameters.items():
... print(param.kind, ':', name, '=', param.default)
...
# inspect._empty表示None,None也是有效的默认值
POSITIONAL_OR_KEYWORD : text = <class 'inspect._empty'>
POSITIONAL_OR_KEYWORD : max_len = 80
获取关于参数的信息的作用:
Python语言是解释性语言,也就是说是先将.py
文件由解释器解释为.pyc
的中间文件,执行程序的时候读取的是.pyc
文件,执行一行将一行翻译为机器码。
像框架与IDE等工具,在将.py
文件转换为.pyc
文件前就会先验证函数的形参与实参之间的对应关系是否正确。
这个时候就需要获取函数的签名信息来验证了,inspect.Signature
对象有个bind方法,它可以把任意个参数绑定到签名中的形参上,所用的规则与实参到形参的匹配方式一样。
使用前边的标签案例:
>>> import inspect
>>> sig = inspect.signature(tag)
>>> my_tag = {'name': 'img', 'title': 'Sunset Boulevard',
... 'src': 'sunset.jpg', 'cls': 'framed'}
>>> bound_args = sig.bind(**my_tag)
>>> bound_args
<inspect.BoundArguments object at 0x...>
>>> for name, value in bound_args.arguments.items():
... print(name, '=', value)
...
name = img
cls = framed
attrs = {'title': 'Sunset Boulevard', 'src': 'sunset.jpg'}
# 将实参去掉部分后,形参与实参不对应就会抛出异常
>>> del my_tag['name']
>>> bound_args = sig.bind(**my_tag)
Traceback (most recent call last):
1.5.2 函数注解
Python 3 提供了一种句法,用于为函数声明中的参数和返回值附加元数据。
注解不会做任何事情,Python 不做检查、不做强制、不做验证,什么操作都不做。只会将注解的信息保存在__annotaions__
属性里,也就是说你写了注解但是没用到__annotaions__
属性里的信息,那么就是白写。
有注解的 clip 函数,与前边的clip函数的唯一区别就是第一行:
def clip(text:str, max_len:'int > 0'=80) -> str:
"""在max_len前面或后面的第一个空格处截断文本
"""
end = None
if len(text) > max_len:
space_before = text.rfind(' ', 0, max_len)
if space_before >= 0:
end = space_before
else:
space_after = text.rfind(' ', max_len)
if space_after >= 0:
end = space_after
if end is None: # 没找到空格
end = len(text)
return text[:end].rstrip()
- 函数声明中的各个参数可以在
:
之后增加注解表达式。 - 如果参数有默认值,注解放在参数名和
=
号之间。 - 如果想注解返回值,在
)
和函数声明末尾的:
之间添加->
和一个表达式。 - 注解中最常用的类型是类(如
str
或int
)和字符串(如'int > 0'
)。在示例中,max_len
参数的注解用的是字符串。
可以通过inspect.signature()
函数提取注解中的信息:
>>> from clip_annot import clip
>>> from inspect import signature
>>> sig = signature(clip)
>>> sig.return_annotation
<class 'str'>
# sig.parameters 属性将参数名映射到Parameter对象上。
# 每个Parameter对象自己也有annotaion 属性
>>> for param in sig.parameters.values():
... note = repr(param.annotation).ljust(13)
... print(note, ':', param.name, '=', param.default)
<class 'str'> : text = <class 'inspect._empty'>
'int > 0' : max_len = 80
函数注解的最大影响是为 IDE 和 lint 程序等工具中的静态类型检查功能提供额外的类型信息。
1.6 支持函数式编程的包
Python中使用函数式编程主要使用operator
和functools
等包。
Python中使用函数式编程主要使用operator
和functools
等包。
1.6.1 operator 包的介绍
operator 使用介绍
-
使用
reduce
和operator.mul
函数计算阶乘:from functools import reduce from operator import mul def fact(n): return reduce(mul, range(1, n+1))
-
使用
itemgetter
从序列中取出元素,排序一个元组列表:
>>> metro_data = [
... ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
... ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
... ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
... ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
... ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
... ]
>>>
>>> from operator import itemgetter
# 这里itemgetter(1) 的作用与 lambda fields: fields[1] 一样
>>> for city in sorted(metro_data, key=itemgetter(1)):
... print(city)
...
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833))
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889))
('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
('Mexico City', 'MX', 20.142, (19.433333, -99.133333))
('New York-Newark', 'US', 20.104, (40.808611, -74.020386))
如果多个参数传给itemgetter
,它的构建函数会返回提取的值构成的元组:
>>> cc_name = itemgetter(1, 0)
>>> for city in metro_data:
... print(cc_name(city))
...
('JP', 'Tokyo')
('IN', 'Delhi NCR')
('MX', 'Mexico City')
('US', 'New York-Newark')
('BR', 'Sao Paulo')
>>>
-
使用
attrgetter
提取对象的属性,把多个属性名传给attrgetter
,返回提取的值构成的元组。构建两个命名元组:
>>> from collections import namedtuple # 表示经纬度 >>> LatLong = namedtuple('LatLong', 'lat long') # 表示城市的地理位置信息 >>> Metropolis = namedtuple('Metropolis', 'name cc pop coord')
城市数据:
>>> metro_data = [ ... ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)), ... ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)), ... ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)), ... ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)), ... ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)), ... ] >>>
利用两个命名元组和城市数据实例化一个列表,列表中存放的是命名元组:
# 这里使用嵌套的元组拆包提取(lat, long)然后使用它们构建 LatLong,作为 Metropolis 的 coord 属性。 >>> metro_areas = [Metropolis(name, cc, pop, LatLong(lat, long)) ... for name, cc, pop, (lat, long) in metro_data] >>> metro_areas[0] Metropolis(name='Tokyo', cc='JP', pop=36.933, coord=LatLong(lat=35.689722, long=139.691667)) >>> metro_areas[0].coord.lat 35.689722
使用
attrgetter
提取对象的属性:>>> from operator import attrgetter >>> name_lat = attrgetter('name', 'coord.lat') >>> >>> for city in sorted(metro_areas, key=attrgetter('coord.lat')): ... print(name_lat(city)) ... ('Sao Paulo', -23.547778) ('Mexico City', 19.433333) ('Delhi NCR', 28.613889) ('Tokyo', 35.689722) ('New York-Newark', 40.808611)
-
methodcaller
创建的函数会在对象上调用参数指定的方法:>>> from operator import methodcaller >>> s = 'The time has come' >>> upcase = methodcaller('upper') >>> upcase(s) 'THE TIME HAS COME' # 第一个参数表示方法名,后边的参数表示该方法需要接收的参数 >>> hiphenate = methodcaller('replace', ' ', '-') >>> hiphenate(s) 'The-time-has-come'
operator
中有很多方法可以使用:
>>> [name for name in dir(operator) if not name.startswith('_')]
['abs', 'add', 'and_', 'attrgetter', 'concat', 'contains',
'countOf', 'delitem', 'eq', 'floordiv', 'ge', 'getitem', 'gt',
'iadd', 'iand', 'iconcat', 'ifloordiv', 'ilshift', 'imod', 'imul',
'index', 'indexOf', 'inv', 'invert', 'ior', 'ipow', 'irshift',
'is_', 'is_not', 'isub', 'itemgetter', 'itruediv', 'ixor', 'le',
'length_hint', 'lshift', 'lt', 'methodcaller', 'mod', 'mul', 'ne',
'neg', 'not_', 'or_', 'pos', 'pow', 'rshift', 'setitem', 'sub',
'truediv', 'truth', 'xor']
1.6.2 使用functools.partial 冻结参数
functools.partial
该函数接收的参数类型有两种:
- 其他函数名
- 其他函数需要的部分参数
该函数就是对其他函数的一个封装并对外提供指定的API,比如说operator
中的mul
用于计算乘法,在使用的时候一般都需要传递两个参数mul(a, b)
,但是我想固定参数a
的值,又不想对外暴露我固定的值,这个时候就可以使用functools.partial
函数进一步封装:
>>> from operator import mul
>>> from functools import partial
>>> triple = partial(mul, 3)
>>> triple(7)
21
>>> list(map(triple, range(1, 10)))
[3, 6, 9, 12, 15, 18, 21, 24, 27]
继续使用1.4节的标签的例子,我现在想封装一个picture
标签,这个时候就需要固定name
、cls
这两个参数:
>>> from tagger import tag
# 原函数id
>>> tag
<function tag at 0x10206d1e0>
# 使用partial封装tag函数,固定指定参数
>>> from functools import partial
>>> picture = partial(tag, 'img', cls='pic-frame')
# 使用封装后的picture对象创建一个tag
>>> picture(src='wumpus.jpeg')
'<img class="pic-frame" src="wumpus.jpeg" />'
# 查看picture对象
>>> picture
functools.partial(<function tag at 0x10206d1e0>, 'img', cls='pic-frame')
# 封装完后返回的picture对象调用的函数的id 和原tag函数是一样的
>>> picture.func
<function tag at 0x10206d1e0>
>>> picture.args
('img',)
>>> picture.keywords
{'cls': 'pic-frame'}