用Python进行基础的函数式编程的教程
|
许多函数式文章讲述的是组合,流水线和高阶函数这样的抽象函数式技术。本文不同,它展示了人们每天编写的命令式,非函数式代码示例,以及将这些示例转换为函数式风格。 文章的第一部分将一些短小的数据转换循环重写成函数式的maps和reduces。第二部分选取长一点的循环,把他们分解成单元,然后把每个单元改成函数式的。第三部分选取一个很长的连续数据转换循环,然后把它分解成函数式流水线。 示例都是用Python写的,因为很多人觉得Python易读。为了证明函数式技术对许多语言来说都相同,许多示例避免使用Python特有的语法:map,reduce,pipeline。 当人们谈论函数式编程,他们会提到非常多的“函数式”特性。提到不可变数据1,第一类对象2以及尾调用优化3。这些是帮助函数式编程的语言特征。提到mapping(映射),reducing(归纳),piplining(管道),recursing(递归),currying4(科里化);以及高阶函数的使用。这些是用来写函数式代码的编程技术。提到并行5,惰性计算6以及确定性。这些是有利于函数式编程的属性。 忽略全部这些。可以用一句话来描述函数式代码的特征:避免副作用。它不会依赖也不会改变当前函数以外的数据。所有其他的“函数式”的东西都源于此。当你学习时把它当做指引。 这是一个非函数式方法: a = 0 def increment1(): global a a += 1 这是一个函数式的方法: def increment2(a): return a + 1 不要在lists上迭代。使用map和reduce。 Map接受一个方法和一个集合作为参数。它创建一个新的空集合,以每一个集合中的元素作为参数调用这个传入的方法,然后把返回值插入到新创建的集合中。最后返回那个新集合。 这是一个简单的map,接受一个存放名字的list,并且返回一个存放名字长度的list: name_lengths = map(len,["Mary","Isla","Sam"]) print name_lengths # => [4,4,3] 接下来这个map将传入的collection中每个元素都做平方操作: squares = map(lambda x: x * x,[0,1,2,3,4]) print squares # => [0,9,16] 这个map并没有使用一个命名的方法。它是使用了一个匿名并且内联的用lambda定义的方法。lambda的参数定义在冒号左边。方法主体定义在冒号右边。返回值是方法体运行的结果。 下面的非函数式代码接受一个真名列表,然后用随机指定的代号来替换真名。 import random names = ['Mary','Isla','Sam'] code_names = ['Mr. Pink','Mr. Orange','Mr. Blonde'] for i in range(len(names)): names[i] = random.choice(code_names) print names # => ['Mr. Blonde','Mr. Blonde','Mr. Blonde'] (正如你所见的,这个算法可能会给多个密探同一个秘密代号。希望不会在任务中混淆。) 这个可以用map重写: import random names = ['Mary','Sam'] secret_names = map(lambda x: random.choice(['Mr. Pink','Mr. Blonde']),names) 练习1.尝试用map重写下面的代码。它接受由真名组成的list作为参数,然后用一个更加稳定的策略产生一个代号来替换这些名字。 names = ['Mary','Sam'] for i in range(len(names)): names[i] = hash(names[i]) print names # => [6306819796133686941,8135353348168144921,-1228887169324443034] (希望密探记忆力够好,不要在执行任务时把代号忘记了。) 我的解决方案: names = ['Mary','Sam'] secret_names = map(hash,names) Reduce(迭代) Reduce 接受一个方法和一个集合做参数。返回通过这个方法迭代容器中所有元素产生的结果。 这是个简单的reduce。返回集合中所有元素的和。 sum = reduce(lambda a,x: a + x,4]) print sum # => 10 x是迭代的当前元素。a是累加和也就是在之前的元素上执行lambda返回的值。reduce()遍历元素。每次迭代,在当前的a和x上执行lambda然后返回结果作为下一次迭代的a。 第一次迭代的a是什么?在这之前没有迭代结果传进来。reduce() 使用集合中的第一个元素作为第一次迭代的a,然后从第二个元素开始迭代。也就是说,第一个x是第二个元素。 这段代码记'Sam'这个词在字符串列表中出现的频率:
sentences = ['Mary read a story to Sam and Isla.','Isla cuddled Sam.','Sam chortled.']
sam_count = 0
for sentence in sentences:
sam_count += sentence.count('Sam')
print sam_count
# => 3
下面这个是用reduce写的:
sentences = ['Mary read a story to Sam and Isla.','Sam chortled.']
sam_count = reduce(lambda a,x: a + x.count('Sam'),sentences,0)
这段代码如何初始化a?出现‘Sam'的起始点不能是'Mary read a story to Sam and Isla.' 初始的累加和由第三个参数来指定。这样就允许了集合中元素的类型可以与累加器不同。 首先,它们大多是一行代码。 二、迭代中最重要的部分:集合,操作和返回值,在所有的map和reduce中总是在相同的位置。 三、循环中的代码可能会改变之前定义的变量或之后要用到的变量。照例,map和reduce是函数式的。 四、map和reduce是元素操作。每次有人读到for循环,他们都要逐行读懂逻辑。几乎没有什么规律性的结构可以帮助理解代码。相反,map和reduce都是创建代码块来组织复杂的算法,并且读者也能非常快的理解元素并在脑海中抽象出来。“嗯,代码在转换集合中的每一个元素。然后结合处理的数据成一个输出。” 五、map和reduce有许多提供便利的“好朋友”,它们是基本行为的修订版。例如filter,all,any以及find。 练习2。尝试用map,reduce和filter重写下面的代码。Filter接受一个方法和一个集合。返回集合中使方法返回true的元素。
people = [{'name': 'Mary','height': 160},{'name': 'Isla','height': 80},{'name': 'Sam'}]
height_total = 0
height_count = 0
for person in people:
if 'height' in person:
height_total += person['height']
height_count += 1
if height_count > 0:
average_height = height_total / height_count
print average_height
# => 120
如果这个比较棘手,试着不要考虑数据上的操作。考虑下数据要经过的状态,从people字典列表到平均高度。不要尝试把多个转换捆绑在一起。把每一个放在独立的一行,并且把结果保存在命名良好的变量中。代码可以运行后,立刻凝练。 我的方案:
people = [{'name': 'Mary',{'name': 'Sam'}]
heights = map(lambda x: x['height'],filter(lambda x: 'height' in x,people))
if len(heights) > 0:
from operator import add
average_height = reduce(add,heights) / len(heights)
写声明式代码,而不是命令式 下面的程序演示三辆车比赛。每次移动时间,每辆车可能移动或者不动。每次移动时间程序会打印到目前为止所有车的路径。五次后,比赛结束。 下面是某一次的输出: - -- -- -- -- --- --- -- --- ---- --- ---- ---- ---- ----- 这是程序:
from random import random
time = 5
car_positions = [1,1]
while time:
# decrease time
time -= 1
print ''
for i in range(len(car_positions)):
# move car
if random() > 0.3:
car_positions[i] += 1
# draw car
print '-' * car_positions[i]
代码是命令式的。一个函数式的版本应该是声明式的。应该描述要做什么,而不是怎么做。 通过绑定代码片段到方法里,可以使程序更有声明式的味道。
from random import random
def move_cars():
for i,_ in enumerate(car_positions):
if random() > 0.3:
car_positions[i] += 1
def draw_car(car_position):
print '-' * car_position
def run_step_of_race():
global time
time -= 1
move_cars()
def draw():
print ''
for car_position in car_positions:
draw_car(car_position)
time = 5
car_positions = [1,1]
while time:
run_step_of_race()
draw()
想要理解这段代码,读者只需要看主循环。”如果time不为0,运行下run_step_of_race和draw,在检查下time。“如果读者想更多的理解这段代码中的run_step_of_race或draw,可以读方法里的代码。 注释没有了。代码是自描述的。 把代码分解提炼进方法里是非常好且十分简单的提高代码可读性的方法。 这个技术用到了方法,但是只是当做常规的子方法使用,只是简单地将代码打包。根据指导,这些代码不是函数式的。代码中的方法使用了状态,而不是传入参数。方法通过改变外部变量影响了附近的代码,而不是通过返回值。为了搞清楚方法做了什么,读者必须仔细阅读每行。如果发现一个外部变量,必须找他它的出处,找到有哪些方法修改了它。 下面是函数式的版本:
from random import random
def move_cars(car_positions):
return map(lambda x: x + 1 if random() > 0.3 else x,car_positions)
def output_car(car_position):
return '-' * car_position
def run_step_of_race(state):
return {'time': state['time'] - 1,'car_positions': move_cars(state['car_positions'])}
def draw(state):
print ''
print 'n'.join(map(output_car,state['car_positions']))
def race(state):
draw(state)
if state['time']:
race(run_step_of_race(state))
race({'time': 5,'car_positions': [1,1]})
(编辑:安卓应用网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
