39

Trying to find a way to clean up some of my code.

So, I have something like this in my Python code:

company = None
country = None

person = Person.find(id=12345)
if person is not None: # found        
    company = Company.find(person.companyId)

    if company is not None:
         country = Country.find(company.countryId)

return (person, company, country)

Having read a tutorial on Haskell's monads (in particular Maybe), I was wondering if it's possible to write it in another way.

Andriy Drozdyuk
  • 58,435
  • 50
  • 171
  • 272

8 Answers8

41
company = country = None
try:
    person  =  Person.find(id=12345)
    company = Company.find(person.companyId)
    country = Country.find(company.countryId)
except AttributeError:
    pass # `person` or `company` might be None

EAFP

hugomg
  • 68,213
  • 24
  • 160
  • 246
Katriel
  • 120,462
  • 19
  • 136
  • 170
  • 8
    This is unequivocally the correct answer for this specific case. The entire purpose of `Maybe` as a monad is to model the EAFP approach explicitly as a first-class entity. In Python, it's both implicit and idiomatic in this form, so use it! – C. A. McCann Dec 14 '11 at 16:27
  • Unfortunately I need to actually "know" which of the person or company are None. – Andriy Drozdyuk Dec 14 '11 at 18:42
  • 3
    @drozzy: If you need to conditionally execute different pieces of code depending on which variables are `None`, then self-evidently you need conditionals. – Katriel Dec 14 '11 at 19:55
  • 1
    @drozzy: if you need to know which of those is None, then you can simply inspect them after the try/except. – Dan Burton Dec 14 '11 at 22:09
  • 3
    Why is Person.find inside the try except block? – Neil G Dec 15 '11 at 11:37
  • 1
    @drozzy: If you need to know where the process stopped you need another monad: the `Maybe` monad will only tell you that you have found nothing. – Giorgio Jan 07 '15 at 00:07
  • 30
    The problem with using exceptions (EAFP) is that you cannot distinguish between errors (function call cannot complete) and empty result (function call completed correctly and returned nothing). The word "exception" means "something that normally should not happen" (probably an error). Using exceptions to model the normal flow of control is misleading. Maybe it would be less bad if they were called throwables. – Giorgio Jan 07 '15 at 00:15
  • 4
    @Giorgio: good point about `AttributeError` catching too much (though normally, it would indicate a bug in the code if `AttributeError` is raised unintentionally). There is a precedent in Python to use exceptions for control flow: `StopIteration` is used to stop a `for`-loop, `GeneratorExit` for `generator.close()`, even `sys.exit()` is just `SystemExit` exception. [Without `AttributeError` it looks less elegant](http://stackoverflow.com/a/8507638/4279). – jfs Apr 17 '15 at 10:43
  • 1
    @JFSebastian Yes it would usually indicate a bug in the code if `AttributeError` was raised unintentionally. That's the point of the criticism; if `find` in any of those class classes has a bug that raises `AttributeError` (an extremely common result of minor programming errors due to the way python works), this code could hide that bug (with another more subtle bug). Or even if any of them just return an object of an unexpected type with no `find` method (which might be a bug in *this* code; a misunderstanding of the guarantees those other methods provide). – Ben Jul 29 '17 at 22:05
  • 1
    idea of Maybe is to describe the value that can be absent! catching the error doesn't have the same semantic (meaning). FP, from which Maybe comes, is about purity (no side effects), throwing/catching error is side effect. I'm not arguing that example will produce the wrong result, but that it's conceptually wrong. – iuriisusuk Dec 22 '17 at 14:20
  • this code may use exception to express business logic. for instance, why a person with id q12345 doesn't exist is an exception? maybe it is totally ok as a business logics. the same happens to company and country. using exception to check business rules is seen as an anti pattern in most cases – danny Jul 01 '19 at 05:43
  • 1
    In Python 3.4+ It could be written `with suppress(AttributeError): ...` that looks slightly nicer than try/except (otherwise the same) – jfs Apr 02 '20 at 17:56
  • Regarding exceptions for flow control, the problem isn't so much with some attribute errors being errors and other being indications of `None` being returned, but that the `try` statement cannot tell them apart and catch them (and handle them) separately. – chepner Mar 10 '23 at 14:11
30

Exploit the short-circuit behavior and that a custom object is true by default and None is false:

person  = Person.find(id=12345)
company = person and person.company
country = company and company.country
jfs
  • 399,953
  • 195
  • 994
  • 1,670
20

Python does not have a particularly nice syntax for monads. That being said, if you want to limit yourself to using something like the Maybe monad (Meaning that you'll only be able to use Maybe; you won't be able to make generic functions that deal with any monad), you can use the following approach:

class Maybe():
    def andThen(self, action): # equivalent to Haskell's >>=
        if self.__class__ == _Maybe__Nothing:
            return Nothing
        elif self.__class__ == Just:
            return action(self.value)

    def followedBy(self, action): # equivalent to Haskell's >>
        return self.andThen(lambda _: action)

class _Maybe__Nothing(Maybe):
    def __repr__(self):
        return "Nothing"

Nothing = _Maybe__Nothing()

class Just(Maybe):
    def __init__(self, v):
        self.value = v
    def __repr__(self):
        return "Just(%r)" % self.value

Then, make all of the methods that currently return None return either Just(value) or Nothing instead. This allows you to write this code:

Person.find(id=12345)
    .andThen(lambda person: Company.find(person.companyId))
    .andThen(lambda company: Country.find(company.countryId))

You can of course adapt the lambdas to store the intermediate results in variables; it's up to you how to do that properly.

CervEd
  • 3,306
  • 28
  • 25
dflemstr
  • 25,947
  • 5
  • 70
  • 105
  • Also, another problem I ran into here, is that I don't get the "intermediate" values - like "person", and "company" in the end. This only gives me a Maybe of country. – Andriy Drozdyuk Dec 14 '11 at 16:05
  • If you want to get all the results, you have to wrap your lambdas like this: `Person.find(id=12345).andThen(lambda person: Company.find(person.companyId).andThen(lambda company: Country.find(company.countryId).andThen(lambda country: Just((person, company, country)))))`. Note the ridiculous amount of parens; they can't be avoided if you want to program in a functional style like this. – dflemstr Dec 14 '11 at 16:10
  • @dflemstr So the last "andThen" is essentially there just to return the result? Interesting. – Andriy Drozdyuk Dec 14 '11 at 18:44
11

Have you checked PyMonad ?

https://pypi.python.org/pypi/PyMonad/

It not only includes a Maybe monad, but also a list monad, a Functor and Applicative functor classes. Monoids and more.

In your case it would be something like:

country = Person.find(id=12345)          >> (lambda person: 
          Company.find(person.companyId) >> (lambda company: 
          Country.find(company.countryId))

Easier to understand and cleaner than EAFP.

pat
  • 12,587
  • 1
  • 23
  • 52
mentatkgs
  • 1,541
  • 1
  • 13
  • 17
7

I think this is a perfect situation for getattr(object, name[, default]):

person  = Person.find(id=12345)
company = getattr(person, 'company', None)
country = getattr(company, 'country', None)
Rotareti
  • 49,483
  • 23
  • 112
  • 108
3
person = Person.find(id=12345)
company = None if person is None else Company.find(person.companyId)
country = None if company is None else Country.find(company.countryId)

return (person, company, country)
juliomalegria
  • 24,229
  • 14
  • 73
  • 89
  • 8
    I'd actually write that the other way round `company = Company.find(person.companyID) if person else None`. It removes the `is None` and the normal case is first, rather than the exceptional one. – Paul S Mar 05 '14 at 13:24
  • @PaulS your expression is not particularly explicit about the intention behind the check. Moreover, `if person` is checking `if bool(person) == False`, which is far less specific than `if person is None`. – Eli Korvigo Nov 20 '19 at 16:21
  • I'd still write `company = Company.find(person.companyId) if person is not None else None`, putting the less exceptional case upfront. – chepner Mar 10 '23 at 14:01
1

More "Pythonic" than trying to implement a different paradigm (not that it is not interesting and cool) would be to add intelligence to your objects so that they can find their attributes (and whether they exist at all), by themselves.

Bellow is an example of a base class that uses your "find" method and the correlation of the Id attribute names and class names to work with your example - I put in minimal Person and Company classes for a search for the company to work:

class Base(object):
    def __getattr__(self, attr):
        if hasattr(self, attr + "Id"):
            return globals()[attr.title()].find(getattr(self, attr + "Id"))
        return None
    @classmethod
    def find(cls, id):
        return "id %d " % id

class Person(Base):
    companyId=5

class Company(Base):
    pass

And on the console, after pasting the code above:

>>> p = Person()
>>> p.company
'id 5 '

With this Base your code above could just be:

person = Person.find(id=12345)
company = person.company
country = company and company.country
jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • Hm... I think you misunderstood me. Find is actually supposed to return a "Person" object, with attributes like "firstName, lastName" etc... It's not supposed to just return the id. Or maybe I am missing the point? – Andriy Drozdyuk Dec 14 '11 at 18:46
  • I did understand you - it is just my implementation of `find`that returns the string, to differeniate it from the id number (hardcoded as 5) - what is new here is the `__getattr__`- you would keep the exact same find method you have now. – jsbueno Dec 15 '11 at 14:02
  • Sorry, but I have no idea what this line does: `globals()[attr.title()].find(getattr(self, attr + "Id"))` – Andriy Drozdyuk Dec 15 '11 at 14:10
  • @drozzy: it converts `person.company` into `Person.find(person.companyId)`. Your actual issue `person is None` is not addressed. `company is None` is addressed using [the `and` operator's short-circuit behavior as in my answer](http://stackoverflow.com/a/8507638/4279) – jfs Sep 19 '15 at 00:44
0

haskell's Maybe analog in python is typing.Optional, but sadly, it is not the same thing.
For more info, see
https://docs.python.org/3/library/typing.html#typing.Optional
How should I use the Optional type hint?

Building chain with typing.Optional can be like this:


    from typing import Callable, Optional

    # some imported func, can't change definition
    def Div3(x: int) -> Optional[int]:
        print(f"-- {x} --", end= "      ->     ")
        return None if x%3!=0 else x//3

    # `bind` function
    # No Type Vars in python, so can't specify  Callable[ T, Optional[S]] -> Callable[ Optional[T], Optional[S]]
    def Optionalize( function:  Callable[ any, Optional[any]] ) -> Callable[ Optional[any], Optional[any]]:
        def _binded( x ):
            return None if x is None else function(x)
        return _binded

    @Optionalize
    def Div2(x: int) -> Optional[int]:
        print(f"-- {x} --", end= "      ->     ")
        return None if x%2!=0 else x//2

    OpD3 = Optionalize( Div3 )

    # we not wrap this one 
    def SPQRT(x: int) -> Optional[int]:
        print(f"-=^\\ {x} /^=-", end= "     ->     ")
        return None if x<0 else x**2

    print(                  SPQRT  (20) )
    print(                  SPQRT  (-1) )
    print(         Div2(    SPQRT  (20)) )   # Haskell would swear here
    print(         OpD3  (  SPQRT  (-1)) )
    print( Div2( Div2( Div2( Div2 ((-1)))))) # Only one set of wings printed (but Haskell would swear here too)
    print(         Div3(    SPQRT  (-1)) )   # TypeError at last

--------------------------

    -=^\ 20 /^=-     ->     400
    -=^\ -1 /^=-     ->     None
    -=^\ 20 /^=-     ->     -- 400 --      ->     200
    -=^\ -1 /^=-     ->     None
    -- -1 --      ->     None
    -=^\ -1 /^=-     ->     -- None --      ->     
    ---------------------------------------------------------------------------
    TypeError: unsupported operand type(s) for %: 'NoneType' and 'int'

Alexey Birukov
  • 1,565
  • 15
  • 22