Search code examples
pythonlistcompositionpython-class

Python: conserving list structure when working with Composition-Composite classes


How to conserve list structure when working with Composition in Python?

For instance,

class Population:
    '''Defines a population of Colonies'''
    def __init__(self, Nc, ...):
        self.number_of_colonies = Nc
        colonies: list[Colony]
        self.colonies = list(Colony(...) for i in range(0, Nc))

class Colony:
    '''Defines a Colony'''
    def __init__(self, ...):
        self.propertyA = 1

myPop = Population(10)

Therefore myPop is a list of 10 colonies, which I access by myPop.colonies. Now, I want a list of propertyA across all 10 colonies. Is there a way to conserve the structure of colony and write myPop.colonies.propertyA to return [1,1,1,1,1,1,1,1,1,1] ?

Basically it'd be nice to have a way around writing [colony.propertyA for colony in myPop.colonies] as it appear very frequently in my code.

I'm reading about composition and composites with multiple compositions in Python, but I haven't found the 'A.B.C' structure.


Solution

  • You can't do this out of the box, because .colonies part is a list. It has access to methods any other list has, which means something like propertyA isn't really defined.

    There are several ways to enable this kind of syntax and I'll present 2 of them, both with some upsides and downsides.

    class Population:
        '''Defines a population of Colonies'''
        def __init__(self, Nc):
            self.number_of_colonies = Nc
            self.colonies = ColonyList(Colony() for _ in range(0, Nc))
    
    class Colony:
        '''Defines a Colony'''
        def __init__(self):
            self.propertyA = 1
    
    class ColonyList(list[Colony]):
        @property
        def propertyA(self):
            return [el.propertyA for el in self]
        
    myPop = Population(10)
    
    print(myPop.colonies.propertyA)
    

    This explicitly defines a propertyA on the list object, meaning only this specific attribute will be able to be accessed this way. This requires more handling for additional attributes, but is less error-prone, as the property is explicitly created.

    Another, slightly similiar (with list subclass) approach is to implement __getattr__ method that will handle all attributes that don't share names with list methods, but has a greater chance of attempting to get a value of attribute that does not exist:

    class ColonyList(list[Colony]):
        def __getattr__(self, name):
            return [getattr(el, name) for el in self]