Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation incorrectly states how descriptors are invoked #75916

Closed
PaulPinterits mannequin opened this issue Oct 9, 2017 · 8 comments
Closed

Documentation incorrectly states how descriptors are invoked #75916

PaulPinterits mannequin opened this issue Oct 9, 2017 · 8 comments
Assignees
Labels
3.9 only security fixes 3.10 only security fixes 3.11 only security fixes docs Documentation in the Doc dir

Comments

@PaulPinterits
Copy link
Mannequin

PaulPinterits mannequin commented Oct 9, 2017

BPO 31735
Nosy @rhettinger, @cryvate, @iritkatriel

Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

Show more details

GitHub fields:

assignee = 'https://github.com/rhettinger'
closed_at = <Date 2021-12-04.01:44:12.434>
created_at = <Date 2017-10-09.15:04:44.804>
labels = ['3.11', 'invalid', '3.9', '3.10', 'docs']
title = 'Documentation incorrectly states how descriptors are invoked'
updated_at = <Date 2021-12-04.01:44:12.433>
user = 'https://bugs.python.org/PaulPinterits'

bugs.python.org fields:

activity = <Date 2021-12-04.01:44:12.433>
actor = 'rhettinger'
assignee = 'rhettinger'
closed = True
closed_date = <Date 2021-12-04.01:44:12.434>
closer = 'rhettinger'
components = ['Documentation']
creation = <Date 2017-10-09.15:04:44.804>
creator = 'Paul Pinterits'
dependencies = []
files = []
hgrepos = []
issue_num = 31735
keywords = []
message_count = 8.0
messages = ['303968', '303978', '303984', '303986', '304021', '304022', '407603', '407623']
nosy_count = 5.0
nosy_names = ['rhettinger', 'docs@python', 'Paul Pinterits', 'cryvate', 'iritkatriel']
pr_nums = []
priority = 'normal'
resolution = 'not a bug'
stage = 'resolved'
status = 'closed'
superseder = None
type = None
url = 'https://bugs.python.org/issue31735'
versions = ['Python 3.9', 'Python 3.10', 'Python 3.11']

@PaulPinterits
Copy link
Mannequin Author

PaulPinterits mannequin commented Oct 9, 2017

The descriptor howto states:

"For example, obj.d looks up d in the dictionary of obj. If d defines the method __get__(), then d.__get__(obj) is invoked [...]"

This is not true - the descriptor obtained from obj's dictionary is never invoked. If it was, the following two snippets would produce output:

class Class:
    pass

obj = Class()
obj.__dict__['d'] = property(lambda: print('called'))

_ = obj.d  # nothing is printed.


class Obj:
    @property
    def d(self):
        print('called')

_ = Obj.d  # nothing is printed.

@PaulPinterits PaulPinterits mannequin assigned docspython Oct 9, 2017
@PaulPinterits PaulPinterits mannequin added 3.7 (EOL) end of life 3.8 only security fixes docs Documentation in the Doc dir labels Oct 9, 2017
@Cryvate
Copy link
Mannequin

Cryvate mannequin commented Oct 9, 2017

You get what you should get: when you print obj.d, Obj.d, you will get:

<property object at 0xDEADBEEF>

which is exactly what you expect:

  • in the first case, you assigned a property object to the dictionary at obj.__dict__, so that's what you get back when you run obj.d.
  • you defined a property on a class called d, and you get it when you run Obj.d

If you run print(Obj().d) you will get a TypeError: your lambda should read:

lambda self: print('called')

Properties should be added to the class not the instance, see https://stackoverflow.com/questions/1325673/how-to-add-property-to-a-class-dynamically and https://eev.ee/blog/2012/05/23/python-faq-descriptors/

@PaulPinterits
Copy link
Mannequin Author

PaulPinterits mannequin commented Oct 9, 2017

I'm aware that descriptors have to exist on the class in order to work. The point is that the documentation states "If d defines the method __get__(), then d.__get__(obj) is invoked" (where d is obj.d), which is simply not true.

@PaulPinterits
Copy link
Mannequin Author

PaulPinterits mannequin commented Oct 9, 2017

If we take this class:

class Obj:
    @property
    def d(self):
        print('called')

And we access Obj.d:

_ = Obj.d

According to the docs, the following should happen:

# obj.d looks up d in the dictionary of obj
d = Obj.__dict__['d']

# If d defines the method __get__(),
if hasattr(d, '__get__'):

    # then d.__get__(obj) is invoked
    d.__get__(Obj)

We know this doesn't happen because nothing is printed to stdout.

@Cryvate
Copy link
Mannequin

Cryvate mannequin commented Oct 10, 2017

"We know this doesn't happen because nothing is printed to stdout."

Try running Obj().d, you will get output.

Obj.d does not work because it is on a *class*, and so it runs, per the docs:

'Obj.__dict__['d'].__get__(None, Obj)'

whereas you consider running it on an instance to get:

b = Obj()
b.d
# equivalent to
type(b).__dict__['d'].__get__(b, type(b))

and you will get output twice.

[Note, on python2 you will get an error, I think this is because your class does not inherit from object.]

@Cryvate
Copy link
Mannequin

Cryvate mannequin commented Oct 10, 2017

I do think though that

"If d defines the method __get__(), then d.__get__(obj) is invoked according to the precedence rules listed below."

seems to contain a mistake in that it should have

d.__get__(obj, type(obj)) instead of d.__get__(obj)

@iritkatriel
Copy link
Member

See also bpo-20751

@rhettinger rhettinger assigned rhettinger and unassigned docspython Dec 3, 2021
@iritkatriel iritkatriel added 3.9 only security fixes 3.10 only security fixes 3.11 only security fixes and removed 3.7 (EOL) end of life 3.8 only security fixes labels Dec 3, 2021
@rhettinger
Copy link
Contributor

it should have

d.__get__(obj, type(obj)) instead of d.__get__(obj)

The objtype argument is optional as shown in all of the examples. The call from object.__getattribute__() always passes in both parameters, even though only the first is required.

################################################################
\# Demonstration of \_\_get__() being called with one or two params
    class A:
        def __init__(self, x):
            self.x = x
        def m(self, y):
            return self.x * y
    >>> a = A(10)
    >>> a.m(5)
    50
    >>> vars(A)['m'].__get__(a)(5)     # objtype is not required
    50
    >>> vars(A)['m'].__get__(a, A)(5)  # objtype may be used
    50
################################################################
# Demonstration of object.__getattribute__ supplying both args
    class Desc:
        def __get__(self, *args):
            return args
      
    class B:
        z = Desc()
     
    >>> b = B()
    >>> b.z
    (<__main__.B object at 0x109156110>, <class '__main__.B'>)

@ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.9 only security fixes 3.10 only security fixes 3.11 only security fixes docs Documentation in the Doc dir
Projects
None yet
Development

No branches or pull requests

2 participants