Home > Mobile >  What is the pythonic way to add a flag to an object after it has been instantiated?
What is the pythonic way to add a flag to an object after it has been instantiated?

Time:07-03

Imagine a factory that produces bottles and at the end, a label is attached to each bottle, indicating its contents.

So we have objects of class Bottle, and a function that attaches an object of type Label to each Bottle.

The issue then is that the label cannot be given to bottle in its __init__ function, as the label is created after the bottle. We could of course just add a flag that equals None, and mutate it later, but then we have two issues: a) mutation and b) we cannot differentiate between the situations where the label has not yet been put on and the situation where the label has been put on with a value equalling None.

What is the pythonic way to handle this?

My solution is below, which looks horrible! What else can I do?

class BottleWithLabel(Bottle):
    def __init__(bottle, label)
        self.bottle = bottle
        self.label = label
   def __getattribute__(attr):
       if attr not in ['label']:
            return getattr(self.bottle, attr)
       return object.__getattribute__(self, attr)

# apply label
bottle = BottleWitLabel(bottle, label)
# get label
bottle.label
# check if bottle has undergone the labelling process
isinstance(bottle, BottleWithLabel)

CodePudding user response:

I think the cleanest way for the use-case you described is to use a sentinel value, i.e. an object that indicates you don't have a label. In that way, your label may be None:

class Unlabelled:
    def __init__(self): raise Exception('Should not be instantiated')


class Bottle:
    def __init__(self):
        self.label = Unlabelled  # Note: not an instance

    def is_labelled(self):  # Alternative method usage
        return self.label is not Unlabelled


b = Bottle()
if b.label is Unlabelled:  # Could also be b.label == Unlabelled
    b.label = None
assert b.is_labelled()

Being mutable or immutable is perpendicular to this implementation: you can add a method to return a copy of the object every time you label it, for example.

As for the "pythonicity" of this solution, it is so common that there is a PEP about that. You can see different implementations (including simpler ones) on that PEP as well.

CodePudding user response:

It might be worth adding more details about the use cases for these classes to get a more specific answer to your question. In general, I would probably just use label=None in __init__ but if you are really opposed to that, there are other ways. Python's hasattr function will tell you if the bottle has a label attribute yet

def hasLabel(bottle : Bottle) -> bool:
    return hasattr(bottle, "label")

Then labeling the bottle is as simple as

bottle1.label = "my Label"
bottle2.label = None # this will still register with hasattr as "having a label"

CodePudding user response:

If you want a way to add a label without any mutation, you can write a method addLabel that creates a new Bottle with a label. Optionally, you could write a hasLabel method that checks whether the bottle has a label.

class Bottle:
    """A bottle. The optional argument specifies a label."""

    def __init__(self, label=None):
        self.label = label

    def addLabel(self, label):
        """Make a new bottle with a label added."""
        return Bottle(label)

    def hasLabel(self):
        """Return True if the bottle has been labelled."""
        return self.label is not None


label = 'Water'
# apply label
bottle = Bottle(label)
# get label
print(bottle.label)
# check if bottle has undergone the labelling process
print(bottle.label is not None)

CodePudding user response:

It sounds to me like you have two different types of thing here - a bottle which hasn't gone through the labelling process, and a bottle which has (though it may have had no label attached to it). Especially if you want to avoid mutation, your solution which makes BottleWithLabel a separate type is the right way to go, but your solution which delegates attribute accesses to the bottle object is not Pythonic, because "explicit is better than implicit". I would simply do this:

from collections import namedtuple
LabelledBottle = namedtuple('label bottle')

bottle = Bottle('freshly pressed apple juice')
labelled_bottle = LabelledBottle('ACME brand apple juice', bottle)

print('A bottle of', labelled_bottle.bottle.contents)
# A bottle of freshly pressed apple juice

Note that this way, you have to explicitly access labelled_bottle.bottle.contents instead of simply labelled_bottle.contents. This means you can't use a Bottle and a LabelledBottle interchangeably, so your functions will need to decide whether they accept bottles or labelled bottles.

That's a good thing! When you're writing a function, you should know whether the bottles it accepts have gone through the labelling process or not. But if you really need the same part of your code to handle both types, consider something like this, to make explicit that this function accepts either type:

def handle_bottle(bottle):
    is_labelled = isinstance(bottle, LabelledBottle)
    label, bottle = bottle if is_labelled else (None, bottle)
    if is_labelled:
        # label might still be None here
        print('A bottle of', bottle.contents, 'with a label of', label)
    else:
        print('A bottle of', bottle.contents, 'with no label')
  • Related