1

I'm wondering how I can update an attribute that has been set using the @property decorator in this case. (the code below will tell you more than words...)

When I try to update the email without a setter, I get an AttributeError: can't set attribute. When I do use a setter, nothing changes. The new email doesn't use neither the firstname nor the lastname.

Could anybody help?

class Employee:
    def __init__(self, first, last):
        self.first = first
        self.last = last

    @property
    def email(self):
        return f"{self.first}.{self.last}@email.com".lower()

    # @email.setter
    # def email(self, new_email):
    #     return new_email
Scott Hunter
  • 48,888
  • 12
  • 60
  • 101
Five
  • 13
  • 2
  • You should use the first and last attributes to change the email otherwise you'd need to ensure that the email when set is in first.last@email.com format then set those attributes then honestly you should just return None or have no return statement at all as None return is implied. Although to be honest you'd have to determine what logic you want to use here in the end either you determine email off first and last or they are seperate. – Jab Sep 05 '19 at 12:41
  • 2
    Your getter constructs an e-mail address on the fly from the first and last names; it doesn't really make sense to set the e-mail address explicitly. Is the `email.com` address just supposed to be a default in the event that no explicit address is set? – chepner Sep 05 '19 at 12:45

2 Answers2

2

I think the most straightforward path here would be to do away with the property, and instead make email an optional paramater that defaults to first.last:

class Employee:
    def __init__(self, first, last, email=None):
        self.first = first
        self.last = last
        self.email = email if email else f"{first}.{last}@email.com".lower()

Now you can modify the email address for an existing instance with the usual dot notation:

>>> e = Employee('John', 'Doe')
>>> e.email
'john.doe@email.com'
>>>
>>> e.email = 'a@b.com'
>>> e.email
'a@b.com'

If you're really intent on keeping the property, the setter needs to update an instance attribute:

class Employee:
    def __init__(self, first, last):
        self.first = first
        self.last = last
        self._email = f"{first}.{last}@email.com".lower()

    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, addr):
        self._email = addr

You might choose this pattern if you need to do some validation on the way in - say, confirm the new email has an @ sign:

    @email.setter
    def email(self, addr):
        if '@' not in addr:
            raise ValueError('nope!')
        self._email = addr

but otherwise the first alternative is a bit simpler.

chris
  • 1,915
  • 2
  • 13
  • 18
  • Thanks a lot, Chris. The only thing that is unclear to me is the use of the single underscore: I can only notice that if I remove it, I get a RecursionError, although I've always read that this single underscore is purely conventional and does not affect the behaviour of your code (unless you're using * imports). Apparently that's not the case... – Five Sep 06 '19 at 14:53
  • The underscore is convention, as you said. If you remove it, you create a situation where you have two things with the same name: the email attribute and the email property. That duplication creates an ambiguity - when you refer to `employee.email`, are you referencing the instance variable or the property? Hence they need separate names. Hope that helps. – chris Sep 06 '19 at 15:13
  • I'm new to programming but I may be a bit thick too...: when I use "_email", the replication attribute/property remains. I'm very confused. – Five Sep 06 '19 at 18:01
  • Have a look at this old thread, good explanation of properties and how they work under the hood here: https://stackoverflow.com/questions/17330160/how-does-the-property-decorator-work – chris Sep 09 '19 at 13:05
0

As I posted in my comment you need to determine if the new email is in first.last@email.com then just set the first and last attributes.

Although I wouldn't use email as a property if you're creating it based on the name you should change the name itself.

class Employee:
    def __init__(self, first, last):
        self.first = first
        self.last = last

    def email(self):
        return f"{self.first}.{self.last}@email.com".lower()

    def __repr__(self):
      return f"<Employee | {self.first} {self.last}>"

john = Employee('John', 'Smith')
print(john)
#<Employee | John Smith>
john.first = "Joe"
print(john)
#<Employee | Joe Smith>

Otherwise if you want a setter for email then I suggest using it to set the first and last attributes but you don't need a return value as you're just setting the email you'd already know it. I'd use the re library to check if the email is in correct format. This is a very rough example:

@email.setter
def email(self, new_email):
    try:
        self.first, self.last = re.search('(?P<first>\w+).(?P<last>\w+)@\S+', email).group('first', 'last'))
    except AttributeError:
        raise ValueError("Email must be in "first.last@email.com")
Jab
  • 26,853
  • 21
  • 75
  • 114