认识函数类别 & 了解可调用对象

首先,明确 Python 中函数是对象。

其次,Python 中有一些将函数作为对象的函数,常见的有 map、reduce、filter、all、any、sorted 等函数。其中一些函数的功能如下:

  • map 函数,将后面的参数传入到第一个对象中,如 map(func, range(10))
  • sorted 函数,对数据进行排序,如 sorted(data_list, key=len)
  • any 函数,存在为 True 的值,返回 True,空集返回 False。
  • all 函数,均为 True 的值,返回 True,空集返回 True。

但上面一部分函数可以通过“列表推导或者生成器表达式”来实现相应功能。

Python 中也可以使用 lambda 表达式来作为匿名函数。例如,sorted(tmp_list, key=lambda x: x[::-1])

除了用户自定义的函数,调用运算符还可以应用到其他对象上。Python 文档中总共包含了 7 种可以调用的对象,具体如下。通过 callable 函数也可以判断一个对象是否属于可调用对象。

  • 用户自定义的函数:def 或 lambda
  • 内置函数:len
  • 内置方法:dict.get
  • 类中定于的函数
  • 类 - 创建类实例
  • 类的实例 ⭐
  • 生成器函数:yield ⭐

类在实现 __call__ 方法后,类的实例就可以变成可调用对象,如下面代码。

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

函数内部属性 & 获取函数参数信息

函数对象还会有很多属性,使用 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__']
>>>

其中,函数对象有个 __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()
 
>>> 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

虽然通过上述属性可以获得函数的参数信息,但是这种方式并不友好。更好的办法是使用 inspect 模块来获取参数等信息。

>>> from clip import clip
>>> from inspect import signature
>>> sig = signature(clip)
>>> sig  # doctest:+ELLIPSIS
<inspect.Signature object at 0x...>
>>> 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.signature 函数返回一个 inspect.Signature 对象,它有一个 parameters 属性,这是一个有序映射,把参数名和 inspect.Parameter 对象对应起来。各个 Parameter 属性也有自己的属性,例如 namedefaultkind。特殊的 inspect._empty 值表示没有默认值,考虑到 None 是有效的默认值(也经常这么做),而且这么做是合理的。

Parameterkind 属性值是 _ParameterKind 类中的 5 个值之一,列举如下:

  • POSITIONAL_OR_KEYWORD:定位参数 or 关键字参数
  • VAR_POSITIONAL:定位参数元组
  • VAR_KEYWORD:关键字参数字典
  • KEYWORD_ONLY:仅限关键字参数(Python 3 新增)
  • POSITIONAL_ONLY:仅限定位参数(Python 声明函数不支持,部分 C 语言实现的支持)

inspect.Parameter 对象还有一个 annotation 属性,值通常为 inspect._empty,除非函数存在注解元数据。

配置函数注解 & 获取函数注解

def clip(text:str, max_len:'int > 0'=80)-> str:
    """在max_len前面或后面的第一个空格处截断文本"""
    pass

参数可以有两种方式:

  1. 各种类,如 str、int 等,或者自定义的类。
  2. 字符串,如 ‘int > 0’

Python 将参数的注解保存到 __annotations__ 属性中,如下所示。Python 并不会使用这个数据,仅作为函数的元数据,可以供 IDE 进行类型检查。

>>> from clip_annot import clip
>>> clip.__annotations__
{'text': <class 'str'>, 'max_len': 'int > 0', 'return': <class 'str'>}

通过上述提到的 inspect 模块,也可以从函数中提取注解。

>>> from clip_annot import clip
>>> from inspect import signature
>>> sig = signature(clip)
>>> sig.return_annotation
<class 'str'>
>>> 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

函数式编程

维基百科对于“函数式编程”的抽象定义:函数式编程是一种编程范式,它将电脑运算视为函数运算,并且避免使用程序状态以及易变对象。

个人理解,函数式编程是将计算机的计算操作转换为函数,例如,求和操作使用 sum 函数。

得益于 operatorfunctools 等包的支持,可以更轻松的进行函数式编程。

operator 求积运算

例如,将求积运算转换为函数,可能需要上面的方式。然而,lambda a,b: a+b 不太优雅,operator 模块提供了更好的实现多个数求积的方式。

# 使用 reduce 函数和一个匿名函数计算阶乘
from functools import reduce
def fact(n):
    return reduce(lambda a, b: a*b, range(1, n+1))
 
# 使用reduce和operator.mul函数计算阶乘
from functools import reduce
from operator import mul
def fact(n):
    return reduce(mul, range(1, n+1))

operator 读取元素或对象属性

operator 模块提供 itemgetterattrgetter 来读取元素或者对象的属性。例如下面,通过 itemgetter 读取元组的第一个元素进行排序。

>>> from operator import itemgetter
>>> metro_data = [
...     ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
...     ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
... ]
>>>
>>> for city in sorted(metro_data, key=itemgetter(1)):
...     print(city)
# ...
>>> cc_name = itemgetter(1, 0)
>>> for city in metro_data:
...     print(cc_name(city))

如果把多个元素下标传给 itemgetter,会读取多个元素,然后存储到一个元组中:

>>> cc_name = itemgetter(1, 0)
>>> for city in metro_data:
...     print(cc_name(city))
...
('JP', 'Tokyo')
('IN', 'Delhi NCR')

itemgetter 使用 [] 运算符,因此它不仅支持序列,还支持映射和任何实现 __getitem__ 方法的类。

attrgetteritemgetter 作用类似,它创建的函数根据名称提取对象的属性。如果把多个属性名传给 attrgetter,它也会返回提取的值构成的元组。此外,如果参数名中包含 .(点号),attrgetter 会深入嵌套对象,获取指定的属性。

>>> from collections import namedtuple
>>> LatLong = namedtuple('LatLong', 'lat long')
>>> Metropolis = namedtuple('Metropolis', 'name cc pop 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
>>> 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)

operator 调用指定方法

operator 另一个不错的函数 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'

functools reduce

functools 冻结参数/方法

functools.partial 这个高阶函数用于“部分应用”一个函数。部分应用是指,基于一个函数创建一个新的可调用对象,把原函数的某些参数固定。使用这个函数可以把接受一个或多个参数的函数改编成需要回调的 API,这样参数更少。

>>> 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]

如果处理多国语言编写的文本,在比较或排序之前可能会想使用 unicode.normalize('NFC', s) 处理所有字符串 s。如果经常这么做,可以定义一个 nfc 函数:

>>> import unicodedata, functools
>>> nfc = functools.partial(unicodedata.normalize, 'NFC')
>>> s1 = 'café'
>>> s2 = 'cafe\u0301'
>>> s1, s2
('café', 'café')
>>> s1 == s2
False
>>> nfc(s1) == nfc(s2)
True

functools.partialmethod 函数(Python 3.4 新增)的作用与 partial 一样,不过是用于处理方法的。

functools LRU 缓存 (TODO)

functools singledispatch (TODO)

functools wraps (TODO)

functools 模块中的 lru_cache 函数令人印象深刻,它会做备忘(memoization),这是一种自动优化措施,它会存储耗时的函数调用结果,避免重新计算。第 7 章将会介绍这个函数,还将讨论装饰器,以及旨在用作装饰器的其他高阶函数:singledispatch 和 wraps。

扩展

《Python Cookbook(第3版)中文版》(David Beazley 和 Brian K. Jones 著)的第 7 章是对本章和第 7 章很好的补充,那一章基本上使用不同的方式探讨了相同的概念。

Python 语言参考手册中的“3.2. The standard type hierarchy”一节对 7 种可调用类型和其他所有内置类型做了介绍。

本章讨论的 Python 3 专有特性有各自的 PEP:“PEP 3102—Keyword-Only Arguments”和“PEP 3107—Function Annotations”。

若想进一步了解目前对注解的使用,Stack Overflow 网站中有两个问答值得一读:一个是“What are good uses for Python3’s‘Function Annotations’”,Raymond Hettinger 给出了务实的回答和深入的见解;另一个是“What good are Python function annotations?”,某个回答中大量引用了 Guido van Rossum 的观点。


无状态下,用函数替代继承类实现策略模式

如果没有状态,使用函数会比使用继承类更轻量。

class Promotion(ABC) : #策略 :抽象基类
    @abstractmethod
    def discount(self, order):
        """返回折扣金额(正值)"""
class FidelityPromo(Promotion):  #第一个具体策略
    """为积分为 1000 或以上的顾客提供 5%折扣"""
    def discount(self, order):
        return order.total() * .05 if order.customer.fidelity >= 1000 else 0
class BulkItemPromo(Promotion):  #第二个具体策略
    """单个商品为 20 个或以上时提供 10%折扣"""
    def discount(self, order):
        discount = 0
        for item in order.cart:
            if item.quantity >= 20:
                discount+= item.total() * .1
        return discount
        
>>> Order(joe, cart, FidelityPromo())
>>> Order(ann, cart, BulkItemPromo())
 
def fidelity_promo(order): 
    """为积分为 1000 或以上的顾客提供 5%折扣"""
    return order.total() * .05 if order.customer.fidelity >= 1000 else 0
def bulk_item_promo(order):
    """单个商品为 20 个或以上时提供 10%折扣"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount+= item.total() * .1
    return discount
 
>>> Order(joe, cart, fidelity_promo)
>>> Order(ann, cart, bulk_item_promo)

假如,此时有个需求,根据 order 选择最佳的策略,会有以下几种实现情况。(其实就是找到所有的策略)

方案一:通过 globals 函数获得当前的全局符号表。但是,这要求文件内只能存放 xxx_promo 函数。

promos = [globals()[name] for name in globals() 
            if name.endswith('_promo')
            and name != 'best_promo'] 
def best_promo(order):
    return max(promo(order) for promo in promos)

方案二:使用 promotions 模块,构建策略列表。inspect.getmembers 函数用于获取对象(这里是 promotions 模块)的属性,第二个参数是可选的判断条件(一个布尔值函数)。唯一重要的是,promotions 模块只能包含计算订单折扣的函数。当然,这是对代码的隐性假设。如果有人破坏这个假设,那就需要增加条件。

promos = [func for name, func in
                inspect.getmembers(promotions, inspect.isfunction)]
def best_promo(order):
    return max(promo(order) for promo in promos)

方案三:使用装饰器。在必要的策略上增加装饰器,装饰器的职责是将当前策略补充到策略列表中。通过这种方式可以自动的生成包含全部策略的策略列表。

promos = []
def promotion(promo_func): 
    promos.append(promo_func)
    return promo_func
@promotion
def fidelity(order):
    """为积分为1000或以上的顾客提供5%折扣"""
    return order.total() * .05 if order.customer.fidelity >= 1000 else 0
@promotion
def bulk_item(order):
    """单个商品为20个或以上时提供10%折扣"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount+= item.total() * .1

xxx

Python 如何计算装饰器句法
Python 如何判断变量是不是局部的
闭包存在的原因和工作原理
nonlocal 能解决什么问题

实现行为良好的装饰器
标准库中有用的装饰器
实现一个参数化装饰器

扩展

《Python Cookbook(第3版)中文版》(David Beazley 和 Brian K. Jones 著)的“8.21 实现访问者模式”使用优雅的方式实现了“访问者”模式,其中的 NodeVisitor 类把方法当作一等对象处理。

《Python 高级编程》(Tarek Ziadé著)是市面上最好的 Python 中级书,第 14 章“有用的设计模式”从 Python 程序员的视角介绍了 7 种经典模式。