认识函数类别 & 了解可调用对象
首先,明确 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 = 80inspect.signature 函数返回一个 inspect.Signature 对象,它有一个 parameters 属性,这是一个有序映射,把参数名和 inspect.Parameter 对象对应起来。各个 Parameter 属性也有自己的属性,例如 name、default 和 kind。特殊的 inspect._empty 值表示没有默认值,考虑到 None 是有效的默认值(也经常这么做),而且这么做是合理的。
Parameter 的 kind 属性值是 _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参数可以有两种方式:
- 各种类,如 str、int 等,或者自定义的类。
- 字符串,如 ‘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 函数。
得益于 operator 和 functools 等包的支持,可以更轻松的进行函数式编程。
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 模块提供 itemgetter 和 attrgetter 来读取元素或者对象的属性。例如下面,通过 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__ 方法的类。
attrgetter 与 itemgetter 作用类似,它创建的函数根据名称提取对象的属性。如果把多个属性名传给 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)
Truefunctools.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() * .1xxx
Python 如何计算装饰器句法
Python 如何判断变量是不是局部的
闭包存在的原因和工作原理
nonlocal 能解决什么问题
实现行为良好的装饰器
标准库中有用的装饰器
实现一个参数化装饰器
扩展
《Python Cookbook(第3版)中文版》(David Beazley 和 Brian K. Jones 著)的“8.21 实现访问者模式”使用优雅的方式实现了“访问者”模式,其中的 NodeVisitor 类把方法当作一等对象处理。
《Python 高级编程》(Tarek Ziadé著)是市面上最好的 Python 中级书,第 14 章“有用的设计模式”从 Python 程序员的视角介绍了 7 种经典模式。