[Python-3000] PEP 3133: Introducing Roles

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.is_rough()
> 
> 
>   class Dog(Animal):
> 
>     def bark(self):
>       return self.goes_ruff()
> 
> 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. ::
> 
>   @perform_role(Doglike)
>   class Dog(Animal):
>     ...

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

>   @perform_role(Treelike)
>   class Tree(Vegetable):
>     ...

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

>   @perform_role(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: ::
> 
>   @perform_role(Guard, MummysLittleDarling)
>   class GermanShepherd(Dog):
> 
>     def guard(self, the_precious):
>       while True:
>         if intruder_near(the_precious):
>           self.growl()
> 
>     def get_petted(self):
>       self.swallow_pride()

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(our_robot, Guard)
>   True

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

> What about that one robot over there? ::
> 
>   >>> performs(that_robot_over_there, 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
> 
>   @perform_role(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. ::
> 
>   @perform_role(FourLegs)
>   class Horse(Mammal):
> 
>     def run_like_teh_wind(self)
>       ...
> 
> This will cause the role system to raise an exception, complaining
> that you're missing a ``run_like_the_wind()`` 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. ::
> 
>   @perform_role(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