Skip to content

Instantly share code, notes, and snippets.

@ItsDrike
Created January 3, 2021 00:42
Show Gist options
  • Save ItsDrike/6e6995f5925aac9aa690aaadbc2c8b58 to your computer and use it in GitHub Desktop.
Save ItsDrike/6e6995f5925aac9aa690aaadbc2c8b58 to your computer and use it in GitHub Desktop.
Explanation of python's descriptors
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