Python的“with”是单子的吗?
就像许多勇敢的先驱一样,我正在努力穿越理解单子(Monad)的无边荒野。
我还在摸索中,但我注意到Python的with
语句似乎有点像单子的特性。想想这个代码片段:
with open(input_filename, 'r') as f:
for line in f:
process(line)
可以把open()
这个调用看作“单位”,而代码块本身则是“绑定”。实际上,单子并没有被直接展示(呃,除非f
就是单子),但这个模式是存在的,对吧?还是我只是把函数式编程(FP)都误认为是单子?或者说现在是凌晨三点,什么都显得合理?
还有一个相关的问题:如果我们有单子,那还需要异常处理吗?
在上面的代码片段中,任何输入输出的失败都可以被隐藏起来。比如磁盘损坏、缺少指定的文件,或者文件为空,这些情况都可以被统一处理。所以不需要明显的输入输出异常。
当然,Scala的Option
类型类已经消除了令人头疼的Null Pointer Exception
。如果你把数字重新理解为单子(把NaN
和DivideByZero
当作特殊情况)……
就像我说的,现在是凌晨三点。
4 个回答
在Haskell中,有一个和Python的with
类似的东西,叫做withFile
。这个:
with open("file1", "w") as f:
with open("file2", "r") as g:
k = g.readline()
f.write(k)
相当于:
withFile "file1" WriteMode $ \f ->
withFile "file2" ReadMode $ \g ->
do k <- hGetLine g
hPutStr f k
现在,withFile
看起来可能有点像单子(monadic)。它的类型是:
withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r
右边看起来像(a -> m b) -> m b
。
还有一个相似之处:在Python中,你可以省略as
,而在Haskell中,你可以用>>
代替>>=
(或者在do
块中不使用<-
箭头)。
所以我来回答这个问题:withFile
是单子吗?
你可以想象它可以这样写:
do f <- withFile "file1" WriteMode
g <- withFile "file2" ReadMode
k <- hGetLine g
hPutStr f k
但这样写是不对的,类型检查不通过。它不能这样写。
这是因为在Haskell中IO单子是顺序的:如果你写
do x <- a
y <- b
c
那么在a
执行后,b
会执行,然后是c
。没有“回溯”来在最后清理a
之类的东西。另一方面,withFile
在块执行完后必须关闭文件句柄。
还有另一种单子,叫做继续单子(continuation monad),它允许做这样的事情。不过,这样你就有了两个单子,IO和继续单子,同时使用这两个单子的效果需要用到单子变换器。
import System.IO
import Control.Monad.Cont
k :: ContT r IO ()
k = do f <- ContT $ withFile "file1" WriteMode
g <- ContT $ withFile "file2" ReadMode
lift $ hGetLine g >>= hPutStr f
main = runContT k return
这就显得很麻烦。所以答案是:有点像,但这需要处理很多细节,让这个问题变得相当复杂。
Python的with
只能模拟单子能做的有限部分——添加进入和结束的代码。我觉得你不能用with
来模拟,比如:
do x <- [2,3,4]
y <- [0,1]
return (x+y)
(可能用一些小技巧可以做到)。相反,使用for
:
for x in [2,3,4]:
for y in [0,1]:
print x+y
而且Haskell中有一个函数可以做到这一点——forM
:
forM [2,3,4] $ \x ->
forM [0,1] $ \y ->
print (x+y)
我推荐你了解一下yield
,它和单子更相似,而不是with
:
http://www.valuedlessons.com/2008/01/monads-in-python-with-nice-syntax.html
一个相关的问题是:如果我们有单子,是否还需要异常?
基本上不需要,与你其余的函数不同的是,你可以写一个返回Either A B
的函数,而不是一个抛出A或返回B的函数。Either A
的单子会像异常一样工作——如果某一行代码返回错误,整个块都会返回错误。
不过,这意味着除法的类型会是Integer -> Integer -> Either Error Integer
,以捕捉除以零的错误。你必须在任何使用除法或有可能出错的代码中检测错误(显式模式匹配或使用绑定)。Haskell使用异常来避免这样做。
这件事可能听起来有点简单,但第一个问题是,with
不是一个函数,也不接受函数作为参数。你可以通过为 with
写一个函数包装器来轻松解决这个问题:
def withf(context, f):
with context as x:
f(x)
因为这件事太简单了,你甚至可以不去区分 withf
和 with
。
第二个问题是,with
作为一个语句而不是表达式,它没有值。如果你要给它一个类型,那就是 M a -> (a -> None) -> None
(这实际上是上面 withf
的类型)。实际上,你可以使用 Python 的 _
来获取 with
语句的值。在 Python 3.1 中:
class DoNothing (object):
def __init__(self, other):
self.other = other
def __enter__(self):
print("enter")
return self.other
def __exit__(self, type, value, traceback):
print("exit %s %s" % (type, value))
with DoNothing([1,2,3]) as l:
len(l)
print(_ + 1)
由于 withf
使用的是函数而不是代码块,_
的一个替代方法是返回函数的值:
def withf(context, f):
with context as x:
return f(x)
还有一个原因让 with
(和 withf
)不能成为单子绑定。代码块的值必须是与 with
项相同类型构造器的单子类型。现在,with
更加通用。考虑到 agf 提到的每个接口都是一个类型构造器,我将 with
的类型定为 M a -> (a -> b) -> b
,其中 M 是上下文管理器接口(包含 __enter__
和 __exit__
方法)。在 bind
和 with
的类型之间是 M a -> (a -> N b) -> N b
的类型。要成为单子,with
必须在运行时失败,当 b
不是 M a
时。此外,虽然你可以将 with
作为绑定操作单子地使用,但这样做通常没有意义。
你需要做这些微妙的区分是因为,如果你错误地认为 with
是单子的,你会误用它,写出由于类型错误而失败的程序。换句话说,你会写出垃圾代码。你需要做的是区分一个特定的构造(例如单子)和一个可以以那种方式使用的构造(例如,再次是单子)。后者需要程序员的自律,或者定义额外的构造来强制执行这种自律。这里有一个几乎是单子的 with
版本(类型是 M a -> (a -> b) -> M b
):
def withm(context, f):
with context as x:
return type(context)(f(x))
最终分析下来,你可以认为 with
像一个组合器,但比单子所需的组合器(即绑定)更通用。使用单子的函数可以比所需的两个更多(例如,列表单子还有 cons、append 和 length),所以如果你为上下文管理器定义了合适的绑定操作符(比如 withm
),那么 with
就可以在涉及单子的意义上成为单子。
是的。
在定义的下面,维基百科上说:
在面向对象编程中,这种类型构造相当于声明一个单子类型,单位函数的作用就像构造方法,而绑定操作则包含执行注册回调(单子函数)所需的逻辑。
这听起来就像上下文管理器协议,对象实现上下文管理器协议,以及 with
语句。
来自 @Owen 在这篇帖子中的评论:
单子在最基本的层面上,基本上是一种很酷的使用继续传递风格的方法:>>= 接受一个“生产者”和一个“回调”;这基本上也是
with
的意思:像 open(...) 这样的生产者和一段代码块,一旦创建就会被调用。
维基百科的完整定义:
一种类型构造,定义了如何为每种基础类型获得相应的单子类型。在 Haskell 的表示法中,单子的名称代表类型构造器。如果 M 是单子的名称,t 是数据类型,那么 “M t” 就是相应的单子类型。
这听起来像是 上下文管理器协议。
一个单位函数,它将基础类型中的一个值映射到相应单子类型中的一个值。结果是相应类型中“最简单”的值,完全保留原始值(简单性在这里是相对于单子来说的)。在 Haskell 中,这个函数叫做 return,因为它在后面描述的 do-notation 中的用法。单位函数的多态类型是 t→M t。
对象实际实现上下文管理器协议。
一个多态类型的绑定操作 (M t)→(t→M u)→(M u),Haskell 用中缀运算符 >>= 表示。它的第一个参数是单子类型中的一个值,第二个参数是一个函数,它将第一个参数的基础类型映射到另一个单子类型,结果在那个其他单子类型中。
这对应于 with
语句及其代码块。
所以我可以说 with
是一个单子。我查找了 PEP 343 以及所有相关的被拒绝和撤回的 PEP,但没有一个提到“单子”这个词。它确实适用,但似乎 with 语句的 目标 是资源管理,而单子只是实现这一目标的一个有用方式。