Jack Heathrow
Jack Heathrow

Reputation: 11

Implement descriptors to validate parameters

So my task is:

*You must implement the class Book with the attributes price, author, and name.

The author and name fields have to be immutable; The price field may have changes but has to be in the 0 <= price <= 100 range. If a user tries to change the author or name fields after an initialization or set price is out of range, the ValueError should be raised.

Implement descriptors PriceControl and NameControl to validate parameters.*

Example:


>>> b = Book("William Faulkner", "The Sound and the Fury", 12)
>>> print(f"Author='{b.author}', Name='{b.name}', Price='{b.price}'")
Author='William Faulkner', Name='The Sound and the Fury', Price='12'

>>> b.price = 55
>>> b.price
55
>>> b.price = -12  # => ValueError: Price must be between 0 and 100.
>>> b.price = 101  # => ValueError: Price must be between 0 and 100.

>>> b.author = "new author"  # => ValueError: Author can not be changed.
>>> b.name = "new name"      # => ValueError: Name can not be changed.

My decision is:

class PriceControl:
    """
    Descriptor which don't allow to set price
    less than 0 and more than 100 included.
    """
    def __init__(self):
        self.price = 0

    def __get__(self, instance, owner):
        return self.price
    def __set__(self, instance, value):
        if value < 0 or value > 100:
            raise ValueError('Price must be between 0 and 100.')
        self.price = value

class NameControl:
    """
    Descriptor which don't allow to change field value after initialization.
    """
    pass

class Book:
    author = NameControl()
    name = NameControl()
    price = PriceControl()
    def __init__(self, author, name, price):
        self.author = author
        self.name = name
        self.price = price

So as you see I implemented a solution for price control but I can't imagine how to make descriptor for author/name control. Can you help me please?


Upvotes: 1

Views: 631

Answers (2)

Igor Dyachenko
Igor Dyachenko

Reputation: 1

Score: 100

class PriceControl:
    __value: float

    def __init__(self, __value: float = None):
        self.__value = __value

    def __get__(self, instance, owner):
        return self.__value

    def __set__(self, instance, value):
        if value < 0 or value > 100:
            raise ValueError(f"ValueError: Price must be between 0 and 100.")
        self.__value = value

    def __str__(self):
        return str(self.__value)


class AuthorControl:
    __value: str

    def __init__(self, __value: str = None):
        self.__value = __value

    def __get__(self, instance, owner):
        return self.__value

    def __set__(self, instance, value):
        if self.__value is not None:
            raise ValueError(f"ValueError: Author can not be changed.")
        self.__value = value


class NameControl:
    __value: str

    def __init__(self, __value: str = None):
        self.__value = __value

    def __get__(self, instance, owner):
        return self.__value

    def __set__(self, instance, value):
        if self.__value is not None:
            raise ValueError(f"ValueError: Name can not be changed.")
        self.__value = value


class Book:
    author = AuthorControl()
    name = NameControl()
    price = PriceControl()

    def __init__(self, author, name, price):
        self.author = author
        self.name = name
        self.price = price
  

Upvotes: 0

chepner
chepner

Reputation: 532093

Descriptors don't provide an automatic way to allow exactly one assignment; you'll still need to define a setter to determine if an assignment has taken place yet.

Note that in both cases, you should attach the value to the instance received as an argument, not the descriptor itself, or else all instances of Book will share the same name, author, and price.

# Hard-coded assumption that PriceControl will operate on
# an attribute named _price; see NameControl for a way to
# derive an attribute name from the name of the descriptor itself.
class PriceControl:
    """
    Descriptor which don't allow to set price
    less than 0 and more than 100 included.
    """

    def __get__(self, instance, owner):
        return instance._price

    def __set__(self, instance, value):
        if value < 0 or value > 100:
            raise ValueError('Price must be between 0 and 100.')
        instance._price = value


class NameControl:
   # This method is invoked at class creation time,
   # with the name of the class attribute bound to the
   # descriptor instance passed as the 'name' argument.
   # I.e.,
   #
   #     class Book:
   #         author = NameControl()
   #
   # causes 
   #
   #     Book.author.__set_name__('author')
   #
   # to be called.
   def __set_name__(self, name):
       self.public_name = name
       self.private_name = "_" + name

   def __get__(self, instance, owner):
       return getattr(instance, self.private_name)

   def __set__(self, instance, value):

       # TODO: check if instance already has the private
       # instance attribute set below.

       setattr(instance, self.private_name, value)

(You may want to modify both __get__ methods to handle them being called before __set__ has a chance to create the underlying instance attribute.)

Upvotes: 0

Related Questions