流畅的Python(十九)-动态属性和特性

一、核心要义

在Python中,数据的属性和处理数据的方法,统称属性。方法,只是可调用的属性。除了这两者之外,我们还可以创建特性(property),在不改变类接口的前提下,使用存取方法(即读值方法和设值方法)修改数据属性。

二、代码示例

0、相关知识点

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/3/13 20:56
# @Author  : Maple
# @File    : 00-相关知识点.py
# @Software: PyCharm

class Animal:
    def __init__(self,name):
        self.name = name

    @property
    def run(self):
        print('Animal is running')


class Computer:
    def __init__(self,brand,price):
        self.brand = brand
        self.price = price

    @classmethod
    def computing(cls):
        print("I love computing")




if __name__ == '__main__':

    # 1.更新字典的值
    person= {"name":"Maple","age":19}
    person.update({"name":"Max","gender":"Maple"})
    print(person)

    # 2.类的__dict__中存放着其属性
    a = Animal('Dog')
    ## 初次查看a的属性
    print(a.__dict__) # {'name': 'Dog'}
    ## 通过更新__dict__,可以给类增加属性
    a.__dict__.update({"age":10})
    ## 查看更新后的a包含的属性
    print(a.__dict__) # {'name': 'Dog', 'age': 10}

    # 3.以访问属性的方式,调用方法
    ## 没有添加 property装饰器之前,通过如下方式调用run方法
    #a.run() # Animal is running

    ## 添加property装饰器之后,可以以引用属性的方式调用run方法
    a.run # Animal is running

    # 4.类相关的一些内置属性
    print('******4.类相关的一些内置属性********')
    print(Animal) #

    ##4-1. 类的名称
    print(Animal.__name__)

    ##4-2 类的类型:为type
    print(Animal.__class__) #
    print(Animal.__class__.__name__)  # type

    ##4-3对比实例的__class__
    print(a.__class__) #
    print(Animal) # 

    print(Animal.run) # 
    print(a.__class__.run) # 


    # 5. 通过实例调用类方法
    com = Computer('Apple', 20000)
    com.computing() # I love computing

    # 6.直接通过类调用类方法
    Computer.computing() # I love computing
    com.__class__.computing() # I love computing


    # 6.从模块的全局作用域中获取对象(包括类),如果获取不到给默认值
    ## 获取Animal的一个实例对象
    print(globals().get('a', Computer))  # <__main__.Animal object at 0x000001D5FB513DC0>
    ## 获取Animal类
    print(globals().get('Animal')) # 

    print(globals().get('Cat',Computer)) # 
    cc = globals().get('Cat', Computer)
    print(issubclass(cc,Computer)) #True

1、使用动态属性访问Json数据(1)

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/3/12 21:17
# @Author  : Maple
# @File    : 01-使用动态属性访问Json数据(1).py
# @Software: PyCharm


from collections import abc
from osconfeed import load


class FrozenJSON:

    def __init__(self,mapping):
        self.__data = dict(mapping)

    def __getattr__(self, item):
        if hasattr(self.__data,item):
            return self.__data[item]
        else:
            return FrozenJSON.build(self.__data[item])

    @classmethod
    def build(cls,obj):
        if isinstance(obj,abc.Mapping):
            # 如果是一个键值对类型,就直接返回一个FrozenJSON对象
            return cls(obj)

        # 如果是一个列表,就为列表中的每个元素创建一个FrozenJSON对象(其实是将JSON中的对象转换成FrozenJSON对象)
        elif isinstance(obj,abc.MutableSequence):
            return [cls(item) for item in obj]

        # 否则就返回对象本身
        else:return obj


if __name__ == '__main__':
    # 1. 获取原始Json对象:实际已转换为Python中的对象字典
    osconfeed  = load()
    print(type(osconfeed)) # 

    #2.JSON对象封装成 FrozenJSON
    print('**** 2-1.封装osconfeed**********')
    ## 2-1.此时f_json仅有一个属性__data,对应的values是osconfeed.json(转成字典格式)
    f_json = FrozenJSON(osconfeed)
    print('f_json:', f_json.__dict__)

    ## 2-2 访问f_json.schedule,因为f_json中并没有schedule属性
    print('**** 2-2.访问schedule**********')
    #      因此会调用 FrozenJSON.build(self.__data[item]),会返回一个FrozenJSON实例对象,且该实例属性名依然为__data,对应的value
    #      则是原json数据中schedule下对应的value
    f_json_schedule = f_json.schedule
    print('f_json_schedule:',f_json_schedule.__dict__)
    # print(f_json.schedule.conference[0].serial) # 115

    ## 2-3 继续访问schedule下的events,由于f_json_schedule中并没有events属性:
    #      因此会调用  FrozenJSON.build(self.__data[item]),由于events下是一个list,因此该方法也会返回一个FrozenJSON对象列表
    print('**** 2-3.访问schedule.events**********')
    FrozenJSON_events_list =  f_json_schedule.events
    for i,frozen_event in enumerate(FrozenJSON_events_list):
        # frozen_event仍然没有serial属性,因此会调用FrozenJSON.build(self.__data[item])
        # 然后 因为self.__data['serial']对应的值既不是Mapping又不是MutableSequence,因此返回值本身:即数字
        print('**2-4.访问schedule.events下的serial****')
        result = frozen_event.serial
        print('第{}个frozen_event下的serial对应的值是{}'.format(i,result))

(1) osconfeed.json数据

{
  "schedule": {
    "conference":[{"serial":115}],
    "events":[
      {"serial":4505,
        "name": "Go to the island",
        "event_type":"escape",
        "venue_serial": 1462,
        "speakers": [8890,8891]
      },
     {"serial":4506,
      "name": "To be my best",
      "event_type":"study",
       "venue_serial": 1463,
       "speakers": [8890,8891]
    }],

    "speakers": [
      {"serial":8890,
        "name": "Jacky",
        "age": 30
      },
      {"serial":8891,
        "name": "Tom",
        "age": 33
      }
    ],
    "venues":[
      {
        "serial":1462,
        "name":"F151",
        "category":"Conference Venues"
      },
      {
        "serial":1463,
        "name":"F152",
        "category":"Super Man Venues"
      }
    ]
  }
}

(2) load方法

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/3/12 21:27
# @Author  : Maple
# @File    : osconfeed.py
# @Software: PyCharm
import json

JSON = r"D:\01-study\python\fluent_python\19-动态属性和特性\data\osconfeed.json"

def load():
    with open(JSON) as f:
        # 返回Python对象
        return json.load(f)


if __name__ == '__main__':

    r = load()
    print(type(r)) # 
    print(r['schedule']['conference'])
    print(r)

2、使用动态属性访问Json数据(2)

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/3/16 20:30
# @Author  : Maple
# @File    : 01-使用动态属性访问Json数据(2).py
# @Software: PyCharm

"""改造FrozenJSON
使用__new__方法构造对象,代替build方法
"""

from collections import  abc
from keyword import iskeyword
from osconfeed import load


class FrozenJSON:

    def __new__(cls, arg):
        if isinstance(arg,abc.Mapping):
            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 iskeyword(key):
                key +='_'
            self.__data[key] = value


    def __getattr__(self, name):
        # 1-1如果实例中有某个对象,就直接返回属性对应的值
        # 2-1 比如去访问schedule属性,发现并没有
        """"
            "schedule": {
            "conference":[{"serial":115}],
            ...
        """
        if hasattr(self.__data,name):
            return self.__data[name]
        else:
            # 1-2否则就返回一个FrozenJSON对象
            # 1-3以下语法会直接去调用FrozenJSON.__new__方法,构建对象实例
            # 2-2 通过如下方式创建FrozenJSON对象,其中self.__data["schedule"]
            return FrozenJSON(self.__data[name])


if __name__ == '__main__':

    # 1.初始化
    osconfeed = load()
    """初始化流程分析
    1.首先走__new__方法,因为初始化参数是一个Mapping,所以会return super().__new__(cls),返回一个FrozenJSON实例化对象
    2.实例化对象传递到__init__方法,对其进行初始化:首先会生成一个实例属性__data,其value为空{}
    3.然后对其进行赋值,且其key为`schedule`,value为{'conference'.....}
    4.最终的结果是f_json有一个__data实例属性,然后其值为 {'schedule': {'conference': [{'serial': 115}],.....}
    """
    f_json = FrozenJSON(osconfeed)
    print(f_json.__dict__)

    # 2.访问f_json的schedule属性
    """访问schedule属性流程分析
    1.由于f_json中并没有 schedule 属性(只有__data属性),所以会走__getattr__的else逻辑
    2.通过FrozenJSON(self.__data[name])构造实例对象(走__new__和__init__逻辑),其中self.__data['schedule']为 {'conference': [{'serial': 115}],...}
    3.因此返回的f_json_schedule会有一个__data属性.而且其值为 {'conference': [{'serial': 115}],...},具体来说key为conference,value为[{"serial":115}]
    """
    f_json_schedule = f_json.schedule
    print(f_json_schedule)

    # 3.访问 f_json_schedule 的 conference 属性
    """访问schedule下的conference属性分析
    1.由于 f_json_schedule 中并没有 conference 属性(只有__data属性),所以会走__getattr__的else逻辑
    2.通过FrozenJSON(self.__data[name])构造实例对象列表(因为self.__data['conference']对应的value为list:[{'serial': 115}]
      走__new__和__init__逻辑),其中self.__data['conference']为 [{'serial': 115}]
    3.因此返回的f_json_schedule列表会有一个__data属性.而且其值为 {'serial': 115},具体来说key为 serial,value为115
    """
    f_json_schedule_conference = f_json_schedule.conference
    print(f_json_schedule_conference)

    # 4. 访问f_json_schedule_conference属性的serial属性
    """访问schedule.conference下的serial属性分析
    1.由于 f_json_schedule_conference 中并没有 serial 属性(只有__data属性),所以会走__getattr__的else逻辑
    2.通过FrozenJSON(self.__data[name])返回serial对应的值(因为self.__data['serial']对应的value为115,会直接返回值本身
    """
    result = f_json_schedule_conference[0].serial
    print(result) # 115

3、使用shelve模块访问Json

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/3/13 21:05
# @Author  : Maple
# @File    : 03-使用shelve模块访问Json.py
# @Software: PyCharm


"""
schedule2.py: traversing OSCON schedule data

    >>> import shelve
    >>> db = shelve.open(DB_NAME)
    >>> if CONFERENCE not in db: load_db(db)

# BEGIN SCHEDULE2_DEMO

    >>> DbRecord.set_db(db)  # <1>
    >>> event = DbRecord.fetch('event.33950')  # <2>
    >>> event  # <3>
    
    >>> event.venue  # <4>
    
    >>> event.venue.name  # <5>
    'Portland 251'
    >>> for spkr in event.speakers:  # <6>
    ...     print('{0.serial}: {0.name}'.format(spkr))
    ...
    speaker.3471: Anna Martelli Ravenscroft
    speaker.5199: Alex Martelli

# END SCHEDULE2_DEMO

    >>> db.close()

"""

# BEGIN SCHEDULE2_RECORD
import warnings
import inspect  # <1>

import osconfeed
import shelve

DB_NAME = 'data/schedule2_db'  # <2>
CONFERENCE = 'conference.115'


class Record:
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

    def __eq__(self, other):  # <3>
        if isinstance(other, Record):
            return self.__dict__ == other.__dict__
        else:
            return NotImplemented
# END SCHEDULE2_RECORD


# BEGIN SCHEDULE2_DBRECORD
class MissingDatabaseError(RuntimeError):
    """Raised when a database is required but was not set."""  # <1>


class DbRecord(Record):  # <2>

    __db = None  # <3>

    @staticmethod  # <4>
    def set_db(db):
        DbRecord.__db = db  # <5>

    @staticmethod  # <6>
    def get_db():
        return DbRecord.__db

    @classmethod  # <7>
    def fetch(cls, ident):
        db = cls.get_db()
        try:
            return db[ident]  # <8>
        except TypeError:
            if db is None:  # <9>
                msg = "database not set; call '{}.set_db(my_db)'"
                raise MissingDatabaseError(msg.format(cls.__name__))
            else:  # <10>
                raise

    def __repr__(self):
        # 如果DbRecord有serial属性,则返回对应serial对象的value值
        if hasattr(self, 'serial'):  # <11>
            # cls_name = 'DbRecord'
            cls_name = self.__class__.__name__
            return '<{} serial={!r}>'.format(cls_name, self.serial)
        else:
            return super().__repr__()  # <12>
# END SCHEDULE2_DBRECORD


# BEGIN SCHEDULE2_EVENT
class Event(DbRecord):  # <1>

    @property
    def venue(self):
        # 注意venue_serial是event数据中的一个key,里面记录了venue的serial id
        key = 'venue.{}'.format(self.venue_serial)
        # self.__class__获得是Event类,然后通过类调用类方法fetch(继承自DbRecord)
        # 为何不直接用self.fetch(key),因为如果Event中如果有一个属性名为fetch,那么self.fetch就直接获取`fetch`属性对应的值了
        # 而不是调用类的fetch方法: 返回db[key]
        return self.__class__.fetch(key)  # <2>

    @property
    def speakers(self):
        if not hasattr(self, '_speaker_objs'):  # <3>
            # event中有一个speaker属性,里面记录了该event对应的speakers id列表
            spkr_serials = self.__dict__['speakers']  # <4>
            fetch = self.__class__.fetch  # <5>
            # 返回db[speaker.xx]对应的值,并存放在_speaker_objs属性中
            self._speaker_objs = [fetch('speaker.{}'.format(key))
                                  for key in spkr_serials]  # <6>
        return self._speaker_objs  # <7>

    def __repr__(self):
        # 如果记录中name属性,则使用如何格式化方式展示数据
        if hasattr(self, 'name'):  # <8>
            cls_name = self.__class__.__name__
            return '<{} {!r}>'.format(cls_name, self.name)
        else:
            return super().__repr__()  # <9>
# END SCHEDULE2_EVENT


# BEGIN SCHEDULE2_LOAD
def load_db(db):
    raw_data = osconfeed.load()
    warnings.warn('loading ' + DB_NAME)
    for collection, rec_list in raw_data['schedule'].items():
        record_type = collection[:-1]  # <1>
        # 比如event变成Event,speakers变成Speakers
        cls_name = record_type.capitalize()  # <2>
        # 从模块的全局作用域中获取cls_name名字对应的对象(也有可能是类,比如Event);如果找不到对象,使用DbRecord
        cls = globals().get(cls_name, DbRecord)  # <3>
        #  如果获取的对象是类 并且是DbRecord类的子类
        if inspect.isclass(cls) and issubclass(cls, DbRecord):  # <4>
            factory = cls  # <5>
        else:
            factory = DbRecord  # <6>

        # rec_list的Sample data:
        """
           "events":[
             {"serial":4505,
             "name": "Go to the island",
             "event_type":"escape"
             },
             {"serial":4506,
             "name": "To be my best",
             "event_type":"study"
            }],

        """

        for record in rec_list:  # <7>
            # record的Sample data:
            """
             {"serial":4505,
             "name": "Go to the island",
             "event_type":"escape"
             },
            """
            # Key = Event.4505
            key = '{}.{}'.format(record_type, record['serial'])
            # record的serial属性值被替换
            """
              {"serial":Event.4505,
             "name": "Go to the island",
             "event_type":"escape"
             },
            """
            record['serial'] = key

            # 将新的record放到db中,具体的数据类型取决于 factory,而factory又取决于全局作用域中定义的类(本例中定义了Event类)
            ## 1.首先raw_data['Schedule']中有四个Key,其经过转换后(掐尾,以及首字母大写)分别变成:Conference,Event,Speaker,Venus
            ## 2.由于本文件中只定义了上面4个key中的一个类Event,因此只有对应的Event数据以Event类型数据存放,其它都是以DbRecord
            ##   类型存放
            db[key] = factory(**record)  # <8>
# END SCHEDULE2_LOAD

if __name__ == '__main__':

    #1. factory(**record)数据类型的验证
    cls1 = globals().get('Event', DbRecord)
    print(cls1) # 
    ## Speaker, Conference 和 Venus 类都未定义,所以factory都指向DbRecord
    cls2 = globals().get('Speaker', DbRecord)
    print(cls2) # 

    cls3 = globals().get('Conference', DbRecord)
    print(cls3) # 

    cls4 = globals().get('Venus', DbRecord)
    print(cls4) # 

    # 2.创建db:会在指定目录自动创建
    db = shelve.open(DB_NAME)

    # 3.将osconfeed.json数据加载到db中
    if CONFERENCE not in db:
        load_db(db)

    print(db)
    for key,value in db.items():
        print('Key:',key,' value:',value)
    """打印结果如下:
    # 说明conference.115对应的value为何是,首先是Event类中__repr__方法的定义,由于conferenc.115没有名字为name的属性
       因此会去调用DbRecord中的__repr__方法,而其返回的是:'<{} serial={!r}>'.format(cls_name, self.serial)
    Key: conference.115  value: 
    
    # event.4505对应的value,由于Event类中__repr__方法的定义,由于event.4505有名字为name的属性
       因此直接返回:  return '<{} {!r}>'.format(cls_name, self.name)
    Key: event.4505  value: 
    Key: event.4506  value: 
    
    # 同conference
    Key: speaker.8890  value: 
    Key: speaker.8891  value: 
    Key: venu.1462  value: 

    """

    # 4.将db赋值给 DbRecord
    DbRecord.set_db(db)

    # 5.获取db中key = event.4505对应的value

    event = DbRecord.fetch('event.4505')

    # 由于Event类中定义了__repr__方法,打印event是会调用该方法
    # return '<{} {!r}>'.format(cls_name, self.name)
    # 其中cls_name是类名,self.name是name关键字对应的Value
    print(event) # 

    ## 观察该event中的属性
    print(event.__dict__)# {'serial': 'event.4505', 'name': 'Go to the island', 'event_type': 'escape', 'venus_serial': 1462, 'speakers': [8890, 8891]}


    # 6.打印该event的中venue:由于给Event类中venue方法添加了property属性,所以能够像属性一样访问该方法
    print(event.venue)  # 
    print(event.venue.name) # F151

    #7.打印speakers
    for spkr in event.speakers:
        """
        speaker.8890:Jacky
        speaker.8891:Tom
        """
        print('{0.serial}:{0.name}'.format(spkr))

4、使用特性验证属性(1)

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/3/16 8:27
# @Author  : Maple
# @File    : 04-使用特性验证属性(1).py
# @Software: PyCharm


class LineItem:
    """需要对类的属性weight做一些限制(使用装饰器的方式实现-比较新的一种方式)
    >> 其值必须大0
    >> 可以通过特性的方式实现

    """
    def __init__(self,description,weight,price):
        self.description = description
        self.weight = weight
        self.price = price

    @property
    def weight(self):
        # weight属性的值真正存放于__weight中
        return self.__weight

    @weight.setter
    def weight(self,value):
        # weight的值必须大于0
        if value > 0:
            self.__weight = value
        else:
            raise ValueError('weight must be > 0')


    def subtotal(self):
        return self.weight * self.price


if __name__ == '__main__':

    # 1.观察类属性:注意LineItem有一个类属性,并且是一个特性property
    print(LineItem.__dict__) # 'weight': 

    # 2.查看LineItem类的属性
    item = LineItem('Bread',20,100)
    ## 注意观察,item的属性有一个是_LineItem__weight,表明weight属性值真正存放于__weight属性(实例属性)中
    print(item.__dict__) # {'description': 'Bread', '_LineItem__weight': 20, 'price': 100}

    # 3. 特性验证:weight是否能设置小于0的值
    # 以下代码会报错
    item2 = LineItem('Bread', -10, 100) # ValueError: weight must be > 0

5、使用特性验证属性(2)

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/3/16 8:38
# @Author  : Maple
# @File    : 05-使用特性验证属性(2).py
# @Software: PyCharm



class LineItem:
    """需要对类的属性weight做一些限制(使用经典方式实现-手动配置和装载property属性)
    >> 其值必须大0
    >> 可以通过特性的方式实现

    """
    def __init__(self,description,weight,price):
        self.description = description
        self.weight = weight
        self.price = price

    def subtotal(self):
        return self.weight * self.price


    # 方法名以get开头只是一种约定俗成
    def get_weight(self):
        return self.__weight

    # 方法名以set开头只是一种约定俗成
    def set_weitht(self,value):
        if value > 0:
            self.__weight = value
        else:
            raise ValueError('weight must be > 0')

    # 手动为weight添加特性
    # property 构造方法的完整签名: property(gfet_None,fset=None,fdel=None,doc= None)
    # 本案例构造了一个特性weight(注意与属性weight同名,此时就涉及到特性与属性优先级的问题,留待下节分解),且为该特性添加了
    #  get_weight和set_weitht方法,这样当通过obj.weight方式访问属性或者赋值时,就会分别走这两个函数的逻辑
    weight = property(get_weight,set_weitht)


if __name__ == '__main__':


    # 1. 特性验证:weight是否能设置小于0的值
    # 以下代码会报错
    try:
        item1 = LineItem('Bread', -10, 100)  # ValueError: weight must be > 0
    except ValueError as e:
        print(e)

    # 2.特性验证: 是否能够将weight的值修改为复数
    item2 = LineItem('Milk', 10, 200)
    print(item2.weight) # 10

    # 2-1 修改weight的值为另外一个大于0的值
    item2.weight = 20
    print(item2.weight) # 20

    # 2-2 试图修改weight的值为复数,会报错
    try:
        item2.weight = -20 # ValueError: weight must be > 0
    except:
        pass

    # 3.观察类属性:注意LineItem有一个类属性,并且是一个特性property
    print(LineItem.__dict__) # weight': 

6、类属性,实例属性和特性的优先级

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/3/16 8:50
# @Author  : Maple
# @File    : 06-类属性,实例属性和特性的优先级.py
# @Software: PyCharm


"""
1. 实例属性会遮盖类属性
2. 实例属性不会遮盖类特性

"""

class Person:
    # 类属性
    data = "I am a Person"

    # 特性
    @property
    def prop(self):
        return 'the prop value'


if __name__ == '__main__':

    # 1. 实例属性会遮盖类属性
    print('*****1. 实例属性会遮盖类属性************')
    p = Person()
    ## 1-1 返回实例属性:为空,因为未定义实例属性
    print(vars(p))

    ## 1-2 访问类属性
    print(p.data) # I am a Person

    ## 1-3 添加实例属性
    p.data = 'Maple'
    # 此时实例会有一个data实例属性
    print(vars(p)) # {'data': 'Maple'}

    ## 1-4 再次通过实例访问data属性
    # 此时访问到的是实例属性,而不是类属性
    print(p.data) # Maple

    ## 1-5 类属性data的值并未发生
    print(Person.data) # I am a Person

    # 2. 实例属性不会遮盖类特性
    print('*****2. 实例属性不会遮盖类特性*************')
    ## 2-1 通过类访问类特性prop,返回特性对象
    print(Person.prop) # 

    ## 2-2 通过实例对象访问特性prop,返回 return的值
    print(p.prop) # the prop value

    ## 2-3 尝试直接设置prop实例属性,会失败
    try:
        p.prop = 'foo'
    except Exception as e:
        print(e) # can't set attribute

    ## 2-4 直接通过self.__dict__方法添加属性
    p.__dict__['prop'] = 'foo'
    # 此时实例对象p中会新增prop实例属性
    print(vars(p)) #{'data': 'Maple', 'prop': 'foo'}

    ## 2-5 但是此时通过实例访问prop,仍然访问的特性(而非实例属性)
    print(p.prop) # the prop value

    ## 2-6 覆盖prop特性
    ## 此时pro还是特性
    print(Person.__dict__) ## {...'prop': ,...}
    ## 覆盖prop特性,此时prop变成类属性
    Person.prop = 'bar'
    print(Person.__dict__)# {...,'prop': 'bar'...}

    ## 2-7 由于实例属性会遮盖类属性
    # 此时访问到的就是 实例属性
    print(p.prop) # foo

    # 3 为类新增一个data`特性`,此时特性会遮盖`实例属性`
    Person.data =property(lambda self: "I am Person prop data")
    # 3-1 此时再访问data就是,访问读取特性的值
    print(p.data) # I am Person prop data

    ## 3-2 删除特性,再次访问就是访问实例属性
    del Person.data
    print(p.data) # Maple

7、定义特性工厂

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/3/16 9:22
# @Author  : Maple
# @File    : 07-定义特性工厂.py
# @Software: PyCharm


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')
    price = quantity('price')

    def __init__(self,description,weight,price):
        self.description = description
        # 注意以下两句已经不是单纯地给LineItem类属性赋值
        # 1. 因为LineItem类定义了两个类属性:  weight = quantity('weight')和 price = quantity('price'),并且其为特性
        #    而类特的优先级比实例属性更高
        # 2. 因此当self调用weight赋值的时,会调用qty_setter方法,并且在该方法中完成实例属性赋值:
        #    instance.__dict__[storage_name] = value【可观察到:实例属性的名字由 storage_name 决定,而storage_name来源于 
        #     quantity()中传递进来的参数名,由此也应该注意:实例属性的名其实并非固定的】
        self.weight = weight
        self.price = price

    def subtotal(self):
        return self.weight * self.price


if __name__ == '__main__':

    # 1.查看类的属性,可以发现weight和price是两个特性
    print(LineItem.__dict__) ## {...'weight': , 'price': ...}

    computer = LineItem('Computer',20,10000)
    # 2. 实例有两个同名的属性:weight和price
    print(computer.__dict__) # {'description': 'Computer', 'weight': 20, 'price': 10000}

    # 3.特性的工作流程
    ## (1) 通过实例访问weight,由于特性优先级大于实例属性,所以实际上会走特性的qty_getter方法
    ## (2) 所以返回computer.__dict['weight'],即实例属性中存放的值
    print(computer.weight) # 20

    ## (1) 通过实例修改weight,由于特性优先级大于实例属性,所以实际上会走特性的qty_setter方法
    ## (2) 先判断新值是否大于0,如果满足条件,则执行:computer.__dict['weight'] = value,即给实例属性赋予新值
    computer.weight = 30
    print(computer.weight) # 30

    ## (1) 通过实例修改weight,由于特性优先级大于实例属性,所以实际上会走特性的qty_setter方法
    ## (2) 先判断新值是否小于0,如果小于0,则会抛出ValueError
    computer.weight = -30 # ValueError: Value must be > 0

8、删除属性

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/3/16 10:12
# @Author  : Maple
# @File    : 08-删除属性.py
# @Software: PyCharm

"""
不常用,仅作展示
"""

class BlackKnight:
    def __init__(self):
        self.members = ['am arm','another arm','a leg','another leg']
        self.phrase = ["'Tis but a scrath'","It's just a fresh 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.phrase.pop(0)))


if __name__ == '__main__':

    knight = BlackKnight()

    # next member is:
    # am arm
    print(knight.member)

    # BLACK KNIGHT (loses am arm)
    #  -- 'Tis but a scrath'
    del knight.member

    # next member is:
    # another arm
    print(knight.member)

你可能感兴趣的:(Python,python,开发语言)