Package: tryingsnake
TryingSnakeA simple,Tryimplementation inspired byscala.util.TryExamplesWrap functions with arguments:>>>fromtryingsnakeimportTry,Try_,Success,Failure>>>fromoperatorimportadd,truediv>>>Try(add,0,1)Success(1)>>>Try(truediv,1,0)# doctest:+ELLIPSISFailure(ZeroDivisionError(...))Avoid sentinel values:>>>defmean_1(xs):...try:...returnsum(xs)/len(xs)...exceptZeroDivisionErrorase:...returnfloat("inf")# What does it mean?>>>mean_1([])infvs.>>>defmean_2(xs):...returnsum(xs)/len(xs)>>>Try(mean_2,[])# doctest:+ELLIPSISFailure(ZeroDivisionError(...))>>>Try(mean_2,["foo","bar"])# doctest:+ELLIPSISFailure(TypeError(...))Follow the happy path:>>>definc(x):returnx+1>>>definv(x):return1./x>>>Success(1).map(inc).map(inv)Success(0.5)>>>Failure(Exception("e")).map(inc).map(inv)# doctest:+ELLIPSISFailure(Exception(...))>>>Success(-1).map(inc).map(inv)# doctest:+ELLIPSISFailure(ZeroDivisionError(...))Recover:>>>defget(url):...if"mirror"inurl:...raiseIOError("No address associated with hostname")...returnurl>>>mirrors=["http://mirror1.example.com","http://example.com"]>>>Try(get,mirrors[0]).recover(lambda_:get(mirrors[1]))Success('http://example.com')Let them fail:>>>fromoperatorimportgetitem>>>Try(getitem,[],0)# doctest:+ELLIPSISFailure(IndexError(...))>>>Try_.set_unhandled([IndexError])>>>Try(getitem,[],0)Traceback(mostrecentcalllast):...IndexError:listindexoutofrangeMake things (relatively) simple:>>>importmath>>>xs=[1.0,0.0,"-1",-3,2,1+2j]>>>sqrts=[Try(math.sqrt,x)forxinxs]>>>[x.get()forxinsqrtsifx.isSuccess][1.0,0.0,1.4142135623730951]>>>defget_etype(e):...returnTry(lambdax:type(x).__name__,e)>>>[x.recoverWith(get_etype).get()forxinsqrtsifx.isFailure]['TypeError','ValueError','TypeError']Inline exception handling:>>>fromtryingsnake.curriedimportTry>>>map(Try(str.split),["foo bar",None])# doctest:+ELLIPSIS<mapat...>Decorate your functions:>>>fromtryingsnake.curriedimportTryastry_>>>@try_...defscale_imag(x):...returncomplex(x.real,x.imag*2)>>>[scale_imag(x)forxin[1+2j,"3",42+0j]][Success((1+4j)),Failure(AttributeError("'str' object has no attribute 'real'")),Success((42+0j))]Wrap generator objects:>>>defget_nth(xs,i):...yieldxs[i]>>>xs=[1,3,5,7]>>>Try(get_nth(xs,3))Success(7)>>>Try(get_nth(xs,11))Failure(IndexError('list index out of range'))>>>deff():...divisor=1...whileTrue:...divisor_=yield1/divisor...divisor=divisor_ifdivisor_isnotNoneelse1>>>g=f()>>>next(g)# Should be primed1.0>>>Try(g,2)Success(0.5)>>>Try(g,0)Failure(ZeroDivisionError('division by zero'))InstallationThis package is available on PYPI:pip install tryingsnakeand conda-forge:conda install -c conda-forge tryingsnakeDependenciestryingsnakesupports Python 3.6 or later and
requires no external dependencies.LicenseMIT, SeeLICENSEFAQQ: Is this project production-ready?A: Sure, for some definition of production-ready. It is a toy project.
It has decent test coverage, stable API, and in general seems to do
what is expected to do. But it is not widely used, and the API design
and overall idea are rather unpythonic.Q: Why to use mixedCase method names instead of lowercase
recommended by PEP8?A: Mostly to make switching between Python and Scala code as
painless as possible.Q: What is the runtime cost?A: As of0088286(releases 0.3 and 0.4 suffered from
severe performance regression caused by usingtyping.Genericas a base of
try. See#18for details)
rough numbers for simple tasks look as follows:Python 3.7.5 (default, Oct 27 2019, 15:43:29)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.11.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: def identity(x): return x
In [2]: from tryingsnake import Try
In [3]: %timeit for i in range(1_000_000): identity(i)
59.8 ms ± 683 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [4]: %timeit for i in range(1_000_000): Try(identity, i)
408 ms ± 4.14 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)and execution time is dominated by the initializer:In [5]: import cProfile
In [6]: cProfile.run("for i in range(1_000_000): Try(identity, i)")
         4000003 function calls in 0.961 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  1000000    0.078    0.000    0.078    0.000 <ipython-input-1-abafd771428d>:1(identity)
        1    0.263    0.263    0.961    0.961 <string>:1(<module>)
  1000000    0.094    0.000    0.094    0.000 __init__.py:234(__init__)
  1000000    0.480    0.000    0.698    0.000 __init__.py:352(Try)
  1000000    0.046    0.000    0.046    0.000 {built-in method builtins.callable}
        1    0.000    0.000    0.961    0.961 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}This is quite a lot for simple functions so you should probably avoid it in such cases, where raw performance is important. It is still possible to amortize the cost in such cases, for example using composition:fromtoolz.functoolzimportcomposefromtryingsnakeimportTryTry(compose(str.split,str.lower,str.strip)," Foo BAR FooBar ")Memory overhead (as measured bymemory-profiler) looks as follows:Line #    Mem usage    Increment   Line Contents
================================================
 6     37.9 MiB     37.9 MiB   @profile
 7                             def f():
 8    155.5 MiB      0.8 MiB       [Try(identity, i) for i in range(1_000_000)]compared to:Line #    Mem usage    Increment   Line Contents
================================================
 6     37.9 MiB     37.9 MiB   @profile
 7                             def f():
 8     77.4 MiB      1.0 MiB       [identity(i) for i in range(1_000_000)]