python函数式编程
在这个由两部分组成的系列文章中,我将讨论如何从函数式编程方法中将思想导入Python,以便兼顾两者。
第一篇文章将探讨不可变数据结构如何提供帮助。 第二部分将使用toolz库探索Python中的高级功能编程概念。
让我们从考虑正方形和矩形开始。 如果我们从接口的角度考虑而忽略了实现细节,那么正方形是矩形的子类型吗?
子类型的定义基于Liskov替换原理 。 为了成为子类型,它必须能够执行超类型所做的所有事情。
我们如何定义矩形的接口?
from zope.
interface
import Interface
class IRectangle
( Interface
) :
def get_length
(
self
) :
"""Squares can do that"""
def get_width
(
self
) :
"""Squares can do that"""
def set_dimensions
(
self
, length
, width
) :
"""Uh oh"""
如果这是定义,则正方形不能是矩形的子类型; 如果长度和宽度不同,它们将无法响应set_dimensions
方法。
另一种方法是选择使矩形不可变 。
class IRectangle
( Interface
) :
def get_length
(
self
) :
"""Squares can do that"""
def get_width
(
self
) :
"""Squares can do that"""
def with_dimensions
(
self
, length
, width
) :
"""Returns a new rectangle"""
现在,正方形可以是矩形。 调用with_dimensions
时,它可以返回一个新的矩形(通常不会是正方形),但它不会停止成为正方形。
这似乎是一个学术问题,直到我们认为正方形和矩形在某种意义上是其侧面的容器。 在我们理解了这个示例之后,更实际的情况是使用了更多的传统容器。 例如,考虑随机存取阵列。
我们有ISquare
和IRectangle
,而ISquare
是IRectangle
的子类型。
我们想将矩形放在随机访问数组中:
class IArrayOfRectangles
( Interface
) :
def get_element
(
self
, i
) :
"""Returns Rectangle"""
def set_element
(
self
, i
, rectangle
) :
"""'rectangle' can be any IRectangle"""
我们也想将正方形放在随机访问数组中:
class IArrayOfSquare
( Interface
) :
def get_element
(
self
, i
) :
"""Returns Square"""
def set_element
(
self
, i
, square
) :
"""'square' can be any ISquare"""
即使ISquare
是IRectangle
的子类型,任何数组都不能同时实现IArrayOfSquare
和IArrayOfRectangle
。
为什么不? 假设bucket
实现。
>>> rectangle
= make_rectangle
(
3
,
4
)
>>> bucket.
set_element
(
0
, rectangle
)
# This is allowed by IArrayOfRectangle
>>> thing
= bucket.
get_element
(
0
)
# That has to be a square by IArrayOfSquare
>>>
assert thing.
height
== thing.
width
Traceback
( most recent call last
) :
File
"<stdin>"
, line
1
,
in
< module
>
AssertionError
即使ISquare
是IRectangle
的子类型,也无法实现两者都意味着它们都不是另一个子类型。 问题是set_element
方法:如果我们有一个只读数组, IArrayOfSquare
将是一个亚型IArrayOfRectangle
。
在可变的IRectangle
接口和可变的IArrayOf*
接口中,可变性都使思考类型和子类型变得更加困难-放弃变异的能力意味着我们期望类型之间的直觉关系真正成立。
突变也可能具有非局部作用。 当两个位置之间的共享对象被一个突变时,就会发生这种情况。 经典示例是一个线程使另一个线程改变共享对象,但是即使在单线程程序中,相距较远的地方也很容易共享。 考虑到在Python中,大多数对象可以从许多地方访问:作为全局模块,在堆栈跟踪中或作为类属性。
如果我们不能限制共享,我们可能会考虑限制可变性。
这是一个利用attrs库的不可变矩形:
@ attr.
s
( frozen
=
True
)
class Rectange
(
object
) :
length
= attr.
ib
(
)
width
= attr.
ib
(
)
@
classmethod
def with_dimensions
( cls
, length
, width
) :
return cls
( length
, width
)
这是一个正方形:
@ attr.
s
( frozen
=
True
)
class Square
(
object
) :
side
= attr.
ib
(
)
@
classmethod
def with_dimensions
( cls
, length
, width
) :
return Rectangle
( length
, width
)
使用frozen
参数,我们可以轻松地使attrs
创建的类是不可变的。 正确编写__setitem__
所有辛苦工作已经由他人完成,并且对我们来说是完全看不见的。
修改对象仍然很容易。 对它们进行突变几乎是不可能的。
too_long
= Rectangle
(
100
,
4
)
reasonable
= attr.
evolve
( too_long
, length
=
10
)
Pyrsistent软件包使我们能够拥有不可变的容器。
# Vector of integers
a = pyrsistent.v(1, 2, 3)
# Not a vector of integers
b = a.set(1, "hello")
虽然b
不是整数的向量,但没有什么能阻止a
成为整数。
如果a
长一百万个元素怎么办? b
会复制其中的999,999吗? Pyrsistent具有“ big O”性能保证:所有操作都花费O(log n)
时间。 它还带有一个可选的C扩展,以提高性能,超越了大O。
为了修改嵌套对象,它带有“变压器”的概念:
blog
= pyrsistent.
m
(
title
=
"My blog"
,
links
= pyrsistent.
v
(
"github"
,
"twitter"
)
,
posts
= pyrsistent.
v
(
pyrsistent.
m
( title
=
"no updates"
,
content
=
"I'm busy"
)
,
pyrsistent.
m
( title
=
"still no updates"
,
content
=
"still busy"
)
)
)
new_blog
= blog.
transform
(
[
"posts"
,
1
,
"content"
]
,
"pretty busy"
)
现在, new_blog
将等于
{
'links' :
[
'github'
,
'twitter'
]
,
'posts' :
[
{
'content' :
"I'm busy"
,
'title' :
'no updates'
}
,
{
'content' :
'pretty busy'
,
'title' :
'still no updates'
}
]
,
'title' :
'My blog'
}
但是blog
还是一样。 这意味着引用旧对象的任何人都不会受到影响:转换仅具有局部效果。
当共享猖ramp时,这很有用。 例如,考虑默认参数:
def silly_sum
( a
, b
, extra
= v
(
1
,
2
)
) :
extra
= extra.
extend
(
[ a
, b
]
)
return
sum
( extra
)
在这篇文章中,我们了解了为什么不变性对于思考我们的代码很有用,以及如何在不牺牲性能的前提下实现它。 下次,我们将学习不可变对象如何使我们能够使用功能强大的编程构造。
翻译自: https://opensource.com/article/18/10/functional-programming-python-immutable-data-structures
python函数式编程