利用Fn.py库在Python中进行函数式编程

尽管Python事实上并不是一门纯函数式编程语言,但它本身是一门多范型语言,并给了你足够的自由利用函数式编程的便利。函数式风格有着各种理论与实际上的好处(你可以在Python的文档中找到这个列表):

  • 形式上可证
  • 模块性
  • 组合性
  • 易于调试及测试

虽然这份列表已经描述得够清楚了,但我还是很喜欢Michael O.Church在他的文章“函数式程序极少腐坏(Functional programs rarely rot)”中对函数式编程的优点所作的描述。我在PyCon UA 2012期间的讲座“Functional Programming with Python”中谈论了在Python中使用函数式方式的内容。我也提到,在你尝试在Python中编写可读同时又可维护的函数式代码时,你会很快发现诸多问题。

fn.py类库就是为了应对这些问题而诞生的。尽管它不可能解决所有问题,但对于希望从函数式编程方式中获取最大价值的开发者而言,它是一块“电池”,即使是在命令式方式占主导地位的程序中,也能够发挥作用。那么,它里面都有些什么呢?
Scala风格的Lambda定义

在Python中创建Lambda函数的语法非常冗长,来比较一下:

Python

map(lambda x: x*2, [1,2,3])

Scala

代码如下:

List(1,2,3).map(_*2)

Clojure

代码如下:

(map #(* % 2) '(1 2 3))

Haskell

代码如下:

map (2*) [1,2,3]

受Scala的启发,Fn.py提供了一个特别的_对象以简化Lambda语法。

from fn import _

assert (_ + _)(10, 5) = 15
assert list(map(_ * 2, range(5))) == [0,2,4,6,8]
assert list(filter(_ < 10, [9,10,11])) == [9]

除此之外还有许多场景可以使用_:所有的算术操作、属性解析、方法调用及分片算法。如果你不确定你的函数具体会做些什么,你可以将结果打印出来:

from fn import _ 

print (_ + 2) # "(x1) => (x1 + 2)"
print (_ + _ * _) # "(x1, x2, x3) => (x1 + (x2 * x3))"

流(Stream)及无限序列的声明

Scala风格的惰性求值(Lazy-evaluated)流。其基本思路是:对每个新元素“按需”取值,并在所创建的全部迭代中共享计算出的元素值。Stream对象支持<<操作符,代表在需要时将新元素推入其中。

惰性求值流对无限序列的处理是一个强大的抽象。我们来看看在函数式编程语言中如何计算一个斐波那契序列。

Haskell

代码如下:

fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

Clojure

代码如下:

(def fib (lazy-cat [0 1] (map + fib (rest fib))))

Scala

代码如下:

def fibs: Stream[Int] =
     0 #:: 1 #:: fibs.zip(fibs.tail).map{case (a,b) => a + b}

现在你可以在Python中使用同样的方式了:

from fn import Stream
from fn.iters import take, drop, map
from operator import add

f = Stream()
fib = f << [0, 1] << map(add, f, drop(1, f))

assert list(take(10, fib)) == [0,1,1,2,3,5,8,13,21,34]
assert fib[20] == 6765
assert list(fib[30:35]) == [832040,1346269,2178309,3524578,5702887]

蹦床(Trampolines)修饰符

fn.recur.tco是一个不需要大量栈空间分配就可以处理TCO的临时方案。让我们先从一个递归阶乘计算示例开始:

def fact(n):
   if n == 0: return 1
   return n * fact(n-1)

这种方式也能工作,但实现非常糟糕。为什么呢?因为它会递归式地保存之前的计算值以算出最终结果,因此消耗了大量的存储空间。如果你对一个很大的n值(超过了sys.getrecursionlimit()的值)执行这个函数,CPython就会以此方式失败中止:

>>> import sys
>>> fact(sys.getrecursionlimit() * 2)
... many many lines of stacktrace ...
RuntimeError: maximum recursion depth exceeded

这也是件好事,至少它避免了在你的代码中产生严重错误。

我们如何优化这个方案呢?答案很简单,只需改变函数以使用尾递归即可:

def fact(n, acc=1):
   if n == 0: return acc
   return fact(n-1, acc*n)

为什么这种方式更佳呢?因为你不需要保留之前的值以计算出最终结果。可以在Wikipedia上查看更多尾递归调用优化的内容。可是……Python的解释器会用和之前函数相同的方式执行这段函数,结果是你没得到任何优化。

fn.recur.tco为你提供了一种机制,使你可以使用“蹦床”方式获得一定的尾递归优化。同样的方式也使用在诸如Clojure语言中,主要思路是将函数调用序列转换为while循环。

from fn import recur

@recur.tco
def fact(n, acc=1):
   if n == 0: return False, acc
   return True, (n-1, acc*n)

@recur.tco是一个修饰符,能将你的函数执行转为while循环并检验其输出内容:

  • (False, result)代表运行完毕
  • (True, args, kwargs)代表我们要继续调用函数并传递不同的参数
  • (func, args, kwargs)代表在while循环中切换要执行的函数

函数式风格的错误处理

假设你有一个Request类,可以按照传入其中的参数名称得到对应的值。要想让其返回值格式为全大写、非空并且去除头尾空格的字符串,你需要这样写:

class Request(dict):
   def parameter(self, name):
     return self.get(name, None)

r = Request(testing="Fixed", empty=" ")
param = r.parameter("testing")
if param is None:
   fixed = ""
else:
   param = param.strip()
   if len(param) == 0:
     fixed = ""
   else:
    fixed = param.upper()

额,看上去有些古怪。用fn.monad.Option来修改你的代码吧,它代表了可选值,每个Option实例可代表一个Full或者Empty(这点也受到了Scala中Option的启发)。它为你编写长运算序列提供了简便的方法,并且去掉除了许多if/else语句块。

from operator import methodcaller
from fn.monad import optionable

class Request(dict):
   @optionable
   def parameter(self, name):
     return self.get(name, None)

r = Request(testing="Fixed", empty=" ")
fixed = r.parameter("testing")
     .map(methodcaller("strip"))
     .filter(len)
     .map(methodcaller("upper"))
     .get_or("")

fn.monad.Option.or_call是个便利的方法,它允许你进行多次调用尝试以完成计算。例如,你有一个Request类,它有type,mimetype和url等几个可选属性,你需要使用最少一个属性值以分析它的“request类型”:

from fn.monad import Option 

request = dict(url="face.png", mimetype="PNG")
tp = Option \
     .from_value(request.get("type", None)) \ # check "type" key first
     .or_call(from_mimetype, request) \ # or.. check "mimetype" key
     .or_call(from_extension, request) \ # or... get "url" and check extension
     .get_or("application/undefined")

其余事项?

我仅仅描述了类库的一小部分,你还能够找到并使用以下功能:

  • 22个附加的itertools代码段,以扩展内置module的功能的附加功能
  • 将Python 2和Python 3的迭代器(iterator)(如range,map及filtter等等)使用进行了统一,这对使用跨版本的类库时非常有用
  • 为函数式组合及partial函数应用提供了简便的语法
  • 为使用高阶函数(apply,flip等等)提供了附加的操作符

正在进行中的工作

自从在Github上发布这个类库以来,我从社区中收到了许多审校观点、意见和建议,以及补丁和修复。我也在继续增强现有功能,并提供新的特性。近期的路线图包括以下内容:

  • 为使用可迭代对象(iterable),如foldl,foldr增加更多操作符
  • 更多的monad,如fn.monad.Either,以处理错误记录
  • 为大多数module提供C-accelerator
  • 为简化lambda arg1: lambda arg2:…形式而提供的curry函数的生成器
  • 更多文档,更多测试,更多示例代码
(0)

相关推荐

  • 实例讲解python函数式编程

    函数式编程是使用一系列函数去解决问题,按照一般编程思维,面对问题时我们的思考方式是"怎么干",而函数函数式编程的思考方式是我要"干什么". 至于函数式编程的特点暂不总结,我们直接拿例子来体会什么是函数式编程. lambda表达式(匿名函数): 普通函数与匿名函数的定义方式: 复制代码 代码如下: #普通函数def add(a,b):    return a + b print add(2,3) #匿名函数add = lambda a,b : a + bprint a

  • Python函数式编程指南(一):函数式编程概述

    1. 函数式编程概述 1.1. 什么是函数式编程? 函数式编程使用一系列的函数解决问题.函数仅接受输入并产生输出,不包含任何能影响产生输出的内部状态.任何情况下,使用相同的参数调用函数始终能产生同样的结果. 在一个函数式的程序中,输入的数据"流过"一系列的函数,每一个函数根据它的输入产生输出.函数式风格避免编写有"边界效应"(side effects)的函数:修改内部状态,或者是其他无法反应在输出上的变化.完全没有边界效应的函数被称为"纯函数式的"

  • Python函数式编程指南(二):从函数开始

    2. 从函数开始 2.1. 定义一个函数 如下定义了一个求和函数: 复制代码 代码如下: def add(x, y):     return x + y 关于参数和返回值的语法细节可以参考其他文档,这里就略过了. 使用lambda可以定义简单的单行匿名函数.lambda的语法是: 复制代码 代码如下: lambda args: expression 参数(args)的语法与普通函数一样,同时表达式(expression)的值就是匿名函数调用的返回值:而lambda表达式返回这个匿名函数.如果我们

  • 用Python进行基础的函数式编程的教程

    许多函数式文章讲述的是组合,流水线和高阶函数这样的抽象函数式技术.本文不同,它展示了人们每天编写的命令式,非函数式代码示例,以及将这些示例转换为函数式风格. 文章的第一部分将一些短小的数据转换循环重写成函数式的maps和reduces.第二部分选取长一点的循环,把他们分解成单元,然后把每个单元改成函数式的.第三部分选取一个很长的连续数据转换循环,然后把它分解成函数式流水线. 示例都是用Python写的,因为很多人觉得Python易读.为了证明函数式技术对许多语言来说都相同,许多示例避免使用Pyt

  • Python函数式编程

    主要内容 1.函数基本语法及特性 2.参数与局部变 3.返回值 4.递归 5.名函数 6.函数式编程介绍 7.阶函数 8.内置函数 函数基本语法及特性 定义 数学函数定义:一般的,在一个变化过程中,如果有两个变量x和y,并且对于x的每一 个确定的值,y都有唯一确定的值与其对应,那么我们就把x称为自变量,把y称为因变 量,y是x的函数.自变量x的取值范围叫做这个函数的定义域. 但编程中的「函数」概念,与数学中的函数是有很 同的 函数是逻辑结构化和过程化的一种编程方法 函数的优点 减少重复代码 使程

  • Python函数式编程指南(四):生成器详解

    4. 生成器(generator) 4.1. 生成器简介 首先请确信,生成器就是一种迭代器.生成器拥有next方法并且行为与迭代器完全相同,这意味着生成器也可以用于Python的for循环中.另外,对于生成器的特殊语法支持使得编写一个生成器比自定义一个常规的迭代器要简单不少,所以生成器也是最常用到的特性之一. 从Python 2.5开始,[PEP 342:通过增强生成器实现协同程序]的实现为生成器加入了更多的特性,这意味着生成器还可以完成更多的工作.这部分我们会在稍后的部分介绍. 4.2. 生成

  • Python函数式编程指南(三):迭代器详解

    3. 迭代器 3.1. 迭代器(Iterator)概述 迭代器是访问集合内元素的一种方式.迭代器对象从集合的第一个元素开始访问,直到所有的元素都被访问一遍后结束. 迭代器不能回退,只能往前进行迭代.这并不是什么很大的缺点,因为人们几乎不需要在迭代途中进行回退操作. 迭代器也不是线程安全的,在多线程环境中对可变集合使用迭代器是一个危险的操作.但如果小心谨慎,或者干脆贯彻函数式思想坚持使用不可变的集合,那这也不是什么大问题. 对于原生支持随机访问的数据结构(如tuple.list),迭代器和经典fo

  • Python装饰器的函数式编程详解

    Python的装饰器的英文名叫Decorator,当你看到这个英文名的时候,你可能会把其跟Design Pattern里的Decorator搞混了,其实这是完全不同的两个东西.虽然好像,他们要干的事都很相似--都是想要对一个已有的模块做一些"修饰工作",所谓修饰工作就是想给现有的模块加上一些小装饰(一些小功能,这些小功能可能好多模块都会用到),但又不让这个小装饰(小功能)侵入到原有的模块中的代码里去.但是OO的Decorator简直就是一场恶梦,不信你就去看看wikipedia上的词条

  • 利用Fn.py库在Python中进行函数式编程

    尽管Python事实上并不是一门纯函数式编程语言,但它本身是一门多范型语言,并给了你足够的自由利用函数式编程的便利.函数式风格有着各种理论与实际上的好处(你可以在Python的文档中找到这个列表): 形式上可证 模块性 组合性 易于调试及测试 虽然这份列表已经描述得够清楚了,但我还是很喜欢Michael O.Church在他的文章"函数式程序极少腐坏(Functional programs rarely rot)"中对函数式编程的优点所作的描述.我在PyCon UA 2012期间的讲座

  • Python中的函数式编程:不可变的数据结构

    让我们首先考虑正方形和长方形.如果我们认为在接口方面,忽略了实现细节,方块是否是矩形的子类型? 子类型的定义取决于Liskov代换原理.为了成为一个子类型,它必须能够完成超级类型所做的一切. 如何定义矩形的接口? zope.interface import Interface class IRectangleInterface: get_length: """Squares can do that""" get_width: "&quo

  • 通过Turtle库在Python中绘制一个鼠年福鼠

    turtle库是一个很经典的绘图库,其最初来自于1967年创造的logo编程语言,之后被Python编写放到了Python的内置模块中.网络上有很多借助于turtle绘制精美图像的案例.比如小猪佩奇.皮卡丘.柯基犬等等.趁着新年假期还未结束,今天州的先生(https://zmister.com)为大家带来一个福鼠的绘制. 一.绘制鼠头 首先,咱们(https://zmister.com)把鼠的头给绘制了.鼠头主要是由圆来构成,脸庞是一个大圆,耳朵.眉毛.眼睛.嘴角和鼻子也都是由不同弧度的圆构成.

  • Python中的并发编程实例

    一.简介 我们将一个正在运行的程序称为进程.每个进程都有它自己的系统状态,包含内存状态.打开文件列表.追踪指令执行情况的程序指针以及一个保存局部变量的调用栈.通常情况下,一个进程依照一个单序列控制流顺序执行,这个控制流被称为该进程的主线程.在任何给定的时刻,一个程序只做一件事情. 一个程序可以通过Python库函数中的os或subprocess模块创建新进程(例如os.fork()或是subprocess.Popen()).然而,这些被称为子进程的进程却是独立运行的,它们有各自独立的系统状态以及

  • python中asyncio异步编程学习

    1.   想学asyncio,得先了解协程 携程的意义: 计算型的操作,利用协程来回切换执行,没有任何意义,来回切换并保存状态 反倒会降低性能. IO型的操作,利用协程在IO等待时间就去切换执行其他任务,当IO操作结束后再自动回调,那么就会大大节省资源并提供性能,从而实现异步编程(不等待任务结束就可以去执行其他代码 2.协程和多线程之间的共同点和区别: 共同点: 都是并发操作,多线程同一时间点只能有一个线程在执行,协程同一时间点只能有一个任务在执行: 不同点: 多线程,是在I/O阻塞时通过切换线

  • 浅析Python中的元编程

    目录 什么是元编程 元编程应用场景 综合实战 什么是元编程 Python元编程是指在运行时对Python代码进行操作的技术,它可以动态地生成.修改和执行代码,从而实现一些高级的编程技巧.Python的元编程包括元类.装饰器.动态属性和动态导入等技术,这些技术都可以帮助我们更好地理解和掌握Python语言的特性和机制.元编程在一些场景下非常有用,比如实现ORM框架.实现特定领域的DSL.动态修改类的行为等.掌握好Python元编程技术可以提高我们的编程能力和代码质量. 想要搞定元编程,必须要理解和

  • JavaScript中的函数式编程详解

    函数式编程 函数式编程是一种编程范式,是一种构建计算机程序结构和元素的风格,它把计算看作是对数学函数的评估,避免了状态的变化和数据的可变,与函数式编程相对的是命令式编程.我们有这样一个需求,给数组的每个数字加一: // 数组每个数字加一, 命令式编程 let arr = [1, 2, 3, 4]; let newArr = []; for(let i = 0; i < arr.length; i++){ newArr.push(arr[i] + 1); } console.log(newArr)

  • Go语言中的函数式编程实践

    本文主要讲解Go语言中的函数式编程概念和使用,分享给大家,具体如下: 主要知识点: Go语言对函数式编程的支持主要体现在闭包上面 闭包就是能够读取其他函数内部变量的函数.只有函数内部的子函数才能读取局部变量,所以闭包可以理解成"定义在一个函数内部的函数".在本质上,闭包是将函数内部和函数外部连接起来的桥梁. 学习闭包的基本使用 标准的闭包具有不可变性:不能有状态,只能有常量和函数,而且函数只能有一个参数,但是一般可以不用严格遵守 使用闭包 实现 斐波那契数列 学习理解函数实现接口 使用

  • Java中的函数式编程

    目录 1.Lambda 2.函数接口 2.1 函数描述符 3.Java函数接口 3.1 Predicate 3.2 Consumer 3.3 Function 3.4 Supplier 3.5 Primitive Specializations 4.类型检查 4.1 Capturing Lambda 5.方法引用 5.1 构造函数引用 5.2 组合Lambda 5.3 Comparators 5.4 Predicates 5.5 Functions 6.总结 函数式编程是一种编程范式,其中程序是

  • 详解Python中的多线程编程

    一.简介 多线程编程技术可以实现代码并行性,优化处理能力,同时功能的更小划分可以使代码的可重用性更好.Python中threading和Queue模块可以用来实现多线程编程. 二.详解 1.线程和进程        进程(有时被称为重量级进程)是程序的一次执行.每个进程都有自己的地址空间.内存.数据栈以及其它记录其运行轨迹的辅助数据.操作系统管理在其上运行的所有进程,并为这些进程公平地分配时间.进程也可以通过fork和spawn操作来完成其它的任务,不过各个进程有自己的内存空间.数据栈等,所以只

随机推荐