Python深入-7-函数是一等对象

一、把函数视为对象

在 Python 中,所有函数都是一等对象。

def factorial(n):
    return 1 if n < 2 else n * factorial(n - 1)


print(factorial(5))  # 120
print(factorial.__doc__)
print(type(factorial))  # <class 'function'>

二、高阶函数

接受函数为参数或者把函数作为结果返回的函数是高阶函数(higher-order function)
map 函数就是一例。此外,内置函数 sorted 也是:通过可选的 key 参数提供一个函数,应用到每一项上进行排序。

fruits = ['strawberry''fig''apple''cherry''raspberry''banana']
print(sorted(fruits, key=len))  # ['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']

在函数式编程范式中,最为人熟知的高阶函数有 map、filter、reduce 和 apply。apply 函数在Python 2.3 中已弃用,在 Python 3 中已正式移除,因为用不到了。如果想使用不定量的参数调用函数,可以编写fn(*args, **kwargs),无须再编写apply(fn, args, kwargs)
map、filter 和 reduce 这 3 个高阶函数还能用到,不过在大多数使用场景中有更好的替代品。
函数式语言通常会提供 map、filter 和 reduce 这 3 个高阶函数(有时使用不同的名称)。在 Python 3中,map 和 filter 还是内置函数,但是由于引入了列表推导式和生成器表达式,因此二者就变得没那么重要了。列表推导式或生成器表达式兼具 map 和 filter 这两个函数的功能,而且代码可读性更高。
在 Python 2 中,reduce 是内置函数,但是 Python 3 把它放到 functools 模块里了。这个函数最常用于求和,但自 2003 年 Python 2.3 发布以来,内置函数 sum 在执行这项操作时效果更好。

# 从 Python 3.0 开始,reduce 不再是内置函数
from functools import reduce
# 导入 add,以免创建一个只求两数之和的函数
from operator import add

print(reduce(add, range(100)))  # 4950
# 使用 sum 执行同一个操作,无须导入并调用 reduce 和 add
print(sum(range(100)))  # 4950

内置的归约函数还有 all 和 any。
all(iterable) :iterable 中没有表示假值的元素时返回 True。all([]) 返回 True。
any(iterable) :只要 iterable 中有元素是真值就返回 True。any([]) 返回 False。

三、匿名函数

lambda 关键字使用 Python 表达式创建匿名函数。
然而,受 Python 简单的句法限制,lambda 函数的主体只能是纯粹的表达式。也就是说,lambda 函数的主体中不能有 while、try 等 Python 语句。使用 = 赋值也是一种语句,不能出现在 lambda 函数的主体中。可以有新出现的 := 赋值表达式。不过,有这种赋值表达式的 lambda 函数可能太过复杂,可读性低,因此建议重构,改成使用 def 定义的常规函数。

fruits = ['strawberry''fig''apple''cherry''raspberry''banana']
print(sorted(fruits, key=lambda word: word[::-1])) # ['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']

除了作为参数传给高阶函数,Python 很少使用匿名函数。由于句法上的限制,非平凡的 lambda 表达式要么难以阅读,要么无法写出。如果发现 lambda 表达式的可读性低,强烈建议你遵照 Fredrik Lundh 的重构建议。

  • 1. 编写注释,说明 lambda 表达式的作用。

  • 2. 研究一会儿注释,找出一个名称来概括注释。

  • 3. 把 lambda 表达式转换成 def 语句,使用那个名称来定义函数。

  • 4. 删除注释。

lambda 句法只是语法糖,lambda 表达式会像 def 语句一样创建函数对象。lambda 表达式只是 Python中几种可调用对象的一种。

四、**9 **种可调用对象

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

4.1 用户定义的函数

使用 def 语句或 lambda 表达式创建的函数。
不仅 Python 函数是真正的对象,而且任何 Python 对象都可以表现得像函数。为此,只需实现实例方法__call__

import random


class BingoCage:
    # __init__ 接受任何可迭代对象。在本地构建一个副本,防止传入的列表参数有什么意外的副作用
    def __init__(self, items):
        self._items = list(items)
        # shuffle 定能打乱顺序,因为 self._items 是列表。
        random.shuffle(self._items)

    # 起主要作用的方法。
    def pick(self):
        try:
            return self._items.pop()
        except IndexError:
            # 如果 self._items 为空,就抛出异常,并设定错误消息。
            raise LookupError('pick from empty BingoCage')

    # bingo() 是 bingo.pick() 的快捷方式。
    def __call__(self):
        return self.pick()


bingo = BingoCage(range(3))
print(bingo.pick())  # 0
print(bingo())  # 1
print(callable(bingo))  # True

实现 __call__ 方法是创建类似函数的对象的简便方式,此时必须在内部维护一个状态,让它在多次调用之间存续,例如 BingoCage 中的剩余元素。__call__ 的另一个用处是实现装饰器。装饰器必须可调 用,而且有时要在多次调用之间“记住”某些事 [ 例如备忘(memoization),即缓存消耗大的计算结果,供后面使用 ],或者把复杂的操作分成几个方法实现

4.2 内置函数

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

4.3 内置方法

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

4.4 方法

在类主体中定义的函数。

4.5 类

调用类时运行类的__new__方法创建一个实例,然后运行__init__方法,初始化实例,最后再把 实例返回给调用方。Python 中没有new运算符,调用类就相当于调用函数。

4.6 类的实例

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

4.7 生成器函数

主体中有yield关键字的函数或方法。调用生成器函数返回一个生成器对象。

4.8 原生协程函数

使用async def定义的函数或方法。调用原生协程函数返回一个协程对象。Python 3.5 新增。

4.9 异步生成器函数

使用async def定义,而且主体中有yield关键字的函数或方法。调用异步生成器函数返回一个异步生成器,供 async for 使用。Python 3.6 新增。

五、支持函数式编程的包

5.1 operator 模块

在函数式编程中,经常需要把算术运算符当作函数使用。

from functools import reduce

def factorial(n):
    return reduce(lambda a, b: a*b, range(1, n+1))

operator 模块为多个算术运算符提供了对应的函数,无须再动手编写像lambda a, b: a*b 这样的匿名函数。

from functools import reduce
from operator import mul

def factorial(n):
    return reduce(mul, range(1, n+1))

operator 模块中还有一类函数,即工厂函数itemgetterattrgetter,能替代从序列中取出项或读取对象属性的 lambda表达式。

from operator import itemgetter

metro_data = [
    ('Tokyo''JP'36.933, (35.689722139.691667)),
    ('Delhi NCR''IN'21.935, (28.61388977.208889)),
    ('Mexico City''MX'20.142, (19.433333-99.133333)),
    ('New York-Newark''US'20.104, (40.808611-74.020386)),
    ('São Paulo''BR'19.649, (-23.547778-46.635833)),
]
for city in sorted(metro_data, key=itemgetter(1)):
    print(city)

itemgetter使用[]运算符,因此它不仅支持序列,还支持映射和任何实现__getitem__方法的类。
itemgetter的作用类似,attrgetter 创建的函数会根据名称来提取对象的属性。如果传给attrgetter 多个属性名,那么它也会返回由提取的值构成的元组。此外,如果参数名中包含 .(点号),那么attrgetter就会深入嵌套对象,检索属性。

from collections import namedtuple
from operator import attrgetter

metro_data = [
    ('Tokyo''JP'36.933, (35.689722139.691667)),
    ('Delhi NCR''IN'21.935, (28.61388977.208889)),
    ('Mexico City''MX'20.142, (19.433333-99.133333)),
    ('New York-Newark''US'20.104, (40.808611-74.020386)),
    ('São Paulo''BR'19.649, (-23.547778-46.635833)),
]

# 使用 namedtuple 定义 LatLon
LatLon = namedtuple('LatLon''lat lon')

# 使用 namedtuple 定义 Metropolis
Metropolis = namedtuple('Metropolis''name cc pop coord')

# 使用 Metropolis 实例构建 metro_areas 列表。注意,这里会使用嵌套的元组拆包提取 (lat,lon),然后使用它们构建 LatLon,作为 Metropolis 的 coord 属性。
metro_areas = [Metropolis(name, cc, pop, LatLon(lat, lon)) for name, cc, pop, (lat, lon) in metro_data]

print(metro_areas[0])

Metropolis(name='Tokyo', cc='JP', pop=36.933, coord=LatLon(lat=35.689722, lon=139.691667))
# 深入 metro_areas[0],获取它的纬度
print(metro_areas[0].coord.lat)

# # 定义一个 attrgetter,获取 name 属性和嵌套的 coord.lat 属性。
name_lat = attrgetter('name''coord.lat')
# 再次使用 attrgetter,按照纬度排序城市列表
for city in sorted(metro_areas, key=attrgetter('coord.lat')):
    print(name_lat(city))

5.2 使用 functools.partial 冻结参数

值得关注的函数是partial,它可以根据提供的可调用对象产生一个新可调用对象,为原可调用对象的某些参数绑定预定的值。使用这个函数可以把接受一个或多个参数的函数改造成需要更少参数的回调的 API。

from operator import mul
from functools import partial

# 使用 mul 创建 triple 函数,把第一个位置参数绑定为 3。
triple = partial(mul, 3)
# 测试 triple 函数。
print(triple(7))  # 21
# 在 map 中使用 triple,在这个示例中不能使用 mul。
print(list(map(triple, range(110))))  # [3, 6, 9, 12, 15, 18, 21, 24, 27]


原文始发于微信公众号(Python之家):Python深入-7-函数是一等对象

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/198398.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!