Emulating membership-test in Python: delegating __contains__ to contained-object correctly
Asked Answered
T

1

6

I am used to that Python allows some neat tricks to delegate functionality to other objects. One example is delegation to contained objects.

But it seams, that I don't have luck, when I want to delegate __contains __:

class A(object):
    def __init__(self):
       self.mydict = {}
       self.__contains__ = self.mydict.__contains__

a = A()
1 in a

I get:

Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: argument of type 'A' is not iterable

What I am making wrong? When I call a.__contains __(1), everything goes smooth. I even tried to define an __iter __ method in A to make A more look like an iterable, but it did not help. What I am missing out here?

Taxiway answered 20/6, 2009 at 20:39 Comment(0)
L
18

Special methods such as __contains__ are only special when defined on the class, not on the instance (except in legacy classes in Python 2, which you should not use anyway).

So, do your delegation at class level:

class A(object):
    def __init__(self):
       self.mydict = {}

    def __contains__(self, other):
       return self.mydict.__contains__(other)

I'd actually prefer to spell the latter as return other in self.mydict, but that's a minor style issue.

Edit: if and when "totally dynamic per-instance redirecting of special methods" (like old-style classes offered) is indispensable, it's not hard to implement it with new-style classes: you just need each instance that has such peculiar need to be wrapped in its own special class. For example:

class BlackMagic(object):
    def __init__(self):
        self.mydict = {}
        self.__class__ = type(self.__class__.__name__, (self.__class__,), {})
        self.__class__.__contains__ = self.mydict.__contains__

Essentially, after the little bit of black magic reassigning self.__class__ to a new class object (which behaves just like the previous one but has an empty dict and no other instances except this one self), anywhere in an old-style class you would assign to self.__magicname__, assign to self.__class__.__magicname__ instead (and make sure it's a built-in or staticmethod, not a normal Python function, unless of course in some different case you do want it to receive the self when called on the instance).

Incidentally, the in operator on an instance of this BlackMagic class is faster, as it happens, than with any of the previously proposed solutions -- or at least so I'm measuring with my usual trusty -mtimeit (going directly to the built-in method, instead of following normal lookup routes involving inheritance and descriptors, shaves a bit of the overhead).

A metaclass to automate the self.__class__-per-instance idea would not be hard to write (it could do the dirty work in the generated class's __new__ method, and maybe also set all magic names to actually assign on the class if assigned on the instance, either via __setattr__ or many, many properties). But that would be justified only if the need for this feature was really widespread (e.g. porting a huge ancient Python 1.5.2 project that liberally use "per-instance special methods" to modern Python, including Python 3).

Do I recommend "clever" or "black magic" solutions? No, I don't: almost invariably it's better to do things in simple, straightforward ways. But "almost" is an important word here, and it's nice to have at hand such advanced "hooks" for the rare, but not non-existent, situations where their use may actually be warranted.

Lachman answered 20/6, 2009 at 20:42 Comment(6)
Ok, this does some explaination to me, but is not totally satisfactory for me, since this type of delegation costs extra time (what I could save in old style classes -- why should I not use them??).Taxiway
"Performance at any cost" is not a sound objective: "premature optimization is the root of all evil in programming" (Knuth, quoting Hoare). Modern-style classes are functionally richer, more general, more regular, and simpler (see the bottom of p.103 of "Python in a Nutshell", which you can read on Google Book Search -- search for "per-instance methods"). In Python 3 and later, legacy classes are gone forever - don't tie your code to them to save nanoseconds of small relevance. BTW, faster is to inherit from dict (that gives you a new-style class, too).Lachman
Hi Alex, thanks for the lecture in programmming, I don't needed ;-) Sorry, but we are not bible-scholars, so just citing some bible-verses or "big teachers" is not good enough for me. The other explanations are good, specially those about legacy classes. I don't want to stick with the old ones -- but I want reasoning! The inheritance solution also came into my mind in the meanwhile and it is satisfactory for my special problem. Still the old style classes are a little more flexible/dynamic in this point (per-instance override of special methods could be good in other situations too!)Taxiway
Addon: What I meant is, that this feature could be useful also in situations where speed is not the top reason. And what I like in Python is its dynamicity -- that is reduced in this respect a little bit.Taxiway
The old-style classes are hackish -- and if you want unbridled dynamicity, Python's not optimal for you (can't monkeypatch builtins like you can in Javascript and Ruby, etc, etc); rather, we're always trying to balance good structure vs dynamicity. If top speed is not the key problem, you can always define the special method on the class and have it redirect functionality as necessary, of course.Lachman
Thank you for the edit! I learned a lot threw this few postings! Ok, I can't see complete what was hackish about old-style classes, because I lack some of the implementation background ... But I don't want to cling on them. I just want to have reasoning and in some (rare) situations, speed is also needed. In most cases, new-style classes make more sense and provide more clearity. Thats true! Programming languages are always a tradeoff between many forces. I found Python to be most of the time being a good one for me. Thanks!Taxiway

© 2022 - 2024 — McMap. All rights reserved.