# refer to https://docs.python.org/3/howto/descriptor.html#descriptor-howto-guide
# KEY SUMMARIES:
# - A descriptor is what we call any object that defines __get__(), __set__(), or __delete__(). Descriptor is a protocol, like golang interface.
# Descriptor protocol:
# descr.__get__(self, obj, type=None) -> value
# descr.__set__(self, obj, value) -> None
# descr.__delete__(self, obj) -> None
#
# - Descriptors only work when used as class variables.
# - the core idea of descriptor is **Callback**, or **hook**.
# The main motivation for descriptors is to provide a hook allowing objects stored in class variables to control what happens during attribute lookup.
# 数据操作的控制权通过 descriptor 的 hook 转移给descriptor本身
import logging
logging.basicConfig(level=logging.INFO)
# LoggedAgeAccess is a descriptor
class LoggedAgeAccess:
# param `obj` means the instance of this descriptor's owner class
def __get__(self, obj, objtype=None):
value = obj._age
logging.info('Accessing %r giving %r', 'age', value)
return value
def __set__(self, obj, value):
logging.info('Updating %r to %r', 'age', value)
obj._age = value
# Person is the owner class of LoggedAgeAccess
class Person:
age = LoggedAgeAccess() # age is a Descriptor instance as a class attribute
def __init__(self, name, input_age):
self.name = name # name is regular instance attribute
# self.age is already a LoggedAgeAccess instance as a class attribute, and the input_age is assigned to it
self.age = input_age # This line actually makes a function call to `self.age.__set__(self, input_age)`
def growup(self):
self.age += 1 # Calls both __get__() and __set__()
# In the above classes,
# age is the public attribute of class Person,
# and _age is the private attribute of Person instance, which is created when LoggedAgeAccess.__set__ is called.
# When the public attribute is accessed, the descriptor logs the lookup or update.
def test_simple_descriptor_usage():
mary = Person('Mary M', 30)
# INFO:root:Updating 'age' to 30
# call vars() to look up the descriptor without triggering it
print(vars(mary))
# {'name': 'Mary M', '_age': 30} # The actual age data is in a private attribute: _age
mary.age
# INFO:root:Accessing 'age' giving 30
mary.growup()
# INFO:root:Accessing 'age' giving 30
# INFO:root:Updating 'age' to 31
# now let's go on how `__set_name__` is used
class LoggedAccess:
# __set_name__ is an optional method for descriptor
# __set_name__ will be called immediately when the owner class of this descriptor is defined
def __set_name__(self, owner, name):
print("owner is ", owner) # in this .py module it prints `owner is <class '__main__.PersonWithFullDescriptor'>`
self.public_name = name
self.private_name = '_' + name
def __get__(self, obj, objtype=None):
value = getattr(obj, self.private_name)
logging.info('Accessing %r giving %r', self.public_name, value)
return value
def __set__(self, obj, value):
logging.info('Updating %r to %r', self.public_name, value)
setattr(obj, self.private_name, value)
# When the PersonWithFullDescriptor class is defined,
# it makes a callback to __set_name__() in LoggedAccess,
# which sets each descriptor its own public_name and private_name
# and sets descriptor's private_name as a dynamic attribute of PersonWithFullDescriptor
class PersonWithFullDescriptor:
name = LoggedAccess() # First descriptor instance
age = LoggedAccess() # Second descriptor instance
def __init__(self, name, age):
# self.name and self.age are both class attribute
self.name = name # Calls the first descriptor
self.age = age # Calls the second descriptor
def growup(self):
self.age += 1
def test_descriptor__set_name__usage():
print(vars(PersonWithFullDescriptor))
# {
# '__module__': '__main__',
# 'name': < __main__.LoggedAccess object at 0x7f3d9dafe3d0 > ,
# 'age': < __main__.LoggedAccess object at 0x7f3d9dafe2b0 > ,
# ...
# }
print(vars(vars(PersonWithFullDescriptor)['name'])) # call vars() to look up the descriptor without triggering it
# {'public_name': 'name', 'private_name': '_name'}
mary = PersonWithFullDescriptor("mary", 10)
# INFO:root:Updating 'name' to 'mary'
# INFO:root:Updating 'age' to 10
print(vars(mary))
# {'_name': 'mary', '_age': 10}
print(mary.age) # access class attribute `age` to access instance private attribute `_age`
# INFO:root:Accessing 'age' giving 10
# 10
if __name__ == "__main__":
# test_simple_descriptor_usage()
test_descriptor__set_name__usage()
# more practical example
# refer to https://docs.python.org/3/howto/descriptor.html#complete-practical-example
网友评论