1

I've a class like following:

class Invoice
    def __init__(self, invoice_id):
        self.invoice_id = invoice_id
        self._amount = None

    @property
    def amount(self):
        return self._amount

    @amount.setter
    def amount(self, amount):
        self._amount = amount

In the above example, whenever I would try to get invoice amount without setting its value, I would get a None, like following:

invoice = Invoice(invoice_id='asdf234')
invoice.amount
>> None

But in this situation, None is not the correct default value for amount. I should be able to differentiate between amount value as None vs amount value not been set at all. So question is following:

  1. How do we handle cases when class property doesn't have a right default value ? In the above example if I remove self._amount = None from init, I would get AttributeError for self._amount and self.amount would return a valid value only after I call invoice.amount = 5. Is this the right way to handle it ? But this also leads to inconsistency in object state as application would be changing instance properties at runtime.
  2. I've kept amount as a property for invoice for better understanding and readability of Invoice and its attributes. Should class properties only be used when we're aware of its init / default values ?
5
  • 2
    Why is there an @property at all if it acts exactly like a field? Anyway, some relevant questions to decide what kind of approach you want are: 1) what does an invoice with an amount of None mean? 2) is an invoice whose amount has never been set supposed to exist, or does its existence always indicate a programming error? Commented Nov 1, 2018 at 17:46
  • Let's say my application is listening an external source which is sending me Invoice id and its amount in two different events. For first event I create an invoice with no amount, make some decisions based on if amount is set or not. On second event, a new instance would have the amount value too. Provider can send a None value too for amount, application service would do certain other operations in that case. Commented Nov 1, 2018 at 17:55
  • Then don't create the Invoice object until you have both the ID and its amount. Commented Nov 1, 2018 at 17:57
  • But I still need to call different services which takes invoice instance as input and perform certain actions on first event when just the invoice id is received. Commented Nov 1, 2018 at 18:05
  • Those services won't work if the invoice has an amount on it? Commented Nov 1, 2018 at 20:15

3 Answers 3

3

None is conventionally used for the absence of a value, but sometimes you need it to actually be an allowable value. If you want to be able to distinguish between None and an un-set value, simply define your own singleton for this purpose.

class Undefined:
    __str__ = __repr__ = lambda self: "Undefined"

Undefined = Undefined()

class Invoice
    def __init__(self, invoice_id):
        self.invoice_id = invoice_id
        self._amount = Undefined

    @property
    def amount(self):
        return self._amount

    @amount.setter
    def amount(self, amount):
        if amount is Undefined:
            raise ValueError("amount must be an actual value")
        self._amount = amount

Of course, you may now need to test for Undefined in other methods to make sure they're not being used before the instance is properly initialized. A better approach might be to set the attribute during initialization and require its value to be passed in to __init__(). That way, you avoid having an Invoice in an invalid (incompletely initialized) state. Someone could still set _amount to an invalid value, but they'd simply get the trouble they were asking for. We're all adults here.

Sign up to request clarification or add additional context in comments.

Comments

2

Don't use a property in this case. Your getters and setters don't do anything except return and set the value. Just use a normal attribute. If you later want to control access, you can just add that property later without changing the interface of your class!

As for dealing with non-set values, just create your own object to represent a value that has never been set. It can be as simple as:

>>> NOT_SET = object()
>>> class Invoice:
...     def __init__(self, invoice_id):
...         self.invoice_id = invoice_id
...         self.amount = NOT_SET
...
>>> inv = Invoice(42)
>>> if inv.amount is NOT_SET:
...     inv.amount = 1
...

You could also use an enum if you want better support for typing.

Comments

1

You can make a static class and use that to determine whether a value has been set at all.

class AmountNotSet(object):
    pass


class Invoice(object):
    def __init__(self, invoice_id):
        self.invoice_id = invoice_id
        self._amount = AmountNotSet
    # ...etc...

Then you can check whether the invoice is set or not like so:

invoice1 = Invoice(1)
invoice2 = Invoice(2)
invoice2.amount = None

invoice1.amount is AmountNotSet  # => True
invoice2.amount is None          # => True

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.