This PEP has helped pushPEP 3119 towards a saner, more minimalisticapproach. But given the latest version ofPEP 3119 I much preferthat. GvR.
Python’s existing object model organizes objects according to theirimplementation. It is often desirable – especially induck typing-based language like Python – to organize objects bythe part they play in a larger system (their intent), rather than byhow they fulfill that part (their implementation). This PEPintroduces the concept of roles, a mechanism for organizingobjects according to their intent rather than their implementation.
In the beginning were objects. They allowed programmers to marryfunction and state, and to increase code reusability through conceptslike polymorphism and inheritance, and lo, it was good. There camea time, however, when inheritance and polymorphism weren’t enough.With the invention of both dogs and trees, we were no longer able tobe content with knowing merely, “Does it understand ‘bark’?”We now needed to know what a given object thought that “bark” meant.
One solution, the one detailed here, is that of roles, a mechanismorthogonal and complementary to the traditional class/instance system.Whereas classes concern themselves with state and implementation, theroles mechanism deals exclusively with the behaviours embodied in agiven class.
This system was originally called “traits” and implemented for SqueakSmalltalk[4]. It has since been adapted for use inPerl 6[3] where it is called “roles”, and it is primarilyfrom there that the concept is now being interpreted for Python 3.Python 3 will preserve the name “roles”.
In a nutshell: roles tell youwhat an object does, classes tell youhow an object does it.
In this PEP, I will outline a system for Python 3 that will make itpossible to easily determine whether a given object’s understandingof “bark” is tree-like or dog-like. (There might also be moreserious examples.)
A syntax proposals in this PEP are tentative and should beconsidered to be strawmen. The necessary bits that this PEP dependson – namelyPEP 3115’s class definition syntax andPEP 3129’s classdecorators – are still being formalized and may change. Functionnames will, of course, be subject to lengthy bikeshedding debates.
Let’s start out by definingTree andDog classes
classTree(Vegetable):defbark(self):returnself.is_rough()classDog(Animal):defbark(self):returnself.goes_ruff()
While both implement abark() method with the same signature,they do wildly different things. We need some way of differentiatingwhat we’re expecting. Relying on inheritance and a simpleisinstance() test will limit code reuse and/or force any dog-likeclasses to inherit fromDog, whether or not that makes sense.Let’s see if roles can help.
@perform_role(Doglike)classDog(Animal):...@perform_role(Treelike)classTree(Vegetable):...@perform_role(SitThere)classRock(Mineral):...
We use class decorators fromPEP 3129 to associate a particular roleor roles with a class. Client code can now verify that an incomingobject performs theDoglike role, allowing it to handleWolf,LaughingHyena andAibo[1] instances, too.
Roles can be composed via normal inheritance:
@perform_role(Guard,MummysLittleDarling)classGermanShepherd(Dog):defguard(self,the_precious):whileTrue:ifintruder_near(the_precious):self.growl()defget_petted(self):self.swallow_pride()
Here,GermanShepherd instances perform three roles:Guard andMummysLittleDarling are applied directly, whereasDoglikeis inherited fromDog.
Roles can be assigned at runtime, too, by unpacking the syntacticsugar provided by decorators.
Say we import aRobot class from another module, and since weknow thatRobot already implements ourGuard interface,we’d like it to play nicely with guard-related code, too.
>>>perform(Guard)(Robot)
This takes effect immediately and impacts all instances ofRobot.
Just because we’ve told our robot army that they’re guards, we’dlike to check in on them occasionally and make sure they’re still attheir task.
>>>performs(our_robot,Guard)True
What about that one robot over there?
>>>performs(that_robot_over_there,Guard)True
Theperforms() function is used to ask if a given objectfulfills a given role. It cannot be used, however, to ask aclass if its instances fulfill a role:
>>>performs(Robot,Guard)False
This is because theRobot class is not interchangeablewith aRobot instance.
Roles are defined like a normal class, but use theRolemetaclass.
classDoglike(metaclass=Role):...
Metaclasses are used to indicate thatDoglike is aRole inthe same way 5 is anint andtuple is atype.
Roles may inherit from other roles; this has the effect of composingthem. Here, instances ofDog will perform both theDoglike andFourLegs roles.
classFourLegs(metaclass=Role):passclassDoglike(FourLegs,Carnivor):pass@perform_role(Doglike)classDog(Mammal):pass
So far we’ve only defined empty roles – not very useful things.Let’s now require that all classes that claim to fulfill theDoglike role define abark() method:
classDoglike(FourLegs):defbark(self):pass
No decorators are required to flag the method as “abstract”, and themethod will never be called, meaning whatever code it contains (if any)is irrelevant. Roles provideonly abstract methods; concretedefault implementations are left to other, better-suited mechanismslike mixins.
Once you have defined a role, and a class has claimed to perform thatrole, it is essential that that claim be verified. Here, theprogrammer has misspelled one of the methods required by the role.
@perform_role(FourLegs)classHorse(Mammal):defrun_like_teh_wind(self)...
This will cause the role system to raise an exception, complainingthat you’re missing arun_like_the_wind() method. The rolesystem carries out these checks as soon as a class is flagged asperforming a given role.
Concrete methods are required to match exactly the signature demandedby the role. Here, we’ve attempted to fulfill our role by defining aconcrete version ofbark(), but we’ve missed the mark a bit.
@perform_role(Doglike)classCoyote(Mammal):defbark(self,target=moon):pass
This method’s signature doesn’t match exactly with what theDoglike role was expecting, so the role system will throw a bitof a tantrum.
The following are strawman proposals for how roles might be expressedin Python. The examples here are phrased in a way that the rolesmechanism may be implemented without changing the Python interpreter.(Examples adapted from an article on Perl 6 roles by Curtis Poe[2].)
@perform_role(Thieving)classElf(Character):...
perform_role() accepts multiple arguments, such that this isalso legal:
@perform_role(Thieving,Spying,Archer)classElf(Character):...
TheElf class now performs both theThieving,Spying,andArcher roles.
ifperforms(my_elf,Thieving):...
The second argument toperforms() may also be anything with a__contains__() method, meaning the following is legal:
ifperforms(my_elf,set([Thieving,Spying,BoyScout])):...
Likeisinstance(), the object needs only to perform a singlerole out of the set in order for the expression to be true.
Early drafts of this PEP[5] envisioned roles as competingwith the abstract base classes proposed inPEP 3119. After furtherdiscussion and deliberation, a compromise and a delegation ofresponsibilities and use-cases has been worked out as follows:
Ordering role might require thatsome set of ordering operators be defined.classOrdering(metaclass=Role):def__ge__(self,other):passdef__le__(self,other):passdef__ne__(self,other):pass# ...and so on
In this way, we’re able to indicate an object’s role or functionwithin a larger system without constraining or concerning ourselveswith a particular implementation.
OrderingMixin that implements several ordering operators interms of other operators.classOrderingMixin:def__ge__(self,other):returnself>otherorself==otherdef__le__(self,other):returnself<otherorself==otherdef__ne__(self,other):returnnotself==other# ...and so on
Using this abstract base class - more properly, a concretemixin - allows a programmer to define a limited set of operatorsand let the mixin in effect “derive” the others.
By combining these two orthogonal systems, we’re able to botha) provide functionality, and b) alert consumer systems to thepresence and availability of this functionality. For example,since theOrderingMixin class above satisfies the interfaceand semantics expressed in theOrdering role, we say the mixinperforms the role:
@perform_role(Ordering)classOrderingMixin:def__ge__(self,other):returnself>otherorself==otherdef__le__(self,other):returnself<otherorself==otherdef__ne__(self,other):returnnotself==other# ...and so on
Now, any class that uses the mixin will automatically – that is,without further programmer effort – be tagged as performing theOrdering role.
The separation of concerns into two distinct, orthogonal systemsis desirable because it allows us to use each one separately.Take, for example, a third-party package providing aRecursiveHash role that indicates a container takes itscontents into account when determining its hash value. SincePython’s built-intuple andfrozenset classes follow thissemantic, theRecursiveHash role can be applied to them.
>>>perform_role(RecursiveHash)(tuple)>>>perform_role(RecursiveHash)(frozenset)
Now, any code that consumesRecursiveHash objects will now beable to consume tuples and frozensets.
Perl 6 allows instances to perform different roles than their class.These changes are local to the single instance and do not affectother instances of the class. For example:
my_elf=Elf()my_elf.goes_on_quest()my_elf.becomes_evil()now_performs(my_elf,Thieving)# Only this one elf is a thiefmy_elf.steals(["purses","candy","kisses"])
In Perl 6, this is done by creating an anonymous class thatinherits from the instance’s original parent and performs theadditional role(s). This is possible in Python 3, though whether itis desirable is still is another matter.
Inclusion of this feature would, of course, make it much easier toexpress the works of Charles Dickens in Python:
>>>fromliteratureimportrole,BildungsRoman>>>fromdickensimportUrchin,Gentleman>>>>>>withBildungsRoman()asOliverTwist:...mr_brownlow=Gentleman()...oliver,artful_dodger=Urchin(),Urchin()...now_performs(artful_dodger,[role.Thief,role.Scoundrel])......oliver.has_adventures_with(ArtfulDodger)...mr_brownlow.adopt_orphan(oliver)...now_performs(oliver,role.RichWard)
Neal Norwitz has requested the ability to make assertions aboutthe presence of attributes using the same mechanism used to requiremethods. Since roles take effect at class definition-time, andsince the vast majority of attributes are defined at runtime by aclass’s__init__() method, there doesn’t seem to be a good wayto check for attributes at the same time as methods.
It may still be desirable to include non-enforced attributes in therole definition, if only for documentation purposes.
Under the proposed semantics, it is possible for roles tohave roles of their own.
@perform_role(Y)classX(metaclass=Role):...
While this is possible, it is meaningless, since rolesare generally not instantiated. There has been someoff-line discussion about giving meaning to this expression, but sofar no good ideas have emerged.
It is currently not possible to ask a class if its instances performa given role. It may be desirable to provide an analogue toperforms() such that
>>>isinstance(my_dwarf,Dwarf)True>>>performs(my_dwarf,Surly)True>>>performs(Dwarf,Surly)False>>>class_performs(Dwarf,Surly)True
An early draft of this PEP included a separate mechanism fordynamically assigning a role to a class. This was spelled
>>>now_perform(Dwarf,GoldMiner)
This same functionality already exists by unpacking the syntacticsugar provided by decorators:
>>>perform_role(GoldMiner)(Dwarf)
At issue is whether dynamic role assignment is sufficiently importantto warrant a dedicated spelling.
Though the phrasings laid out in this PEP are designed so that theroles system could be shipped as a stand-alone package, it may bedesirable to add special syntax for defining, assigning andquerying roles. One example might be a role keyword, which wouldtranslate
classMyRole(metaclass=Role):...
into
roleMyRole:...
Assigning a role could take advantage of the class definitionarguments proposed inPEP 3115:
classMyClass(performs=MyRole):...
A reference implementation is forthcoming.
Thanks to Jeffery Yasskin, Talin and Guido van Rossum for severalhours of in-person discussion to iron out the differences, overlapand finer points of roles and abstract base classes.
This document has been placed in the public domain.
Source:https://github.com/python/peps/blob/main/peps/pep-3133.rst
Last modified:2025-02-01 08:59:27 GMT