0

I have a design question regarding "dynamic information hiding" vs static typing.

I have for the following python code

class RuleInput:
    attr1 = ...
    atrr2 = ...
    ...

def example_rule(input: RuleInput) -> bool:
   # does something and returns bool

I have many of these rule functions, each calculating its output based on the attributes in RuleInput (let's assume it's immutable).

For this code, it is clear what the input should be and tools like mypy can be used to do static type checking. Here also features like auto-completion work.

However, most of the rules only require a small number of attributes from the RuleInput. So developers could make mistakes by accessing the wrong information.

Hence, I had the idea of dynamically reducing the content of the RuleInput specific to each rule (rules just specify what they need beforehand). When doing this I lose the possibility of performing static type checking and features like auto-completion.

Here is the "dynamic information hiding" solution:

RULE_INPUTS = [
    ('attr1', int, field(default=0)),
    ('attr2', int, field(default=0))
]

RuleInputNames = Enum(
    "RuleInputNames",
    " ".join([x[0] for x in RULE_INPUTS])
)

def check(required_props):
    def extract_required_properties(full_input, required_properties):
        all_props = asdict(full_input)
        selected_props = {}
        for key in required_properties:
            selected_props[key] = all_props[key]
        return selected_props

    req_props = [x.name for x in required_props]

    RequiredProperties = make_dataclass(
        "RequiredProperties",
        [x for x in RULE_INPUTS if x[0] in req_props]
    )

    def decorator(func):
        def wrapper(full_rule_input):
            return func(
                RequiredProperties(
                    **extract_required_properties(full_rule_input,
                                                  req_edge_props),
                )
            )
        return wrapper
    return decorator

@rule(required_props=[ParticlePropertyNames.attr1])
def some_rule(rule_input):
    rule_input.attr1 # works
    rule_input.attr2 # wont work

What do you think about this "dynamic information hiding" vs static type checking? Which of these two approaches would you recommend?

2
  • How many attributes are in a RuleInput instance?
    – ddcastrodd
    Jun 28, 2020 at 1:18
  • @ddcastrodd around 15 attributes
    – steve
    Jun 28, 2020 at 7:27

1 Answer 1

3

The OOP solution is interface segregation. All rule functions you have should accept an interface exposing the methods and attributes they need to function. This allows client code to decide on a given implementation. While beeing very clear on what the rules need to function.

Let's say.

from abc import ABC

def rule_1(input: Rule1Input) -> bool: pass
class Rule1Input(ABC):
    ## define the api the rule function needs
    pass

def rule_2(input: Rule2Input) -> bool: pass
class Rule2Input(ABC):
    ## define the api the rule function needs
    pass

Client code would then be free to use the same object for all rules. Or an object per rule, or an object per every 2 rules.. it will be free on tht regard.

class RuleInput(Rule1Input, Rule2Input):
    pass

An object of RuleInput can them be passed to both rules without them never needing to know it is the same object.

rules = RuleInput()

rule_1_res = rule_1(rules)
rule_2_res = rule_2(rules)

In python, you could just use duck typing. But then you would fallback to the same issue of developers using data they should not.

You can either go all out in static typing or have tests to ensure proper calling of objects by your rule implementations. Nothing non pythonic about neither.

this is one possible solution, the nature and complexity of your problem do matter in the complexity of the code that models it.

11
  • For now, this is the best approach in my experience. However, Mypy also supports structural subtyping via protocols since Python 3.8 (with backports to older versions available as a separate package).
    – amon
    Jul 5, 2020 at 19:44
  • @amon I'm not very familiar with mypy. may I ask what would it do in practice? could one simply use duck typing instead of defining interfaces left and right? Jul 5, 2020 at 19:52
  • 1
    Duck typing is the classic “Pythonic” way but then you can't use type annotations. ABCs as in your answer is a common approach to make this type-checkable with mypy, and I have used exactly that strategy in the past. But since this in nominative typing, it is necessary to inherit from those ABCs. Protocols make it possible to do structural typing, i.e. something like static duck typing as in Typescript or Go. It's like defining an ABC, except that you don't have to inherit from it and only describe a type signature.
    – amon
    Jul 5, 2020 at 21:40
  • Hi @PedroRodrigues, thanks for your post! Since all my rules are completely different, I would define an separate interface for them, which kind of loses the meaning right? Also I don't really know how I can make the code which calls the rules feed the correct arguments (I can only think of if else statements)
    – steve
    Jul 6, 2020 at 7:23
  • 1
    My solution is exactly equal to what you are doing right now, the only difference is explicity (is that a word. In other words, I'm beeing explicit by declaring an interface, you are being implicit by using duck typing. Does interface segration result in more code than duck typing? Yes. Is that boilerplate code? maybe, in your case I would argue it is not. Python is not statically typing, so to fix the issue of devs using the wrong attributes from the parameters, you would need tests. Are tests boilerplate? I don't think so, so neither are my interfaces since they serve the exact same porpuse. Jul 6, 2020 at 10:37

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.