Scala’s Case Class in Python with Case Matching
I’ve been doing a lot of Scala programming lately. Scala is a statically typed language that compiles to the JVM. Scala doesn’t break away…
I’ve been doing a lot of Scala programming lately. Scala is a statically typed language that compiles to the JVM. Scala doesn’t break away from object oriented programming but has a lot of functional programming features.
One of my favorite features of Scala is its case classes. Case classes are a lot like regular classes but they’re easier to setup and usually used to model immutable data. They also allow for easy pattern matching.
case class Person(
name: String,
age: Int
)
val bob = Person(name="bob", age=20)
Values in a case class are publicly accessible and immutable by default. Note there is no new
keyword when instantiating a case class.
Case classes can be compared:
val bob2 = Person(name="bob", age=20)
println(bob == bob2) // true
Since case classes are usually used to model immutable data, Scala gives you a copy
function where you can override certain fields. Copy returns a new instance
val youngBob = bob.copy(age=15)
Scala also allows for matching.
bob match {
case Person("bob", _) => println("hi bob!")
case _ => println("I don't know you")
}
If you are interested in learning more about Scala, I recommend Scala for the Impatient. If you want to just play around, check out ScalaFiddle.
I like Scala case classes so I thought it would be fun to build them out in Python.
Case Classes in Python
See full code below and skip down for discussion and examples:
def CaseClass(case_class_name, **kwargs): types = {} for k, v in kwargs.items(): if type(v) is not type: raise ValueError("{} is not a type".format(v)) types[k] = v class CaseClassImpl(): initialized = False def __init__(self, **kwargs): self._name = case_class_name self._keys = list(kwargs.keys()) self._types = types expected_keys = set(self._types.keys()) actual_keys = set(kwargs.keys()) extra_keys = actual_keys.difference(expected_keys) missing_keys = expected_keys.difference(actual_keys) if missing_keys: raise ValueError("Missing values {}".format(missing_keys)) if extra_keys: raise ValueError("Extra values {}".format(extra_keys)) for k, v in kwargs.items(): expected_type = self._types.get(k) actual_type = type(v) if actual_type is not expected_type: raise ValueError("{} is of {}, must be of {}".format(k, actual_type, expected_type )) self.__setattr__(k, v) self.initialized = True def __repr__(self): inner = ", ".join(["{}={}".format(k, self.__getattribute__(k)) for k in self._keys]) return "{}({})".format(self._name, inner) def __str__(self): return self.__repr__() def __eq__(self, other): return type(self) is type(other) and \ self._name == other._name and \ set(self._keys) == set(other._keys) and \ all([other.__getattribute__(k) == self.__getattribute__(k) for k in self._keys]) def __setattr__(self, name, value): if not self.initialized: return super.__setattr__(self, name, value) else: if name in self._keys: raise AttributeError("Reassignment to val") else: raise AttributeError("Value {} is no a member of {}".format(name, self._name)) def copy(self, **kwargs): """ Returns a copy of the instance with optional overrides """ orig = {k: self.__getattribute__(k) for k in self._keys} updated = {**orig, **kwargs} return CaseClassImpl(**updated) def match(self, *others): """ matches on a tuple of match keys and return values e.g. >>> case_class.match( ({'key1': 'val1'}, "match on first!"), ({'key1': 'val2', 'key2': 'val2'}, "match on second!"), ({}, "match on thrid!"), ) """ for match_key, match_val in others: if match_key is None or self.copy(**match_key) == self: return match_val return CaseClassImpl
CaseClass is not actually a class, but a function that returns a class. The function takes in any number of named parameters and their types (Scala is strongly typed after-all). The function raises a ValueError if your parameter value is not a type.
Initializing a CaseClass can be seen below. Remember, the function returns the class.
Person = CaseClass("Person", name=str, age=int)
Now that we have a Person case class, we can create a few Person instances:
bob = Person(name="bob", age=20)
Our case class enforces the right parameters are passed and even the right types:
Person(name="bob") # ValueError: Missing values {'age'}
Person(name="bob", age=20, weight=170) # ValueError: Extra values {'weight'}
Person(name="bob", age=20.5) # ValueError: age is of <class 'float'>, must be of <class 'int'>
And case classes are meant to model immutable data, our values are immutable:
bob.name = "jimmy" # AttributeError: Reassignment to val
bob.zip_code = 90210 # AttributeError: Value zip_code is not a member of Person
We also have some useful features, like equality:
bob2 = Person(
name="bob",
age=20
)
young_bob = Person(name="bob", age=15)
bob == bob2 # Truebob == young_bob # False
and copy:
big_bob = bob.copy(name="big bob")
and of course match:
bob.match(
({"name": "alice"}, "Its alice!"),
({"name": "bob"}, "Hi bob!"),
(None, "I don't know you")
)
bob.match(
({"name": "bob", "age": 15}, "Hi young bob!"),
({"name": "bob", "age": 20}, "Hi older bob!"),
(None, "I don't know you")
)
Sure, it’s not as nice as Scala, but pattern matching is long overdue in Python. Scala has some other features that I didn’t build out like guards on matching (conditional statements), but I’ll leave that to the reader.