Understanding the Descriptor Protocol
In this step, we're going to learn how descriptors work in Python by creating a simple Stock
class. Descriptors in Python are a powerful feature that allow you to customize how attributes are accessed, set, and deleted. The descriptor protocol consists of three special methods: __get__()
, __set__()
, and __delete__()
. These methods define how the descriptor behaves when an attribute is accessed, assigned a value, or deleted, respectively.
First, we need to create a new file called stock.py
in the project directory. This file will contain our Stock
class. Here's the code you should put in the stock.py
file:
## stock.py
class Stock:
__slots__ = ['_name', '_shares', '_price']
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
self._name = value
@property
def shares(self):
return self._shares
@shares.setter
def shares(self, value):
if not isinstance(value, int):
raise TypeError('Expected an integer')
if value < 0:
raise ValueError('Expected a positive value')
self._shares = value
@property
def price(self):
return self._price
@price.setter
def price(self, value):
if not isinstance(value, (int, float)):
raise TypeError('Expected a number')
if value < 0:
raise ValueError('Expected a positive value')
self._price = value
def cost(self):
return self.shares * self.price
def sell(self, amount):
self.shares -= amount
def __repr__(self):
return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'
In this Stock
class, we're using the property
decorator to define getter and setter methods for the name
, shares
, and price
attributes. These getter and setter methods act as descriptors, which means they control how these attributes are accessed and set. For example, the setter methods validate the input values to ensure they are of the correct type and within an acceptable range.
Now that we have our stock.py
file ready, let's open a Python shell to experiment with the Stock
class and see how descriptors work in action. To do this, open your terminal and run the following commands:
cd ~/project
python3 -i stock.py
The -i
option in the python3
command tells Python to start an interactive shell after executing the stock.py
file. This way, we can directly interact with the Stock
class we just defined.
In the Python shell, let's create a stock object and try accessing its attributes. Here's how you can do it:
s = Stock('GOOG', 100, 490.10)
s.name ## Should return 'GOOG'
s.shares ## Should return 100
When you access the name
and shares
attributes of the s
object, Python is actually using the descriptor's __get__
method behind the scenes. The property
decorators in our class are implemented using descriptors, which means they handle the access and assignment of attributes in a controlled way.
Let's take a closer look at the class dictionary to see the descriptor objects. The class dictionary contains all the attributes and methods defined in the class. You can view the keys of the class dictionary using the following code:
Stock.__dict__.keys()
You should see output similar to this:
dict_keys(['__module__', '__slots__', '__init__', 'name', '_name', 'shares', '_shares', 'price', '_price', 'cost', 'sell', '__repr__', '__doc__'])
The keys name
, shares
, and price
represent the descriptor objects created by the property
decorators.
Now, let's examine how descriptors work by manually calling their methods. We'll use the shares
descriptor as an example. Here's how you can do it:
## Get the descriptor object for 'shares'
q = Stock.__dict__['shares']
## Use the __get__ method to retrieve the value
q.__get__(s, Stock) ## Should return 100
## Use the __set__ method to set a new value
q.__set__(s, 75)
s.shares ## Should now be 75
## Try to set an invalid value
try:
q.__set__(s, '75') ## Should raise TypeError
except TypeError as e:
print(f"Error: {e}")
When you access an attribute like s.shares
, Python calls the __get__
method of the descriptor to retrieve the value. When you assign a value like s.shares = 75
, Python calls the __set__
method of the descriptor. The descriptor can then validate the data and raise errors if the input value is not valid.
Once you're done experimenting with the Stock
class and descriptors, you can exit the Python shell by running the following command:
exit()