Function call in (kw)args
I propose that: def foo(print(x)): pass becomes: def foo(x): x = print(x) pass or, alternatively we could have decorators in function args: def foo(@print x): pass which would probably be more aligned with the rest of python actually. anyway, this would be nice I think. I could really use it.
On 2/9/2020 7:34 AM, Soni L. wrote:
I propose that:
def foo(print(x)): pass
becomes:
def foo(x): x = print(x) pass
or, alternatively we could have decorators in function args:
def foo(@print x): pass
which would probably be more aligned with the rest of python actually.
anyway, this would be nice I think. I could really use it.
I'm having a hard time imagining where this would be useful. Could you give a concrete example where it would make some code clearer? Surely you wouldn't use it with print(), which returns None. Eric
On 2020-02-09 9:44 a.m., Eric V. Smith wrote:
On 2/9/2020 7:34 AM, Soni L. wrote:
I propose that:
def foo(print(x)): pass
becomes:
def foo(x): x = print(x) pass
or, alternatively we could have decorators in function args:
def foo(@print x): pass
which would probably be more aligned with the rest of python actually.
anyway, this would be nice I think. I could really use it.
I'm having a hard time imagining where this would be useful. Could you give a concrete example where it would make some code clearer? Surely you wouldn't use it with print(), which returns None.
Of course not. It'd be useful with traits. Turn this: from traitobject import Trait, TraitObject, impl class MyTrait(Trait): def x(self): raise NotImplementedError class MyClass(TraitObject): @impl(MyTrait) class MyTrait: def x(self): print("Hello,") self.x() def x(self): print("World!") def my_fn(x): x = MyTrait(x) x.x() my_fn(MyClass()) into this: from traitobject import Trait, TraitObject, impl class MyTrait(Trait): def x(self): raise NotImplementedError class MyClass(TraitObject): @impl(MyTrait) class MyTrait: def x(self): print("Hello,") self.x() def x(self): print("World!") def my_fn(@MyTrait x): x.x() my_fn(MyClass()) (this may not be "real code" but it's one of the test cases in my trait lib project)
Eric _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/VGIQ35... Code of Conduct: http://python.org/psf/codeofconduct/
On 9 Feb 2020, at 14:01, Soni L. <fakedme+py@gmail.com> wrote:
into this:
[snip]
def my_fn(@MyTrait x): x.x()
Can't you look at typing info or something instead? That is a lot of boilerplate and ceremony even after your proposed addition. So: @traits def my_fn(x: MyTrait): x.x() / Anders
On 2020-02-09 10:28 a.m., Anders Hovmöller wrote:
On 9 Feb 2020, at 14:01, Soni L. <fakedme+py@gmail.com> wrote:
into this:
[snip]
def my_fn(@MyTrait x): x.x()
Can't you look at typing info or something instead? That is a lot of boilerplate and ceremony even after your proposed addition.
So:
@traits def my_fn(x: MyTrait): x.x()
/ Anders
not happy about how that looks and I'd need to create wrapper functions and deal with mixed args and kwargs. as far as I know there aren't libraries that do it for me, either. I'd rather have arg decorators tbh. they feel more natural than hacking on the type annotations.
On Feb 9, 2020, at 05:40, Soni L. <fakedme+py@gmail.com> wrote:
I'd rather have arg decorators tbh. they feel more natural than hacking on the type annotations.
Annotations look a lot more natural to me here. Maybe that’s because your only example (besides the useless original one) looks like an implicit cast a la C++ and zillions of other languages: void cheese(Spam spam) { // whatever } Eggs eggs(42); cheese(eggs); Oversimplifying the C++ rules a bit, this will call the Spam::Spam(Eggs) constructor on eggs to get a Spam, and that will be the value of spam. Which is exactly what you’re trying to do, so spelling it similarly seems reasonable: @implicit_cast def cheese(spam: Spam): # whatever eggs = Eggs(42) cheese(eggs) This would of course raise a TypeError at runtime instead of at compile time if there is no way to construct a Spam from an Eggs, and it requires a decorator because it’s not built deep into the language as it is in C++, but otherwise it’s the same idea and the most obvious way to translate its spelling. Maybe if you had a good example that wasn’t an implicit type cast, this similarity would be an argument against using annotations rather than an argument for using them? But it’s hard to know without seeing such an example, or even knowing if you have such an example.
I'd need to create wrapper functions and deal with mixed args and kwargs. as far as I know there aren't libraries that do it for me, either.
You only need to create one decorator that creates wrapping functions. There is a library that deals with args and kwargs for you, and it comes with Python: inspect. There are also libraries in PyPI to help write decorators, to do cast-like stuff, etc. If there isn’t a library that puts it all together the way you want, then that probably means there isn’t nearly enough demand for this feature to even put it in the stdlib, much less add new syntax for it. But that also means you can write it yourself and put it on PyPI and proselytize its use, and maybe create that demand.
On Feb 9, 2020, at 11:49, Andrew Barnert <abarnert@yahoo.com> wrote:
If there isn’t a library that puts it all together the way you want
I figured there probably was, so I decided to take a look. I got a whole page of possibly useful things, and the first one I looked at, typical, shows this as its first example: @typic.al def multi(a: int, b: int): return a + b … which does exactly what you want here. Meanwhile, here’s something I slapped together in however long it was from my last email to a couple minutes before this one, to show how you could get started in case none of the many PyPI libraries is exactly what you want: def implicit_cast(f): sig = inspect.signature(f) def cast(param, arg): typ = sig.parameters[param].annotation if typ is sig.empty: return arg if isinstance(typ, str): typ = eval(typ) return typ(arg) @functools.wraps(f) def wrapper(*args, **kw): bound = sig.bind(*args, **kw) bound.apply_defaults() castargs = {param: cast(param, arg) for param, arg in bound.arguments.items()} return f(**castargs) return wrapper @implicit_cast def f(x: int, y=2, *, z) -> int: return x+y+z
On 2020-02-09 4:46 p.m., Andrew Barnert wrote:
On Feb 9, 2020, at 05:40, Soni L. <fakedme+py@gmail.com> wrote:
I'd rather have arg decorators tbh. they feel more natural than hacking on the type annotations.
Annotations look a lot more natural to me here.
Maybe that’s because your only example (besides the useless original one) looks like an implicit cast a la C++ and zillions of other languages:
void cheese(Spam spam) { // whatever }
Eggs eggs(42); cheese(eggs);
Oversimplifying the C++ rules a bit, this will call the Spam::Spam(Eggs) constructor on eggs to get a Spam, and that will be the value of spam. Which is exactly what you’re trying to do, so spelling it similarly seems reasonable:
@implicit_cast def cheese(spam: Spam): # whatever
eggs = Eggs(42) cheese(eggs)
This would of course raise a TypeError at runtime instead of at compile time if there is no way to construct a Spam from an Eggs, and it requires a decorator because it’s not built deep into the language as it is in C++, but otherwise it’s the same idea and the most obvious way to translate its spelling.
Maybe if you had a good example that wasn’t an implicit type cast, this similarity would be an argument against using annotations rather than an argument for using them? But it’s hard to know without seeing such an example, or even knowing if you have such an example.
I'd need to create wrapper functions and deal with mixed args and kwargs. as far as I know there aren't libraries that do it for me, either.
You only need to create one decorator that creates wrapping functions. There is a library that deals with args and kwargs for you, and it comes with Python: inspect. There are also libraries in PyPI to help write decorators, to do cast-like stuff, etc. If there isn’t a library that puts it all together the way you want, then that probably means there isn’t nearly enough demand for this feature to even put it in the stdlib, much less add new syntax for it. But that also means you can write it yourself and put it on PyPI and proselytize its use, and maybe create that demand. Don't forget: kw-only arguments, positional-only arguments, arguments
That's C++. But consider this: class Foo: def bar(self): pass x = Foo() y = x.bar The traits thing is just as much of an implicit cast as Python methods are an implicit cast. Which is to say, it isn't. Traits don't have a constructor - instead, calling them with an object will, if the object allows it, create a wrapper object to handle the trait method lookups and any necessary wrapping or unwrapping of objects. This is also the way to access conflicting names: MyTrait(MyClass()).x() is defined in the @impl(MyTrait) (and does print("Hello,") then print("World!")), whereas MyClass().x() is the inherent method in MyClass (and only prints "World!"). Note also that the @impl gets the full unwrapped object, and not a TraitWrapper - this is by design. This has many parallels to how methods in python do the "self" thing, altho I can understand the confusion if you're not familiar with traits in e.g. Rust. that can be either, default values, etc.
On Mon, Feb 10, 2020 at 7:21 AM Soni L. <fakedme+py@gmail.com> wrote:
Maybe if you had a good example that wasn’t an implicit type cast, this similarity would be an argument against using annotations rather than an argument for using them? But it’s hard to know without seeing such an example, or even knowing if you have such an example.
That's C++. But consider this:
class Foo: def bar(self): pass
x = Foo() y = x.bar
The traits thing is just as much of an implicit cast as Python methods are an implicit cast. Which is to say, it isn't. Traits don't have a constructor - instead, calling them with an object will, if the object allows it, create a wrapper object to handle the trait method lookups and any necessary wrapping or unwrapping of objects. This is also the way to access conflicting names: MyTrait(MyClass()).x() is defined in the @impl(MyTrait) (and does print("Hello,") then print("World!")), whereas MyClass().x() is the inherent method in MyClass (and only prints "World!"). Note also that the @impl gets the full unwrapped object, and not a TraitWrapper - this is by design.
This entire proposal is based on the need to disambiguate conflicting method names, correct? Can you show any sort of example of something where this is actually useful, and can't be better done with composition rather than inheritance (since composition, in effect, namespaces the methods - thing.trait1.x() and thing.trait2.x() rather than multiply inheriting)? I'm having trouble wrapping my head around the meaningful identities of the various different objects, since all the examples use placeholder names with no true purpose. And yes, I'm aware that what I'm asking for might end up too big for a simple post. But it would be helpful to have a reference of some sort. Chris
On 2020-02-09 5:31 p.m., Chris Angelico wrote:
On Mon, Feb 10, 2020 at 7:21 AM Soni L. <fakedme+py@gmail.com> wrote:
Maybe if you had a good example that wasn’t an implicit type cast, this similarity would be an argument against using annotations rather than an argument for using them? But it’s hard to know without seeing such an example, or even knowing if you have such an example.
That's C++. But consider this:
class Foo: def bar(self): pass
x = Foo() y = x.bar
The traits thing is just as much of an implicit cast as Python methods are an implicit cast. Which is to say, it isn't. Traits don't have a constructor - instead, calling them with an object will, if the object allows it, create a wrapper object to handle the trait method lookups and any necessary wrapping or unwrapping of objects. This is also the way to access conflicting names: MyTrait(MyClass()).x() is defined in the @impl(MyTrait) (and does print("Hello,") then print("World!")), whereas MyClass().x() is the inherent method in MyClass (and only prints "World!"). Note also that the @impl gets the full unwrapped object, and not a TraitWrapper - this is by design.
This entire proposal is based on the need to disambiguate conflicting method names, correct? Can you show any sort of example of something where this is actually useful, and can't be better done with composition rather than inheritance (since composition, in effect, namespaces the methods - thing.trait1.x() and thing.trait2.x() rather than multiply inheriting)? I'm having trouble wrapping my head around the meaningful identities of the various different objects, since all the examples use placeholder names with no true purpose.
Traits are composition tho. I think you missed something I said.
And yes, I'm aware that what I'm asking for might end up too big for a simple post. But it would be helpful to have a reference of some sort.
Chris _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/XEGUKW... Code of Conduct: http://python.org/psf/codeofconduct/
On 10 Feb 2020, at 04:18, Soni L. <fakedme+py@gmail.com> wrote:
Traits are composition tho. I think you missed something I said.
In that case I think we all missed it and then isn't that on you? Remember it is you who are trying to convince the list so any information missed or misunderstood is your problem :) Again: an example that we can understand would help.
On 2020-02-10 2:03 a.m., Anders Hovmöller wrote:
On 10 Feb 2020, at 04:18, Soni L. <fakedme+py@gmail.com> wrote:
Traits are composition tho. I think you missed something I said.
In that case I think we all missed it and then isn't that on you? Remember it is you who are trying to convince the list so any information missed or misunderstood is your problem :)
Again: an example that we can understand would help.
I am trying to implement a Rust-inspired trait system in pure python. part of what makes Rust so great is that you can have your type implement the whole stdlib of traits (which you wouldn't, but I digress), because traits effectively act like namespaces. and one of the key aspects of that is that you disambiguate at *call* site. the existing "trait" libs I've looked at were more like mixins, requiring you to disambiguate at *definition* site, which means there's no namespacing and you basically just have to hope the "traits" you picked have API-compatible methods/names/etc. for context, here's what some libs look like: - "traits" is strictly about type-checked attributes/properties. it never touches calling functions with those objects, or interacting with those objects directly (or rather, it does, but it's still more of a mixin kinda thing.) - "py3traits" is also mixin-like, with some ECS-like properties. still doesn't touch the issue of name/attribute ambiguities. - "strait" comes extremely close(!), which is nice, but it falls short by making the composability a runtime error when hitting name conflicts. "if traits T1 and T2 have names in common, enhancing a class both with T1 and T2 raises an error;" is explicitly the *opposite* of what I want. it also relies on you calling trait methods on the object itself, which leads to all the issues raised by mixins - especially if the class shadows a name defined by a trait. "strait" is promising but the author's tradeoffs have turned it into, basically, mixins with extra steps. and it just keeps going like that. searching for "traits" on pypi those are the 3 main packages, and then there are a bunch of packages that use them. but it is all effectively mixins. or runtime typechecking I guess. (fwiw, my take on python traits is rather duck-typed and you can make an object fit the trait model without going through the Trait or TraitObject classes or using @impl, but I digress.) I just find it kinda... I wouldn't say "sad", but something like it, that nobody has made it so you can explicitly call a trait method regardless of "names in common" either between 2 (or more) traits or between traits and the class that implements them. and yes, this does require a somewhat typed system, such as defining types and wrapping objects in the function signature, but you can still have a duck-typed trait system. (nothing prevents you from taking an arbitrary object and setting obj._traits_={MyTrait: something} on it (other than __slots__). I consider this duck-typing.)
On Feb 9, 2020, at 12:20, Soni L. <fakedme+py@gmail.com> wrote: On 2020-02-09 4:46 p.m., Andrew Barnert wrote: On Feb 9, 2020, at 05:40, Soni L. <fakedme+py@gmail.com> wrote:
I'd rather have arg decorators tbh. they feel more natural than hacking on the type annotations. Annotations look a lot more natural to me here. Maybe that’s because your only example (besides the useless original one) looks like an implicit cast a la C++ and zillions of other languages: void cheese(Spam spam) { // whatever } Eggs eggs(42); cheese(eggs);
That's C++.
Yes, it is.
But consider this:
class Foo: def bar(self): pass
x = Foo() y = x.bar
So what? Consider this: auto y = &Foo::bar; Neither one is relevant here. I think you’re just being misled by the fact that the implementation has to deal with a few of the same issues; to the user, method lookup and constructor calls look and act nothing alike, in both languages.
The traits thing is just as much of an implicit cast as Python methods are an implicit cast.
No, there are two huge differences. First, you call the trait as a constructor, passing it the object to get back the object-as-this-trait, explicitly. While you _can_ do that with methods (`types.MethodType(type(x).bar, x)`), you don’t; you just write `x.bar`, and the actual method constructor is buried under two levels of abstraction (oversimplifying slightly, Python turns that into `type(bar:=type(x).bar).__get__(bar, x)`, and then `FunctionType.__get__(self, x)` returns `MethodType(self, x)`). Second, even on the rare occasions when you do call the method constructor directly, it still doesn’t look like a cast—it isn’t a constructor from one object, but from two.
Which is to say, it isn't. Traits don't have a constructor - instead, calling them with an object will, if the object allows it, create a wrapper object to handle the trait method lookups and any necessary wrapping or unwrapping of objects.
But that is a constructor, for that wrapper object. (I’m pretty sure all of the traits libraries on PyPI even implement it with either __new__ on the trait base class or __call__ on a metaclass, but that’s not important.) “Constructor” is a much looser term in Python than in C++, where we call things like factory functions and classmethods “constructors” too because, as far as the caller is concerned, they’re completely interchangeable with calling a type directly. But if you don’t like that naming, fine. Forget about “cast” and “constructor”: You can use anything that makes sense as an annotation, and can be called with a single argument to give the intended parameter value. Which is exactly what you’re trying to do. That’s why the first syntax you suggested, `def f(spam(x)):`, is workable in the first place. Libraries like (I assume; I haven’t tested it) typical, or the quick&dirty thing I wrote will do exactly what you want here. And that includes traits as you defined them and as they’re implemented in libraries like strait. And if you don’t like the name `@typic.al` or `@implicit_cast` there’s nothing stopping you from calling it `@resolve_trait` (and restricting it to only applying annotations that are traits, while you’re at it). So, why do we need new syntax?
This is also the way to access conflicting names: MyTrait(MyClass()).x() is defined in the @impl(MyTrait) (and does print("Hello,") then print("World!")), whereas MyClass().x() is the inherent method in MyClass (and only prints "World!"). Note also that the @impl gets the full unwrapped object, and not a TraitWrapper - this is by design.
This has many parallels to how methods in python do the "self" thing,
It has more parallels with super than with methods, but it’s not really the same as either.
I'd need to create wrapper functions and deal with mixed args and kwargs. as far as I know there aren't libraries that do it for me, either. You only need to create one decorator that creates wrapping functions. There is a library that deals with args and kwargs for you, and it comes with Python: inspect. There are also libraries in PyPI to help write decorators, to do cast-like stuff, etc. If there isn’t a library that puts it all together the way you want, then that probably means there isn’t nearly enough demand for this feature to even put it in the stdlib, much less add new syntax for it. But that also means you can write it yourself and put it on PyPI and proselytize its use, and maybe create that demand. Don't forget: kw-only arguments, positional-only arguments, arguments that can be either, default values, etc.
I didn’t forget them. More to the point, the inspect module didn’t forget them, so you can just rely on it. The quick&dirty hack I posted was for Python 3.7, so it doesn’t support positional-only parameters because they don’t exist in 3.7, but it will handle keyword-only parameters and positional-or-keyword parameters, and default values, and decorated functions and partials and all kinds of other things I probably wouldn’t think to deal with at first, because the inspect module already deals with them for me.
participants (5)
-
Anders Hovmöller
-
Andrew Barnert
-
Chris Angelico
-
Eric V. Smith
-
Soni L.