|
1732 | 1732 | { |
1733 | 1733 | "data": { |
1734 | 1734 | "text/plain": [ |
1735 | | - "['c', 'h', 'b', 'a', 'f', 'g', 'd', 'e']" |
| 1735 | + "['g', 'c', 'f', 'd', 'a', 'b', 'e', 'h']" |
1736 | 1736 | ] |
1737 | 1737 | }, |
1738 | 1738 | "execution_count": null, |
|
1782 | 1782 | "cell_type": "markdown", |
1783 | 1783 | "metadata": {}, |
1784 | 1784 | "source": [ |
1785 | | - "## Other Helpers" |
| 1785 | + "## `SaveReturn` and `save_iter` Variants" |
| 1786 | + ] |
| 1787 | + }, |
| 1788 | + { |
| 1789 | + "cell_type": "markdown", |
| 1790 | + "metadata": {}, |
| 1791 | + "source": [ |
| 1792 | + "These utilities solve a common problem in Python: how to extract additional information from generator functions beyond just the yielded values.\n", |
| 1793 | + "\n", |
| 1794 | + "In Python, generator functions can `yield` values and also `return` a final value, but the return value is normally lost when you iterate over the generator:" |
| 1795 | + ] |
| 1796 | + }, |
| 1797 | + { |
| 1798 | + "cell_type": "code", |
| 1799 | + "execution_count": null, |
| 1800 | + "metadata": {}, |
| 1801 | + "outputs": [], |
| 1802 | + "source": [ |
| 1803 | + "def example_generator():\n", |
| 1804 | + " total = 0\n", |
| 1805 | + " for i in range(3):\n", |
| 1806 | + " total += i\n", |
| 1807 | + " yield i\n", |
| 1808 | + " return total # This gets lost!\n", |
| 1809 | + "\n", |
| 1810 | + "# The return value (3) is lost\n", |
| 1811 | + "values = list(example_generator()) # [0, 1, 2]" |
| 1812 | + ] |
| 1813 | + }, |
| 1814 | + { |
| 1815 | + "cell_type": "code", |
| 1816 | + "execution_count": null, |
| 1817 | + "metadata": {}, |
| 1818 | + "outputs": [], |
| 1819 | + "source": [ |
| 1820 | + "#| exports\n", |
| 1821 | + "class SaveReturn:\n", |
| 1822 | + " \"Wrap an iterator such that the generator function's return value is stored in `.value`\"\n", |
| 1823 | + " def __init__(self, its): self.its = its\n", |
| 1824 | + " def __iter__(self):\n", |
| 1825 | + " self.value = yield from self.its\n", |
| 1826 | + " return self.value" |
| 1827 | + ] |
| 1828 | + }, |
| 1829 | + { |
| 1830 | + "cell_type": "markdown", |
| 1831 | + "metadata": {}, |
| 1832 | + "source": [ |
| 1833 | + "`SaveReturn` is the simplest approach to solving this problem - it wraps any existing (non-async) generator and captures its return value. This works because `yield from` (used internally in `SaveReturn`) returns the value from the `return` of the generator function." |
| 1834 | + ] |
| 1835 | + }, |
| 1836 | + { |
| 1837 | + "cell_type": "code", |
| 1838 | + "execution_count": null, |
| 1839 | + "metadata": {}, |
| 1840 | + "outputs": [ |
| 1841 | + { |
| 1842 | + "name": "stdout", |
| 1843 | + "output_type": "stream", |
| 1844 | + "text": [ |
| 1845 | + "Values: [0, 1, 2, 3, 4]\n" |
| 1846 | + ] |
| 1847 | + }, |
| 1848 | + { |
| 1849 | + "data": { |
| 1850 | + "text/plain": [ |
| 1851 | + "10" |
| 1852 | + ] |
| 1853 | + }, |
| 1854 | + "execution_count": null, |
| 1855 | + "metadata": {}, |
| 1856 | + "output_type": "execute_result" |
| 1857 | + } |
| 1858 | + ], |
| 1859 | + "source": [ |
| 1860 | + "def sum_range(n):\n", |
| 1861 | + " total = 0\n", |
| 1862 | + " for i in range(n):\n", |
| 1863 | + " total += i\n", |
| 1864 | + " yield i\n", |
| 1865 | + " return total # This value is returned by yield from\n", |
| 1866 | + "\n", |
| 1867 | + "sr = SaveReturn(sum_range(5))\n", |
| 1868 | + "values = list(sr) # This will consume the generator and get the return value\n", |
| 1869 | + "print(f\"Values: {values}\")\n", |
| 1870 | + "sr.value" |
| 1871 | + ] |
| 1872 | + }, |
| 1873 | + { |
| 1874 | + "cell_type": "markdown", |
| 1875 | + "metadata": {}, |
| 1876 | + "source": [ |
| 1877 | + "In order to provide an accurate signature for `save_iter`, we need a version of `wraps` that removes leading parameters:" |
1786 | 1878 | ] |
1787 | 1879 | }, |
1788 | 1880 | { |
|
1846 | 1938 | "class _save_iter:\n", |
1847 | 1939 | " def __init__(self, g, *args, **kw): self.g,self.args,self.kw = g,args,kw\n", |
1848 | 1940 | " def __iter__(self): yield from self.g(self, *self.args, **self.kw)\n", |
| 1941 | + " def __aiter__(self): return self.g(self, *self.args, **self.kw)\n", |
1849 | 1942 | "\n", |
1850 | 1943 | "def save_iter(g):\n", |
| 1944 | + " \"Decorator that allows a generator function to store values in the returned iterator object\"\n", |
| 1945 | + " @trim_wraps(g)\n", |
| 1946 | + " def _(*args, **kwargs): return _save_iter(g, *args, **kwargs)\n", |
| 1947 | + " return _" |
| 1948 | + ] |
| 1949 | + }, |
| 1950 | + { |
| 1951 | + "cell_type": "markdown", |
| 1952 | + "metadata": {}, |
| 1953 | + "source": [ |
| 1954 | + "`save_iter` modifies generator functions to store state in the iterator object itself. The generator receives an object as its first parameter, which it can use to store attributes. You can store values during iteration, not just at the end,\n", |
| 1955 | + "and you can store multiple attributes if needed." |
| 1956 | + ] |
| 1957 | + }, |
| 1958 | + { |
| 1959 | + "cell_type": "code", |
| 1960 | + "execution_count": null, |
| 1961 | + "metadata": {}, |
| 1962 | + "outputs": [], |
| 1963 | + "source": [ |
| 1964 | + "@save_iter\n", |
| 1965 | + "def sum_range(o, n): # Note: 'o' parameter added\n", |
| 1966 | + " total = 0\n", |
| 1967 | + " for i in range(n):\n", |
| 1968 | + " total += i\n", |
| 1969 | + " yield i\n", |
| 1970 | + " o.value = total # Store directly on the iterator object" |
| 1971 | + ] |
| 1972 | + }, |
| 1973 | + { |
| 1974 | + "cell_type": "markdown", |
| 1975 | + "metadata": {}, |
| 1976 | + "source": [ |
| 1977 | + "Because iternally `save_iter` uses `trim_wraps`, the signature of `sum_range` correctly shows that you should *not* pass `o` to it; it's injected by the decorating function." |
| 1978 | + ] |
| 1979 | + }, |
| 1980 | + { |
| 1981 | + "cell_type": "code", |
| 1982 | + "execution_count": null, |
| 1983 | + "metadata": {}, |
| 1984 | + "outputs": [ |
| 1985 | + { |
| 1986 | + "name": "stdout", |
| 1987 | + "output_type": "stream", |
| 1988 | + "text": [ |
| 1989 | + "(n)\n" |
| 1990 | + ] |
| 1991 | + } |
| 1992 | + ], |
| 1993 | + "source": [ |
| 1994 | + "print(sum_range.__signature__)" |
| 1995 | + ] |
| 1996 | + }, |
| 1997 | + { |
| 1998 | + "cell_type": "code", |
| 1999 | + "execution_count": null, |
| 2000 | + "metadata": {}, |
| 2001 | + "outputs": [ |
| 2002 | + { |
| 2003 | + "name": "stdout", |
| 2004 | + "output_type": "stream", |
| 2005 | + "text": [ |
| 2006 | + "Values: [0, 1, 2, 3, 4]\n", |
| 2007 | + "Sum stored: 10\n" |
| 2008 | + ] |
| 2009 | + } |
| 2010 | + ], |
| 2011 | + "source": [ |
| 2012 | + "sr = sum_range(5)\n", |
| 2013 | + "print(f\"Values: {list(sr)}\")\n", |
| 2014 | + "print(f\"Sum stored: {sr.value}\")" |
| 2015 | + ] |
| 2016 | + }, |
| 2017 | + { |
| 2018 | + "cell_type": "code", |
| 2019 | + "execution_count": null, |
| 2020 | + "metadata": {}, |
| 2021 | + "outputs": [], |
| 2022 | + "source": [ |
| 2023 | + "#| export\n", |
| 2024 | + "def asave_iter(g):\n", |
| 2025 | + " \"Like `save_iter`, but for async iterators\"\n", |
1851 | 2026 | " @trim_wraps(g)\n", |
1852 | 2027 | " def _(*args, **kwargs): return _save_iter(g, *args, **kwargs)\n", |
1853 | 2028 | " return _" |
|
1857 | 2032 | "cell_type": "markdown", |
1858 | 2033 | "metadata": {}, |
1859 | 2034 | "source": [ |
1860 | | - "`save_iter` is a decorator that allows a generator function to store values in the returned iterator object. The generator receives an object as its first parameter, which it can use to store attributes." |
| 2035 | + "`asave_iter` provides the same functionality as `save_iter`, but for async generator functions. `yield from` and `return` can not be used with async generator functions, so `SaveReturn` can't be used here." |
1861 | 2036 | ] |
1862 | 2037 | }, |
1863 | 2038 | { |
|
1875 | 2050 | } |
1876 | 2051 | ], |
1877 | 2052 | "source": [ |
1878 | | - "@save_iter\n", |
1879 | | - "def sum_range(self, n):\n", |
| 2053 | + "@asave_iter\n", |
| 2054 | + "async def asum_range(self, n):\n", |
1880 | 2055 | " total = 0\n", |
1881 | 2056 | " for i in range(n):\n", |
1882 | 2057 | " total += i\n", |
1883 | 2058 | " yield i\n", |
1884 | 2059 | " self.value = total\n", |
1885 | 2060 | "\n", |
1886 | | - "sr = sum_range(5)\n", |
1887 | | - "print(f\"Values: {list(sr)}\")\n", |
1888 | | - "print(f\"Sum stored: {sr.value}\") # Sum stored: 10" |
| 2061 | + "asr = asum_range(5)\n", |
| 2062 | + "print(f\"Values: {[o async for o in asr]}\")\n", |
| 2063 | + "print(f\"Sum stored: {asr.value}\")" |
| 2064 | + ] |
| 2065 | + }, |
| 2066 | + { |
| 2067 | + "cell_type": "markdown", |
| 2068 | + "metadata": {}, |
| 2069 | + "source": [ |
| 2070 | + "## Other Helpers" |
1889 | 2071 | ] |
1890 | 2072 | }, |
1891 | 2073 | { |
|
0 commit comments