[Python-3000] PEP 3133: Introducing Roles (original) (raw)

Benji York benji at benjiyork.com
Mon May 14 23:35:34 CEST 2007


Collin Winter wrote:

PEP: 3133 Title: Introducing Roles

Everything included here is included in zope.interface. See in-line comments below for the analogs.

[snip]

Performing Your Role ====================

Static Role Assignment ---------------------- Let's start out by defining Tree and Dog classes :: class Tree(Vegetable): def bark(self): return self.isrough()

class Dog(Animal): def bark(self): return self.goesruff() While both implement a bark() method with the same signature, they do wildly different things. We need some way of differentiating what we're expecting. Relying on inheritance and a simple isinstance() test will limit code reuse and/or force any dog-like classes to inherit from Dog, whether or not that makes sense. Let's see if roles can help. :: @performrole(Doglike) class Dog(Animal): ...

class Dog(Animal): zope.interface.implements(Doglike)

@performrole(Treelike) class Tree(Vegetable): ...

 class Tree(Vegetable):
     zope.interface.implements(Treelike)

@performrole(SitThere) class Rock(Mineral): ...

 class Rock(Mineral):
     zope.interface.implements(SitThere)

We use class decorators from PEP 3129 to associate a particular role or roles with a class.

zope.interface.implements should be usable with the PEP 3129 syntax, but I showed the current class decorator syntax throughout.

Client code can now verify that an incoming object performs the Doglike role, allowing it to handle Wolf, LaughingHyena and Aibo [#aibo] instances, too.

Roles can be composed via normal inheritance: :: @performrole(Guard, MummysLittleDarling) class GermanShepherd(Dog): def guard(self, theprecious): while True: if intrudernear(theprecious): self.growl() def getpetted(self): self.swallowpride()

class GermanShepherd(Dog): zope.interface.implements(Guard, MummysLittleDarling)

[rest of class definition is the same]

Here, GermanShepherd instances perform three roles: Guard and MummysLittleDarling are applied directly, whereas Doglike is inherited from Dog.

Assigning Roles at Runtime -------------------------- Roles can be assigned at runtime, too, by unpacking the syntactic sugar provided by decorators. Say we import a Robot class from another module, and since we know that Robot already implements our Guard interface, we'd like it to play nicely with guard-related code, too. :: >>> perform(Guard)(Robot) This takes effect immediately and impacts all instances of Robot.

 >>> zope.interface.classImplements(Robot, Guard)

Asking Questions About Roles ----------------------------

Just because we've told our robot army that they're guards, we'd like to check in on them occasionally and make sure they're still at their task. :: >>> performs(ourrobot, Guard) True

 >>> zope.interface.directlyProvides(our_robot, Guard)

What about that one robot over there? ::

>>> performs(thatrobotoverthere, Guard) True

 >>> Guard.providedBy(that_robot_over_there)
 True

The performs() function is used to ask if a given object fulfills a given role. It cannot be used, however, to ask a class if its instances fulfill a role: ::

>>> performs(Robot, Guard) False

 >>> Guard.providedBy(Robot)
 False

This is because the Robot class is not interchangeable with a Robot instance.

But if you want to find out if a class creates instances that provide an interface you can::

 >>> Guard.implementedBy(Robot)
 True

Defining New Roles ================== Empty Roles ----------- Roles are defined like a normal class, but use the Role metaclass. :: class Doglike(metaclass=Role): ...

Interfaces are defined like normal classes, but subclass zope.interface.Interface:

 class Doglike(zope.interface.Interface):
     pass

Metaclasses are used to indicate that Doglike is a Role in the same way 5 is an int and tuple is a type.

Composing Roles via Inheritance ------------------------------- Roles may inherit from other roles; this has the effect of composing them. Here, instances of Dog will perform both the Doglike and FourLegs roles. :: class FourLegs(metaclass=Role): pass class Doglike(FourLegs, Carnivor): pass @performrole(Doglike) class Dog(Mammal): pass

 class FourLegs(zope.interface.Interface):
     pass

 class Doglike(FourLegs, Carnivore):
     pass

 class Dog(Mammal):
     zope.interface.implements(Doglike)

Requiring Concrete Methods --------------------------

So far we've only defined empty roles -- not very useful things. Let's now require that all classes that claim to fulfill the Doglike role define a bark() method: :: class Doglike(FourLegs): def bark(self): pass

 class Doglike(FourLegs):
     def bark():
         pass

No decorators are required to flag the method as "abstract", and the method will never be called, meaning whatever code it contains (if any) is irrelevant. Roles provide only abstract methods; concrete default implementations are left to other, better-suited mechanisms like mixins.

Once you have defined a role, and a class has claimed to perform that role, it is essential that that claim be verified. Here, the programmer has misspelled one of the methods required by the role. :: @performrole(FourLegs) class Horse(Mammal): def runliketehwind(self) ... This will cause the role system to raise an exception, complaining that you're missing a runlikethewind() method. The role system carries out these checks as soon as a class is flagged as performing a given role.

zope.interface does no runtime checking. It has a similar mechanism in zope.interface.verify::

 >>> from zope.interface.verify import verifyObject
 >>> verifyObject(Guard, our_robot)
 True

Concrete methods are required to match exactly the signature demanded by the role. Here, we've attempted to fulfill our role by defining a concrete version of bark(), but we've missed the mark a bit. ::

@performrole(Doglike) class Coyote(Mammal): def bark(self, target=moon): pass This method's signature doesn't match exactly with what the Doglike role was expecting, so the role system will throw a bit of a tantrum.

zope.interface doesn't do anything like this. I suspect *args, and **kws make it impractical to do so (not mentioning whether or not it's a good idea).

The rest of the PEP concerns implementation and other details, so eliding that.

Benji York http://benjiyork.com



More information about the Python-3000 mailing list