Skip to content

一等函数

此笔记记录于《流畅的python》,大部分为其中的摘要,少部分为笔者自己的理解;笔记为jupyter转的markdown,原始版jupyter笔记在这个仓库

编程语言理论家把“一等对象”定义为满足下述条件的程序实体:

  • 在运行时创建
  • 能赋值给变量或数据结构中的元素
  • 能作为参数传给函数
  • 能作为函数的返回结果

在Python中,所有函数都是一等对象

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__

python
# 除了__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最好的特性之一就是提供了极为灵活的参数处理机制。调用函数时使用***“展开”可迭代对象,映射到单个参数。

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)
python
tag('br')
'<br />'
python
tag('p', 'hello')
'<p>hello</p>'
python
print(tag('p', 'hello', 'world'))
<p>hello</p>
<p>world</p>
python
print(tag('p', 'hello', id=33))
<p id="33">hello</p>
python
print(tag('p', 'hello', 'world', cls='sidebar'))
<p class="sidebar">hello</p>
<p class="sidebar">world</p>
python
tag(content='testing', name='img')
'<img content="testing" />'
python
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参数只能通过关键字参数指定,它一定不会捕获未命名的定位参数。定义函数时若想指定仅限关键字参数,要把它们放到前面有*的参数后面。如果不想支持数量不定的定位参数,但是想支持仅限关键字参数,在签名中放一个*,如下所示:

python
def f(a, *, b):
    return a, b

f(1, b=2) # (1, 2)
(1, 2)

获取关于参数的信息

python
import bobo
@bobo.query('/')
def hello(person):
    return 'Hello%s!'%person

这里的关键是,Bobo会内省hello函数,发现它需要一个名为person的参数,然后从请求中获取那个名称对应的参数,将其传给hello函数,因此程序员根本不用触碰请求对象。

Bobo是怎么知道函数需要哪个参数的呢?它又是怎么知道参数有没有默认值呢?函数对象有个__defaults__属性,它的值是一个元组,里面保存着定位参数和关键字参数的默认值。仅限关键字参数的默认值在__kwdefaults__属性中。然而,参数的名称在__code__属性中,它的值是一个code对象引用,自身也有很多属性。

python
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([])
python
clip.__defaults__ # 默认值
(80,)
python
clip.__code__
<code object clip at 0x000001E88F48ECE0, file "C:\Users\21974\AppData\Local\Temp\ipykernel_37168\446632398.py", line 1>
python
clip.__code__.co_varnames
('text', 'max_len', 'end', 'space_before', 'space_after')
python
clip.__code__.co_argcount
2
python
clip.__code__
<code object clip at 0x000001E88F48ECE0, file "C:\Users\21974\AppData\Local\Temp\ipykernel_37168\446632398.py", line 1>

参数的默认值只能通过它们在__defaults__元组中的位置确定,因此要从后向前扫描才能把参数和默认值对应起来。

幸好,我们有更好的方式——使用inspect模块。

python
from inspect import signature
sig = signature(clip)
sig
<Signature (text, max_len=80)>
python
str(sig)
'(text, max_len=80)'
python
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方法,它可以把任意个参数绑定到签名中的形参上,所用的规则与实参到形参的匹配方式一样。框架可以使用这个方法在真正调用函数前验证参数

python
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'})>
python
for name, value in bound_args.arguments.items():
    print(name, '=', value)
name = img
cls = framed
attrs = {'title': 'Sunset Boulevard', 'src': 'sunset.jpg'}
python
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'

函数注解

用于为函数声明中的参数和返回值附加元数据

python
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__属性(一个字典)中:

python
clip.__annotations__
{'text': str, 'max_len': 'int > 0', 'return': str}

Python对注解所做的唯一的事情是,把它们存储在函数的__annotations__属性里。仅此而已,Python不做检查、不做强制、不做验证,什么操作都不做。换句话说,注解对Python解释器没有任何意义。注解只是元数据,可以供IDE、框架和装饰器等工具使用。

支持函数式编程的包

Python的目标不是变成函数式编程语言,但是得益于operator和functools等包的支持,函数式编程风格也可以信手拈来。

python
from functools import reduce


def fact(n):
    return reduce(lambda a, b: a*b, range(1, n+1))
python
from functools import reduce
from operator import mul


def fact(n):
    return reduce(mul, range(1, n+1))

使用functools.partial冻结某些函数的参数

python
from operator import mul
from functools import partial
triple = partial(mul, 3) # 固定为3的乘法函数
triple(7) # 21
21