How to pythonically have partially-mutually exclusive optional arguments?

后端 未结 7 2254
鱼传尺愫
鱼传尺愫 2021-02-13 18:04

As a simple example, take a class Ellipse that can return its properties such as area A, circumference C, major/minor axis a/b

7条回答
  •  独厮守ぢ
    2021-02-13 18:59

    Included below is an approach which I've used before for partial data dependency and result caching. It actually resembles the answer @ljetibo provided with the following significant differences:

    • relationships are defined at the class level
    • work is done at definition time to permute them into a canonical reference for dependency sets and the target variables that may be calculated if they are available
    • calculated values are cached but there is no requirement that the instance be immutable since stored values may be invalidated (e.g. total transformation is possible)
    • Non-lambda based calculations of values giving some more flexibility

    I've written it from scratch so there may be some things I've missed but it should cover the following adequately:

    • Define data dependencies and reject initialising data which is inadequate
    • Cache the results of calculations to avoid extra work
    • Returns a meaningful exception with the names of variables which are not derivable from the specified information

    Of course this can be split into a base class to do the core work and a subclass which defines the basic relationships and calculations only. Splitting the logic for the extended relationship mapping out of the subclass might be an interesting problem though since the relationships must presumably be specified in the subclass.

    Edit: it's important to note that this implementation does not reject inconsistent initialising data (e.g. specifying a, b, c and A such that it does not fulfil the mutual expressions for calculation). The assumption being that only the minimal set of meaningful data should be used by the instantiator. The requirement from the OP can be enforced without too much trouble via instantiation time evaluation of consistency between the provided kwargs.

    import itertools
    
    
    class Foo(object):
        # Define the base set of dependencies
        relationships = {
            ("a", "b", "c"): "A",
            ("c", "d"): "B",
        }
    
        # Forumulate inverse relationships from the base set
        # This is a little wasteful but gives cheap dependency set lookup at
        # runtime
        for deps, target in relationships.items():
            deps = set(deps)
            for dep in deps:
                alt_deps = deps ^ set([dep, target])
                relationships[tuple(alt_deps)] = dep
    
        def __init__(self, **kwargs):
            available = set(kwargs)
            derivable = set()
            # Run through the permutations of available variables to work out what
            # other variables are derivable given the dependency relationships
            # defined above
            while True:
                for r in range(1, len(available) + 1):
                    for permutation in itertools.permutations(available, r):
                        if permutation in self.relationships:
                            derivable.add(self.relationships[permutation])
                if derivable.issubset(available):
                    # If the derivable set adds nothing to what is already noted as
                    # available, that's all we can get
                    break
                else:
                    available |= derivable
    
            # If any of the variables are underivable, raise an exception
            underivable = set(self.relationships.values()) - available
            if len(underivable) > 0:
                raise TypeError(
                    "The following properties cannot be derived:\n\t{0}"
                    .format(tuple(underivable))
                )
            # Store the kwargs in a mapping where we'll also cache other values as
            # are calculated
            self._value_dict = kwargs
    
        def __getattribute__(self, name):
            # Try to collect the value from the stored value mapping or fall back
            # to the method which calculates it below
            try:
                return super(Foo, self).__getattribute__("_value_dict")[name]
            except (AttributeError, KeyError):
                return super(Foo, self).__getattribute__(name)
    
        # This is left hidden but not treated as a staticmethod since it needs to
        # be run at definition time
        def __storable_property(getter):
            name = getter.__name__
    
            def storing_getter(inst):
                # Calculates the value using the defined getter and save it
                value = getter(inst)
                inst._value_dict[name] = value
                return value
    
            def setter(inst, value):
            # Changes the stored value and invalidate saved values which depend
            # on it
                inst._value_dict[name] = value
                for deps, target in inst.relationships.items():
                    if name in deps and target in inst._value_dict:
                        delattr(inst, target)
    
            def deleter(inst):
                # Delete the stored value
                del inst._value_dict[name]
    
            # Pass back a property wrapping the get/set/deleters
            return property(storing_getter, setter, deleter, getter.__doc__)
    
        ## Each variable must have a single defined calculation to get its value
        ## Decorate these with the __storable_property function
        @__storable_property
        def a(self):
            return self.A - self.b - self.c
    
        @__storable_property
        def b(self):
            return self.A - self.a - self.c
    
        @__storable_property
        def c(self):
            return self.A - self.a - self.b
    
        @__storable_property
        def d(self):
            return self.B / self.c
    
        @__storable_property
        def A(self):
            return self.a + self.b + self.c
    
        @__storable_property
        def B(self):
            return self.c * self.d
    
    
    if __name__ == "__main__":
        f = Foo(a=1, b=2, A=6, d=10)
        print f.a, f.A, f.B
        f.d = 20
        print f.B
    

提交回复
热议问题