Skip to content

Latest commit

 

History

History
200 lines (147 loc) · 3.65 KB

descriptors.md

File metadata and controls

200 lines (147 loc) · 3.65 KB

Extended doctests for descriptors

>>> from descriptors import *

>>> from itertools import count

>>> def info(obj):
...     "Print the object's type and public attributes."
...     print(type(obj).__name__)
...     for attr in dir(obj):
...         if not attr.startswith('_'):
...             print('  -', attr)

>>> class Class:
...     @lazyattr
...     def number(self, *, numbers=count(1)):
...         n = next(numbers)
...         print(self, '->', n)
...         return n

>>> info(Class)
type
  - number

>>> Class.number
<descriptors.lazyattr object at ...>

>>> Class.number()
Traceback (most recent call last):
    ...
TypeError: 'lazyattr' object is not callable

The lazyattr appears in the instance's dir() even before it has been evaluated.

>>> obj = Class()

>>> vars(obj)
{}
>>> info(obj)
Class
  - number

When the attribute is first accessed, the wrapped method is called, and the returned value is set on the instance.

>>> obj.number
<__main__.Class object at ...> -> 1
1

>>> vars(obj)
{'number': 1}
>>> info(obj)
Class
  - number

Since the lazyattr is a non-data descriptor, subsequent accesses of the same attribute simply return the instance attribute, and the method is not called again, unless the instance attribute is deleted.

>>> obj.number
1

>>> vars(obj)
{'number': 1}
>>> info(obj)
Class
  - number

>>> del obj.number

>>> vars(obj)
{}
>>> info(obj)
Class
  - number

>>> obj.number
<__main__.Class object at ...> -> 2
2

>>> vars(obj)
{'number': 2}
>>> info(obj)
Class
  - number

lazyattrs can be patched like normal attributes.

>>> obj.number = 'ayy'
>>> vars(obj)
{'number': 'ayy'}

>>> del obj.number
>>> vars(obj)
{}

>>> obj.number
<__main__.Class object at ...> -> 3
3
>>> vars(obj)
{'number': 3}

TODO: handle aliases better:

>>> Class.alias = Class.number

>>> info(Class)
type
  - alias
  - number

>>> vars(obj)
{'number': 3}
>>> info(obj)
Class
  - alias
  - number

>>> obj.alias
<__main__.Class object at ...> -> 4
4

>>> vars(obj)
{'number': 4}
>>> info(obj)
Class
  - alias
  - number

Comparison with property:

>>> class Class:
...     @property
...     def number(self, *, numbers=count(1)):
...         n = next(numbers)
...         print(self, '->', n)
...         return n

>>> Class.number
<property object at ...>

>>> Class.number()
Traceback (most recent call last):
    ...
TypeError: 'property' object is not callable

>>> obj = Class()

>>> info(obj)
Class
  - number

>>> obj.number
<__main__.Class object at ...> -> 1
1
>>> obj.number
<__main__.Class object at ...> -> 2
2

Comparison with regular method:

>>> class Class:
...     def number(self, *, numbers=count(1)):
...         n = next(numbers)
...         print(self, '->', n)
...         return n

>>> Class.number
<function Class.number at ...>

>>> Class.number()
Traceback (most recent call last):
    ...
TypeError: number() missing 1 required positional argument: 'self'

>>> obj = Class()

>>> info(obj)
Class
  - number

>>> obj.number
<bound method Class.number of <__main__.Class object at ...>>

>>> obj.number()
<__main__.Class object at ...> -> 1
1
>>> obj.number()
<__main__.Class object at ...> -> 2
2