0

流畅的 Python 读书笔记(四)

 2 years ago
source link: https://yanbin.blog/fluent-python-reading-notes-4/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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 模块里,看来是不那么重要。

>>> from functools import reduce
>>> numbers = [1, 2, 3, 4, 5, 6]
>>> reduce(lambda a, b: a * b + b, numbers)

类似于 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. __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 来反射函数

from inspect import signature
def foo(a, /, b=1, *, c,  **kwargs):
    return a+b
sig = signature(foo)
for name, param in sig.parameters.items():
    print(f'{param.kind}: name={param.default}')

列出各个参数

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 规范化了,让冒号后的内容更有意义

比如可声明下面的函数

def fetch(url: "web url", depth: "input int > 0", https_only: bool=False) -> str:
    print(url, depth)

参数名冒号后可用字符串,或类型,函数声明后用 -> 也能用字符串或类型, 非类型字符串必须用引号括起来,不能写成 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 也能获得函数的注解

from inspect import signature
sig = signature(fetch)
for param in sig.parameters.values():
    print(f'{param.name}={param.annotation}')
print(f'{sig.return_annotation=}')

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

>>> operator.itemgetter(0)(dir(foo))
'__annotations__'
>>> operator.attrgetter("__code__")(foo)
<code object foo at 0x104d1c3a0, file "<stdin>", line 1>
>>> operator.attrgetter("__code__.co_name")(foo)
'foo'

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

operator.methodcaller 的使用

>>> from operator import methodcaller
>>> upcase = methodcaller('upper')
>>> upcase('abC')
'ABC'
>>> hiphenate = methodcaller('replace', ' ', '_')
>>> hiphenate('hello world')
'hello_world'

上面第二种用法与 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

from functools import partial
print = partial(print, flush=True)
print(123)
print(123, flush=False)

以后调用 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)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK