子类str,并使新方法与+ =具有相同的效果

时间:2016-01-16 21:03:09

标签: python string subclass immutability built-in

我正在尝试子类str - 不是为了重要的事情,只是一个实验来了解有关Python内置类型的更多信息。我已经用str这种方式进行了子类化(使用__new__因为str是不可变的):

class MyString(str):
    def __new__(cls, value=''):
        return str.__new__(cls, value)
    def __radd__(self, value):  # what method should I use??
        return MyString(self + value)  # what goes here??
    def write(self, data):
        self.__radd__(data)

据我所知,它正确初始化。但我无法使用+ =运算符进行就地修改。我已尝试覆盖__add____radd____iadd__以及各种其他配置。使用return语句,我设法让它返回正确的附加MyString的新实例,但不能在适当的位置修改。成功看起来像:

b = MyString('g')
b.write('h')  # b should now be 'gh'

有什么想法吗?

更新

为了可能添加某人可能想要这样做的原因,我遵循了创建以下在内部使用普通字符串的可变类的建议:

class StringInside(object):

    def __init__(self, data=''):
        self.data = data

    def write(self, data):
        self.data += data

    def read(self):
        return self.data

并使用timeit进行测试:

timeit.timeit("arr+='1234567890'", setup="arr = ''", number=10000)
0.004415035247802734
timeit.timeit("arr.write('1234567890')", setup="from hard import StringInside; arr = StringInside()", number=10000)
0.0331270694732666

number上升时差异迅速增加 - 在100万次互动中,StringInside比我愿意等待返回的时间更长,而纯str版本在〜{ 100毫秒。

更新2

对于后人,我决定编写一个包含C ++字符串的cython类,看看性能是否可以提高,而松散的基于MikeMüller的更新版本,我成功了。我意识到cython是“作弊”,但我提供的只是为了好玩。

python版本:

class Mike(object):

    def __init__(self, data=''):
        self._data = []
        self._data.extend(data)

    def write(self, data):
        self._data.extend(data)

    def read(self, stop=None):
        return ''.join(self._data[0:stop])

    def pop(self, stop=None):
        if not stop:
            stop = len(self._data)
        try:
            return ''.join(self._data[0:stop])
        finally:
            self._data = self._data[stop:]

    def __getitem__(self, key):
        return ''.join(self._data[key])

cython版本:

from libcpp.string cimport string

cdef class CyString:
    cdef string buff
    cdef public int length

    def __cinit__(self, string data=''):
        self.length = len(data)
        self.buff = data

    def write(self, string new_data):
        self.length += len(new_data)
        self.buff += new_data

    def read(self, int length=0):
        if not length:
            length = self.length
        return self.buff.substr(0, length)  

    def pop(self, int length=0):
        if not length:
            length = self.length
        ans = self.buff.substr(0, length)
        self.buff.erase(0, length)
        return ans

性能:

>>> timeit.timeit("arr.write('1234567890')", setup="from pyversion import Mike; arr = Mike()", number=1000000)
0.5992741584777832
>>> timeit.timeit("arr.write('1234567890')", setup="from cyversion import CyBuff; arr = CyBuff()", number=1000000)
0.17381906509399414

>>> timeit.timeit("arr.write('1234567890'); arr.read(5)", setup="from pyversion import Mike; arr = Mike()", number=1000000)
1.1499049663543701
>>> timeit.timeit("arr.write('1234567890'); arr.read(5)", setup="from cyversion import CyBuff; arr = CyBuff()", number=1000000)
0.2894480228424072

弹出

>>> # note I'm using 10e3 iterations - the python version wouldn't return otherwise
>>> timeit.timeit("arr.write('1234567890'); arr.pop(5)", setup="from pyversion import Mike; arr = Mike()", number=10000)
0.7390561103820801
>>> timeit.timeit("arr.write('1234567890'); arr.pop(5)", setup="from cyversion import CyBuff; arr = CyBuff()", number=10000)
0.01501607894897461

1 个答案:

答案 0 :(得分:5)

解决方案

这是对更新后问题的回答。

您可以使用列表来保存数据,只在读取时才构造字符串:

class StringInside(object):

    def __init__(self, data=''):
        self._data = []
        self._data.append(data)

    def write(self, data):
        self._data.append(data)

    def read(self):
        return ''.join(self._data)

效果

本课程的表现:

%%timeit arr = StringInside()
arr.write('1234567890')
1000000 loops, best of 3: 352 ns per loop

更接近原生str

%%timeit str_arr = ''
str_arr+='1234567890'
1000000 loops, best of 3: 222 ns per loop

与您的版本比较:

%%timeit arr = StringInsidePlusEqual()
arr.write('1234567890')
100000 loops, best of 3: 87 µs per loop

原因

构建字符串的my_string += another_string方式长期以来一直是反模式的表现。 CPython对这种情况有一些优化。似乎CPython无法检测到此处使用此模式。这可能是因为它有点隐藏在一个类中。

由于各种原因,并非所有实现都具有此优化。例如。 PyPy,通常比CPython快得多,对于这个用例来说要慢得多:

PyPy 2.6.0(Python 2.7.9)

>>>> import timeit
>>>> timeit.timeit("arr+='1234567890'", setup="arr = ''", number=10000)
0.08312582969665527

CPython 2.7.11

>>> import timeit
>>> timeit.timeit("arr+='1234567890'", setup="arr = ''", number=10000)
0.002151966094970703

可切片版

此版本支持切片:

class StringInside(object):

    def __init__(self, data=''):
        self._data = []
        self._data.extend(data)

    def write(self, data):
        self._data.extend(data)

    def read(self, start=None, stop=None):
        return ''.join(self._data[start:stop])

    def __getitem__(self, key):
        return ''.join(self._data[key])

你可以按正常方式切片:

>>> arr = StringInside('abcdefg')
>>> arr[2]
'c'
>>> arr[1:3]
'bc'

现在,read()还支持可选的开始和停止索引:

>>>  arr.read()
'abcdefg'
>>> arr.read(1, 3)
'bc'

>>> arr.read(1)
'bcdefg'