流畅的Python:把函数视作对象之一等函数

学习《流畅的Python》,后续系列博客大部分均摘自本书,仅用于交流、学习和记录。

把函数视作对象

1. 一等函数

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

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

在 Python 中,整数、字符串和字典都是一等对象。函数也可以作为一等对象。

def factorial(n):
    """
    阶乘
    :param n: num 
    :return: num!
    """
    return 1 if n < 1 else n * factorial(n-1)
  
factorial.__doc__
Out[2]: '\n    阶乘\n    :param n: num \n    :return: num!\n    '
factorial(30)
Out[3]: 265252859812191058636308480000000
type(factorial)
Out[4]: function

这是一个控制台会话,def factorial(n):是在“运行时”创建一个函数。__doc__ 是函数对象众多属性中的一个。 factorial 是 function 类的实例。

函数对象的“一等”本性:
我们可以把 factorial 函数赋值给变量 fact,然后通过变量名调用。我们还能把它作为参数传给 map 函数。map 函数返回一个可迭代对象,里面的元素是把第一个参数(一个函数)应用到第二个参数(一个可迭代对象,这里 是 range(11))中各个元素上得到的结果。

# 通过别的名称使用函数,再把函数作为参数传递
fact = factorial
fact(5)
Out[5]: 120
type(fact)
Out[6]: function
  
map(fact, range(5))
Out[7]: <map at 0x212ba2663c8>
list(map(fact, range(5)))
Out[8]: [1, 1, 2, 6, 24]

有了一等函数,就可以使用函数式风格编程。函数式编程的特点之一是使用高阶函数。


2. 高阶函数

接受函数为参数,或者把函数作为结果返回的函数是高阶函数(higher-order function)。在函数式编程范式中,最为人熟知的高阶函数有 map、filter、reduce。

此外,内置函数 sorted 也是:可选的 key 参数用于提供一个函数,它会应用到各个元素上进行排序。

def reverse(word): 
    return word[::-1] 
fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key=reverse)
Out[16]: ['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']

map、filter和reduce的现代替代品:

map 和 filter 还是内置函数,但是由于引入了列表推导和生成器表达式,它们变得没那么重要了。列表推导或生成器表达式具有 map 和 filter 两个函数的功能,而且更易于阅读。

# Python3 中,map 和 filter 返回生成器(一种迭代器),因此现在它们的直接替代品是生成器表达式
list(map(fact,range(6)))
Out[17]: [1, 1, 2, 6, 24, 120]
[fact(n) for n in range(6)]   # 使用列表推导执行相同的操作。
Out[18]: [1, 1, 2, 6, 24, 120]
  
list(map(fact, filter(lambda n:n%2, range(6))))
Out[19]: [1, 6, 120]
[fact(n) for n in range(6) if n%2]  # 列表推导式,取代map、filter,并避免了lambda 表达式
Out[20]: [1, 6, 120]
# 在 Python2中,reduce 是内置函数,但是在 Python3 中放到 functools 模块里了。
# 这个函数最常用于求和,自2003年发布的 Python2.3开始,最好使用内置的sum函数。
# 在可读性和性能方面,这是一项重大改善.
from functools import reduce
from operator import add
reduce(add, range(6))
Out[25]: 15

sum 和 reduce 的通用思想是把某个操作连续应用到序列的元素上,累计之前的结果,把一系列值归约成一个值。all 和 any 也是内置的归约函数。

all(iterable): 如果 iterable 的每个元素都是真值,返回 True;all([]) 返回 True。 有0即为假

any(iterable) : 只要 iterable 中有元素是真值,就返回 True;any([]) 返回 False。 有1即为真

Python 3.6.0 (v3.6.0:41df79263a11, Dec 23 2016, 08:06:12) [MSC v.1900 64 bit (AMD64)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> all([1,0])
False
>>> all([1])
True
>>> all([])
True
>>> any([1,0])
True
>>> any([0])
False
>>> any([])
False

3. 可调用对象

除了用户定义的函数,调用运算符(即 ())还可以应用到其他对象上。如果想判断对象能否调用,可以使用内置的 callable() 函数。Python 数据模型文档列出了 7 种可调用对象

  1. 用户定义的函数

    使用 def 语句或 lambda 表达式创建。

  2. 内置函数

    使用 C 语言(CPython)实现的函数,如 len 或 time.strftime。

  3. 内置方法

    使用 C 语言实现的方法,如 dict.get。

  4. 方法

    在类的定义体中定义的函数。

  5. 调用类时会运行类的 __new__方法创建一个实例,然后运行__init__方法,初始化实例,最后把实例返回给调用方。因为 Python 没有 new 运算符,所以调用类相当于调用函数。(通常,调用类会创建那个类的实例,不过覆盖 __new__方法的话,也可能出现 其他行为。)

  6. 类的实例

    如果类定义了__call__方法,那么它的实例可以作为函数调用。

  7. 生成器函数

    使用 yield 关键字的函数或方法。调用生成器函数返回的是生成器对象。

Python 中有各种各样可调用的类型,因此判断对象能否调用,最安全的方法是使用内置的 callable() 函数:

>>> [callable(item) for item in [str, abs, 100]]
[True, True, False]

4. 用户可调用的类型

不仅 Python 函数是真正的对象,任何 Python 对象都可以表现得像函数。为此,只需实现实例方法 __call__

import random

class BingoCase(object):
    def __init__(self, items):
        self._items = list(items)
        random.shuffle(self._items)

    def pickitem(self):
        try:
            return self._items.pop()
        except IndexError as e:
            raise LookupError('pick from empty BingoCase')

    def __call__(self, *args, **kwargs):  # bingo.pickitem()的快捷方式为bingo()
        return self.pickitem()


bingo = BingoCase([1, 2, 3, 3, 4, 5])
print(bingo.pickitem())
print(bingo())
print(callable(bingo))

实现__call__方法的类是创建函数类对象的简便方式,此时必须在内部维护一个状态,让它在调用之间可用,例如 BingoCage 中的剩余元素。装饰器就是这样,有时要在多次调用之间“记住”某些事(例如备忘memoization),即缓存消耗大的计算结果,供后面使用。

创建保有内部状态的函数,还有一种截然不同的方式——闭包。


5. 函数注解

Python3提供了一种句法,用于为函数声明中的参数和返回值附加元数据。

def clip(text: str, max_len: 'int > 0' = 8) -> str:  # 有注解的函数声明
    """
    在max_len前面或后面的第一个空格处截断文本
    """
    end = None
    space_before = space_after = ''
    if len(text) > max_len:
        space_before = text.rfind(' ', 0, max_len)
        print(space_before)
        if space_before >= 0:
            end = space_before
        else:
            space_after = text.rfind(' ', max_len)
            print(space_after)
            if space_after >= 0:
                end = space_after
    if end is None:  # 没找到空格
        end = len(text)
    return text[:end].rstrip()

print(clip("1adsfd2sdfjkl 3dsfa 4jskldf"))
print(clip.__annotations__)
'''
1adsfd2sdfjkl 3dsfa
{'text': <class 'str'>, 'max_len': 'int > 0', 'return': <class 'str'>}
'''

函数声明中的各个参数可以在 : 之后增加注解表达式。如果参数有默认值,注解放在参数名和 = 号之间。如果想注解返回值,在 ) 和函数声明末尾的 : 之间添加 -> 和一个表达式。那个表达式可以是任何类型。注解中最常用的类型是类(如 str 或 int)和字符串(如’int > 0’)。在示例中,max_len 参数的注解用的是字符串。

注解不会做任何处理,只是存储在函数的 __annotations__属性(一个字典)中。

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


6. 支持函数式编程的包

operator模块

operator 模块为多个算术运算符提供了对应的函数,从而避免编写 lambda a, b: a*b 这种平凡的匿名函数。

from functools import reduce
def func(n):	
    return reduce(lambda a, b:a*b, range(1,n+1))
func(4)
Out[2]: 24
  
from operator import mul
def fact(n):
    return reduce(mul, range(1,n+1))
fact(4)
Out[3]: 24

operator 模块中还有一类函数,能替代从序列中取出元素或读取对象属性的 lambda 表达式:因此,itemgetter 和 attrgetter 其实会自行构建函数。

itemgetter 使用 [] 运算符,因此它不仅支持序列,还支持映射和任何实现 __getitem__ 方法的类。itemgetter 的常见用途:根据元组的某个字段给元组列表排序

metro_data = [ 
  ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)), 
  ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)), 
  ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)), 
  ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)), 
  ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)), ]

from operator import itemgetter
for city in sorted(metro_data, key=itemgetter(1)):
    print(city)
    
# 如果把多个参数传给 itemgetter,它构建的函数会返回提取的值构成的元组:
names = itemgetter(0, 1)
for city in metro_data:
    print(names(city))
"""
('Tokyo', 'JP')
('Delhi NCR', 'IN')
('Mexico City', 'MX')
('New York-Newark', 'US')
('Sao Paulo', 'BR')
"""

attrgetter 与 itemgetter 作用类似,它创建的函数根据名称提取对象的属性。如果把多个属性名传给 attrgetter,它也会返回提取的值构成的元组。此外,如果参数名中包含 .(点号),attrgetter 会深入嵌套对象,获取指定的属性。这些行为如下例所示,这个控制台会话不短,因为我们要构建一个嵌套结构,这样才能展示attrgetter 如何处理包含点号的属性名。

from collections import namedtuple 

metro_data = [ 
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)), 
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)), 
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)), 
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)), 
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)), ]

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]
Out[3]: Metropolis(name='Tokyo', cc='JP', pop=36.933, coord=LatLong(lat=35.689722, long=139.691667))
metro_areas[0].coord.lat
Out[4]: 35.689722
  
from operator import attrgetter
name_lat = attrgetter('name', 'coord.lat')  # 定义一个 attrgetter,获取 name 属性和嵌套的 coord.lat 属性
for city in sorted(metro_areas, key=attrgetter('coord.lat')): # 使用 attrgetter,按照纬度排序城市列表。
    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 模块中定义的部分函数(省略了以 _ 开头的名称,因为它们基本上是实现细节):

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

methodcaller 它的作用与 attrgetter和 itemgetter 类似,它会自行创建函数。methodcaller 创建的函数会在对象上调用参数指定的方法。

from operator import methodcaller

s = "abcd efg"
upcase = methodcaller('upper')
upcase(s)
Out[16]: 'ABCD EFG'
  
replace_case = methodcaller('replace',' ','-')
replace_case(s)
Out[18]: 'abcd-efg'
发布了87 篇原创文章 · 获赞 43 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qq_31362767/article/details/103229896