Python学习笔记——作为一等公民的函数

前言

在前面的分享中,我们聊了Python独特的以鸭子类型为核心的面向对象风格,并通过《流畅的Python》中提供的两个经典示例,介绍了Python中的序列和分片的实现逻辑,并从中抽象出Python是如何实现接口和多态的,从而开启了我们的Python学习之路。
延续之前的风格,本系列不作为一种Python“从入门到精通”式速成教程,而是作为对Python底层设计逻辑和一些核心编程思想作探讨的分享式博客系列。
今天,我们来探讨Python第二个与传统编译型语言有重大区别的特性——作为一等公民的函数。如果你曾今也是Java的疯狂推崇者,那么,当你看到Python的这一特性时,也会为这种新奇的使用方案感到兴奋,从而进一步的赞叹:Python作为解释性语言,真的把灵活性发挥到了极致。在过去你无法想象,在命令行里,你可以临时定义一个函数,并把该函数赋值给一个对象,从而动态的实现了对象的行为替换。
而这种令人赞叹的灵活性带来的第一个直观的改变就是:过去面向类定义的设计模式可以简化了,使用过Python的朋友一定会不假思索的联想到装饰器模式。过去在Java中实现装饰器模式需要进行复杂的类定义,而真正进行“装饰”的可能只是类中的一个方法而已,但是不得不使用大量的样板代码。另一方面,装饰类的使用需要加载大量的父类,从而也导致了装饰器的使用会损耗不小的性能。今天所介绍的基于Python的装饰器实现,将能看到一种近乎完美的实现方式。
聊到函数,不可避免的一个话题即是:参数作用域。从进入大学的第一门编程语言C的学习开始,我们就知道作用域的问题一直都是一个让人心烦的问题。而在Python中,由于函数作为一等公民了,那么嵌套函数这一使用方式变成了一种自然,学过JavaScript的同学自然知道,嵌套函数下又会出现闭包问题。
那么今天,咱们将来详细的聊一聊Python中作为一等公民的函数,同时解释这种机制所带来的新的问题,最后给出Python官方所提供的解决方案。

作为一等公民的函数

对于一等公民这个描述,大家自然会问到,在编程语言的世界里,难道有三六九等的划分么,那么这种划分的方式又是什么。
对于第一个问题,我的理解是:没错,在编程语言的世界里,不同类型的对象有不同的权利、作用范围以及生命周期等,同时其赋值、传递和使用方式也大为不同。举一个简单的例子,在过去以Java为代表的编译型语言,哪一类型的对象拥有最多的权限可以作为一等公民呢,显而易见的,大家会提到类(Class)这一概念。对于类(Class),我们可以自定义创建,可以动态赋值,同时能在函数间传递类的实例化对象,同时类能够拥有大量的原生属性,以及Java字节码为其提供支持,由JVM进行直接管理。以类为参考,函数的地位实在太低了,在Java中,函数只能依赖类或者接口而存在,函数在字节码中也不存在自己的属性,更别提动态赋值、参数传递什么的了,所有的这些动态特性,都必须依赖类为媒介,显然成为了一个寄人篱下的弱势公民。这样的局面,很多编程爱好者为函数的地位感到愤慨不平,显然,函数的作用和其地位出现了严重的不对等。
在Python中,函数荣升为一等公民,拥有了自己的原生属性,能够获得Python解释器的直接管理,能够在函数间以参数的形式直接传递,能够进行直接赋值。同时,在Python中,我们可以在模块中,直接定义函数,而不用依赖类。

一个简单示例
>>> def factorial(n):
...     return 1 if n < 2 else n * factorial(n-1)
... 
>>> factorial(5)
120
>>> type(factorial)
<class 'function'>
>>> dir(factorial)
['__annotations__', '__call__', '__class__', '__closure__',...]

该示例演示了在Python客户端中定义的一个简单函数:实现阶层计算。通过内置函数type可以看出Python中对函数有固定的类型支持<class 'function'>,同时通过内省函数dir可查看自定义函数factorial所内置的函数,从而简单的感受了Python对函数的支持。

高阶函数

我们提到一等函数的一个特点即是可以作为参数进行传递。当然,在Python中,将函数作为参数传递给另一个函数,是一个非常简单的操作。同时,我们把能够接受函数这一类型参数的函数称作高阶函数。
同样,我们以一个示例来作为介绍,其中使用到的内置map函数,可接受两个参数,一个是函数,一个是操作数的可迭代对象(如list)

>>> res =  map(factorial, range(1,6))
>>> for temp in res:
...     print(temp)
... 
1
2
6
24
120

除此之外,高阶函数使用的最多的场景即是:在排序或过滤的场景中提供排序条件或过滤条件,另一方面即是在著名的装饰器模式中,以函数作为参数传递给函数修饰器,对于后者,将在后文的专门章节进行介绍,接下来详细介绍第一个使用场景:对于排序的支持。

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

示例解释:首先定义了反转函数reverse,通过分片的小技巧实现字符串倒置,然后依据倒置后的字符串的字典顺序进行排序。
在这个示例中,演示了函数可作为参数进行传递的用法,这个功能在Java中,只能通过复杂的匿名内部类来进行实现,同样会在样板代码以及性能上付出较大的代价。当然,在Java8中千呼万唤始出来的函数式编程才很大程度上缓解了Java在这一方面解决方案匮乏的尴尬局面。

其他对于一等公民函数的支持

前面两节,对作为一等公民的函数做了简单的用法介绍。当然Python对此的支持还不限于此,但由于篇幅的限制以及本系列的设计目标,将不再进行详细的教程般的叙述,更多内容可参考Python的官方文档以及《Python CookBook》和《流畅的 Python》这两本神书。

Pythonic的设计模式

正如前面所提到的Python对于函数的一等公民的支持,那么,自然很多设计模式都可以进行Pythonic的改造实现。

装饰器模式

最为Python使用者所熟练的设计模式自然是装饰器模式了,相比于基于类的修饰器实现方案的好处,在前言中已详细讨论,接下来,咱们简单的看一看这种模式在Python里是怎样的:

>>> def deco(func):
...     def inner():
...             print('running inner()')
...     return inner
... 
>>> @deco
... def target():
...     print('running target()')
... 
>>> target()
running inner()
>>> target
<function deco.<locals>.inner at 0x10b33fb70>

示例注解:这个简单示例只实现了对函数输出内容的替换,当然函数对象本身已经发生了变换,同时本示例也展现出了Python中对注解实现方案的演示。相比于Java,实在变轻了很多。在Python中装饰器的实现逻辑一般分为两种:一种是对函数本身的装饰,能够不替换函数的扩展功能;另一种则是如上例所示,直接替换掉原函数,返回新的函数引用。这个区别,读者可通过内置函数id进行测试。
由于篇幅限制,不再详细介绍装饰器的其他特征,只陈述其一个重要特点:装饰器将在模块加载时立即执行。

其他Pytonic的设计模式实现

除了装饰器以外,对于策略者模式的实现,也能展现出Python函数独特且卓越的实现版本,同时在组合模式、工厂模式方面,我们依旧可以进行Pythonic的改造,本系列将不再一一详细描述,不过这一块在国内外还鲜有专门的书籍,未来可作为一个新的系列进行研究。
至此,有关Python的作为一等公民的函数的讨论就到此为止,相信无论是C阵营的推崇者还是Java的拥护者都会对Python提供的这种函数特性所着迷,从而能有越来越多聪明的头脑加入到Python的阵营。
学习到这,说点题外话,编程语言的魅力不就在于此吗,总结经典并能提出创新,解决传统语言备受诟病的问题,又吸纳和总结出过去各种语言的优点,最后成为一个集大成者,于此,就能成为一个领域的王者,在后C、C++、Java时代,我认为,单从编程语言的设计学逻辑方面,Python就是这样的一个集大成者,能够延续并发掘自身的面向对象风格,同时也能兼顾函数式编程的优势,将函数提升为一等公民,如此,Python也在编程语言的潮流中慢慢的成为佼佼者,咱们这里贴一个2018年全球编程语言的排名:
在这里插入图片描述

一等公民函数带来的闭包问题

第一次接触闭包这个概念,是在JavaScript中,最大的体验就是在Ajax的函数体中,对外部变量引用出现的莫名异常,为什么是莫名呢?因为当时不懂什么是闭包,但是对一个正常的参数访问和修改始终不对,甚是难受。后来,慢慢的了解和学习了闭包这个概念。
那么,首先还是给闭包一个定义:

闭包指:延伸了作用域的函数,其中包含函数定义体中引用、但是不再定义体中定义的非全局变量。

对这个定义,一个最简单的理解就是:只有涉及嵌套函数时才有闭包问题。
那么,我们通过《流畅的Python》中提供的一个经典示例,来讨论Python中函数闭包问题,以及官方对这个问题给出的解决方案。

一个闭包的正确示例:
>>> def  ():
...     series = []
...     
...     def averager(new_value):
...             series.append(new_value)
...             total = sum(series)
...             return total/len(series)
...     
...     return averager
... 
>>> avg(10)
10.0
>>> avg(11)
10.5

示例注解:这个示例中最让人难解的是series变量,任何语言中,只要是对变量作用域有过研究的同学都会问到,series作为一个make_average函数的局部变量,当该函数的一次执行完成后,该变量理所当然的应该被释放掉,那么如何能实现求平均值的功能呢?因此,这里就引出了闭包的概念,在make_average函数中,series是一种自由变量,这是一个全新的概念:

自由变量是指:在本地作用域中绑定的变量

我们可通过函数审查发现这一特征:

>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(<cell at 0x10b297438: list object at 0x10b33c908>,)
>>> avg.__closure__[0].cell_contents
[10, 11]

最后,我们借用Lucina Ramalho对闭包给出的结论:闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定值。

一个错误的示例及解决方案
>>> def make_averager():
...     count = 0
...     total = 0
...     def averager(new_value):
...             count += 1
...             total += new_value
...             return total/count
... 
>>> avg = make_average()
>>> avg(10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable

示例注解:这种改进中,由于对count和total进行了重新的变量绑定,导致将这两变量更改为局部变量,从而导致了出错。对数字、字符串和元组等不可变类型来说,只能读取,不能更新,更新将产生重新绑定的问题,从而将自由变量转换为局部变量。而上例中series通过append的操作进行数据更新,自然规避了此问题。
而对此,Python通过引入nonlocal关键字,将变量声明为自由变量,从而解决了这一问题。

总结

本博客,以一等公民的函数为中心,介绍了Python中对函数的独特支持,同时介绍了这种支持所带来的性能提升,另一方面,由于使用方式的灵活性也带来了新的问题,即闭包问题,最后介绍了Python官方提供的解决方案。至此,将整个逻辑框架进行了大体的梳理,不意在为读者提供全面详实的教程介绍,而意在介绍一种全新的编程语言设计思路,与探讨核心的底层设计逻辑。
通过本文的介绍,有经验的开发者,能学习一种集大成者的全新编程方式;而对于Python新手,可以快速的了解这种Python的高级特性,能够更有动力学习这门很酷的语言。
本系列的初心是希望通过对传统经典的底层实现方案进行分析,从而为大家提供一些最精巧和令人赞叹的设计思路。当你在工作和学习中思路枯竭时,抑或是你遇到难题而愁眉不展时,希望本系列博客能为你带来新的思路源泉。
最后,有任何不正之处,望各位指出,不甚感激~

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值