[Twisted-Python] A Python metaclass for Twisted allowing __init__ to return a Deferred

glyph at divmod.com glyph at divmod.com
Mon Nov 3 05:49:12 MST 2008


On 2 Nov, 11:48 pm, terry at jon.es wrote:
>Very briefly, I wrote a metaclass to allow you to write classes whose
>__init__ method uses deferreds. Your __init__ can create deferreds, 
>call
>functions that return deferreds, and of course return a deferred itself
>(that's the whole point). Your class instance wont be available until 
>after
>the deferred your __init__ returns has fired.

As a general stylistic thing, I've been writing a lot more classmethods 
lately to determine arguments to __init__, rather than trying to make 
__init__ itself do interesting tricks.  I can't find a name for this 
"design pattern", so let me describe it:

One very common use-case is that we have some object - let's say an RFC 
5322 email address - which is typically created from a string.  An 
idiomatic way to do that might be like this:


    import rfc822

    class Address:
        def __init__(self, addrstr):
            l = list(rfc822.AddressList(addrstr))
            if len(l) != 1:
                raise ValueError("Too many or too few addresses.")
            else:
                desc, addr = l[0]
                self.description = desc
                self.localpart, self.domain = addr.split("@")

But this is problematic.  With this class, it's hard to convert from a 
different format of storing email addresses that has already been 
parsed.  In order to create an Address from, i.e., a database record 
containing a description, localpart, and domain, I now need to smash 
everything back into a string, worrying about trivia like quoting; or I 
need to resort to hacks like calling __new__ instead of __init__.  It 
makes testing more difficult: in my tests I need to start having 
formatted email addresses in strings instead of simply creating Address 
objects.  If this class were hypothetically a bit smarter and dealt 
nicely with unicode, my tests would need to learn about email-address 
quoting rules in order to generate addresses with non-ASCII characters, 
rather than leaving that logic entirely in the Address class.  Ugly all 
around.

However, I can pull the parsing logic out and separate it from the 
initialization logic, and all of that gets much easier:

    class Address:
        def __init__(self, localpart, domain, description):
            self.localpart = localpart
            self.domain = domain
            self.description = description

        @classmethod
        def fromString(cls, addrstr):
            l = list(rfc822.AddressList(addrstr))
            if len(l) != 1:
                raise ValueError("Too many or too few addresses.")
            else:
                desc, addr = l[0]
                loc, dom = addr.split("@")
                return cls(loc, dom, desc)

With this improved class, I can easily create Address objects in other 
ways from other code.  Since it's a classmethod rather than a function, 
it's just as friendly to inheritance as a constructor; perhaps even 
moreso.  It opens the door to the evolution of other creation methods, 
fromXXX classmethods, without breaking the constructor's signature or 
changing the fromString method.

You don't give a concrete example in your blog post, but I can imagine 
that all these points apply twice over to any code that would use 
Deferreds.  An __init__ that returns a Deferred means that in the 
testing case, not only is there no way to directly construct the object 
you want, there might be no way to even get one without spinning the 
reactor.  What is that Deferred doing?  Maybe there's no way to get one 
without actually generating network traffic!  Obviously, not an ideal 
scenario.  For the tests for the code making the deferred request 
itself, there will obviously need to be fake sources of data, but for 
other tests that just want to interact with one of your objects, direct 
construction is pretty much always easier.

However, thanks for sharing nonetheless.  Although I wouldn't use it 
personally, your code makes an interesting rhetorical point.  There's a 
great deal of whinging that goes on around Deferreds being hard to work 
with.  This metaclass is just another in a long line of tools that says 
"see?  it really isn't so hard to deal with a Deferred if you need to."




More information about the Twisted-Python mailing list