Package: rbco.caseclasses
ContentsOverviewCompatibilityInstalationUsageBasicsMutability and __slots__Structural equalityCopy-constructorConversion from/to dictionary and tupleCustom membersInheritanceOverriding case class behaviorUsingCaseClassMixinfor more flexibilityLimitationsMotivation, design decisions and other implementationsComparison with MacroPyOther implementationsDiscarded implementation ideasFunctional syntaxFields specification as parameters to the class decoratorFields specification as class attributesContributingChangelog1.0.2 (2020-04-27)1.0.1 (2014-05-09)1.0.0 (2014-05-09)OverviewProvide a compact syntax to define simple “struct-like” (or “record-like”, “bean-like”) classes.
The resulting classes are very similar tonamedtuple, but mutable, with a nicer syntax, more
flexibility and more features.Here’s a summary of the features:It’s possible to define default values for fields.Useful__repr__and__str__implementations.Structural equality, i.e, useful__eq__and__ne__implementations.copymethod (copy-constructor).Conversion from/to dictionary and tuple.__slots__declaration to improve performance and prevent assignment on unknown fields.It’s possible to define custom methods.Supports inheritance.See also themotivationsection for other implementations of the concept, speciallyMacroPywhich was the inspiration for this project and uses a very different approach.CompatibilityCurrently only Python 2.7 is supported.InstalationThe usual:pip install rbco.caseclassesOr:easy_install rbco.caseclassesUsageBasicsLet’s start by creating a simple case class:>>>fromrbco.caseclassesimportcase>>>>>>@case...classPerson(object):..."""Represent a person."""...def__init__(self,name,age=None,gender=None):passThe declared__init__is just a stub. The parameters defines which fields the class will have
and its default values. The__init__method is replaced by a new one, which takes care of
assigning the values of the fields.The constructor works as expected, according to the provided__init__stub:>>>Person('John')Person(name='John',age=None,gender=None)>>>Person('John',30,'m')Person(name='John',age=30,gender='m')>>>Person(name='John',age=30,gender='m')Person(name='John',age=30,gender='m')>>>Person('John',gender='m')Person(name='John',age=None,gender='m')Note that in the string representation the fields are in the same order as defined in the
constructor.The docstring of the class is preserved:>>>Person.__doc__'Represent a person.'The signature of the constructor is not preserved. The resulting__init__method signature
is a generic one, taking only*argsand**kwargs:>>>frominspectimportgetargspec>>>getargspec(Person.__init__)ArgSpec(args=['self'],varargs='args',keywords='kwargs',defaults=None)However the docstring contains the original signature:>>>Person.__init__.__doc__'Original signature: (self, name, age=None, gender=None)'It’s not possible to create a case class without a constructor:>>>fromrbco.caseclassesimportcase>>>>>>@case...classFoo(object):passTraceback(mostrecentcalllast):...RuntimeError:Caseclassmustdefineaconstructor.Mutability and __slots__Instances are mutable:>>>p=Person('John')>>>pPerson(name='John',age=None,gender=None)>>>p.name='Bob'>>>p.age=35>>>pPerson(name='Bob',age=35,gender=None)However it’s not possible to assign to unknown attributes:>>>p.department='sales'Traceback(mostrecentcalllast):...AttributeError:'Person'objecthasnoattribute'department'This is because of the__slots__declaration:>>>p.__slots__['name','age','gender']Structural equalityStructural equality is supported:>>>p1=Person('John',30)>>>p2=Person('Bob',25)>>>p1==p2False>>>p1!=p2True>>>p2.name='John'>>>p2.age=30>>>p1==p2True>>>p1!=p2False>>>p2.gender='m'>>>p1==p2FalseCopy-constructorA copy-constructor is provided:>>>p1=Person('John',30)>>>copy_of_p1=p1.copy()>>>p1Person(name='John',age=30,gender=None)>>>copy_of_p1Person(name='John',age=30,gender=None)>>>p1iscopy_of_p1False>>>p2=p1.copy(name='Bob',gender='m')>>>p2Person(name='Bob',age=30,gender='m')Conversion from/to dictionary and tupleConversion from/to dictionary is easy. Theas_dictmethod return anOrderedDict:>>>p1=Person('Mary',33)>>>p1Person(name='Mary',age=33,gender=None)>>>p1.as_dict()OrderedDict([('name','Mary'),('age',33),('gender',None)])>>>Person(**p1.as_dict())Person(name='Mary',age=33,gender=None)Conversion from/to tuple is also possible:>>>p1=Person('John',30)>>>p1Person(name='John',age=30,gender=None)>>>p1.as_tuple()('John',30,None)>>>Person(*p1.as_tuple())Person(name='John',age=30,gender=None)Custom membersCase classes are very much like regular classes. It’s possible to define any kind of custom
members.The most common case should be adding a custom instance method:>>>importmath>>>@case...classPoint(object):...def__init__(self,x,y):pass......defdistance(self,other):...returnmath.sqrt((self.x-other.x)**2+(self.y-other.y)**2)>>>p1=Point(0,0)>>>p2=Point(10,0)>>>p1.distance(p2)10.0Other kinds of class members are supported as well:>>>@case...classExample(object):...class_attribute='some value'......def__init__(self,field1):pass......@staticmethod...defstatic_method():...print'This is an static method.'......@classmethod...defclass_method(cls):...print'This is a class method of the class{}.'.format(cls.__name__)...>>>e=Example('example')>>>Example.class_attribute'some value'>>>e.class_attribute'some value'>>>Example.static_method()Thisisanstaticmethod.>>>Example.class_method()ThisisaclassmethodoftheclassExample.InheritanceLet’s create a base case class and a derived one:>>>@case...classPerson(object):...def__init__(self,name,age=None,gender=None):pass......defpresent(self):...print"I'm{},{}years old and my gender is '{}'.".format(...self.name,...self.age,...self.gender...)...>>>@case...classEmployee(Person):...def__init__(self,name,age=None,gender=None,department=None):passIt’s necessary to repeat the fields of the base class, but you would have to do that anyway if
you were implementing the case classes manually.Methods from the base class are inherited:>>>p=Person('John',30,'m')>>>p.present()I'm John, 30 years old and my gender is 'm'.>>>e=Employee('Mary',33,'f','sales')>>>e.present()I'm Mary, 33 years old and my gender is 'f'.Instances ofPersonandEmployeewill always be considered different, since employees
have an extra field:>>>p=Person('John')>>>e=Employee('John')>>>p==eFalseOverriding a base class method works as expected:>>>@case...classImprovedEmployee(Employee):...defpresent(self):...super(ImprovedEmployee,self).present()...print'I work at the{}department.'.format(self.department)...>>>ie=ImprovedEmployee(name='Mary',department='marketing',age=33,gender='f')>>>ie.present()I'm Mary, 33 years old and my gender is 'f'.Iworkatthemarketingdepartment.Overriding case class behaviorIt’s possible to override the standard case class methods (__repr__,__eq__, etc).
For example:>>>@case...classFoo(object):...def__init__(self,bar):pass......def__eq__(self,other):...returnTrue# All `Foo`s are equal....>>>Foo('bar')==Foo('baz')TrueIt’s even possible to call the original version on the subclass method:>>>@case...classFoo(object):...def__init__(self,bar):...pass......def__repr__(self):...return'This is my string representation: '+super(Foo,self).__repr__()...>>>Foo('bar')Thisismystringrepresentation:Foo(bar='bar')It’s not possible to override the__init__method, because it’s replaced when the@casedecorator is applied. If a custom constructor is needed using theCaseClassMixincan be
a solution.UsingCaseClassMixinfor more flexibilityThe classes created by the@casedecorator inherits fromCaseClassMixin.>>>fromrbco.caseclassesimportCaseClassMixin>>>issubclass(Person,CaseClassMixin)TrueTheCaseClassMixinprovides all the “case class” behavior, except for the constructor.
To useCaseClassMixindirectly the only requirement the subclass must match is to provide a__fields__attribute, containing a sequence of field names.This can be useful if greater flexibility is required. In the following example we create a case
class with a custom constructor:>>>classFoo(CaseClassMixin):...__fields__=('field1','field2')......def__init__(self,field1,*args):...self.field1=field1+'_modified'...self.field2=list(args)...>>>Foo('bar',1,2)Foo(field1='bar_modified',field2=[1,2])LimitationsThe constructor of a case class cannot be customized because it’s replaced when the@casedecorator is applied. See the section aboutCaseClassMixinfor an alternative.It’s not possible to assign to unknow fields because of the__slots__declaration.The constructor cannot take*argsor**kwargs:>>>@case...classFoo(object):...def__init__(self,**kwargs):passTraceback(mostrecentcalllast):...RuntimeError:Caseclassconstructorcannottake*argsor**kwargs.See the section aboutCaseClassMixinfor an alternative.Motivation, design decisions and other implementationsComparison with MacroPyThe idea for this project came fromMacroPy. It provides an implementation of case classes using
syntactic macros, which results in a very elegant way to define the case classes.
The motivation was to provide similar functionality without resorting to syntactic macros nor
string evaluation (the approach took by namedtuple). In other words: to provide the best
implementation possible without using much magic.The comparison toMacroPycan be summarized as follows:Advantages:No magic.Allows any kind ofcustom members, including instance methods.Since case classes are just regular classes, any kind of inheritance is allowed.Disadvantages:MacroPy syntax is much nicer. The__init__stub thing can be considered kind of ugly
in comparison.Do not support custom initialization logic. This can be achieved by usingCaseClassMixinbut
additional work will have to be done by the programmer.Do not support*argsand**kwargsin the constructor. Again, this can be achieved by
usingCaseClassMixinat the expense of doing more work.Other implementationsOther implementations of the “case class” concept (or similar) in Python exists:The constructor stub mechanism idea was stole fromthis implementationby hwiechers.A simple implementation by Brian Wickman can be found inthis Gist.This discussionon stackoverflow has some implementation ideas.Discarded implementation ideasSome implementation ideas were considered but discarded afterwards. Here some of them are
discussed.Functional syntaxThis means using a function to generate the class. This would be something like this:Person=case_class('Person','name',age=None,gender=None)The first problem with this idea is that there’s no way to preserve the order of the fields.
Thecase_classfunction would have to be defined like this:defcase_class(__name__,*args,**kwargs):...**kwargsis a unordered dictionary, so the order of the fields is lost.To overcome this the following syntax could be used:Person=case_class('Person','name','age','gender',age=None,gender=None)I thinks this syntax is not elegant enough. I don’t like the repetition of field names and to have
field names represented as both strings and parameter names.Perhaps something like this would work too:Person=case_class('Person',['name','age','gender'],{'age':None,'gender':None})But again I think the syntax is not elegant.Also, some functionalities would be difficult to support using this syntax, namely:Custom members. This would mean complicate the signature of thecase_classfunction or
add the custom members after the class is created. Like this:Person=case_class('Person',...)defpresent(self):print...Person.present=presentNot very elegant.Inheritance. This would require a new parameter to thecase_classfunction, to allow to
pass in a base class.Fields specification as parameters to the class decoratorThis would end the necessity to define an empty constructor. The syntax would be like this:@case(name,age=None,gender=None)classPerson(object):'Represent a person.'The same problem faced by the function syntax arises: field ordering is not preserved, since
thecasefunction would have to accept a**kwargsargument, which is an unordered dict.Alternate syntaxes, similar to the ones presented for the functional syntax, could overcome the
field ordering problem. However I think the solution using a__init__stub to define the
fields is more elegant.Fields specification as class attributesThe syntax would be like this:@caseclassPerson(object):name=NO_DEFAULT_VALUEage=Nonegender=NoneAgain, there’s no way to preserve the order of the fields. Thecasefunction would have to
retrieve the class attributes fromPerson.__dic__, which is unordered.Maybe something like this would work:@caseclassPerson(object):__fields__=(('name',NO_DEFAULT_VALUE),('age',None),('gender',None))However I think the solution using a__init__stub to define the fields is more elegant.ContributingPlease fork this project and submit a pull request if you would like to contribute.
Thanks in advance !Changelog1.0.2 (2020-04-27)Allow to use more recent versions offuncsigs.
[ale-rt]Minor doc tweaks.
[rafaelbco]Improve docstrings.
[rafaelbco]1.0.1 (2014-05-09)Improve documentation.Minor refactoring.1.0.0 (2014-05-09)First release.