Created
January 3, 2021 00:42
-
-
Save ItsDrike/6e6995f5925aac9aa690aaadbc2c8b58 to your computer and use it in GitHub Desktop.
Explanation of python's descriptors
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ClassicalItem: | |
""" | |
The classical solution, there's nothing special about it, | |
but if we came from old implementation without this protection, | |
we would loose backwards compatibility, because `amount` attribute | |
wouldn't be accessible anymore. | |
""" | |
def __init__(self, description, amount, price): | |
self.description = description | |
self.set_amount(amount) | |
self.price = price | |
def subtotal(self): | |
return self.get_amount() * self.price | |
def set_amount(self, value): | |
if value > 0: | |
self.__amount = value | |
else: | |
raise ValueError('value must be > 0') | |
def get_amount(self): | |
return self.__amount | |
class BetterItem: | |
""" | |
This solution is better because it provides you with the | |
`amount` attribute, which means backwards compatibility | |
won't be lost and it still introduces the protection we need, | |
because it's implemented with `property` decorator using | |
getters and setters. | |
""" | |
def __init__(self, description, amount, price): | |
self.description = description | |
self.amount = amount | |
self.price = price | |
def subtotal(self): | |
return self.amount * self.price | |
@property | |
def amount(self): | |
return self.__amount | |
@amount.setter | |
def amount(self, value): | |
if value > 0: | |
self.__amount = value | |
else: | |
raise ValueError('value must be > 0') | |
class FullyProtectedItem: | |
""" | |
Here, the same property implementation is used on both | |
`amount` and `price` attributes. This is a perfectly valid | |
and functional implementation, but it's quite repetitive, | |
especially if there were more positive integer attributes added | |
later on. | |
""" | |
def __init__(self, description, amount, price): | |
self.description = description | |
self.amount = amount | |
self.price = price | |
def subtotal(self): | |
return self.amount * self.price | |
@property | |
def price(self): | |
return self.__price | |
@price.setter | |
def price(self, value): | |
if value > 0: | |
self.__price = value | |
else: | |
raise ValueError('value must be > 0') | |
@property | |
def amount(self): | |
return self.__amount | |
@amount.setter | |
def amount(self, value): | |
if value > 0: | |
self.__amount = value | |
else: | |
raise ValueError('value must be > 0') | |
class PositiveInteger: | |
""" | |
This is a basic descriptor implementation, which stores given | |
values into the instances of objects this is used with. | |
This means we must also ensure that we're using unique keys. | |
This is done by a counter class attribute, which gets incremented | |
every time this class is initialized. | |
This is perfectly valid implementation and it will work, but it | |
can be a little confusing to figure out, why it's using the | |
counter attribute. | |
In fact, this is probably the best implementation you can get if | |
you're using python version < 3.6, but if you aren't there is a | |
better way, which comes with PEP 487. | |
""" | |
__counter = 0 | |
def __init__(self): | |
prefix = '_' + self.__class__.__name__ | |
key = self.__class__.__counter | |
# This is a way of ensuring unique parameter names | |
# for storing items. | |
self.target_name = f"{prefix}_{key}" | |
self.__class__.__counter += 1 | |
def __get__(self, instance, owner): | |
return getattr(instance, self.target_name) | |
def __set__(self, instance, value): | |
if value > 0: | |
setattr(instance, self.target_name, value) | |
else: | |
raise ValueError("value must be > 0") | |
class BetterFullyProtectedItem: | |
""" | |
This implementation uses custom `PositiveInteger` descriptor | |
to enforce the same constrains on the setters for both | |
`amount` and `price` attributes, this `PositiveInteger` is a | |
custom descriptor class which does all the work for us and can | |
be re-used many times without repetition, making it perfect for | |
this usecase. | |
""" | |
amount = PositiveInteger() | |
price = PositiveInteger() | |
def __init__(self, description, amount, price): | |
self.description = description | |
self.amount = amount | |
self.price = price | |
def subtotal(self): | |
return self.amount * self.price | |
class BetterPositiveInteger: | |
""" | |
This works on the same basic concepts as the `PositiveInteger`, | |
but it uses a more intuitive way of storing it's values | |
""" | |
def __set_name__(self, owner, name): | |
""" | |
This method is called once the descriptor is initialized, | |
it automatically receives `owner` (the holding class) and | |
`name` parameters. The most important one is `name`, because | |
it tells use the attribute name used within the `owner` class. | |
This means that we don't have to bother with making unique names, | |
because that was already handled for use by the user. | |
Only aviable in python 3.6 and higher (PEP 487) | |
""" | |
self._name = name | |
def __get__(self, instance, owner): | |
return instance.__dict__.get(self._name) | |
def __set__(self, instance, value): | |
""" | |
We can use `instance.__dict__`, which is the namespace holder | |
of all variables stored in the instance itself, initially this | |
might seem wrong because we'd be overriding the descriptor instance | |
itself by some other given `value`, making this one-time only use, | |
but that's not what happens, because the descriptor was defined in | |
the class itself, not just the instace, this means instance's __dict__, | |
doesn't actually hold the descriptor, making it a safe space to store | |
the held value. | |
""" | |
if value > 0: | |
instance.__dict__[self._name] = value | |
else: | |
raise ValueError("value must be > 0") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment