如何创建一个像字符串一样的类?

3 投票
6 回答
4518 浏览
提问于 2025-04-16 05:23

我有一个上下文管理器,它可以把一段代码块的输出捕获到一个字符串里,这段代码块是在一个with语句下面缩进的。当这个代码块执行完后,这个上下文管理器会返回一个自定义的结果对象,这个对象里会包含捕获到的输出。

from contextlib import contextmanager

@contextmanager
def capturing():
    "Captures output within a 'with' block."
    from cStringIO import StringIO

    class result(object):
        def __init__(self):
            self._result = None
        def __str__(self):
            return self._result

    try:
        stringio = StringIO()
        out, err, sys.stdout, sys.stderr = sys.stdout, sys.stderr, stringio, stringio
        output = result()
        yield output
    finally:
        output._result, sys.stdout, sys.stderr = stringio.getvalue(), out, err
        stringio.close()

with capturing() as text:
    print "foo bar baz",

print str(text)   # prints "foo bar baz"

当然,我不能直接返回一个字符串,因为字符串是不可变的,也就是说用户从with语句得到的字符串在代码块执行后是不能改变的。不过,事后还要用str来把结果对象转换成字符串,这样有点麻烦(我也尝试过让这个对象可以被调用,作为一种语法上的小技巧)。

那么,是否有可能让这个结果对象像字符串一样工作,也就是说当你用名字去调用它时,实际上返回的是一个字符串呢?我尝试实现__get__,但这似乎只适用于属性。或者我想做的事情根本就不可能?

6 个回答

2

我觉得没有一个简单的方法可以做到你想要的事情。text是在模块的globals()字典里定义的。你需要从capturing对象内部去修改这个globals()字典。

下面的代码如果你试着在一个函数里面使用with的话就会出问题,因为那时候text会在函数的范围内,而不是在全局范围。

import sys
import cStringIO

class capturing(object):
    def __init__(self,varname):
        self.varname=varname
    def __enter__(self):
        self.stringio=cStringIO.StringIO()
        self.out, sys.stdout = sys.stdout, self.stringio
        self.err, sys.stderr = sys.stderr, self.stringio        
        return self
    def __exit__(self,ext_type,exc_value,traceback):
        sys.stdout = self.out
        sys.stderr = self.err
        self._result = self.stringio.getvalue()
        globals()[self.varname]=self._result
    def __str__(self):
        return self._result


with capturing('text') as text:
    print("foo bar baz")

print(text)   # prints "foo bar baz"
# foo bar baz

print(repr(text))
# 'foo bar baz\n'
5

如何创建一个像字符串一样的类?

可以通过创建一个str的子类来实现。

import os
class LikeAStr(str):
    '''Making a class like a str object; or more precisely
    making a str subclass with added contextmanager functionality.'''

    def __init__(self, diff_directory):
        self._iwd = os.getcwd()
        self._cwd = diff_directory

    def __enter__(self):
        return self

    def __exit__(self, ext_typ, exc_value, traceback):
        try: os.chdir(self._iwd) # might get deleted within the "with" statement
        except: pass

    def __str__(self):
        return self._cwd

    def __repr__(self):
        return repr(self._cwd)


astr = LikeAStr('C:\\')

with LikeAStr('C:\\') as astr:
    print 1, os.getcwd()
    os.chdir( astr ) # expects str() or unicode() not some other class
    print 2, os.getcwd()
    #

# out of with block
print 3, os.getcwd()
print 4, astr == 'C:\\'

输出结果:

1 D:\Projects\Python\
2 C:\
3 D:\Projects\Python\
4 True
2

乍一看,UserString(其实是MutableString,但在Python 3.0中会被去掉)看起来基本上符合我的需求。不过,UserString的表现和字符串不太一样;我在用print语句打印以逗号结尾的内容时,出现了一些奇怪的格式问题,而用str字符串却没有这个问题。(好像如果不是“真正的”字符串,就会多出一个空格,或者其他什么原因。)我自己创建了一个玩具类来尝试包装字符串,也遇到了同样的问题。我没有花时间去找出原因,但看起来UserString更像是一个示例,实际用处不大。

最后,我选择使用bytearray,因为它在大多数情况下的表现和字符串差不多,而且是可变的。我还写了一个单独的版本,可以用splitlines()把文本分割成一个列表。这种方法效果很好,实际上更适合我当前的需求,就是去掉各种函数拼接输出中的“多余”空行。以下是这个版本:

import sys
from contextlib import contextmanager

@contextmanager
def capturinglines(output=None):
    "Captures lines of output to a list."
    from cStringIO import StringIO

    try:
        output = [] if output is None else output
        stringio = StringIO()
        out, err = sys.stdout, sys.stderr
        sys.stdout, sys.stderr = stringio, stringio
        yield output
    finally:
        sys.stdout, sys.stderr = out, err
        output.extend(stringio.getvalue().splitlines())
        stringio.close()

用法:

with capturinglines() as output:
    print "foo"
    print "bar"

print output
['foo', 'bar']

with capturinglines(output):   # append to existing list
    print "baz"

print output
['foo', 'bar', 'baz']

撰写回答