February 18, 2016
Building on my thoughts from Handling Types in Python – Ignore it, Convert it, or Check it, I’m continuing to build up to my talk at PyCaribbean in Santa Domingo Dominican Republic. My title will be “Python: As much typing as you want,” obviously this means that I’ve been doing some thinking quite a bit about Python typing of late.
Previously I looked at the three basic strategies Python has for dealing with types; ignoring them, converting them, or checking types. All of these are very practical in their own right, but basically limited to working inside a given function call. However Python is an object oriented language and we do have the option to work with that and create an object attribute which has a type enforced on it.
Getters and Setters
Getters and setters are a common way to handle this. We could write these in Python, it would look something like this:
def set_age(self, age):
self.__age = int(age)
assert norm.get_age() == 23
Of course this is combining with a converting strategy to ensure that age is always an integer, set_age(‘Green’) will cause a ValueError, obviously we could do type check instead.
Properties via @property
Of course in Python we don’t do it that way. In Python we can create a property using the @property decorator like this:
def name(self, value):
self.__dict__['name'] = str(value)
norm.name = "Andy"
assert norm.name == "Andy"
norm.name = 42
assert norm.name == "42"
Again we can enforce any amount of typecasting either within the getter or the setter
Properties via property()
Since decorators were added to Python in 2.4 this has been by far the preferred way to declare properties. However, before decorators we already had properties the syntax we used then is still usable and for enforcing types it’s really handy. Combining our getter and setter from the age with the alternate syntax we can create an age property:
age = property(get_age,set_age)
norm.age = '28'
assert norm.age == 28
in and of itself the age property is like any other. This will however allow us to get some much better code reuse. Let’s assume we want to have a few properties with the same constraint, we’ll enforce them as Decimals so we can store money. Let’s write a function that creates a set of getter, setter, and deleter for this purpose:
def setter(self, value):
self.__dict__[name] = Decimal(value)
return getter, setter, deleter
now we can use it like this:
price = property(*create_gsd_convert_Decimal('price'))
price = 380.23
norm.price = price
assert isinstance(price, float)
assert not isinstance(price, Decimal)
assert isinstance(norm.price, Decimal)
assert not isinstance(norm.price, float)
using a function to return a tuple is a bit clunky, so lets clean that up a touch, I’ll continue to use the function creator function above adding only the call to property:
much cleaner to use now
tax = enforce_Decimal('tax')
The interface here is pretty much as clean as it’s going to get without introducing a metaclass, but in my talk and a future blog post I’m going to explore how to use the descriptor protocol to simplify the mechanics behind this.