On Tue, Jun 22, 2021 at 11:43:09AM -0700, Brendan Barnwell wrote:

> ### file1.py
> @extend(list)
> def len2(self):
>       return len(self)**2
> 
> ### file2.py
> # or whatever I do to say "I want to use extensions to list defined in 
> file1"
> from file1 extend list
> 
> def coolness(some_list):
>       return some_list.len2() + 1
> 
> my_list = [1, 2, 3]
> print("My list len2:", my_list.len2())
> print("My list coolness:", coolness(my_list))

Right, as far as code executing within the "file2" scope is concerned, 
lists have a "len2" method.

 
> ### file3.py
> import file2
> 
> other_list = [1, 2, 3, 4]
> print("Other list len2:", other_list.len2())
> print("other list coolness:", file2.coolness(other_list))


The call to other_list.len2 would fail with AttributeError, because the 
current execution scope when the attribute is looked up is file3, not 
file2.

The call to file2.coolness() would succeed, because the function 
coolness is executed in the scope of file2, which is using the 
extension method.


This is similar to the way name lookups work:

    # A.py
    x = 1
    def func():
        print(x)  # This always looks up x in A's scope.


Obviously calling func() in its own module will succeed. But if you call 
func() from another module, it doesn't look up "x" in the caller's 
module, it still refers back to the module it was defined. Namely A.

    # B.py
    x = 9999
    from A import func
    func()  # prints 1, not 9999


This is standard Python semantics, and you probably don't even think 
about it. Extension methods would work similarly: whether the attribute 
name is visible or not depends on which scope the attribute lookup 
occurs in, not who is calling it.

So if you can understand Python scoping, extension methods will be quite 
similar.

Remember:

- global `x` in A.py and global `x` in B.py are different names in 
  different scopes;

- functions remember their original global scope and lookup names
  in that, not the caller's global scope.

Attribute lookups involving extension methods would be similar:

- whether the extension method is seen or not depends on the
  which scope the look up occurs, not where the caller is.


That lets us see what will happen in file3.py here:


> print("My list len2 from outside:", file2.my_list.len2())

It doesn't matter where the list comes from. What matters is where 
the attribute access occurs. In the first line there, the attribute 
access occurs in the scope of file3, so it fails.

It doesn't matter whether you write `other_list.len2` or 
`file2.my_list.len2`, both are equivalent to:

- get a list instance (file2.my_list, but it could be anything)
- doesn't matter where the instance comes from
- look up the method "len2" on that instance **in the file3 scope**

Hence it fails.

The line that follows:

> print("My list coolness from outside:", file2.coolness(file2.my_list))

does this:

- get a list instance (file2.my_list)
- pass it to file2.coolness
- which looks up the method "len2" **in the file2 scope**

and hence it succeeds.


>       The list object my_list in file2 is the same object accessed as 
> file2.my_list in file3.  Likewise coolness and file2.coolness.  It is 
> going to be super confusing if calling the same function object with the 
> same list object argument gives different results depending on which 
> file you're in.

"It is going to be super confusing if calling the same function with the 
same *name* gives different results depending on which file you're in."

-- me, 25 years ago or so, when I first started learning Python

And probably you to.

Attribute lookups are just another form of name lookup. Name lookups 
depend on the current execution scope, not the caller's scope. With 
extension methods, so do attribute lookups.

If you can cope with name lookups, you can cope with extension methods.


[...]
> But what if the extension is an override of an existing method?  Is that 
> not allowed?)

In C#, you can define extension methods with the same name as an 
existing method, but they will never be seen. The extension methods are 
only used if the normal method lookup fails.


>       In addition, if there is a difference between my_list and 
>       other_list,

No, absolutely not. It isn't two different sorts of list.

Here is some pseudo-code that might help clarify the behaviour.

    _getattr = getattr  # original that we know and love

    def getattr(obj, name):  # called on obj.name
        # New, improved version
        try:
            return _getattr(obj, name)
        except AttributeError:
            if current execution scope is using extensions for type(obj):
                return extension_method(type(self), name)


Think of that as a hand-wavy sketch of behaviour, not an exact 
specification.



> Are both objects lists?  If they are, then how can they have different 
> methods?

Here's another sketch of behaviour.

    class object:
        def __getattr__(self, name):
            # only called if the normal obj.name fails
            if current execution scope is using extensions for type(self):
                return extension_method(type(self), name)


There's nothing in this proposal that isn't already possible. In fact, I 
reckon that some of the big frameworks like Django and others probably 
already do stuff like this behind the scene.

The `extension_method(type, name)` look up could be nothing more than a 
dictionary keyed with types:

    {list: {'flatten': <function at 0x123456abcd>, ...},
     int:  { ... },
     }

I'm not really sure how to implement this test:

    if current execution scope is using extensions for type(self)

I have some ideas, but I'm not sure how viable or fast they would be.


>       That to me is the exact opposite of encapsulation.  Encapsulation 
>       means the object itself contains all its behavior.  If there is some 
> getattr-like hook in some other module somewhere that is lying in wait 
> to override attribute access on a given object "only sometimes" then 
> that's not encapsulation at all.  It's almost as bad as the infamous 
> COME FROM statement!

And yet the legions of people using C#, Java, Swift, TypeScript, Kotlin, 
and others find it invaluable. One of the most popular libraries in 
computing, LINQ, works through extension methods.

COME FROM was a joke command in a joke language, Intercal. Nobody really 
used it except as a joke. For you to compare a powerful and much-loved 
feature used by millions of programmers to COME FROM is as perfect a 
demonstration of the Blub factor in action.

It may be that there are technical reasons why extension methods are not 
viable in Python, but "it's almost as bad as COME FROM" is just silly. 
Have a bit of respect for the people who designed extension methods, 
implemented them in other languages, and use them extensively. They're 
not all idiots.

And for that matter, neither are Python programmers. Do we really 
believe that Python programmers are too dim witted to understand the 
concept of conditional attribute access?

"Descriptors, async, multiple inheritance, comprehensions, circular 
imports, import hooks, context managers, namespaces, threads, 
multiprocessing, generators, iterators, virtual subclasses, metaclasses, 
I can understand all of those, but *conditional attribute access* makes 
my brain explode!!!"

I don't believe it for a second.



>       Existing mechanisms like __getattribute__ are not parallel at all. 

I just demonstrated that it would be plausible to implement extension 
methods via `__getattr__`. Perhaps not efficiently enough to be useful, 
perhaps not quite with the semantics desired, but it could be done.



-- 
Steve
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@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/ODUPKFEMTZR4PHPY3GMZHCECASARGZ2H/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to