A guide to Python's @property Decorator

Python's @property Decorator

Dylan | Jun 08, 2020

Post Thumbnail

The @property decorator is used in Python to simplify creating getters and setters in Object-Oriented Programming. Don’t worry if you don’t consider yourself well-versed on these concepts, we’ll break everything down piece by piece, using a hero in a new video game we’re creating as an example.

Our Hero Class

Let’s pretend we want our hero to start with 1 armor, but our player will be able to upgrade the armor rating as they progress in the game, we could setup something as simple as this:



class Hero:
def __init__(self):
self.armor = 1

Now our hero’s armor is accessible through Hero.armor and we can increase it in the following way.



arthur = Hero()
arthur.armor += 1 # add one to whatever our hero’s armor currently is

This will work, but what if our hero reaches 100 armor and no longer takes any damage? It would be best if we could enforce some sort of cap to the armor. Let’s say we want to make the max armor 5. How could we do this?

Getters and Setters

We can add methods to our classes known as Getters to retrieve class attributes (arthur.armor) without calling it directly. Let’s pretend there are 5 types of armor that correspond to the 5 possible armor ratings. We will create a dictionary to perform a lookup so our class will know which rating corresponds to which type of armor in a human-readable string. Our getter method will be able to perform some logic to immediately return a string with the type of armor our hero has equipped.

By introducing Setters to our classes, we can validate incoming changes to our hero’s armor attribute to make sure only known armor types can be added. Let’s expand our basic Hero class we wrote above.



class Hero:
def __init__(self):
self.armor = 1
self.armor_types = {'cloth': 1,
'leather': 2,
'bronze': 3,
'iron': 4,
'steel': 5}

def get_armor_name(self):
# loop through each key/value pair in the self.armor_types dictionary
for key, value in self.armor_types.items():
if self.armor == value:
return key

def set_armor_name(self, armor_name):
if armor_name in self.armor_types:
self.armor = self.armor_type[armor_name]
else:
raise ValueError(f"{armor_name} is an unknown armor type")


Using our getter method "get_armor_type()", we’re able to get the name of our armor by looking up our current armor level in our armor_types dictionary. We can hide any complicated logic inside a getter method.

Using our setter method "set_armor_type()", we can change our hero’s armor rating when the player purchases new armor, but before we change the armor attribute, we can validate the passed armor_name to make sure it is a valid armor_type. If the armor type is unknown, we can raise a ValueError to help us catch mistakes as we continue developing our game.

The Property Class

We can create a property object inside of our Hero class to automatically refer to the getters and setters that we’ve created whenever we call that object from our class. Take a look at the addition below.



class Hero:
def __init__(self):
self.armor = 1
self.armor_types = {'cloth': 1,
'leather': 2,
'bronze': 3,
'iron': 4,
'steel': 5}

def get_armor_name(self):
for key, value in self.armor_types.items():
if self.armor == value:
return key

def set_armor_name(self, armor_name):
if armor_name in self.armor_types:
self.armor = self.armor_type[armor_name]
else:
raise ValueError(f"{armor_name} is an unknown armor type")

# create a property object inside our class
armor_name = property(get_armor_name, set_armor_name)


Let’s see what adding this property object does when we reference it.



arthur = Hero():
print(arthur.armor_name)

arthur.armor_name('iron')
print(arthur.armor_name)

OUTPUT:
>> 'cloth'
>> 'iron'




It automatically referred to our get_armor_name() method when we asked it to print and it also automatically referred to our set_armor_name() method when we passed a parameter into it!

If you’re a little confused about how it works, it may help to think about it this way. When we added the armor_name object to our Hero class, we were essentially doing the following:



class Hero:

armor_name = property()
# add a getter to the armor_name object
armor_name = armor_name.getter(get_armor_name)
# add a setter to the armor_name object
armor_name = armor_name.setter(set_armor_name)

Now let’s see how we can make this even easier using a decorator.

Python’s @property Decorator

By using Python decorators, we can streamline the process of creating property objects in our classes while also avoiding cluttering our namespaces with so many "gets" and "sets". Python decorators begin with "@" and are placed on the line above the method they alter. Here is our Hero class rewritten with the @property decorator.



class Hero:
def __init__(self):
self.armor = 1
self.armor_types = {'cloth': 1,
'leather': 2,
'bronze': 3,
'iron': 4,
'steel': 5}

@property
def armor_name(self):
for key, value in self.armor_types.items():
if self.armor == value:
return key

@armor_name.setter
def armor_name(self, armor_name):
if armor_name in self.armor_types:
self.armor = self.armor_type[armor_name]
else:
raise ValueError(f"{armor_name} is an unknown armor type")


We don’t have to add the property object to our class anymore. Everything is handled automatically by the decorators. @property will create armor_name into a property object and then later our @armor_name.setter assigns whatever setter method we would like to our property object. Also, note that "get" and "set" have been removed from our method names.

I hope this introduction was helpful! If this is your first time encountering decorators in Python, don’t feel bad if you still seem a little confused. They take a little time to wrap your head around but once you understand them, you’ll be able to write cleaner and more Pythonic code! If you have any questions about @property or Python decorators in general, please ask me in the comments below! As always, I encourage you to go out and try to apply this in your own projects. Thanks for reading and happy coding from Nimble Coding!

Comments