一等函数
此笔记记录于《流畅的 python》,大部分为其中的摘要,少部分为笔者自己的理解;笔记为 jupyter 转的 markdown,原始版 jupyter 笔记在这个仓库
编程语言理论家把“一等对象”定义为满足下述条件的程序实体:
- 在运行时创建
- 能赋值给变量或数据结构中的元素
- 能作为参数传给函数
- 能作为函数的返回结果
在 Python 中,所有函数都是一等对象
def fn():
print('hello world')
ffn = fn # 展示函数对象的“一等”本性
ffn
<function __main__.fn()>
接受函数为参数,或者把函数作为结果返回的函数是高阶函数(higher-order function)
lambda 关键字在 Python 表达式内创建匿名函数
如果想判断对象能否调用,可以使用内置的 callable( )函数,Python 数据模型文档列出了 7 种可调用对象:
类型 | 描述 |
---|---|
用户定义的函数 | 使用 def 语句或 lambda 表达式创建 |
内置函数 | 由 C 语言实现的函数,例如 len 或 time.strftime |
内置方法 | 由 C 语言实现的方法,例如 dict.get |
方法 | 在类的定义体中定义的函数 |
类 | 当类被调用时,其方法__new__和__init__会被执行 |
类的实例 | 如果类定义了__call__方法,则它的实例可以被调用 |
生成器函数 | 使用 yield 表达式的函数或方法,当调用时返回一个迭代器 |
不仅 Python 函数是真正的对象,任何 Python 对象都可以表现得像函数。为此,只需实现实例方法__call__
# 除了__doc__,函数对象还有很多属性。使用 dir 函数可以探知 factorial 具有下述属性
dir(fn)
['__annotations__',
'__builtins__',
'__call__',
'__class__',
'__closure__',
'__code__',
'__defaults__',
'__delattr__',
'__dict__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__get__',
'__getattribute__',
'__globals__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__kwdefaults__',
'__le__',
'__lt__',
'__module__',
'__name__',
'__ne__',
'__new__',
'__qualname__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__']
与用户定义的常规类一样,函数使用__dict__属性存储赋予它的用户属性。这相当于一种基本形式的注解。一般来说,为函数随意赋予属性不是很常见的做法,但是 Django 框架这么做了。
从定位参数到仅限关键字参数
python 最好的特性之一就是提供了极为灵活的参数处理机制。调用函数时使用*
和**
“展开”可迭代对象,映射到单个参数。
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>
print(tag('p', 'hello', id=33))
<p id="33">hello</p>
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" />'
my_tag = {'name':'img', 'title':'Sunset Boulevard', 'src':'sunset.jpg', 'cls':'framed'}
tag(**my_tag)
'<img class="framed" src="sunset.jpg" title="Sunset Boulevard" />'
cls 参数只能通过关键字参数指定,它一定不会捕获未命名的定位参数。定义函数时若想指定仅限关键字参数,要把它们放到前面有*
的参数后面。如果不想支持数量不定的定位参数,但是想支持仅限关键字参数,在签名中放一个*
,如下所示:
def f(a, *, b):
return a, b
f(1, b=2) # (1, 2)
(1, 2)
获取关于参数的信息
import bobo
@bobo.query('/')
def hello(person):
return 'Hello%s!'%person
这里的关键是,Bobo 会内省 hello 函数,发现它需要一个名为 person 的参数,然后从请求中获取那个名称对应的参数,将其传给 hello 函数,因此程序员根本不用触碰请求对象。
Bobo 是怎么知道函数需要哪个参数的呢?它又是怎么知道参数有没有默认值呢?函数对象有个__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.__defaults__ # 默认值
(80,)
clip.__code__
<code object clip at 0x000001E88F48ECE0, file "C:\Users\21974\AppData\Local\Temp\ipykernel_37168\446632398.py", line 1>
clip.__code__.co_varnames
('text', 'max_len', 'end', 'space_before', 'space_after')
clip.__code__.co_argcount
2
clip.__code__
<code object clip at 0x000001E88F48ECE0, file "C:\Users\21974\AppData\Local\Temp\ipykernel_37168\446632398.py", line 1>
参数的默认值只能通过它们在__defaults__元组中的位置确定,因此要从后向前扫描才能把参数和默认值对应起来。
幸好,我们有更好的方式——使用 inspect 模块。
from inspect import signature
sig = signature(clip)
sig
<Signature (text, max_len=80)>
str(sig)
'(text, max_len=80)'
for name, param in sig.parameters.items():
print(param.kind, ':', name, '=', param.default)
POSITIONAL_OR_KEYWORD : text = <class 'inspect._empty'>
POSITIONAL_OR_KEYWORD : max_len = 80
这是一个有序映射,把参数名和 inspect.Parameter 对象对应起来。各个 Parameter 属性也有自己的属性,例如 name、default 和 kind。特殊的 inspect._empty 值表示没有默认值,考虑到 None 是有效的默认值(也经常这么做),而且这么做是合理的。
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
<BoundArguments (name='img', cls='framed', attrs={'title': 'Sunset Boulevard', 'src': 'sunset.jpg'})>
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'] # 把必须指定的参数 name 删除
bound_args = sig.bind(**my_tag) # 报错,抱怨缺少必须的参数 name
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[33], line 2
1 del my_tag['name']
----> 2 bound_args = sig.bind(**my_tag)
File d:\anaconda3\lib\inspect.py:3185, in Signature.bind(self, *args, **kwargs)
3180 def bind(self, /, *args, **kwargs):
3181 """Get a BoundArguments object, that maps the passed `args`
3182 and `kwargs` to the function's signature. Raises `TypeError`
3183 if the passed arguments can not be bound.
3184 """
-> 3185 return self._bind(args, kwargs)
File d:\anaconda3\lib\inspect.py:3100, in Signature._bind(self, args, kwargs, partial)
3098 msg = 'missing a required argument: {arg!r}'
3099 msg = msg.format(arg=param.name)
-> 3100 raise TypeError(msg) from None
3101 else:
3102 # We have a positional argument to process
3103 try:
TypeError: missing a required argument: 'name'
函数注解
用于为函数声明中的参数和返回值附加元数据
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([])
注解不会做任何处理,只是存储在函数的__annotations__
属性(一个字典)中:
clip.__annotations__
{'text': str, 'max_len': 'int > 0', 'return': str}
Python 对注解所做的唯一的事情是,把它们存储在函数的
__annotations__
属性里。仅此而已,Python 不做检查、不做强制、不做验证,什么操作都不做。换句话说,注解对 Python 解释器没有任何意义。注解只是元数据,可以供 IDE、框架和装饰器等工具使用。
支持函数式编程的包
Python 的目标不是变成函数式编程语言,但是得益于 operator 和 functools 等包的支持,函数式编程风格也可以信手拈来。
from functools import reduce
def fact(n):
return reduce(lambda a, b: a*b, range(1, n+1))
from functools import reduce
from operator import mul
def fact(n):
return reduce(mul, range(1, n+1))
使用functools.partial
冻结某些函数的参数
from operator import mul
from functools import partial
triple = partial(mul, 3) # 固定为 3 的乘法函数
triple(7) # 21
21