[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