Home > database >  How do I use same getter and setter properties and functions for different attributes of a class the
How do I use same getter and setter properties and functions for different attributes of a class the

Time:12-09

I've got this class that I'm working on that stores Employees details. I want all attributes to be protected and be set and gotten with specific logic, but not all in a unique way. I would like the same logic to apply to my _f_name and to my _l_name attributes, I would like the same logic perhaps to be applied to attributes that take in booleans and other general cases.

I've got this for the first attribute:

@property
def f_name(self):
    return self.f_name
@f_name.setter
def f_name(self, f_name):
    if f_name != str(f_name):
        raise TypeError("Name must be set to a string")
    else:
        self._f_name = self._clean_up_string(f_name)
        
@f_name.deleter
def available(self):
    raise AttributeError("Can't delete, you can only change this value.")

How can I apply the same functions and properites to other attributes?

Thaaaanks!

CodePudding user response:

While it may seem like defining a subclass of property is possible, too many details of how a particular property work is left to the getter and setter to define, meaning it's more straightforward to define a custom property-like descriptor.

class CleanableStringProperty:
    def __set_name__(self, owner, name):
        self._private_name = "_"   name
        self.name = name

    def __get__(self, obj, objtype=None):
        # Boilerplate to handle accessing the property
        # via a class, rather than an instance of the class.
        if obj is None:
            return self
        return getattr(obj, self._private_name)

    def __set__(self, obj, value):
        if not isinstance(value, str):
            raise TypeError(f'{self.name} value must be a str')
        setattr(obj, self._private_name, obj._clean_up_string(value))

    def __delete__(self, obj):
        raise AttributeError("Can't delete, you can only change this value.")

__set_name__ constructs the name of the instance attribute that the getter and setter will use. __get__ acts as the getter, using getattr to retrieve the constructed attribute name from the given object. __set__ validates and modifies the value before using setattr to set the constructed attribute name. __del__ simply raises an attribute error, independent of whatever object the caller is trying to remove the attribute from.

Here's a simple demonstration which causes all values assigned to the "properties" to be put into title case.

class Foo:
    f_name = CleanableStringProperty()
    l_name = CleanableStringProperty()

    def __init__(self, first, last):
        self.f_name = first
        self.l_name = last

    def _clean_up_string(self, v):
        return v.title()

f = Foo("john", "doe")
assert f.f_name == "John"
assert f.l_name == "Doe"
try:
    del f.f_name
except AttributeError:
    print("Prevented first name from being deleted")

It would also be possible for the cleaning function, rather than being somethign that obj is expected to provide, to be passed as an argument to CleanableStringProperty itself. __init__ and __set__ would be modified as

def __init__(self, cleaner):
    self.cleaner = cleaner

def __set__(self, obj, value):
    if not isinstance(value, str):
        raise TypeError(f'{self.name} value must be a str')
    setattr(obj, self._private_name, self.cleaner(value))

and the descriptor would be initialized with

class Foo:
    fname = CleanableStringProperty(str.title)

Note that Foo is no longer responsible for providing a cleaning method.

CodePudding user response:

A property is just an implementation of a descriptor, so to create a property, you just need an object with a __get__, __set__, and/or __delete__ method.

In your case, you could do something like this:

from typing import Any, Callable, Tuple

class ValidatedProperty:
    def __init__(self, name: str, default_value: Any=None, validation: Callable[[Any], Tuple[str, Any]]=None):
        """Initializes a ValidatedProperty object

        Args:
            name (str): The name of the property
            default_value (Any, optional): The default value of the property. Defaults to None.
            read_only (bool, optional): True to create a read-only property. Defaults to False.
            validation (Callable[[Any], Tuple[str, Any]], optional): A Callable that takes the given value and returns an error string (empty string if no error) and the cleaned-up value. Defaults to None.
        """
        self.name = name
        self.value = default_value
        self.validation = validation

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

    def __set__(self, instance, value):
        if self.validation: 
            error, value = self.validation(value)
            if error:
                raise ValueError(f"Error setting property {self.name}: {error}")

        self.value = value

Let's define an example class to use this:

class User:
    def __name_validation(value):
        if not isinstance(value, str):
            return ("Expected string value", None)

        return ("", value.strip().title())

    f_name = ValidatedProperty("f_name", validation=__name_validation)
    l_name = ValidatedProperty("l_name", validation=__name_validation)

    def __init__(self, fname, lname):
        self.f_name = fname

        self.l_name = lname

and test:

u = User("[email protected]", "Test", "User")

print(repr(u.f_name)) # 'Test'

u.f_name = 123  # ValueError: Error setting property f_name: Expected string value

u.f_name = "robinson " # Notice the trailing space
print(repr(u.f_name))  # 'Robinson'

u.l_name = "crusoe "
print(repr(u.l_name))  # 'Crusoe'
  • Related