implicit receivers¶
a callable type may declare a receiver: int.() -> str is a callable that runs
against an int. the receiver is the callable's leading positional parameter,
so nothing about the callable itself is special — any function of that shape
satisfies it, and it can be called directly:
def render(value: int) -> str:
return str(value)
def apply(fn: int.() -> str) -> str:
return fn(1)
apply(render)
what the receiver adds is two ways of reading it back out
calling through the receiver¶
a name in scope declared as a receiver callable can be called as a method of a matching receiver:
receiver.fn on its own is the callable with the receiver already supplied —
() -> str. it lowers to a functools.partial, exactly as a bound method would
carry its receiver:
resolution is a last fallback, so nothing that resolves today changes meaning:
- a real member of the receiver type always wins (
(1).bit_length()is untouched, even with abit_lengthreceiver callable in scope) - an extension member wins over a receiver callable
- the name must be declared — a receiver callable is only ever spelled as an annotation, and a declaration means the same thing everywhere it is visible
- the receiver must be assignable to the callable's receiver parameter
- a scope that binds the name to anything else shadows it, exactly as it would shadow an ordinary load of that name
an access on an optional chain (a?.fn()) is rejected —
the chain lowers to its own conditional, which the receiver rewrite cannot yet be
spliced into
trailing lambda blocks¶
when a trailing lambda block fills a receiver callback,
the block binds that receiver itself. the body sees the receiver's members
unqualified, and spells the receiver self; the block's implicit it parameter
is the callback's own argument, the one after the receiver:
def apply(fn: int.(str) -> None):
fn(1, "a")
apply:
print(self) # 1
print(imag) # 0 — a member of `self`
print(it) # "a"
→
def _trailing_lambda_0(_by_self=None, it=None):
print(_by_self)
print(_by_self.imag)
print(it)
apply(fn=_trailing_lambda_0)
the receiver lands in a parameter the source cannot spell, so nothing the block binds can redirect the members read off it
priority¶
unlike x.fn, this is not a fallback. the receiver joins the scope chain at the
block's own level — inside the names the block itself binds, and outside
everything else — so its members win over an enclosing function's local, a module
global and a builtin alike:
only the block itself outranks it, and only where the block declares the name:
a bare assignment declares nothing, so it does not take the name — it writes the receiver's member, and the reads around it go on meaning that member:
→
the write is checked against the member like any other attribute write, and it captures nothing — there is no name for the closure to write back to. where the receiver has no member of that name, a bare assignment is an ordinary block local, as it always was
declaring a name the receiver has a member for is reported by
shadowed-receiver-member: it is how you ask for a local of your own, and the
warning is there because the two forms mean opposite things one line apart
self is no exception. inside a method the block's self is the block's
receiver, and the method's own receiver is not reachable from the body
a call is the one thing that can turn the receiver down. a name used as a callee takes the receiver's member only if that member accepts the call, and otherwise carries on outwards to whatever else declares the name:
class Repeater:
def emit(self, times: int): ...
def apply(fn: Repeater.() -> None): ...
def emit(label: str, times: int): ...
apply:
emit(2) # `self.emit`
emit("a", 2) # the module-level `emit`
what counts is the shape of the call — how many positional arguments it passes and which keywords — never the types of the arguments. two candidates that differ only in what their parameters accept do not disambiguate this way; the receiver's wins and the call is checked against it.
if no candidate anywhere accepts the call, the receiver's is used, so the call
reports its own mismatch rather than an unresolved name. a name that resolves
nowhere and is not a member of the receiver stays an unresolved-reference error
a block still returns None, so the callback must be declared to return a type
that accepts it — int.() -> None, not int.() -> str (see
trailing lambdas) — and it binds one argument
beyond the receiver, so int.(str, str) -> None is rejected with
trailing-lambda-parameters
syntax¶
the receiver precedes the parameter list, separated by a .:
a: int.() -> str # receiver only
b: str.(int) -> bytes # receiver plus parameters
c: int.(str, *, flag: bool) -> None # any parameter form
d: list[int.() -> str] # nests like any type expression
e: int.() -> str.() -> bytes # the return type may be one too
. followed by ( is never valid python, so the form is unambiguous. like the
callable arrow it is parsed anywhere an expression is, and is
meaningful only in a type expression; a value-position one is a syntax error in
.py files and has no type in .by files
lowering¶
the receiver is the leading positional parameter of the lowered type:
| basedpython | python |
|---|---|
int.() -> str |
Callable[[int], str] |
int.(str) -> bytes |
Callable[[int, str], bytes] |
int.(**P) -> str |
Callable[Concatenate[int, P], str] |
int.(a: str) -> None |
a Protocol whose __call__ takes (_receiver, /, a) |
int.(...) -> str |
Callable[..., str] |
the gradual form is the one lossy case: Callable[..., str] already accepts the
receiver-first call, and Concatenate[int, ...] is not spellable on every
supported python version. the receiver is still a real parameter to the checker
reverse transpiling never produces the receiver form — a lowered
Callable[[int], str] reads back as (int) -> str, since which parameter was
the receiver is not recoverable