├── tests ├── __init__.py ├── requirements.in └── test_all.py ├── placeholder ├── py.typed ├── partials.c └── __init__.py ├── docs ├── index.md ├── reference.md └── examples.ipynb ├── setup.cfg ├── .gitignore ├── setup.py ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── release.yml ├── Makefile ├── LICENSE.txt ├── mkdocs.yml ├── pyproject.toml ├── CHANGELOG.md └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /placeholder/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | py_limited_api = cp310 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .coverage 3 | site/ 4 | *.so 5 | *.o 6 | uv.lock 7 | -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | ::: placeholder.F 4 | 5 | ::: placeholder.M 6 | -------------------------------------------------------------------------------- /tests/requirements.in: -------------------------------------------------------------------------------- 1 | setuptools 2 | pytest-cov 3 | pytest-parametrized 4 | pytest-codspeed 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, Extension 2 | 3 | ext_module = Extension('placeholder.partials', ['placeholder/partials.c'], py_limited_api=True) 4 | setup(ext_modules=[ext_module]) 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | groups: 9 | action-updates: 10 | patterns: ["*"] 11 | 12 | - package-ecosystem: "pip" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | python setup.py build_ext -i 3 | 4 | all: 5 | uv run python setup.py build_ext -i 6 | 7 | check: all 8 | uv run pytest -s --cov 9 | 10 | bench: all 11 | uv run pytest --codspeed 12 | 13 | lint: 14 | uvx ruff check 15 | uvx ruff format --check 16 | uvx ty check placeholder 17 | 18 | html: all 19 | uv run --group docs mkdocs build 20 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2022 Aric Coady 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: placeholder 2 | site_url: https://coady.github.io/placeholder/ 3 | site_description: Operator overloading for fast anonymous functions. 4 | theme: material 5 | 6 | repo_name: coady/placeholder 7 | repo_url: https://github.com/coady/placeholder 8 | edit_uri: "" 9 | 10 | nav: 11 | - Introduction: index.md 12 | - Reference: reference.md 13 | - Examples: examples.ipynb 14 | 15 | plugins: 16 | - search 17 | - mkdocstrings: 18 | handlers: 19 | python: 20 | options: 21 | show_root_heading: true 22 | - mkdocs-jupyter: 23 | execute: true 24 | allow_errors: false 25 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | permissions: 4 | id-token: write 5 | 6 | on: [workflow_dispatch, push, pull_request] 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] 14 | steps: 15 | - uses: actions/checkout@v6 16 | - uses: astral-sh/setup-uv@v7 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - run: make check 20 | - run: uv run coverage xml 21 | - uses: codecov/codecov-action@v5 22 | with: 23 | use_oidc: true 24 | 25 | lint: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v6 29 | - uses: astral-sh/setup-uv@v7 30 | - run: make lint 31 | 32 | docs: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v6 36 | - uses: astral-sh/setup-uv@v7 37 | - run: make html 38 | 39 | bench: 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v6 43 | - uses: astral-sh/setup-uv@v7 44 | - uses: CodSpeedHQ/action@v4 45 | with: 46 | mode: simulation 47 | run: make bench 48 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | permissions: 4 | id-token: write 5 | pages: write 6 | 7 | on: [workflow_dispatch] 8 | 9 | jobs: 10 | sdist: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v6 14 | - uses: astral-sh/setup-uv@v7 15 | - run: uv build --sdist 16 | - uses: actions/upload-artifact@v6 17 | with: 18 | name: artifact-sdist 19 | path: dist/ 20 | 21 | wheels: 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest, macos-latest, windows-latest] 26 | steps: 27 | - uses: actions/checkout@v6 28 | - if: runner.os == 'Linux' 29 | uses: docker/setup-qemu-action@v3 30 | with: 31 | platforms: all 32 | - uses: pypa/cibuildwheel@v3.3 33 | env: 34 | CIBW_BUILD: cp310-* 35 | CIBW_ARCHS_LINUX: auto aarch64 36 | CIBW_ARCHS_MACOS: x86_64 arm64 37 | - uses: actions/upload-artifact@v6 38 | with: 39 | name: artifact-${{ matrix.os }} 40 | path: wheelhouse/ 41 | 42 | publish: 43 | needs: [sdist, wheels] 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/download-artifact@v7 47 | with: 48 | path: dist/ 49 | pattern: artifact-* 50 | merge-multiple: true 51 | - uses: pypa/gh-action-pypi-publish@release/v1 52 | 53 | pages: 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v6 57 | - uses: astral-sh/setup-uv@v7 58 | - run: make html 59 | - uses: actions/upload-pages-artifact@v4 60 | with: 61 | path: site/ 62 | - uses: actions/deploy-pages@v4 63 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "placeholder" 7 | version = "1.5.1" 8 | description = "Operator overloading for fast anonymous functions." 9 | readme = "README.md" 10 | requires-python = ">=3.10" 11 | license = {file = "LICENSE.txt"} 12 | authors = [{name = "Aric Coady", email = "aric.coady@gmail.com"}] 13 | keywords = ["functional", "lambda", "scala", "underscore"] 14 | classifiers = [ 15 | "Development Status :: 5 - Production/Stable", 16 | "Intended Audience :: Developers", 17 | "Operating System :: OS Independent", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Programming Language :: Python :: 3.13", 23 | "Programming Language :: Python :: 3.14", 24 | "Topic :: Software Development :: Libraries :: Python Modules", 25 | "Typing :: Typed", 26 | ] 27 | 28 | [project.urls] 29 | Homepage = "https://github.com/coady/placeholder" 30 | Documentation = "https://coady.github.io/placeholder" 31 | Changelog = "https://github.com/coady/placeholder/blob/main/CHANGELOG.md" 32 | Issues = "https://github.com/coady/placeholder/issues" 33 | 34 | [tool.setuptools] 35 | packages = ["placeholder"] 36 | 37 | [tool.ruff] 38 | line-length = 100 39 | 40 | [tool.ruff.format] 41 | quote-style = "preserve" 42 | 43 | [tool.ty.rules] 44 | unused-ignore-comment = "error" 45 | 46 | [tool.coverage.run] 47 | source = ["placeholder"] 48 | branch = true 49 | 50 | [tool.pytest.ini_options] 51 | markers = ["benchmark"] 52 | 53 | [dependency-groups] 54 | dev = ["setuptools", "pytest-cov", "pytest-codspeed", "pytest-parametrized"] 55 | docs = ["mkdocstrings[python]", "mkdocs-jupyter"] 56 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). 5 | 6 | ## Unreleased 7 | 8 | ## [1.5.1](https://pypi.org/project/placeholder/1.5.1/) - 2025-10-27 9 | ### Changed 10 | * Python >=3.10 required 11 | 12 | ## [1.5](https://pypi.org/project/placeholder/1.5/) - 2023-11-10 13 | ### Changed 14 | * Python >=3.8 required 15 | 16 | ## [1.4](https://pypi.org/project/placeholder/1.4/) - 2022-09-13 17 | ### Changed 18 | * Stable abi wheels 19 | 20 | ### Removed 21 | * Removed `func` attribute 22 | 23 | ## [1.3](https://pypi.org/project/placeholder/1.3/) - 2021-09-12 24 | * Python >=3.7 required 25 | * Deprecated accessing `func` attribute of partial object 26 | 27 | ## [1.2.1](https://pypi.org/project/placeholder/1.2.1/) - 2021-02-26 28 | * Setup fix 29 | 30 | ## [1.2](https://pypi.org/project/placeholder/1.2/) - 2020-10-24 31 | * Python >=3.6 required 32 | * Optimized `partial` implementation 33 | 34 | ## [1.1](https://pypi.org/project/placeholder/1.1/) - 2019-12-07 35 | * Additional unary functions 36 | 37 | ## [1.0](https://pypi.org/project/placeholder/1.0/) - 2018-12-08 38 | * Removed `__` (double underscore) 39 | * Variable arguments of first function 40 | * Method callers and multi-valued getters 41 | 42 | ## [0.7](https://pypi.org/project/placeholder/0.7/) - 2017-12-10 43 | * Deprecated `__` (double underscore) 44 | 45 | ## [0.6](https://pypi.org/project/placeholder/0.6/) - 2017-01-02 46 | * Optimized composite functions 47 | * Renamed to `_` (single underscore) for consistency 48 | 49 | ## [0.5](https://pypi.org/project/placeholder/0.5/) - 2015-09-05 50 | * Unary operators 51 | * `__call__` implements `methodcaller` 52 | * `__getitem__` supports only single argument 53 | * Improved error handling 54 | * `composer` object deprecated in favor of optimized `F` expression 55 | -------------------------------------------------------------------------------- /placeholder/partials.c: -------------------------------------------------------------------------------- 1 | #define Py_LIMITED_API 0x03100000 2 | #include "Python.h" 3 | 4 | typedef struct { 5 | PyObject_HEAD 6 | PyObject *func; 7 | PyObject *arg; 8 | } partial; 9 | 10 | static void 11 | partial_dealloc(partial *self) 12 | { 13 | Py_DecRef(self->func); 14 | Py_DecRef(self->arg); 15 | PyObject_Free(self); 16 | } 17 | 18 | static int 19 | partial_init(partial *self, PyObject *args, PyObject *kwargs) 20 | { 21 | static char *kwlist[] = {"func", "arg", NULL}; 22 | if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OO:partial", kwlist, &self->func, &self->arg)) 23 | return -1; 24 | Py_IncRef(self->func); 25 | Py_IncRef(self->arg); 26 | return 0; 27 | } 28 | 29 | static PyObject * 30 | partial_str(partial *self) 31 | { 32 | return PyUnicode_FromFormat("placeholder.partial(%S, %S)", self->func, self->arg); 33 | } 34 | 35 | static PyObject * 36 | partial_left(partial *self, PyObject *arg) 37 | { 38 | return PyObject_CallFunctionObjArgs(self->func, arg, self->arg, NULL); 39 | } 40 | 41 | static PyObject * 42 | partial_right(partial *self, PyObject *arg) 43 | { 44 | return PyObject_CallFunctionObjArgs(self->func, self->arg, arg, NULL); 45 | } 46 | 47 | static PyMethodDef partial_methods[] = { 48 | {"left", (PyCFunction)partial_left, METH_O, "Call binary function with left arg."}, 49 | {"right", (PyCFunction)partial_right, METH_O, "Call binary function with right arg."}, 50 | {NULL} /* Sentinel */ 51 | }; 52 | 53 | static PyType_Slot partial_slots[] = { 54 | {Py_tp_doc, PyDoc_STR("Partially bound binary function.")}, 55 | {Py_tp_dealloc, partial_dealloc}, 56 | {Py_tp_init, partial_init}, 57 | {Py_tp_str, partial_str}, 58 | {Py_tp_methods, partial_methods}, 59 | {0, NULL} 60 | }; 61 | 62 | static PyType_Spec partial_spec = { 63 | "placeholder.partial", 64 | sizeof(partial), 65 | 0, 66 | Py_TPFLAGS_DEFAULT, 67 | partial_slots, 68 | }; 69 | 70 | static int 71 | partials_mod_exec(PyObject *module) 72 | { 73 | PyObject *PartialType = PyType_FromSpec(&partial_spec); 74 | if (!PartialType) 75 | return -1; 76 | if (!PyModule_AddObject(module, "partial", PartialType)) 77 | return 0; 78 | Py_DecRef(PartialType); 79 | return -1; 80 | } 81 | 82 | static PyModuleDef_Slot partials_slots[] = { 83 | {Py_mod_exec, partials_mod_exec}, 84 | {0, NULL} 85 | }; 86 | 87 | static PyModuleDef partialsmodule = { 88 | PyModuleDef_HEAD_INIT, 89 | .m_name = "partials", 90 | .m_doc = PyDoc_STR("Partially bound binary functions."), 91 | .m_slots = partials_slots, 92 | }; 93 | 94 | PyMODINIT_FUNC PyInit_partials(void) 95 | { 96 | return PyModuleDef_Init(&partialsmodule); 97 | } 98 | -------------------------------------------------------------------------------- /tests/test_all.py: -------------------------------------------------------------------------------- 1 | import math 2 | import pytest 3 | from parametrized import parametrized 4 | from placeholder import F, _, m 5 | 6 | 7 | def test_object(): 8 | assert type(_) is F 9 | assert F({}.get) > 1 10 | assert (_ == 1)(1.0) is (1 == _)(1.0) is True 11 | assert list(F(len)) == [len] 12 | 13 | 14 | @pytest.mark.benchmark 15 | def test_getters(): 16 | assert (_.append)(list) is list.append 17 | with pytest.raises(AttributeError): 18 | _.name(None) 19 | assert (_[0])({0: None}) is (_[0])([None]) is None 20 | with pytest.raises(KeyError): 21 | _[0]({}) 22 | with pytest.raises(IndexError): 23 | _[0]([]) 24 | assert sorted(enumerate('cba'), key=_[1]) == [(2, 'a'), (1, 'b'), (0, 'c')] 25 | assert m.split('-')('a-b') == ['a', 'b'] 26 | assert m('real', 'imag')(1) == (1, 0) 27 | assert m[0, -1]('abc') == ('a', 'c') 28 | 29 | 30 | @pytest.mark.benchmark 31 | def test_math(): 32 | assert (_ + 1)(2) == (1 + _)(2) == 3 33 | assert (_ - 1)(2) == (3 - _)(2) == 1 34 | with pytest.raises(TypeError): 35 | (_ + 1)(2, x=None) 36 | assert [x + 1 for x in range(3)] == list(map(_ + 1, range(3))) 37 | assert (_ * 2)(3) == (2 * _)(3) == 6 38 | assert (_ / 2)(3) == (3 / _)(2) == 1.5 39 | assert (_ // 2)(3) == (3 // _)(2) == 1 40 | assert (_ % 2)(3) == (3 % _)(2) == 1 41 | assert divmod(_, 2)(3) == divmod(3, _)(2) == (1, 1) 42 | assert (_**3)(2) == (2**_)(3) == 8 43 | assert (_ + [1])([0]) == ([0] + _)([1]) == [0, 1] 44 | assert (_ * [0])(2) == ([0] * _)(2) == [0, 0] 45 | 46 | 47 | def test_binary(): 48 | assert (_ << 2)(1) == (1 << _)(2) == 4 49 | assert (_ >> 2)(7) == (7 >> _)(2) == 1 50 | assert (_ & 3)(5) == (5 & _)(3) == 1 51 | assert (_ | 3)(5) == (5 | _)(3) == 7 52 | assert (_ ^ 3)(5) == (5 ^ _)(3) == 6 53 | 54 | 55 | @pytest.mark.benchmark 56 | @parametrized.zip 57 | def test_comparisons(x=(1, 1.0), y=(2, 2.0)): 58 | assert (_ < y)(x) and (_ > x)(y) 59 | assert (_ <= y)(x) and (_ >= x)(y) 60 | assert (_ == x)(x) and (_ != x)(y) 61 | 62 | 63 | def test_composition(): 64 | f = F(len) + 1 65 | assert f('') == 1 66 | assert (f * 2)('') == 2 67 | mean = (_ + _) / 2.0 68 | assert mean(0, 1) == 0.5 69 | 70 | 71 | @parametrized 72 | def test_errors(op='+ - * // / % ** << >> & ^ | @'.split()): 73 | for expr in ('_ {} None', 'None {} _'): 74 | func = eval(expr.format(op)) 75 | with pytest.raises(TypeError): 76 | func(0) 77 | 78 | 79 | def test_unary(): 80 | assert (-_)(1) == -1 81 | assert (+_)(-1) == -1 82 | assert (~_)(0) == -1 83 | 84 | assert abs(_)(-1) == 1 85 | assert list(reversed(_)('abc')) == ['c', 'b', 'a'] 86 | assert math.trunc(_)(-1.1) == -1 87 | 88 | assert round(_)(0.1) == 0 89 | assert round(_, 1)(0.11) == 0.1 90 | assert math.floor(_)(-1.1) == -2 91 | assert math.ceil(_)(-1.1) == -1 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![image](https://img.shields.io/pypi/v/placeholder.svg)](https://pypi.org/project/placeholder/) 2 | ![image](https://img.shields.io/pypi/pyversions/placeholder.svg) 3 | [![image](https://pepy.tech/badge/placeholder)](https://pepy.tech/project/placeholder) 4 | ![image](https://img.shields.io/pypi/status/placeholder.svg) 5 | [![build](https://github.com/coady/placeholder/actions/workflows/build.yml/badge.svg)](https://github.com/coady/placeholder/actions/workflows/build.yml) 6 | [![image](https://codecov.io/gh/coady/placeholder/branch/main/graph/badge.svg)](https://codecov.io/gh/coady/placeholder/) 7 | [![CodeQL](https://github.com/coady/placeholder/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/coady/placeholder/actions/workflows/github-code-scanning/codeql) 8 | [![CodSpeed Badge](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/coady/placeholder) 9 | [![image](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) 10 | [![image](https://mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) 11 | 12 | A `placeholder` uses operator overloading to create partially bound functions on-the-fly. When used in a binary expression, it will return a callable object with the other argument bound. It's useful for replacing `lambda` in functional programming, and resembles Scala's placeholders. 13 | 14 | ## Usage 15 | ```python 16 | from placeholder import _ # single underscore 17 | 18 | _.age < 18 # lambda obj: obj.age < 18 19 | _[key] ** 2 # lambda obj: obj[key] ** 2 20 | ``` 21 | 22 | Note `_` has special meaning in other contexts, such as the previous output in interactive shells. Assign to a different name as needed. Kotlin uses `it`, but in Python `it` is a common short name for an iterator. 23 | 24 | `_` is a singleton of an `F` class, and `F` expressions can also be used with functions. 25 | 26 | ```python 27 | from placeholder import F 28 | 29 | -F(len) # lambda obj: -len(obj) 30 | ``` 31 | 32 | All applicable double underscore methods are supported. 33 | 34 | ## Performance 35 | Every effort is made to optimize the placeholder instance. It's 20-40x faster than similar libraries on PyPI. 36 | 37 | Placeholders are also iterable, allowing direct access to the underlying functions. 38 | 39 | ```python 40 | (func,) = _.age # operator.attrgetter('age') 41 | ``` 42 | 43 | Performance should generally be comparable to inlined expressions, and faster than lambda. Below are some example benchmarks. 44 | 45 | ```python 46 | min(data, key=operator.itemgetter(-1)) # 1x 47 | min(data, key=_[-1]) # 1.3x 48 | min(data, key=lambda x: x[-1]) # 1.6x 49 | ``` 50 | 51 | ## Installation 52 | ```console 53 | pip install placeholder 54 | ``` 55 | 56 | ## Tests 57 | 100% branch coverage. 58 | 59 | ```console 60 | pytest [--cov] 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/examples.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Examples\n", 8 | "## `F` and `_` (singleton)\n", 9 | "IPython uses `_` as the previous output, so `x` is used here instead." 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from placeholder import _ as x\n", 19 | "\n", 20 | "x.real" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "nums = (0 + 2j), (1 + 1j), (2 + 0j)\n", 30 | "min(nums, key=x.real)" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "min(nums, key=x.imag)" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "x[0]" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "metadata": {}, 55 | "outputs": [], 56 | "source": [ 57 | "pairs = 'ac', 'bb', 'ca'\n", 58 | "min(pairs, key=x[0])" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": null, 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [ 67 | "min(pairs, key=x[-1])" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": null, 73 | "metadata": {}, 74 | "outputs": [], 75 | "source": [ 76 | "import itertools\n", 77 | "\n", 78 | "list(itertools.accumulate(range(1, 6), x * x))" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "list(itertools.filterfalse(x % 2, range(10)))" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": null, 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | "list(filter(x % 2 == 0, range(10)))" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "list(filter(abs(x) < 1, [-1, 0, 1]))" 106 | ] 107 | }, 108 | { 109 | "cell_type": "markdown", 110 | "metadata": {}, 111 | "source": [ 112 | "## `M` and `m` (singleton)\n", 113 | "Support for `attrgetter(*)`, `itemgetter(*)`, and `methodcaller`." 114 | ] 115 | }, 116 | { 117 | "cell_type": "code", 118 | "execution_count": null, 119 | "metadata": {}, 120 | "outputs": [], 121 | "source": [ 122 | "from placeholder import m\n", 123 | "\n", 124 | "m('real', 'imag')" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": null, 130 | "metadata": {}, 131 | "outputs": [], 132 | "source": [ 133 | "m[0, -1]" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": null, 139 | "metadata": {}, 140 | "outputs": [], 141 | "source": [ 142 | "m.split('-')" 143 | ] 144 | } 145 | ], 146 | "metadata": { 147 | "celltoolbar": "Edit Metadata", 148 | "language_info": { 149 | "codemirror_mode": { 150 | "name": "ipython", 151 | "version": 3 152 | }, 153 | "file_extension": ".py", 154 | "mimetype": "text/x-python", 155 | "name": "python", 156 | "nbconvert_exporter": "python", 157 | "pygments_lexer": "ipython3", 158 | "version": "3.8.6-final" 159 | } 160 | }, 161 | "nbformat": 4, 162 | "nbformat_minor": 2 163 | } 164 | -------------------------------------------------------------------------------- /placeholder/__init__.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import math 3 | import operator 4 | from collections.abc import Callable, Iterable, Iterator, Sequence 5 | from functools import partial 6 | from . import partials # type: ignore 7 | 8 | 9 | def update_wrapper(wrapper: Callable, func: Callable): 10 | wrapper.__doc__ = func.__doc__ 11 | wrapper.__name__ = func.__name__ # type: ignore 12 | wrapper.__annotations__['return'] = 'F' 13 | return wrapper 14 | 15 | 16 | def pipe(funcs: Sequence[Callable], *args, **kwargs): 17 | value = funcs[0](*args, **kwargs) 18 | for func in funcs[1:]: 19 | value = func(value) 20 | return value 21 | 22 | 23 | def methods(func: Callable): 24 | def left(self, other): 25 | if isinstance(other, F): 26 | return type(self)(self, func) 27 | return type(self)(self, partials.partial(func, other).left) 28 | 29 | def right(self, other): 30 | return type(self)(self, partials.partial(func, other).right) 31 | 32 | return update_wrapper(left, func), update_wrapper(right, func) 33 | 34 | 35 | def unary(func: Callable): 36 | return update_wrapper(lambda self: type(self)(self, func), func) 37 | 38 | 39 | class F(partial): 40 | """Singleton for creating composite functions. 41 | 42 | Args: 43 | *funcs (Callable): ordered callables 44 | """ 45 | 46 | def __new__(cls, *funcs): 47 | funcs = (func if isinstance(func, cls) else [func] for func in funcs) 48 | funcs = tuple(itertools.chain(*funcs)) 49 | return partial.__new__(cls, *(funcs if len(funcs) == 1 else (pipe, funcs))) # type: ignore 50 | 51 | def __iter__(self) -> Iterator[Callable]: 52 | """Return composed functions in order.""" 53 | args = super().__getattribute__('args') 54 | return iter(args[0] if args else [super().__getattribute__('func')]) 55 | 56 | def __getattribute__(self, attr: str) -> 'F': 57 | """Return `attrgetter`.""" 58 | if attr.startswith('__') and attr.endswith('__'): 59 | return super().__getattribute__(attr) 60 | return type(self)(self, operator.attrgetter(attr)) 61 | 62 | def __getitem__(self, item) -> 'F': 63 | """Return `itemgetter`.""" 64 | return type(self)(self, operator.itemgetter(item)) 65 | 66 | def __round__(self, ndigits: int | None = None) -> 'F': 67 | """Return `round(...)`.""" 68 | return type(self)(self, round if ndigits is None else partial(round, ndigits=ndigits)) 69 | 70 | __neg__ = unary(operator.neg) 71 | __pos__ = unary(operator.pos) 72 | __invert__ = unary(operator.invert) 73 | 74 | __abs__ = unary(abs) 75 | __reversed__ = unary(reversed) 76 | 77 | __trunc__ = unary(math.trunc) 78 | __floor__ = unary(math.floor) 79 | __ceil__ = unary(math.ceil) 80 | 81 | __add__, __radd__ = methods(operator.add) 82 | __sub__, __rsub__ = methods(operator.sub) 83 | __mul__, __rmul__ = methods(operator.mul) 84 | __floordiv__, __rfloordiv__ = methods(operator.floordiv) 85 | __truediv__, __rtruediv__ = methods(operator.truediv) 86 | 87 | __mod__, __rmod__ = methods(operator.mod) 88 | __divmod__, __rdivmod__ = methods(divmod) 89 | __pow__, __rpow__ = methods(operator.pow) 90 | __matmul__, __rmatmul__ = methods(operator.matmul) 91 | 92 | __lshift__, __rlshift__ = methods(operator.lshift) 93 | __rshift__, __rrshift__ = methods(operator.rshift) 94 | 95 | __and__, __rand__ = methods(operator.and_) 96 | __xor__, __rxor__ = methods(operator.xor) 97 | __or__, __ror__ = methods(operator.or_) 98 | 99 | __eq__ = methods(operator.eq)[0] 100 | __ne__ = methods(operator.ne)[0] 101 | __lt__, __gt__ = methods(operator.lt) 102 | __le__, __ge__ = methods(operator.le) 103 | 104 | 105 | class M: 106 | """Singleton for creating method callers and multi-valued getters.""" 107 | 108 | def __getattr__(cls, name: str) -> F: 109 | """Return a `methodcaller` constructor.""" 110 | return F(partial(operator.methodcaller, name), F) 111 | 112 | def __call__(self, *names: str) -> F: 113 | """Return a tupled `attrgetter`.""" 114 | return F(operator.attrgetter(*names)) 115 | 116 | def __getitem__(self, keys: Iterable) -> F: 117 | """Return a tupled `itemgetter`.""" 118 | return F(operator.itemgetter(*keys)) 119 | 120 | 121 | _ = F() 122 | m = M() 123 | --------------------------------------------------------------------------------