[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