from urllib.request import urlopen
import warnings
import os
import json

URL = 'http://www.oreilly.com/pub/sc/osconfeed'
JSON = '/home/kaka/osconfeed.json'

def load():
    if not os.path.exists(JSON):
        msg = 'downloading {} to {}'.format(URL, JSON)
        warnings.warn(msg)
        with urlopen(URL) as remote, open(JSON, 'wb') as local: # with 语句使用两个上下文管理器，用来读取和保存远程文件
            local.write(remote.read())
    with open(JSON) as fp: # 解析 JSON 文件，返回 Python 原生对象，这里数据有下面几种类型：dict, list, str, int
        return json.load(fp)
load()

feed = load()
sorted(feed['Schedule'].keys())

for key, value in sorted(feed['Schedule'].items()):
    print('{:3} {}'.format(len(value), key)) # 显示各个集合中记录数量

feed['Schedule']['speakers'][-1]['name'] # 深入嵌套的字典和列表，获取最后一个演讲者名字

feed['Schedule']['speakers'][-1]['serial'] # 演讲者编号

feed['Schedule']['events'][40]['name']

feed['Schedule']['events'][40]['speakers'] # 每个事件都有一个 'speakers' 字段，列出 0 个或多个演讲者编号

"""
Explanation: Python 中，数据的属性和处理数据的方法统称属性（attribute）。其实，方法只是可调用的属性。除了这二者之外，我们还可以创建特性（property），在不改变类接口的前提下，使用存取方法（即读值方法和设值方法）修改数据属性。
除了特性，Python 还提供了丰富的 API，用于控制属性的访问权限，以及实现动态属性。使用点号访问属性时，Python 会调用特殊的方法（如 __getattr__ 和 __setattr__）计算属性。用户自定义的类可以通过 __getattr__ 方法实现 “虚拟属性”，当访问不存在的属性时，即时计算属性值
动态创建属性是一种元变成，框架作者经常这么做，在 Python 中，这种技术很简单，任何人都可以使用，甚至在日常数据转换任务都能用到
使用动态属性转换数据
我们编写个脚本下载 OSCON 数据源，这是一份 JSON 数据，我们后面来解析它：
End of explanation
"""


from collections import abc

class FrozenJSON:
    
    def __init__(self, mapping):
        # 这么做有两个目的，首先确保传入的是字典，其次为了创建一个副本
        self.__data = dict(mapping)
    
    def __getattr__(self, name): # 仅当没有 name 属性时才调用此方法(通常在属性查找找不到时候调用)
        if hasattr(self.__data, name):
            return getattr(self.__data, name) # 如果 name 是 `__data` 的属性，返回那个属性，例如字典自带的 keys 属性
        else:
            # 否则，从 self.__data 中获取 name 键对应的元素，返回调用 FrozenJSON.build() 方法得到的结果
            return FrozenJSON.build(self.__data[name]) 
    
    @classmethod 
    def build(cls, obj): # 这是一个备选构造方法，classmethod 装饰器经常这么用
        if isinstance(obj, abc.Mapping): # 如果 obj 是映射，那么构造一个 FrozenJSON 对象
            return cls(obj)
        # 如果是 MutableSequence，必然是列表。因此我们把 obj 中的每个元素递归传给 .build() 方法，构建一个列表
        elif isinstance(obj, abc.MutableSequence):
            return [cls.build(item) for item in obj]
        else:
            # 既不是字典又不是列表，原封不动返回
            return obj

"""
Explanation: 使用动态属性访问 JSON 类数据
上面这种 feed['Schedule']['events'][40]['name']  语法太长，在 JavaScript 中可以使用 feed.Schedule.events[40].name 获取那个值，在 Python 中可以实现一个近似字典的类（网上有很多）达到同样效果，这里我们自己实现一个 FrozenJSON 类，比大多数实现都简单，因为只支持读取，即只能访问数据，不过，这个类能递归，自动处理嵌套的映射和列表
End of explanation
"""


raw_feed = load()
feed = FrozenJSON(raw_feed)
len(feed.Schedule.speakers)

sorted(feed.Schedule.keys())

for key, value in sorted(feed.Schedule.items()):
    print('{:3} {}'.format(len(value), key))

feed.Schedule.speakers[-1].name

talk = feed.Schedule.events[40]
type(talk)

talk.name

talk.speakers

talk.flavor # 读取不存在的实例抛出 KeyError 异常，而不是通常的 AttributeError 异常

"""
Explanation: FrozenJSON 中，尝试获取其它属性会出发解释器调用 __getattr__ 方法，这个方法首先查看 self.__data 有没有指定属性名（而不是键），这样 FrozenJSON 实例便可以处理字典的所有方法，例如把 items 方法委托给 self.__data.items() 方法。如果 self.__data 没有指定名称属性，那么 __getattr__ 方法以那个名称为键，从 self.__data 中获取一个元素，传给 FrozenJSON.build 方法。这样就能深入 JSON 数据的嵌套结构，使用类方法 build 把每一层嵌套转成一个 FrozenJSON 实例，我们没有缓存或转换原始数据，在迭代数据过程中，嵌套的数据结构不断被转成 FrozenJSON 对象，这么做没问题，因为我们的数据量不大
End of explanation
"""


from collections import abc
import keyword

class FrozenJSON:
    
    def __init__(self, mapping):
        self.__data = {}
        for key, value in mapping.items():
            if keyword.iskeyword(key): # 判断是不是保留字
                key += '_'
            self.__data[key] = value
    def __getattr__(self, name): 
        if hasattr(self.__data, name):
            return getattr(self.__data, name)
        else:
            return FrozenJSON.build(self.__data[name]) 
    
    @classmethod 
    def build(cls, obj): 
        if isinstance(obj, abc.Mapping): 
            return cls(obj)
        elif isinstance(obj, abc.MutableSequence):
            return [cls.build(item) for item in obj]
        else:
            return obj

grad = FrozenJSON({'name': 'Jim Bo', 'class': 1982})
grad.class_

"""
Explanation: 处理无效属性名
FrozenJSON 有个缺陷，没有对名称为 Python 关键字的属性做特殊处理。比如说像下面这样构建一个对象：
grad = FrozenJSON({'name': 'Jim Bo', 'class': 1982})
此时无法读取 grad.class 的值，因为 Python 中 class 是保留关键字
File "&lt;ipython-input-29-742ecb9642c1&gt;", line 2
    grad.class
             ^
SyntaxError: invalid syntax
当然，也可以这么做：
```
getattr(grad, 'class')
1982
```
但是， FrozenJSON 类的目的是为了便于访问数据，因此更好的方法是检查传给 FrozenJSON.__init__ 方法的映射中是否有关键字名称，如果有在键名后加上 _，然后通过下面方式读取
grad.class_
End of explanation
"""


def object_maker(the_class, some_arg):
    new_object = the_class.__new__(some_arg)
    if isinstance(new_object, the_class):
        the_class.__init__(new_object, some_arg)
    return new_object

# 下面两个语句作用基本相同
# x = Foo('bar')
# x = object_maker(Foo, 'bar')

"""
Explanation: 如果 JSON 对象中的键不是有效 Python 标识符，也会遇到类似问题
```
grad.2be
File "<ipython-input-50-1081492d1dd5>", line 3
    grad.2be
         ^
SyntaxError: invalid syntax
```
这个问题在 Python 3 中很好解决，可以用 str 提供的 s.isidentifier() 方法能根据语言的语法判断 s 是否为有效的 Python 标识符。但是，把无效的标识符变成有效的属性名不容易，对此，有两个很简单的解决办法，一个是抛出异常，另一个是把无效的键转成通用名称，例如 attr_0，attr_1 等等。这里不做演示
使用 __new__ 方法以灵活的方式创建对象
我们通常把 __init__ 称为构造方法，这是从其他语言借鉴的术语，其实用于构建示例的特殊方法是 __new__，这是个类方法（使用特殊方式处理，因此不用加 @classmethod 装饰器），必须返回一个实例。返回的实例会作为第一个参数（即 self）传给 __init__ 方法。因为调用 __init__ 方法时要传入实例，而且禁止返回任何值，所以 __init__ 其实就是初始化方法。真正的构造方法是 __new__。我们几乎不需要自己编写 __new__ 方法，因为从 object 类继承的实现就够用了
刚才说明的过程是 __new__ 方法到 __init__ 方法，是最常见的，但不是唯一的。__new__ 方法也可以返回其他类的实例，此时解释器不会调用 __init__ 方法
也就是说。Python 构建对象的过程可以使用下面伪代码概括:
End of explanation
"""


from collections import abc
import keyword

class FrozenJSON:
    
    def __new__(cls, arg): # 这是类方法，第一个参数是类本身，余下的参数与 `__init__` 方法一样，只不过没有 self
        if isinstance(arg, abc.Mapping):
            # 默认行为是委托给超类的 __new__` 方法。这里调用的是 object 基类的 __new__ 方法，把唯一的参数设为 FrozenJSON
            return super().__new__(cls)
        elif isinstance(arg, abc.MutableSequence):
            return [cls(item) for item in arg]
        else:
            return arg
        
    def __init__(self, mapping):
        self.__data = {}
        for key, value in mapping.items():
            if keyword.iskeyword(key): # 判断是不是保留字
                key += '_'
            self.__data[key] = value
    def __getattr__(self, name): 
        if hasattr(self.__data, name):
            return getattr(self.__data, name)
        else:
            return FrozenJSON(self.__data[name])  # 之前调用的是 build 方法，现在直接调用构造方法

"""
Explanation: 使用 __new__ 代替 build 方法：
End of explanation
"""


import warnings

DB_NAME = '/home/kaka/schedule1_db'
CONFERENCE = 'conference.115'

class Record:
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs) # 关键字参数构建实例属性的简便方式
        
        
def load_db(db):
    raw_data = load()
    warnings.warn('loading ' + DB_NAME)
    for collection, rec_list in raw_data['Schedule'].items():
        record_type = collection[:-1] # 去掉尾部的 's'，例如 events 变成 event
        for record in rec_list:
            key = '{}.{}'.format(record_type, record['serial'])
            record['serial'] = key
            db[key] = Record(**record)
            

import shelve
db = shelve.open(DB_NAME) 
if CONFERENCE not in db:
    load_db(db)
    
speaker = db['speaker.3471']
type(speaker)

speaker.name, speaker.twitter

db.close()

"""
Explanation: OSCON 的 JSON 数据源有一个明显的缺点:索引为 40 的事件,即名为'There Will Be Bugs' 的那个,有两位演讲者,3471 和 5199,但却不容易找到他们,因为提供的是编号,而 Schedule.speakers 列表没使用编号建立索引。此外,每条事件记录中都有 venue_serial字段,存储的值也是编号,但是如果想找到对应的记录,那就要线性搜索 Schedule.venues列表
使用 shelve 模块调整 OSCON 数据源结构
标准库中有个 shelve（架子）模块，如果你知道 pickle（泡菜）模块是 Python 对象序列化格式的名字，你就明白 shelve 命令的原因，泡菜坛子摆在架子上，因此 shelve 模块提供了 pickle 存储方式
shelve.open 高阶函数返回一个 shelve.Shelf 实例，这是简单的键值对象数据库，背后由 dbm 模块支持，具有下面特点


shelve.Shelf 是 abc.MutableMapping 的子类，因此提供了处理映射类型的重要方法


shelve.Shelf 还提供了几个管理 I/O 的方法，如 sync 和 close；它也是一个上下文管理器


只要把新值赋予键，就会保存键和值


键必须是字符串


值必须是 pickle 模块能处理的对象


shelve 模块为识别 OSCON 的日程数据提供了一种简单有效的方式。我们将从 JSON 文件中读取所有记录,将其存在一个 shelve.Shelf 对象中,键由记录类型和编号组成(例如,'event.33950' 或 'speaker.3471'),而值是我们即将定义的 Record 类的实例
End of explanation
"""


import warnings 
import inspect # load_db 函数中用

DB_NAME = '/home/kaka/schedule2_db' # 因为要存储几个不同类的实例，所以常见使用不同的数据库文件
CONFERENCE = 'conference.115'

class Record:
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)  # 动态添加属性
    def __eq__(self, other): # 测试时候很有用
        if isinstance(other, Record):
            return self.__dict__ == other.__dict__
        else:
            return NotImplemented
        
class MissingDatabaseError(RuntimeError):
    '''需要数据库但没有指定数据库时抛出''' # 自定义异常通常是标志类，没有定义体
    
class DbRecord(Record):
    
    __db = None # 存储打开的 shelve.Shelf 数据库引用
   
    @staticmethod # 静态方法，以此强调不管调用多少次，效果始终一样
    def set_db(db):
        DbRecord.__db = db
        
    @staticmethod
    def get_db():
        return DbRecord.__db
    
    @classmethod 
    def fetch(cls, ident): #类方法，因此在子类中易于定制它的行为
        db = cls.get_db()
        try:
            return db[ident]
        except TypeError:
            if db is None:
                msg = 'database not set; call "{}.set_db(my_db)"'
                raise MissingDatabaseError(msg.format(cls.__name__))
            else:
                raise # 如果数据库已经打开，我们不知道怎么处理此异常，继续抛出
                
    def __repr__(self):
        if hasattr(self, 'serial'):
            cls_name = self.__class__.__name__
            return '<{} serial = {!r}>'.format(cls_name, self.serial)
        else:
            return super().__repr__()
        
class Event(DbRecord):
    @property #可以当属性访问
    def venue(self):
        key = 'venue.{}'.format(self.venue_serial)
        # 传给 DbRecord 类的 fetch 类方法
        #不直接用　self.fetch(key)　的原因是为了防止在事件记录中有 'fetch' 键
        # 那么在事件记录中获取的是 fetch 字段的值，而不是 event 继承自 DbRecord 的 fetch 类方法
        return self.__class__.fetch(key) 
    @property
    def speakers(self):
        if not hasattr(self, '_speaker_objs'):
            # 没有 _speaker_objs 属性就直接从 __dict__ 实例属性中获取 speakers 属性的值，
            # 防止无限递归，因为这个特性公开名称也是 speakers
            spkr_serials = self.__dict__['speakers']
            fetch = self.__class__.fetch # 获取 fetch 类方法的引用
            self._speaker_objs = [fetch('speaker.{}'.format(key))
                                  for key in spkr_serials]
            return self._speaker_objs
        
    def __repr__(self):
        if hasattr(self, 'name'): # 如果记录有 name 属性，在字符串使用
            cls_name = self.__class__.__name__
            return '<{} {!r}>'.format(cls_name, self.name)
        else:
            return super().__repr__()
        
def load_db(db):
    raw_data = load()
    warnings.warn('loading ' + DB_NAME)
    for collection, rec_list in raw_data['Schedule'].items():
        record_type = collection[:-1] # 去掉尾部的 's'，例如 events 变成 event
        cls_name = record_type.capitalize() # 把 record_type 变量首字母变大写
        cls = globals().get(cls_name, DbRecord) # 从模块全局作用域获取那个名称对应的对象，找不到使用 DbRecord
        if inspect.isclass(cls) and issubclass(cls, DbRecord): # 如果获取的对象是类,而且是 DbRecord 的子类
            factory = cls
        else:
            factory = DbRecord
        
        for record in rec_list:
            key = '{}.{}'.format(record_type, record['serial'])
            record['serial'] = key
            # 存储在数据库中的对象由 factory 构建,factory 可能是
            # DbRecord 类,也可能是根据 record_type 的值确定的某个子类。
            db[key] = factory(**record) 


DbRecord.set_db(db) #db 为 /home/kaka/schedule2_db
event = DbRecord.fetch('event.33950')
event

event.venue

event.venue.name

for spkr in event.speakers:
    print('{0.serial}: {0.name}'.format(spkr))

"""
Explanation: 在 Python 中至少有两个类与 Record 类似，我们这里自己写是为了说明一个重要的做法：在 __init__ 方法中更新实例的 __dict__ 属性
像上面那样调整日程数据集后，我们可以扩展 Record 类，让它提供一个有用的服务：自动获取 event 记录引用的 venue 和 speaker 记录。这与 Django ORM 访问 models.ForeignKey 字段所做的事类似：得到的不是键，而是链接的模型对象
使用特性获取链接的记录
对于从 Shelf 对象中获取的 event 记录来说，读取它的 venue 或 speakers 属性返回的不是编号，而是完整记录对象
End of explanation
"""


class LineItem:
    
    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price
        
    def subtotal(self):
        return self.weight * self.price

"""
Explanation: LineItem 类第一版，表示订单中商品的类
假设有个销售有机食物的电商应用，客户可以按照重量订购坚果，干果和杂粮。在这个系统中，每个订单都有一系列商品，每个商品可以用下面的类表示
End of explanation
"""


raisins = LineItem('Golden raisins', 10, 6.95)
raisins.subtotal()

raisins.weight = -20
raisins.subtotal()

"""
Explanation: 这个类设计的太精简了，有很多问题，例如：
End of explanation
"""


class LineItem:
    
    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price
        
    def subtotal(self):
        return self.weight * self.price
    
    @property # 实现读值方法
    def weight(self):
        return self.__weight # 真正的值存到私有属性 __weight 中
    
    @weight.setter #这个属性也是装饰器，这个装饰器把读值方法和设值方法绑定在一起
    def weight(self, value):
        if value > 0: # 避免值为负
            self.__weight = value
        else:
            raise ValueError('value must be > 0')

wlanuts = LineItem('wlanuts', 0, 10.00) # 现在不能设值负的重量

"""
Explanation: 看到重量竟然能变成负的，我们可以修改 LineItem 类的接口，使用读值的方法和设值方法管理 weight 属性，但是，如果能直接设定商品 weight 属性，显得更加自然，此外，系统可能在生产环境中，其他buff已经直接访问 item.weight 了。此时，符合 Python 风格做法是，把数据属性换成特性
LineItem 类第二版，能验证值的特性
实现特性后，我们可以使用读值和设值的方法，但是 LineItem 类的接口保持不变（即，设置 LineItem 对象的 weight 仍然写成 raisins.weight = 12)
End of explanation
"""


class LineItem:
    
    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price
        
    def subtotal(self):
        return self.weight * self.price
    
    def get_weight(self):
        return self.__weight # 真正的值存到私有属性 __weight 中
    
    def set_weight(self, value):
        if value > 0: # 避免值为负
            self.__weight = value
        else:
            raise ValueError('value must be > 0')
    
    weight = property(get_weight, set_weight) # 构建 property 对象，然后赋值给公开的类属性

"""
Explanation: 现在我们禁止用户为 weight 属性提供负值或 0，但是工作人员可能会犯错，把金额设成负的，为了防止这种情况，我们可以把 price 属性也变成特性，但是这样我们的 Python 代码就存在一些重复
Paul Graham 有句名言，“当我在自己的程序中发现用到了模式，我觉得这就表明某个地方出错了”，去除重复的方法是抽象，抽象特性的定义有两种方式：使用特性工厂函数，或者用描述符类。后者更灵活，下章会讨论。其实，特性本身就是使用描述符类实现的。不过这里我们要继续讨论特性，实现一个特性工厂函数
特性全解析
虽然内置的 property 经常用作装饰器，但它其实是一个类。在 Python 中，函数和类通常可以互换，因为二者都是可调用的对象，而且没有实例化对象的 new 运算符，所以调用构造方法和调用工厂函数没有区别。此外，只要能返回新的可调用对象，代替被装饰的函数，二者都可以用作装饰器
property 构造方法完整签名如下
property(fget=None, fset=None, fdel=None, doc=None) -&gt; property attribute
所有参数都是可选的，如果没有把函数传给某个参数，那么特性对象就不允许执行相应操作。
不适用装饰器定义特性的 “经典” 语法如下：
End of explanation
"""


class Class:
    data = 'the class data attr'
    @property
    def prop(self):
        return 'the prop value'\
    
    
obj = Class()
vars(obj) # 返回 obj 的 __dict__ 属性,表明没有实例属性

obj.data

obj.data = 'bar'
vars(obj)

obj.data # 遮盖类属性

Class.data

"""
Explanation: 某些情况下，这种经典形式比装饰器语法好，稍后讨论的特性工厂函数就是一个例子。但是如果方法特别多的话，使用装饰器，一眼就能看出来哪些是读值方法，哪些是设置方法，很方便
特性会覆盖实例属性
如果实例和所属的类有同名数据属性，那么实例属性会覆盖（或遮盖）类属性
End of explanation
"""


Class.prop

obj.prop

obj.prop = 'foo'

obj.__dict__['prop'] = 'foo'
vars(obj)

obj.prop # 仍然运行特征读值的方法，特性没有被实例属性覆盖

Class.prop = 'baz' # 覆盖 Class.prop 特性，销毁特征对象
obj.prop # 现在获取的是实力属性，Class.prop 不是特性了，因此不会覆盖 obj.prop

"""
Explanation: 下面尝试覆盖 obj 实例的 prop 属性
End of explanation
"""


obj.data

Class.data

Class.data = property(lambda self: 'the "data" prop value') # 用新特性覆盖 Class.data
obj.data

del Class.data
obj.data

"""
Explanation: 下面为 Class 类添加一个新特性，覆盖实例属性
End of explanation
"""


class Foo:
    @property
    def bar(self):
        '''The bar attribute'''
        return self.__dict__['bar']
    
    @bar.setter
    def bar(self, value):
        self.__dict__['bar'] = value

help(Foo)

help(Foo.bar)

"""
Explanation: 本节主要观点是，obj.attr 这样的表达式不会从 obj 开始寻找 attr，而是从 obj.__class__ 开始，而且，仅当类中没有名为 attr 特性时，Python 才会从 obj 实例寻找。这条规则不仅适用与特性，还适用与一整类描述符 -- 覆盖型描述符，下张慧进一步讨论
特性的文档
控制台中 help() 函数或 IDE 等工具要显示特性的文档时，会从特性的 __doc__ 属性中提取信息。如果是经典调用语法，为 property 对象设置文档字符串传入的是 doc 参数
weight = property(get_weight, set_weight, doc='weight in kilograms')
使用装饰器创建 property 对象时，读值方法（有 @property 装饰器的方法）的文档字符串作为一个整体，变成特性的文档
End of explanation
"""


def quantity(storage_name):
    
    def qty_getter(instance):
        return instance.__dict__[storage_name]
    
    def qty_setter(instance, value):
        if value > 0:
            instance.__dict__[storage_name] = value
        else:
            raise ValueError('value must be > 0')
            
    return property(qty_getter, qty_setter)

class LineItem:
    weight = quantity('weight') # 用工厂函数将自定义特性 weight 设为类属性
    price = quantity('price') # 记住，赋值语句的右面先计算，因此调用 quantity 时，price 属性还不存在
    
    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight # 这里，特性已经激活，确保 weight 不能为负数或 0
        self.price = price 
        
    def subtotal(self):
        return self.weight * self.price # 这里也用到了特性，使用特性获取实例中存储的值


nutmeg = LineItem('Moluccan nutmeg', 8, 13.95)
nutmeg.weight, nutmeg.price # 使用特性读取 weight 和 price，会遮盖同名实例属性

sorted(vars(nutmeg).items()) # 查看真正存储值的实例属性

"""
Explanation: 至此面我们介绍了特性的重要知识，下面回过头解决前面遇到的问题：保护 LineItem 对象的 weight 和 price 属性，只允许设置大于 0 的值，但是不允许定义两个几乎一样的读值和设值方法
定义一个特性工厂函数
我们将定义一个特性工厂函数，去这个名字是因为，在应用中要管理的属性表示不能为负数或 0 的量。下面是 LineItem 的简洁版，用到了 quantity 特性的两个实例：一个用于管理 weight 属性，一个用于管理 price 属性
End of explanation
"""


class BlackKnight:
    
    def __init__(self):
        self.members = ['an arm', 'another arm', 
                        'a leg', 'another leg']
        self.phrases = ["'Tis but a scratch.",
                        "It's just a flesh wound.",
                        "I'm invincible!",
                        "All right, we'll call it a draw."]
        
    @property
    def member(self):
        print('next member is:')
        return self.members[0]
    
    @member.deleter
    def member(self):
        text = 'BLACK KNIGHT (loses {})\n -- {}'
        print(text.format(self.members.pop(0), self.phrases.pop(0)))

knight = BlackKnight()
knight.member

del knight.member

del knight.member

del knight.member

"""
Explanation: 注意工厂函数构建的特性，weight 特性覆盖了 weight 实例属性，因此对 self.weight 或 nutmeg.weight 的每个引用都由特性函数处理，只有直接存取 __dict__ 属性才能跳过特性的处理逻辑
在真实的系统中，分散在多个类的多个字段可能要做同样的验证，此时最好把 quantity 工厂函数放到实用工具模块中，方便重复使用。最终可能重构那个简单的工厂函数，改成更易扩展的描述符类，然后使用专门的子类做不同的验证，在下章会这么做
处理属性删除操作
我们可以用 del 语句将对象属性删除
del my_object.an_attribute
其实，使用 Python 变编程时不常删除属性，通过特性删除属性更少见。但是，Python 支持这么做，我们可以虚构一个实例，演示一下:
End of explanation
"""


