Repository: dgasmith/opt_einsum

Issue:
allow `backend=None` as shorthand for 'auto'
A couple of times I've be caught out (e.g. https://github.com/jcmgray/quimb/issues/130) that the special value for inferring the backend is not `backend=None`. Might be nice to allow both?

If so would just require a simple:
```python
if backend not in ("auto", None):
    return backend
```
in `parse_backend` of `contract.py`.

Discussion:

Patch:
diff --git a/opt_einsum/contract.py b/opt_einsum/contract.py
index 0df94d0..0f70379 100644
--- a/opt_einsum/contract.py
+++ b/opt_einsum/contract.py
@@ -4,6 +4,7 @@ Contains the primary optimization and contraction routines.
 
 from collections import namedtuple
 from decimal import Decimal
+from functools import lru_cache
 from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
 
 from . import backends, blas, helpers, parser, paths, sharing
@@ -542,15 +543,20 @@ def contract(*operands_: Any, **kwargs: Any) -> ArrayType:
     return _core_contract(operands, contraction_list, backend=backend, **einsum_kwargs)
 
 
+@lru_cache(None)
+def _infer_backend_class_cached(cls: type) -> str:
+    return cls.__module__.split(".")[0]
+
+
 def infer_backend(x: Any) -> str:
-    return x.__class__.__module__.split(".")[0]
+    return _infer_backend_class_cached(x.__class__)
 
 
-def parse_backend(arrays: Sequence[ArrayType], backend: str) -> str:
+def parse_backend(arrays: Sequence[ArrayType], backend: Optional[str]) -> str:
     """Find out what backend we should use, dipatching based on the first
     array if ``backend='auto'`` is specified.
     """
-    if backend != "auto":
+    if (backend != "auto") and (backend is not None):
         return backend
     backend = infer_backend(arrays[0])
 
@@ -565,7 +571,7 @@ def parse_backend(arrays: Sequence[ArrayType], backend: str) -> str:
 def _core_contract(
     operands_: Sequence[ArrayType],
     contraction_list: ContractionListType,
-    backend: str = "auto",
+    backend: Optional[str] = "auto",
     evaluate_constants: bool = False,
     **einsum_kwargs: Any,
 ) -> ArrayType:
@@ -703,7 +709,7 @@ class ContractExpression:
         self._evaluated_constants: Dict[str, Any] = {}
         self._backend_expressions: Dict[str, Any] = {}
 
-    def evaluate_constants(self, backend: str = "auto") -> None:
+    def evaluate_constants(self, backend: Optional[str] = "auto") -> None:
         """Convert any constant operands to the correct backend form, and
         perform as many contractions as possible to create a new list of
         operands, stored in ``self._evaluated_constants[backend]``. This also
@@ -746,7 +752,7 @@ class ContractExpression:
         self,
         arrays: Sequence[ArrayType],
         out: Optional[ArrayType] = None,
-        backend: str = "auto",
+        backend: Optional[str] = "auto",
         evaluate_constants: bool = False,
     ) -> ArrayType:
         """The normal, core contraction."""
diff --git a/opt_einsum/paths.py b/opt_einsum/paths.py
index eac3630..ac8ccd5 100644
--- a/opt_einsum/paths.py
+++ b/opt_einsum/paths.py
@@ -1217,7 +1217,7 @@ class DynamicProgramming(PathOptimizer):
         output = frozenset(symbol2int[c] for c in output_)
         size_dict_canonical = {symbol2int[c]: v for c, v in size_dict_.items() if c in symbol2int}
         size_dict = [size_dict_canonical[j] for j in range(len(size_dict_canonical))]
-        naive_cost = naive_scale * len(inputs) * functools.reduce(operator.mul, size_dict)
+        naive_cost = naive_scale * len(inputs) * functools.reduce(operator.mul, size_dict, 1)
 
         inputs, inputs_done, inputs_contractions = _dp_parse_out_single_term_ops(inputs, all_inds, ind_counts)
 


Test patch:
diff --git a/opt_einsum/tests/test_backends.py b/opt_einsum/tests/test_backends.py
index 87d2ba2..7c81c67 100644
--- a/opt_einsum/tests/test_backends.py
+++ b/opt_einsum/tests/test_backends.py
@@ -443,6 +443,7 @@ def test_auto_backend_custom_array_no_tensordot():
     # Shaped is an array-like object defined by opt_einsum - which has no TDOT
     assert infer_backend(x) == "opt_einsum"
     assert parse_backend([x], "auto") == "numpy"
+    assert parse_backend([x], None) == "numpy"
 
 
 @pytest.mark.parametrize("string", tests)
diff --git a/opt_einsum/tests/test_contract.py b/opt_einsum/tests/test_contract.py
index 453a779..ef3ee5d 100644
--- a/opt_einsum/tests/test_contract.py
+++ b/opt_einsum/tests/test_contract.py
@@ -13,6 +13,7 @@ tests = [
     "a,->a",
     "ab,->ab",
     ",ab,->ab",
+    ",,->",
     # Test hadamard-like products
     "a,ab,abc->abc",
     "a,b,ab->ab",
diff --git a/opt_einsum/tests/test_paths.py b/opt_einsum/tests/test_paths.py
index c6fef4f..eb4b18b 100644
--- a/opt_einsum/tests/test_paths.py
+++ b/opt_einsum/tests/test_paths.py
@@ -58,6 +58,7 @@ path_scalar_tests = [
     ["ab,->ab", 1],
     [",a,->a", 2],
     [",,a,->a", 3],
+    [",,->", 2],
 ]