Python:如何动态修改字典和列表对象的方法?

3 投票
2 回答
626 浏览
提问于 2025-04-15 16:51

这里是我想做的一个示意图:

alist = [1,2,3,4,5]   # create a vanilla python list object
replacef (alist)      # replace __setitem__, extend,... with custom functions
alist[0]=2            # now the custom __setitem__ is called

这是一个DSL(领域特定语言)项目,语法应该尽量接近普通的Python,所以不希望通过子类化列表来实现,比如让用户写成 alist = MyList(1,2,3,4,5) 这样的方式。而且,由于这个DSL需要和其他库一起使用,全球性地改变列表和字典的行为也是不现实的……

我尝试过创建实例方法,并直接在对象上设置它们,比如 alist.append = myFunc,但Python说这些属性是只读的。改变 __class__ 属性似乎也不被允许。

我想做的事情在Python中真的可能吗?

更新:这里是一些关于对象子类可以做的事情的发现:

假设:

class y(object):
    def __init__(self,a,b,c):
    self.a = a
    self.b = b
    self.c = c
def f(self):
    print self
    print self.a
    print self.b
    print self.c

class x(object):
def __init__(self,a,b,c):
    self.a = a
    self.b = b
    self.c = c
def f(self):
    print "x.f()"

>>> objy = y(1,2,3)
>>> objx = x(4,5,6)
>>> objy.f()
  <__main__.y object at 0x02612650>
  1
  2
  3
>>> objx.f()
  x.f()

可以像这样动态地改变 objx 的类:

>>> objx.__class__ = y
>>> objx.f()
<__main__.y object at 0x02612D90>
4
5
6

此外,还可以像在JavaScript中一样,动态添加或改变对象的方法:

>>> def g(self,p):
       print self
       print p
>>> import new
>>> objy.g = new.instancemethod(g,objy,y)
>>> objy.g(42)
<__main__.y object at 0x02612650>
42

然而,这两种方法在像字典和列表这样的C语言实现的对象上会失败:

>>> def mypop(self):
        print "mypop"
        list.mypop(self)


>>> alist.pop = new.instancemethod(mypop,alist,list) 

AttributeError: 'list' object attribute 'pop' is read-only

>>> x = [1,2,3,4,5]
>>> x.__class__ = mylist

TypeError: __class__ assignment: only for heap types

建议的做法是使用类似 alist = replacef(alist) 的方式,但在这里行不通,因为 replacef 可能会在 __setattribute__ 调用中被调用,像这样:

alist = [1,2,3,4,5]
aDSLObject.items = alist #  __setattribute__ calls replacef on alist
alist[0] = ....

所以,虽然确实可以动态改变对象的方法,但似乎无法改变C语言实现的对象的行为——或者我是不是漏掉了什么?

2 个回答

2

"明确比隐含更好。" 你应该清楚地告诉用户,他们需要使用 MyList() 而不是 list()。这样你和你的用户在长远来看都会更开心。

顺便说一下,Ruby 语言是允许你这样做的。在 Ruby 中,你可以打开全局对象并添加新的功能。我觉得这有点吓人,而且 Ruby 的运行速度比 Python 慢,所以我并不是在鼓励你转向 Ruby。

所以,提供 MyList(),并提供一种方法来将现有的列表包装成 MyList()

你可以让 MyList() 接受一堆参数,这样就能很方便地创建一个列表:

class MyList(object):
    def __init__(self, *args):
        self.lst = args
    # add other methods to MyList() here

然后你可以这样调用它:MyList(1, 2, 3, 4),这个实例会有一个叫做 lst 的成员,值是:[1, 2, 3, 4]

但是现在,如果你想把一个列表变成 MyList 的实例,该怎么做呢?如果你这样做:

new_list = [1, 3, 5]
x = MyList(new_list)

那么你就把 x 设置成了:[[1, 3, 5]]

所以我建议你让 MyList() 只接受一个列表作为参数并保存它:

def ensure_list(seq):
    try:
        seq[0]
        return seq
    except TypeError:
        return list(seq)

class MyList(object):
    def __init__(self, seq):
        self.lst = ensure_list(seq)
    # add other methods to MyList() here

我刚刚编辑了这个回答。最开始,我在这里调用了 list(),现在我改成了调用 ensure_list()ensure_list() 会检查你能否对它的参数进行索引;如果可以,它要么已经是一个列表,要么是可以像列表一样使用的东西,这样就会原封不动地返回。如果不能索引,那么 ensure_list() 会调用 list(seq) 来尝试把它变成一个列表。这对于迭代器和生成器也有效,强制它们展开成一个列表。

4

也许你可以在 replaceref 函数里面做类似 "alist = MyList(1,2,3,4,5)" 这样的事情?

def replacef(l):
    return MyList(l) # Return instance of list subclass that has custom __setitem__

alist = [1,2,3,4,5]
alist = replaceref(alist)

这样用户在定义列表的时候仍然可以使用标准的列表语法,但在调用 replaceref 之后就能得到新的 setitem 功能。

撰写回答