流畅的 Python 读书笔记(四)

Python 的函数是一等对象

因为它符合编程语言理论家对 "一等对象 -- first-class object" 的定义

  1. 运行时创建
  2. 可赋值给变量或数据结构的属性
  3. 能作为函数参数
  4. 能被函数返回 

依据这种定义,还有我们最为熟悉的 JavaScript 的函数也是一等对象,Java 的函数都是依附于类或对象存在的,不是一等对象。

Python 的文档字符串(docstring) 是放在模块,函数,类中的第一个纯字符串。可用单个引号(单引号或双引),通常因为有大段的文字会用三引号的字符串,比如

def foo():
    '''doing nathing'''
    pass

代码中用 foo.__doc__ 能查看到到 docstring,或用 help(foo), doc(foo) 都能输出包含 docstring 的信息

高阶函数

接受函数为参数,或把函数作为结果返回的函数称之为高阶函数(higher-order function), 即符合以上的 #3 或 #4,入口或出口有函数的函数是高阶函数。像最常见的 map, filter, reduce。

在 Python3 中 map 和 filter 是内置函数,使用列表推导和生成器表达式可替代 map 和  filter 这两个高阶函数。Python2 中的内置函数 reduce 被移到的 Python3 的 functools 模块里,看来是不那么重要。

类似于 Java 8 Stream 中的 anyMatch, allMatch, noneMatch, Python 有内置的 all 和 any 函数用来检查迭代中是否全为 True, 或至少一个 True。它只能元素的 bool(x) 值来判断,不接受其他条件判断函数,如果需要转换的话先 map 或推导。

匿名函数

即 Lambda 表达式, 回看前面 reduce 中的例子。它以 lambda 开始,只能用纯表达式,不能用赋值或 while/try 等语句。这限制了 Python 的 Lambda 表达式的普遍使用,也就避免了把 Lambda 搞复杂的可能。

可调用对象

即 callable(x) 测试为 True, 也就是后面能加一对圆括进行调用, 包括

  1. 各种函数(用户自定义的函数,内置函数,方法,生成器函数)
  2. 定义了 __call__ 方法的实例 (参考 Python 对象当函数使及动态添加方法)
  3. 类本身, 创建实例时就是一种调用,如 buffer = BytesIO(),调用类时先调用类的 __new__ 方法创建一个实例,然后调用 __init__ 初始化。覆盖 __new__ 可实现单例,也可能出现其他的行为

装饰器就需要一个  callable() 的对象,只要能满足这一条件就能采用上面几种方式来实现。关于装饰器已写过多篇,请参考

  1. 熟悉和应用 Python 的装饰器
  2. Python 中带属性的装饰器
  3. Python 类实现的装饰器及简陋 REST API

函数的自省

它像是反射,可能过函数的下列属性

  1. __annotations__: 参数和返回值的注解
  2. __call__: 实现 () 运行符,即可调用对象协议
  3. __closure__: 函数闭包,即自由变量的绑定(通常是 None)
  4. __code__: 编译成字节码的函数元数据和函数定义体
  5. __default__: 形式参数的默认值 -- 可变默认值要多加小心
  6. __get__: 实现只读描述符协议
  7. __globals__: 函数所在模块中的全局变量
  8. __kwdefaults__: 仅限关键字形式参数的默认值
  9. __name__: 函数名称
  10. __qualname__: 函数的限定名称,如 Random.choice

比如我们定义了一个函数 foo, 可通过 foo.__code__.co_name, foo.__code__.co_varnames, foo.__code__.co_argcount 得到函数名和函数参数名和参数个数; foo.__defaults__ 中有定位参数的和关键字参数的默认认,foo.__kwdefaults__ 中有仅限关键字参数的默认值。

关于 Python 函数参数定义的知识可参考 由 Python 的 Ellipsis 到 *, /, *args, **kwargs 函数参数.

Python 还可用 inspect.signature 来反射函数

列出各个参数

POSITIONAL_ONLY: name=<class 'inspect._empty'>
POSITIONAL_OR_KEYWORD: name=1
KEYWORD_ONLY: name=<class 'inspect._empty'>
VAR_KEYWORD: name=<class 'inspect._empty'>

 参数类型除以上外还有 VAR_KEYWORD, 如  *args 的形式, <class 'inspect._empty'> 表示没有默认值,必须传递,和 None 是不同的。

inspect.Signature 对象的 bind 方法,在框架中可用它在真正调用函数前进行参数验证

sig.bind('for_a', **{'b': 3, 'd': 4})

注意,参数 a 是 POSITIONAL_ONLY, 所以必须写出来,不能放在字典中。这里没有 c 的值,所以绑定失败,报错

TypeError: missing a required argument: 'c'

函数注解(Function Annotations)

以前所知道的是 Python 的 Type Hints, 现在才知道更早的时候有个 Function Annotations, 它们还存在些类似的东西。

Function Annotations 只是一个语法,冒号后随意写,Type Hints 规范化了,让冒号后的内容更有意义

比如可声明下面的函数

参数名冒号后可用字符串,或类型,函数声明后用 -> 也能用字符串或类型, 非类型字符串必须用引号括起来,不能写成 url: abc, 会报错 NameError: name 'abc' is not defined。 上面的函数可正确被 Python 解释执行,但通不过 mypy 的检测。

__annotations__ 查看 fetch 函数

fetch.__annotations__
{'url': 'web url', 'depth': 'input int > 0', 'https_only': <class 'bool'>, 'return': 'hello'}

Python 只是把函数的注解存储到 __annotations__ 属性,不做额外的检查,强制,验证。通过 inspect.signature 也能获得函数的注解

得到

url=web url
depth=input int > 0
https_only=<class 'bool'>
sig.return_annotation='hello'

虽然 Python 对函数注解无所作为,不过 IDE 或某些框架可以利用函数注解。特别是新的 Type Hints 让函数注解变得更有用,像 mypy, Flask/FastAPI 就依据 Type Hints 来决定输入参数的校验。

有用的 operator 和 functools 模块

operator 中提供了常用运算的函数/callable 类,如 add, mul, reduce, itemgetter, attrgetter, 在写简单的 Lambda 表达式时先考虑能不用利用 operator 中的函数, 用 dir(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', 'imatmul', 'imod', 'imul', 'index', 'indexOf', 'inv', 'invert', 'ior', 'ipow', 'irshift', 'is_', 'is_not', 'isub', 'itemgetter', 'itruediv', 'ixor', 'le', 'length_hint', 'lshift', 'lt', 'matmul', 'methodcaller', 'mod', 'mul', 'ne', 'neg', 'not_', 'or_', 'pos', 'pow', 'rshift', 'setitem', 'sub', 'truediv', 'truth', 'xor']

假如定义了一个函数 foo, 下面用 itemgetter 和 attrgetter

attrgetter 在获取属性时支持 . 连接的方式。

operator.methodcaller 的使用

上面第二种用法与 functools.partial 冻结参数作用类似。

同样的方式查看一下  functools 中包含了些什么可调用对象

>> [name for name in dir(functools) if not name.startswith('_')]
['GenericAlias', 'RLock', 'WRAPPER_ASSIGNMENTS', 'WRAPPER_UPDATES', 'cache', 'cached_property', 'cmp_to_key', 'get_cache_token', 'lru_cache', 'namedtuple', 'partial', 'partialmethod', 'recursive_repr', 'reduce', 'singledispatch', 'singledispatchmethod', 'total_ordering', 'update_wrapper', 'wraps']

如之前一篇 Python print 立即打印内容到重定向的文件 希望 print 总是带有 flush=True, 我们就可以利用  partial 来重定义 print

以后调用 print 函数时就会自动应用 flush=True 参数,但仍可再提供 flush 参数。如果有定位参数,可以这样

picture = partial(tag, 'img', cls='pic-frame')   # img 的 tag 的定位参数

偏函数输出的描述是

>>> print
functools.partial(<built-in function print>, flush=True)
>>> print.func
<built-in function print>
>>> print.args
()
>>> print.keywords
{'flush': True}

functools.partial 就是计算机概念中的偏函数(Partial Application), 即一个函数的封装,把多元函数转换为更低元的函数,如提供默认值的方式,把三形参的函数转换为二形参的函数,类似于降维。类似的一个概念有柯理化(Currying), 多参调用的函数逐步补全参数,最后才执行,如 foo(a, b, c) 的函数,调用时用 foo(1, 2)(3)。它们都是高阶函数。

functools.partialmethod 是用来处理方法的,与 partial 功能一样。

延伸部分

Bobo: 轻量级的支持 WSGI 的 Python Web 框架,可直接映射 URL 到对象层次结构上。

去争论编程语言是 OO 还是函数式编程没多大意义了,只要是好的东西就可加以借鉴。lambda, map, filter 和 reduce 首次出现在 Lisp 中,后来 Python 从 Haskell 中借用列表推导后,对前面那几个函数需求就极大的减少了。Python 至今也没有尾递归消除(tail-recursion elimination)

类别: Python. 标签: . 阅读(36). 订阅评论. TrackBack.
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x