如何使len()函数在不修改类的情况下,能够适用于类的不同实例的不同方法?

19

有没有一种方法可以在不修改类的情况下,使len()与实例方法一起使用?

我的问题示例:

>>> class A(object):
...     pass
...
>>> a = A()
>>> a.__len__ = lambda: 2
>>> a.__len__()
2
>>> len(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'A' has no len()

注意:

  • A 的不同实例将附加有不同的 __len__ 方法。
  • 我无法更改类 A

注意:

  • A 的不同实例将附加有不同的__len__方法。
  • 我不能改变类A

1
你的问题在于当你定义__len__时,尝试将其定义为类的实际方法而不是实例属性。 - jesseops
1
@PadraicCunningham 请看我修改后的问题。这不允许A的不同实例有不同的__len__方法。 - ARF
1
@ARF,我猜你不能修改这个类? - Padraic Cunningham
1
@PadraicCunningham 您是正确的,我无法修改这个类。根据我得到的答案,我现在意识到我真的应该提到这一点。 - ARF
1
你能否解释一下用了什么技巧? - Fruch
显示剩余2条评论
5个回答

27

不。Python总是通过对象的类来查找特殊方法。这样做有几个很好的理由,其中之一是repr(A)应该使用type(A).__repr__而不是A.__repr__,后者旨在处理A的实例而不是A类本身。

如果您希望A的不同实例以不同的方式计算其len,请考虑将__len__委托给另一个方法:

class A(object):
    def __len__(self):
        return self._len()

a = A()
a._len = lambda: 2

2
好的答案,考虑了 OP 的要求:使用不同实例来确定长度的方法。但是为什么这是一个要求却超出了我的理解范围… - jesseops
5
任何以__开头和结尾的方法都需要进行这种处理。由于有双下划线,它们有时被称为"dunder"方法。"肮脏的行为和它们的dunder首领!" - kindall

10

必须在类上定义特殊方法,例如__len__(双下划线或“dunder”方法)。如果仅在实例上定义,则无法正常工作。

可以在实例上定义非dunder方法。但是,您必须通过添加包装器将函数转换为实例方法,以便传递self参数(当访问方法时通常会执行此操作,因为在类上定义的方法是返回包装器的描述符)。 这样可以通过以下方式完成:

a.len = (lambda self: 2).__get__(a, type(a))

结合这些想法,我们可以在类上编写一个__len__(),将其委托给实例上我们可以定义的len()

class A(object):
     def __len__(self):
         return self.len()

a = A()
a.len = (lambda self: 2).__get__(a, type(a))

print(len(a))  # prints 2
你的情况可以简化,因为你不需要使用 self 来返回常量值 2。所以你可以直接赋值 a.len = lambda: 2。但是如果你需要使用 self,那么你需要创建一个方法包装器。

你能再解释一下wrapper吗?@user2357112的回答似乎让我即使没有wrapper也可以访问实例的self。 - ARF
@ARF:不,我只是省略了那个部分,因为你的示例 lambda 实际上并没有尝试访问 self。如果你确实需要 self,你需要以不同的方式获取它(也许是通过闭包变量或 functools.partial),或者你需要使用 __get__ 方法。 - user2357112
@user2357112 谢谢,经过一些尝试,我现在明白了包装器问题。 - ARF

6

根据Alex Martelli的答案,实际上可以在不修改类的情况下进行操作:

class A(object):
    def __init__(self, words):
        self.words = words

    def get_words(self):
        return self.words


a = A("I am a")
b = A("I am b")

def make_meth(inst, _cls, meth, lm):
    inst.__class__ = type(_cls.__name__, (_cls,), {meth: lm})

make_meth(a, A, "__len__", lambda self: 12)
make_meth(b, A, "__len__", lambda self: 44)

print(len(b))
print(len(a))
print(a.get_words())
print(b.get_words())

如果我们运行以下代码:
In [15]: a = A("I am a")    
In [16]: b = A("I am b")    
In [17]: make_meth(a, A, "__len__", lambda self: 12)
In [18]: make_meth(b, A, "__len__", lambda self: 44) 
In [19]: print(len(b))
44    
In [20]: print(len(a))
12

In [21]: print(a.get_words())
I am a    
In [22]: print(b.get_words())
I an b

根据链接答案的最后一部分,一旦使用了 inst.__class__ = type(... ,您可以使用 inst.specialmethod 在每个实例上添加任何方法:
In [34]: b.__class__.__str__ =  lambda self: "In b"

In [35]: print(str(a))
<__main__.A object at 0x7f37390ae128>

In [36]: print(str(b))
In b

重新分配对象的类?我想这应该可以工作,只要A的实例有一个__dict__(否则会出现TypeError)。这还有一些其他的影响可能会让那些必须处理以这种方式修改的对象的人感到困惑,但也许并不比已经改变了别人类上的__len__行为更令人困惑。 - user2357112
@user2357112,是的,它可以在不更改类的情况下工作,这是OP要求但未在问题中添加的内容。我并没有说这是最好的解决方案,但它表明这是可能的。 - Padraic Cunningham
@ARF,应该是 print(a.get_words()) 带一个s。我已经编辑了答案,忘记添加s了。 - Padraic Cunningham
@ARF,别担心,这是我的错。 - Padraic Cunningham
@PadraicCunningham:哦,你是在问“别人的类”的那一部分吗?我并不是说这里涉及到整个类的更改。它只是表示,如果这些对象是你无法控制的类的实例(如果你无法更改__len__的实现,则很可能是这种情况),那么改变它们如何响应len可能会引起很多意外,比重新分配类的“其他影响”更多。 - user2357112
显示剩余8条评论

5

您需要在定义类时定义特殊方法:

class A(object):
    def __len__(self):
        return self.get_len()

a = A()
a.get_len = lambda: 2
print len(a)

“您必须在类的定义时间定义特殊方法”这种说法是错误的。class C: pass 后跟 c=C() C.__len__ = lambda self: 2 print(len(c)) 可以正常工作。 - Terry Jan Reedy
@TerryJanReedy:OP使用新式类,而你使用旧式类。那么有什么意义呢? - Daniel
@Daniel 在我所使用的3.x版本中,旧式类甚至已经不存在了。 - Terry Jan Reedy

1
你没有说明你使用的是Python 2.x还是3.x,这取决于情况!在3.x中,其他人已经回答了你的问题。在2.x中,你的问题实际上有一个简单的答案:停止从object派生!(当然,如果你遵循这个方法,直到你找到另一种方法来做到这一点,你就不能升级到3.x,所以除非你没有计划立即升级,否则不要采取我建议的做法。)
Type "copyright", "credits" or "license()" for more information.
>>> class A:pass

>>> a=A()
>>> a.__len__ = lambda: 2
>>> len(a)
2
>>> 

享受吧 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接