4

This is a little difficult to explain, so let's hope I'm expressing the problem coherently:

Say I have this list:

my_list = ["a string", 45, 0.5]

The critical point to understand in order to see where the question comes from is that my_list is generated by another function; I don't know ahead of time anything about my_list, specifically its length and the datatype of any of its members.

Next, say that every time <my_list> is generated, there is a number of predetermined operations I want to perform on it. For example, I want to:

my_text = my_list[1]+"hello"
some_var = my_list[10]
mini_list = my_list[0].split('s')[1]
my_sum = my_list[7]+2

etc. The important point here is that it's a large number of operations.

Obviously, some of these operations would succeed with any given my_list and some would fail and, importantly, those which fail will do so with an unpredictable Error type; but I need to run all of them on every generation of my_list.

One obvious solution would be to use try/except on each of these operations:

try:
  my_text = my_list[1]+"hello"
except:    
  my_text = "None"

try:
  some_var = my_list[10]
except:
  some_var = "couldn't do it"

etc.

But with a large number of operations, this gets very cumbersome. I looked into the various questions about multiple try/excepts, but unless I'm missing something, they don't address this.

Based on someone's suggestion (sorry, lost the link), I tried to create a function with a built-in try/except, create another list of these operations, and send each operation to the function. Something along the lines of

def careful(op):
  try:
    return op
  else:
    return "None"

And use it with, for example, the first operation:

my_text = careful(my_list[1]+"hello")

The problem is python seems to evaluate the careful() argument before it's sent out to the function and the error is generated before it can be caught...

So I guess I'm looking for a form of a ternary operator that can do something like:

my text = my_list[1]+"hello" if (this doesn't cause any type of error) else "None"

But, if one exist, I couldn't find it...

Any ideas would be welcome and sorry for the long post.

Jack Fleeting
  • 24,385
  • 6
  • 23
  • 45
  • Ah, you want a monad :) – chepner Jul 16 '20 at 15:31
  • There is no expression form of a `try` statement, though [not for lack of trying](https://www.python.org/dev/peps/pep-0463/). – chepner Jul 16 '20 at 15:33
  • @chepner I wanted a monad and I didn't even know about it! How un-self aware is that! Where do I find one? – Jack Fleeting Jul 16 '20 at 15:34
  • Sadly, there is none in Python, hence the smiley in the comment. Another related proposal (which has only been deferred, rather than rejected) is [exception-aware operators](https://www.python.org/dev/peps/pep-0505/#exception-aware-operators). – chepner Jul 16 '20 at 15:35
  • @chepner Looks like there's something new I need to learn. Which languages have monads? – Jack Fleeting Jul 16 '20 at 15:59
  • 1
    Mostly functional languages (Haskell is famous for using monads for modeling all sorts of computation effects, not just exceptions.) The idea is that you chain operations together in a way that knows how to handle failure. See [this section](https://www.python.org/dev/peps/pep-0505/#built-in-maybe) of PEP 505 of an example taken from an existing 3rd-party library for handling failures. – chepner Jul 16 '20 at 16:09

3 Answers3

2

Maybe something like this?

def careful(op, default):
  ret = default
  try:
    ret = computation()
  else:
    pass
  return ret
Be Chiller Too
  • 2,502
  • 2
  • 16
  • 42
  • 2
    `*args`, and `**kwargs` anre missing and the code could be cleaner `try: / return func(*args, **kwargs) / except Exception: / return _default`, but IMHO that's probably all what Python offers in this area (+1). – VPfB Jul 16 '20 at 16:05
  • The problem with the function approach, as I mentioned in the question, is that `op` is commandeered by python before it reaches `careful()` with the error generated at that point. – Jack Fleeting Jul 16 '20 at 16:52
  • @JackFleeting The intended usage is that you pass a function+args to be called inside the try/except block. Example with a lambda: `my_text = careful(lambda x,y: x[1]+y, my_list, "hello", _default="none")` – VPfB Jul 16 '20 at 17:03
  • @VPfB Sorry; I misunderstood the approach. I'll have to work on it now. – Jack Fleeting Jul 16 '20 at 17:19
  • @VPfB Now that I had some time to digest: I actually thought about a rudimentary form of this approach (question was already too long, so didn't want to get into it): break the operation into its components and send them separately to `careful()`. It works, but only if the operation is easily separable (as in the example in your comment). But, alas, what do you with, for example, chained operations, like `target.find('div', {'id' : 'hey'}).find_next('span').get_text(strip=True)`? Making it `careful()`-ready may be impossible. – Jack Fleeting Jul 16 '20 at 18:15
  • @JackFleeting Maybe ugly, but why impossible? If the whole chain is one logical operation with one default replacement value, just write a function for computing the whole chain. If every step is considered a single operation, do it step by step. – VPfB Jul 16 '20 at 19:48
1

If you must do this, consider keeping a collection of the operations as strings and calling exec on them in a loop

actions = [
    'my_text   = my_list[1]+"hello"',
    'some_var  = my_list[10]',
    'mini_list = my_list[0].split("s")[1]',
    'my_sum    = my_list[7]+2',
]

If you make this collection a dict, you may also assign a default

Note that if an action default (or part of an action string) is meant to be a string, it must be quoted twice. Consider using block-quotes for this if you already have complex escaping, like returning a raw strings or a string representing a regular expression

{
    "foo = bar": r"""r'[\w]+baz.*'"""
}

complete example:

>>> actions_defaults = {
...     'my_text   = my_list[1]+"hello"':       '"None"',
...     'some_var  = my_list[10]':              '"couldn\'t do it"',
...     'mini_list = my_list[0].split("s")[1]': '"None"',
...     'my_sum    = my_list[7]+2':             '"None"',
... }
>>>
>>> for action, default in actions_defaults.items():
...     try:
...         exec(action)
...     except Exception:  # consider logging error
...         exec("{} = {}".format(action.split("=")[0], default))
...
>>> my_text
'None'
>>> some_var
"couldn't do it"

Other notes

  • this is pretty evil
  • declaring your vars before running to be their default values is probably better/clearer (sufficient to pass in the except block, as the assignment will fail)
  • you may run into weird scoping and need to access some vars via locals()
ti7
  • 16,375
  • 6
  • 40
  • 68
  • I'll need to digest and definitely revert. But in the interim: why is it "pretty evil" (not that there's anything wrong with "evil"...) – Jack Fleeting Jul 16 '20 at 17:17
  • Realistically, any use of `exec` is going to be vaguely-evil because it steps outside normal program logic and allows creating sorts of monstrosities (also see: lambdas in f-strings) and user input catastrophes (see: php and sql injection hoopla); while one's use may be sensible, later enhancements or repurposing may not have such prudence.. – ti7 Jul 16 '20 at 17:21
  • `exec` also tends to step around normal linting workflows, which frustrates maintenance/change/testing attempts. For a variety of other fun ways to do this, check out https://github.com/Droogans/unmaintainable-code – ti7 Jul 16 '20 at 17:23
  • Lambdas in f-string, eh? Boy, do I have a lot to learn. I don't know when I'm going to be able to rest on my laurels (such as they are)... – Jack Fleeting Jul 16 '20 at 17:29
  • Well, "slightly evil" vs. "highly effective": who do you think wins? Tried it on actual real life code - and it works. I guess I'll have to assume the attendant risks. Thanks for that. Now I'll go and get familiar with monads, Haskell, lambdas in f-strings and a bunch of other stuff whose existence I didn't even suspect... – Jack Fleeting Jul 16 '20 at 23:47
0

This sounds like an XY Problem

If you can make changes to the source logic, returning a dict may be a much better solution. Then you can determine if a key exists before doing some action, and potentially also look up the action which should be taken if the key exists in another dict.

ti7
  • 16,375
  • 6
  • 40
  • 68
  • Yes, I know it does look like an XY problem. But, first, it isn't. Second, even if it were - it's now a matter of principle... – Jack Fleeting Jul 16 '20 at 16:01
  • That's fair; I'll leave this answer as it may help others with something similar! – ti7 Jul 16 '20 at 17:13
  • Yes, by all means - let the answer live. Not everyone needs to be as high minded and principled as I am at times :) – Jack Fleeting Jul 16 '20 at 17:15