Late allows for late binding of default arguments
Project description
包 Late 1.3.0b1
Late binding for Python default arguments
What is it?
包 Late provides decorators and functions to work around the issues that early binding of default argument values produces in Python.
What follows is not intuitive for newcomers to Python, but it's something that everyone learns quickly:
>>> def f(x=[]):
... x.append(1)
... return x
...
>>> f()
[1]
>>> f()
[1, 1]
>>> f()
[1, 1, 1]
The behavior in Python is that the same initializer value is passed on every function invocation, so using mutable values produces the above results.
The coding pattern to work around the above is to use None
as the initializer, and check for
the argument value at the start of the function code:
>>> def f(x=None):
... if x is None:
... x = []
... x.append(1)
... return x
...
>>> f()
[1]
>>> f()
[1]
>>> f()
[1]
It's ugly, but it works.
Now comes the other ugly part. When using type annotations, the above function must be declared
in a way so that type checkers do not complain about using None
as the default value:
def f(x: list[Any] | None = None) -> list[Any]:
or:
def f(x: Optional[list[Any]] = None) -> list[Any]:
Another problem with the above declarations is that calling f(None)
passes type checking,
when that's probably not the preferred situation.
A solution
包 Late provides a way to solve the above ugliness with some decorator magic. This is how the code looks with some of that magic:
from late import latebinding, __
@latebinding
def f(x: list[Any] = __([])) -> list[Any]:
x.append(1)
return x
assert f() == [1]
assert f() == [1]
assert f() == [1]
For constructors for basic structured types, the __()
call may be omitted:
@latebinding
def f(x: list[Any] = []) -> list[Any]:
Working with classes
包 Late also works with classes and dataclass
. The @latebinding
decorator
must be the outer one:
@latebinding
@dataclass
class C:
x: list[Any] = __([]) # noqa
c = C()
assert c.x == []
d = C()
assert d.x == []
c.x = [1]
assert c.x == [1]
assert d.x == []
assert d.x is not c.x
Working with iterators
包 Late allows passing an iterator as a default argument value, and it will provide the next value on each function call. The usefulness of this feature is unknown, but it's something that came up during the discussions about default arguments, so 包 Late implements it.
def fib() -> Iterator[int]:
x, y = 0, 1
while True:
yield x
x, y = y, x + y
@latebinding
def f(x: int = __(fib())) -> int:
return x
assert f() == 0
assert f() == 1
assert f() == 1
assert f() == 2
assert f() == 3
assert f() == 5
This is a possible use for the iterator feature. Imagine a function that requires a unique ID, and will generate one if none is provided. Without 包 Late the declaration would be:
def get_session(uniqueid: int | None = None) -> Session:
if uniqueid is None:
uniqueid = make_unique_id()
Using 包 Late, the declaration can be:
def unique_id_generator() -> Iterator[int]:
while True:
yield make_unique_id()
@latebinding
def get_session(uniqueid: int = __(unique_id_generator())) -> Session:
Working with functions
包 Late also allows late-binding for functions, so the above example could be implemented using a function instead of a generator:
@latebinding
def get_session(uniqueid: int = __(make_unique_id)) -> Session:
The given function will be called once every time the uniqueid
argument is omitted.
About name choice
The names of what 包 Late exports are chosen to be explicit where it matters, and to not get in
the way of the visuals of a declaration. In particular, __()
was chosen to interfere the least
possible with reading a function declaration (late()
is another name for it, and __
is
seldom used in Python code).
At any rate, 包 Late is so simple and so small that you can apply any changes you like and use it as another part of your code instead of installing it as a library.
How does it work?
For values of immutable types, __()
will return the same value. For all other types __()
will wrap the value in a special namedtuple(actual=value)
. At function invocation time, this it what happens:
- if the argument name is already in
kwargs
, nothing is done - if the wrapped value is an iterator, then
next(actual)
is used - if the wrapped value is a function, then
actual()
is used - in all other cases
copy.deepcopy(actual)
is used
For convenient type checking, __()
is declared so its type will be the desired one depending
on the argument:
def late(o: _T | Iterator[_V] | Callable[[], _R]) -> _T | _V | _R:
Late binding?
The definition of Late Binding that 包 Late uses is that of what is resolved at runtime instead of at compile time.
Why doesn't the Python interpreter solve this?
Although the ugliness and inconvenience in the current situation have been acknowledged and discussed for a very long time, there has never been an agreement about the usefulness, the semantics, nor the syntax of a solution. That way the status quo has remained unchanged.
You can find a recent discussion about these topics on the Python Ideas site.
Installation
$ pip install Late
License
包 Late is licensed as reads in LICENSE.
And now... this!
You can use 包
, the Kanji for "wrap", instead of __
to late-bind
an argument.
@latebinding
def f(x: list[Any] = 包([])) -> list[Any]:
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file Late-1.2.2.tar.gz
.
File metadata
- Download URL: Late-1.2.2.tar.gz
- Upload date:
- Size: 8.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.2 CPython/3.12.0
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 9a15d44d20e53b046a125e45561f99dde4e9126fa5d19871b608cfad34b5a9a4 |
|
MD5 | 0241cde8503183f14a3314386d7a5727 |
|
BLAKE2b-256 | 6133ed8ff374932b2f2a406370530d7223f7ac9de3dda23f4150aefaf0e7afd9 |
File details
Details for the file Late-1.2.2-py3-none-any.whl
.
File metadata
- Download URL: Late-1.2.2-py3-none-any.whl
- Upload date:
- Size: 12.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.2 CPython/3.12.0
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 0a28987d056a9e0ec39564d429c6dc5804b71decd3d5eefdf2b1c0f778115e99 |
|
MD5 | 5f121d0fcf7f3ec78fc66ab18743bed1 |
|
BLAKE2b-256 | 251f209df37c2cf89ec9d3f41d5cf598477b1da74603d4efe427b18cecc23e81 |