《流畅的Python》笔记。
本篇主要讲述Python中函数的进阶内容。包括函数和对象的关系,函数内省,Python中的函数式编程。
本片首先介绍函数和对象的关系;随后介绍函数和可调用对象的关系,以及函数内省。函数内省这部分会涉及不少与IDE和框架相关的东西,若是平时并不写框架,能够略过此部分。最后介绍函数式编程的相关概念,以及与之相关的两个重要模块:operator模块和functools模块。python
首先补充“一等对象”的概念。“一等对象”通常定义以下:编程
从上述定义能够看出,Python中的函数符合上述四点,因此在Python中函数也被视做一等对象。bash
“把函数视做一等对象”简称为“一等函数”,但这并非指有一类函数是“一等函数”,在Python中全部函数都是一等函数!微信
为了代表Python中函数就是对象,咱们可使用type()
函数来判断函数的类型,而且访问函数的__doc__
属性,同时咱们还将函数赋值给一个变量,而且将函数做为参数传入另外一个函数:数据结构
def factorial(n): """return n!""" return 1 if n < 2 else n * factorial(n - 1) # 在Python控制台中,help(factorial)也会访问函数的__doc__属性。 print(factorial.__doc__) print(type(factorial)) # 把函数赋值给一个变量 fact = factorial print(fact) fact(5) # 把函数传递给另外一个函数 print(list(map(fact, range(11)))) # 结果: return n! <class 'function'> <function factorial at 0x000002421033C2F0> [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
从上述结果能够看出,__doc__
属性保存了函数的文档字符串,而type()
的结果说明函数实际上是function
类的一个实例。将函数赋值给一个变量和将函数做为参数传递给另外一个函数则体现了“一等对象”的特性。闭包
接收函数做为参数,或者把函数做为结果返回的函数叫作高阶函数(higher-order function),上述的map
函数就是高阶函数,还有咱们经常使用的sorted
函数也是。app
你们或多或少见过map
,filter
和reduce
三个函数,这三个就是高阶函数,在过去很经常使用,但如今它们都有了替代品:框架
map
和filter
依然是内置函数,但因为有了列表推导和生成器表达式,这两个函数已不经常使用;reduce
已不是内置函数,它被放到了functools模块中。它常被用于求和,但如今求和最好用内置的sum
函数。sum
和reduce
这样的函数叫作归约函数,它们的思想是将某个操做连续应用到一系列数据上,累计以前的结果,最后获得一个值,即将一系列元素归约成一个值。ssh
内置的归约函数还有all
和any
:编程语言
all(iterable)
:若是iterable
中每一个值都为真,则返回True
;all([])
返回True
;any(iterable)
:若是iterable
中有至少一个元素为真,则返回True
;any([])
返回False
。lambda
关键字在Python表达式内建立匿名函数,但在Python中,匿名函数内不能赋值,也不能使用while
,try
等语句。但它和def
语句同样,实际建立了函数对象。
若是使用lambda
表达式致使一段代码难以理解,最好仍是将其转换成用def
语句定义的函数。
函数其实一个可调用对象,它实现了__call__
方法。Python数据模型文档列出了7种可调用对象:
def
语句或lambda
表达式建立;len
或time.strftime
;dict.get
;__new__
方法建立一个实例,而后运行__init__
方法初始化实例,最后把实例返回给调用方。由于Python没有new
运算符,因此调用类至关于调用函数;__call__
方法,那么它的实例能够做为函数调用;yield
关键字的函数或方法。调用生成器函数返回的是生成器对象。任何Python对象均可以表现得像函数,只要实现__call__
方法。
class SayHello: def sayhello(self): print("Hello!") def __call__(self): self.sayhello() say = SayHello() say.sayhello() say() print(callable(say)) # 结果: Hello! Hello! True
实现__call__
方法的类是建立函数类对象的简便方式。有时这些类必须在内部维护一些状态,让它在调用之间可用,好比装饰器。装饰器必须是函数,并且有时还要在屡次调用之间保存一些数据。
如下内容在编写框架和IDE时用的比较多。
笔者以前偶有见到”内省“,但一直不明白”内省“这个词到底是什么意思。“自我检讨”?其实在编程中,这个词的意思就是:让代码自动肯定某一段代码能干什么。若是以函数举例,就是函数A自动肯定函数B是什么,包含哪些信息,能干什么。不过在讲Python函数的内省以前,先来看看函数都有哪些属性和方法。
dir
函数能够检测一个参数所含有的属性和方法。咱们能够用该函数查看一个函数所包含的属性和方法:
>>> dir(factorial) ['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
其中大多数属性是Python对象共有的。函数独有的属性以下:
>>> class C:pass >>> obj = C() >>> def func():pass >>> sorted(set(dir(func)) - set(dir(obj))) ['__annotations__', '__call__', '__closure__', '__code__', '__defaults__', '__get__', '__globals__', '__kwdefaults__', '__name__', '__qualname__']
与用户定义的常规类同样,函数使用__dict__
属性存储用户赋予它的属性。这至关于一种基本形式的注解。
这里可能有人以为别扭:以前都是给变量或者对象赋予属性,如今是给函数或者方法赋予属性。不过正如前面说的,Python中函数就是对象。
通常来讲,为函数赋予属性不是个常见的作法,但Django框架就有这样的行为:
def upper_case_name(obj): return ("%s %s" % (obj.first_name, obj.last_name)).upper() upper_case_name.short_description = "Customer name" # 给方法赋予了一个属性
从这里开始就是函数内省的内容。在HTTP为框架Bobo中有个使用函数内省的例子,它以装饰器的形式展现:
import bobo @bobo.query("/") def hello(person): return "Hello %s!" % person
经过装饰器bobo.query
,Bobo会内省hello
函数:Bobo会发现这个hello
函数须要一个名为person
的参数,而后它就会从请求中获取这个参数,并将这个参数传给hello
函数。
有了这个装饰器,咱们就不用本身处理请求对象来获取person
参数,Bobo框架帮咱们自动完成了。
那这到底是怎么实现的呢?Bobo怎么知道咱们写的函数须要哪些参数?它又是怎么知道参数有没有默认值呢?
这里用到了函数对象特有的一些属性(若是不了解参数类型,能够阅读笔者的“Python学习之路7”中的相关内容):
__defaults__
的值是一个元组,存储着关键字参数的默认值和位置参数;__kwdefaults__
存储着命名关键字参数的默认值;__code__
属性存储参数的名称,它的值是一个code
对象引用,自身也有不少属性。下面经过一个例子说明这些属性的用途:
def func(a, b=10): """This is just a test""" c = 20 if a > 10: d = 30 else: e = 30 print(func.__defaults__) print(func.__code__) print(func.__code__.co_varnames) print(func.__code__.co_argcount) # 结果: (10,) <code object func at 0x0000021651851DB0, file "mytest.py", line 1> ('a', 'b', 'c', 'd', 'e') 2
能够看出,这种信息的组织方式并不方便:
__code__.co_varnames
中,它同时还存储了函数定义体中的局部变量,所以,只有前__code__.co_argcount
个元素是参数名(不包含前缀为*
何**
的的变长参数);__default__
属性,好比上例中关键字参数b
的默认值10
。不过,咱们并非第一个发现这种方式很不方便。已经有人为咱们造好了轮子。
>>> from mytest import func >>> from inspect import signature >>> sig = signature(func) # 返回一个inspect.Signature对象(签名对象) >>> sig <Signature (a, b=10)> >>> str(sig) '(a, b=10)' >>> for name, param in sig.parameters.items(): ... print(param.kind, ":", name, "=",param.default) ... POSITIONAL_OR_KEYWORD : a = <class 'inspect._empty'> # 表示没有默认值 POSITIONAL_OR_KEYWORD : b = 10
inspect.Signature
对象有一个属性parameters
,该属性是个有序映射,把参数名和inspect.Parameter
对象对应起来。inspect.Parameter
也有本身的属性,如:
name
:参数的名称;default
:参数的默认值;kind
:参数的类型,有5种,POSITIONAL_OR_KEYWORD
,VAR_POSITIONAL
(任意数量参数,以一个*号开头的那种参数),VAR_KEYWORD
(任意数量的关键字参数,以**开头的那种参数),KEYWORD_ONLY
(命名关键字参数)和POSITIONAL_ONLY
(Python句法不支持该类型)annotation
和return_annotation
:参数和返回值的注解,后面会讲到。inspect.Signature
对象有个bind
方法,它可把任意个参数绑定到Singature
中的形参上,框架可以使用这个方法在真正调用函数前验证参数是否正确。好比你本身写的框架中的某函数A自动获取用户输入的参数,并根据这些参数调用函数B,但在调用B以前,你想检测下这些参数是否符合函数B对形参的要求,此时你就有可能用到这个bind
方法,看能不能将这些参数绑定到函数B上,若是能,则可认为可以根据这些参数调用函数B:
>>> from mytest import func >>> from inspect import signature >>> sig = signature(func) >>> my_tag = {"a":10, "b":20} >>> bound_args = sig.bind(**my_tag) >>> bound_args <BoundArguments (a=10, b=20)> >>> for name, value in bound_args.arguments.items(): ... print(name, "=", value) a = 10 b = 20 >>> del my_tag["a"] >>> bound_args = sig.bind(**my_tag) Traceback (most recent call last): TypeError: missing a required argument: 'a'
Python3提供了一种句法,用于为函数声明中的参数和返回值附加元数据。以下:
# 未加注解 def func(a, b=10): return a + b # 添加注解 def func(a: int, b: 'int > 0' = 10) -> int: return a + b
各个参数能够在冒号后面增长注解表达式,若是有默认值,注解放在冒号和等号之间。上述-> int
是对返回值添加注解的形式。
这些注解都存放在函数的__annotations__
属性中,它是一个字典:
print(func.__annotations__) # 结果 # 'return'表示返回值 {'a': <class 'int'>, 'b': 'int > 0', 'return': <class 'int'>}
Python只是将注解存储在函数的__annotations__
属性中,除此以外,再无任何操做。换句话说,这些注解对Python解释器来讲没有意义。而这些注解的真正用途是提供给IDE、框架和装饰器等工具使用,好比Mypy静态类型检测工具,它就会根据你写的这些注解来检测传入的参数的类型是否符合要求。
inspect
模块能够获取这些注解。inspect.Signature
有个一个return_annotation
属性,它保存返回值的注解;inspect.Parameter
对象中的annotation
属性保存了参数的注解。
函数内省的内容到此结束。后面将介绍标准库中为函数式编程提供支持的经常使用包。
Python并非一个函数式编程语言,但经过operator和functools等包的支持,也能够写出函数式风格的代码。
在函数式编程中,常常须要把算术运算符当作函数使用,好比非递归求阶乘,实现以下:
from functools import reduce def fact(n): return reduce(lambda a, b: a * b, range(1, n + 1))
operator模块为多个算术运算符提供了对应的函数。使用算术运算符函数可将上述代码改写以下:
from functools import reduce from operator import mul def fact(n): return reduce(mul, range(1, n + 1))
operator模块中还有一类函数,能替代从序列中取出元素或读取对象属性的lambda
表达式:itemgetter
和attrgetter
。这两个函数其实会自行构建函数。
如下代码展现了itemgetter
的常见用途:
from operator import itemgetter test_data = [ ("A", 1, "Alpha"), ("B", 3, "Beta"), ("C", 2, "Coco"), ] # 至关于 lambda fields: fields[1] for temp in sorted(test_data, key=itemgetter(1)): print(temp) # 传入多个参数时,它构建的函数返回下标对应的值构成的元组 part_tuple = itemgetter(1, 0) for temp in test_data: print(part_tuple(temp)) # 结果: ('A', 1, 'Alpha') ('C', 2, 'Coco') ('B', 3, 'Beta') (1, 'A') (3, 'B') (2, 'C')
itemgetter
内部使用[]
运算符,所以它不只支持序列,还支持映射和任何实现了__getitem__
方法的类。
attrgetter
和itemgetter
做用相似,它建立的函数根据名称提取对象的属性。若是传入多个属性名,它也会返回属性名对应的值构成的元组。这里要展现的是,若是参数名中包含句点.
,attrgetter
会深刻嵌套对象,获取指定的属性:
from collections import namedtuple from operator import attrgetter 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)), ] 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] # 返回新的元组,获取name属性和嵌套的coord.lat属性 name_lat = attrgetter("name", "coord.lat") for city in sorted(metro_areas, key=attrgetter("coord.lat")): # 嵌套 print(name_lat(city)) # 结果: ('Mexico City', 19.433333) ('Delhi NCR', 28.613889) ('Tokyo', 35.689722)
从名字也可看出,它建立的函数会在对象上调用参数指定的方法(注意是方法,而不是函数)。
>>> from operator import methodcaller >>> s = "The time has come" >>> upcase = methodcaller("upper") >>> upcase(s) # 至关于s.upper() 'THE TIME HAS COME' >>> hiphenate = methodcaller("replace"," ","-") >>> hiphenate(s) # 至关于s.replace(" ", "-") 'The-time-has-come'
从hiphenate
这个例子能够看出,methodcaller
还能够冻结某些参数,即部分应用(partial application),这与functools.partial
函数的做用相似。
functool模块提供了一系列高阶函数,reduce
函数相信你们已经很熟悉了,本节主要介绍其中两个颇有用的函数partial
和它的变体partialmethod
。
functools.partial
用到了一个“闭包”的概念,这个概念的详细内容下一篇再介绍。使用这个函数能够把接收一个或多个参数的函数改编成须要回调的API,这样参数更少。
>>> from operator import mul >>> from functools import partial >>> triple = partial(mul, 3) >>> triple(7) 21 >>> list(map(triple, range(1,10))) # 这里没法直接使用mul函数 [3, 6, 9, 12, 15, 18, 21, 24, 27] >>> triple.func # 访问原函数 <built-in function mul> >>> triple.args # 访问固定参数 (3,) >>> triple.keywords # 访问关键字参数 {}
functools.partialmethod
函数的做用于partial
同样,只不过partialmethod
用于方法,partial
用于函数。
补充:回调函数(callback function)能够简单理解为,当一个函数X被传递给函数A时,函数X就被称为回调函数,函数A调用函数X的过程叫作回调。
本篇首先介绍了函数,包括函数与对象的关系,高阶函数和匿名函数,重点是函数就是对象;随后介绍了函数和可调用对象的关系,以及函数的内省;最后,咱们介绍了关于函数式编程的概念以及与之相关的两个重要模块。
迎你们关注个人微信公众号"代码港" & 我的网站 www.vpointer.net ~