[Python-Dev] PEP 362: 4th edition
Nick Coghlan
ncoghlan at gmail.com
Tue Jun 19 03:22:37 CEST 2012
On Mon, Jun 18, 2012 at 5:08 PM, Jim Jewett <jimjjewett at gmail.com> wrote:
> On Sat, Jun 16, 2012 at 11:27 AM, Nick Coghlan <ncoghlan at gmail.com> wrote:
>> No. This is the full set of binding behaviours. "self" is just an
>> ordinary "POSITIONAL_OR_KEYWORD" argument (or POSITIONAL_ONLY, in some
>> builtin cases).
>
> Or no longer a "parameter" at all, once the method is bound. Except
> it sort of still is. Same for the space parameter in PyPy. I don't
> expect the stdlib implementation to support them initially, but I
> don't want it to get in the way, either. A supposedly closed set gets
> in the way.
It's not supposedly closed, it *is* closed: Python doesn't support any
other ways of binding arguments to parameters. Now, you can have
additional state on a callable that gets used by that callable (such
as __closure__ and __globals__ on a function, or __self__ on a method,
or arbitrary state on an object that implements __call__) but that
extra state is not part of the call *signature*, and thus should not
be exposed on the result of inspect.getsignature(). Remember, this
object is not meant to be a representation of the full state of a
callable, it's solely about providing a consistent introspection
mechanism that allows arbitrary callables to define how they will bind
arguments to parameters.
That's why I keep pointing out that there will always need to be a
higher level object that brings in other related information, such as
the docstring, the name of the callable, etc. This is not an API that
describes *everything* that is worth knowing about an arbitrary
callable, nor is it intended to be.
I believe you have raised a legitimate question regarding whether or
not there is sufficient variation in behaviour amongst the parameter
kinds for it to be worth using a subclassing approach to override
__str__ and __eq__, compared to just implementing a few "kind" checks
in the base implementation.
However, given that the possible binding behaviours (positional only,
keyword-or-positional, excess positional, keyword-only, excess
keywords) *is* a closed set, I think a subclass based solution is
overkill and adds excessive complexity to the public API.
Cheers,
Nick.
P.S. A more complete response to the question of what constitutes
"suitable complexity" for this API.
Consider the following implementation sketch for a kind based
implementation that pushes as much behaviour as is reasonable into the
Parameter object (including the definition of equality and decisions
on how the parameter should be displayed):
_sentinel = object()
class Parameter:
def __init__(self, name, kind, default=_sentinel,
annotation=_sentinel):
if not name:
raise ValueError("All parameters must be named for
introspection purposes (even positional-only parameters)")
self.name = name
if kind not in Parameter.KINDS:
raise ValueError("Unrecognised parameter binding type
{}".format(kind))
self.kind = kind
if default is not _sentinel:
if kind.startswith("VAR"):
raise ValueError("Cannot specify default value
for {} parameter".format(kind))
self.default = default
if annotation is not _sentinel:
self.annotation = annotation
def _get_key(self):
default = getattr(self, "default", _sentinel)
annotation = getattr(self, "annotation", _sentinel)
if self.kind in (Parameter.KEYWORD_OR_POSITIONAL,
Parameter.KEYWORD_ONLY):
# The name is considered significant for parameters
that can be specified
# as keyword arguments
return (self.name, self.kind, default, annotation)
# Otherwise, we don't really care about the name
return (self.kind, default, annotation)
def __eq__(self, other):
if not isinstance(other, Parameter):
return NotImplemented
return self._get_key() == other._get_key()
def __str__(self):
kind = self.kind
components = []
if kind == Parameter.VAR_POSITIONAL:
components += ["*"]
elif kind == Parameter.VAR_KEYWORD:
components += ["**"]
if kind == Parameter.POSITIONAL_ONLY:
components += ["<", self.name, ">"]
else:
components += [self.name]
try:
default = self.default
except AttributeError:
pass
else:
components += ["=", repr(default)]
try:
annotation = self.annotation
except AttributeError:
pass
else:
components += [":", repr(annotation)]
return "".join(components)
The points of variation:
- VAR_POSITIONAL and VAR_KEYWORD do not permit a "default" attribute
- VAR_POSITIONAL adds a "*" before the name when printed out
- VAR_KEYWORD adds a "**" before the name when printed out
- POSITIONAL_ONLY adds "<>" around the name when printed out
- all three of those ignore "name" for comparisons
Now, suppose we dispense with the kind attribute and use subclassing instead:
_sentinel = object()
class _ParameterBase:
"""Common behaviour for all parameter types"""
def __init__(self, name, default=_sentinel, annotation=_sentinel):
if not name:
raise ValueError("All parameters must be named for
introspection purposes")
self.name = name
self.kind = kind
if default is not _sentinel:
self.default = default
if annotation is not _sentinel:
self.annotation = annotation
def _get_key(self):
default = getattr(self, "default", _sentinel)
annotation = getattr(self, "annotation", _sentinel)
return (self.name, default, annotation)
def __eq__(self, other):
if not isinstance(other, type(self)):
return NotImplemented
return self._get_key() == other._get_key()
def __str__(self):
components = [self.name]
try:
default = self.default
except AttributeError:
pass
else:
components += ["=", repr(default)]
try:
annotation = self.annotation
except AttributeError:
pass
else:
components += [":", repr(annotation)]
return "".join(components)
class Parameter(_ParameterBase):
"""Representation of a normal Python function parameter"""
class KeywordOnlyParameter(_ParameterBase):
"""Representation of a keyword-only Python function parameter"""
class PositionalOnlyParameter(_ParameterBase):
"""Representation of a positional-only parameter"""
def __init__(self, index, name=None,
default=_sentinel, annotation=_sentinel):
if name is not None:
display_name = "<{}:{}>".format(index, name)
else:
display_name = "<{}>".format(index)
super().__init__(display_name, default, annotation)
def _get_key(self):
default = getattr(self, "default", _sentinel)
annotation = getattr(self, "annotation", _sentinel)
return (default, annotation)
class _VarParameterBase(_ParameterBase):
"""Common behaviour for variable argument parameters"""
class VarPositionalParameter(_VarParameterBase):
"""Representation of a parameter for excess positional arguments"""
def __str__(self):
return "*" + super().__str__()
class VarKeywordParameter(_VarParameterBase):
"""Representation of a parameter for excess keyword arguments"""
def __str__(self):
return "**" + super().__str__()
Cheers,
Nick.
--
Nick Coghlan | ncoghlan at gmail.com | Brisbane, Australia
More information about the Python-Dev
mailing list