流畅的python,Fluent Python 第十九章笔记 (动态属性和特性)

19.1使用动态属性转换数据。

首先去oreilly网站下载一份标准的json数据。

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


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


def load():
    # 查寻文件是否存在,不存在去下载。
    if not os.path.exists(JSON):
        msg = 'downloading {} to {}'.format(URL, JSON)
        warnings.warn(msg)
        # with使用了两个上下文管理器,分别用于读取读取和保存文件
        with urlopen(URL) as remote, open(JSON, 'wb') as local:
            local.write(remote.read())

    with open(JSON) as fp:
        return json.load(fp)


if __name__ == '__main__':
    print(load())
    # pprint.pprint(load())

 根据书中的一些方式对该数据进行一系列读取操作。

In [20]: feed = load()                                                                               

In [21]: sorted(feed['Schedule'].keys())                                                             
Out[21]: ['conferences', 'events', 'speakers', 'venues']

In [22]: for key, value in sorted(feed['Schedule'].items()): 
    ...:     print('{:3} {}'.format(len(value), key)) 
    ...:                                                                                             
  1 conferences
494 events
357 speakers
 53 venues

In [23]: feed['Schedule']['speakers'][-1]['name']                                                    
Out[23]: 'Carina C. Zona'

In [24]:                                                                                             

 这种读取,从我的理解为字典套字典套列表最后套字典。

 

使用动态属性访问json数据。

书中想通过编写一个特定的类,用.的方式读取数据,也就是通过读取对象属性的方法读取数据。

主要使用了__getattr__实现。

from collections import abc
from osconfeed import load

class FrozenJSON:

    def __init__(self, mapping):
        # 创建一个字典的副本,浅拷贝
        self.__data = dict(mapping)

       # 当对象没有该属性时候,进行调用。
    def __getattr__(self, name):
        # 查寻对象属性__data是否有那么属性,有直接返回
        if hasattr(self.__data, name):
            return getattr(self.__data, name)
        else:
            # 尝试读取name索引的value,并调用类方法。
            try:
                return FrozenJSON.build(self.__data[name])
            except KeyError as e:
                raise AttributeError(*e.args) from e

    @classmethod
    # 调用类方法对参数进行判断
    def build(cls, obj):
        '''
        JSON数据标准给格式就是字典与列表的集合类型 
        '''
        # 如果为映射类型返回FrozenJSON实例
        if isinstance(obj, abc.Mapping):
            return cls(obj)
        # 如果为列表,递归将每一个元素传给build
        elif isinstance(obj, abc.MutableSequence):
            return [cls.build(item) for item in obj]
        # 其它情况返回该数据
        else:
            return obj

if __name__ == '__main__':
    l = load()
    feed = FrozenJSON(l)
    print(feed.Schedul)

 这是按照书中的方法做了自己的解释的代码,除了最终数据或者列表能返回数据(已经处理过了),另外情况下,都是返回FrozenJSON的对象。按照书中要求进行一系列操作。

In [8]: from osconfeed import load                                                                   

In [9]: from explore0 import FrozenJSON                                                              

In [10]: raw_feed = load()                                                                           

In [11]: feed = FrozenJSON(raw_feed)                                                                 

In [12]: len(feed.Schedule.speakers)                                                                 
Out[12]: 357

In [13]: for key, value in sorted(feed.Schedule.items()): 
    ...:     print('{:3} {}'.format(len(value), key)) 
    ...:                                                                                             
  1 conferences
494 events
357 speakers
 53 venues

In [14]: feed.Schedule.speakers[-1].name                                                             
Out[14]: 'Carina C. Zona'

In [15]: talk = feed.Schedule.events[40]                                                             

In [16]: type(talk)                                                                                  
Out[16]: explore0.FrozenJSON

In [17]: talk.name                                                                                   
Out[17]: 'There *Will* Be Bugs'

In [18]: talk.speakers                                                                               
Out[18]: [3471, 5199]

In [19]: talk.flavor                                                                                 
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
~/study/Fluent_Python/第19章/explore0.py in __getattr__(self, name)
     18             try:
---> 19                 return FrozenJSON.build(self.__data[name])
     20             except KeyError as e:

KeyError: 'flavor'

The above exception was the direct cause of the following exception:

AttributeError                            Traceback (most recent call last)
 in 
----> 1 talk.flavor

~/study/Fluent_Python/第19章/explore0.py in __getattr__(self, name)
     19                 return FrozenJSON.build(self.__data[name])
     20             except KeyError as e:
---> 21                 raise AttributeError(*e.args) from e
     22 
     23     @classmethod

AttributeError: flavor

In [20]: talk.keys()                                                                                 
Out[20]: dict_keys(['serial', 'name', 'event_type', 'time_start', 'time_stop', 'venue_serial', 'description', 'website_url', 'speakers', 'categories'])

In [21]: talk.items()                                                                                
Out[21]: dict_items([('serial', 33950), ('name', 'There *Will* Be Bugs'), ('event_type', '40-minute conference session'), ('time_start', '2014-07-23 14:30:00'), ('time_stop', '2014-07-23 15:10:00'), ('venue_serial', 1449), ('description', 'If you're pushing the envelope of programming (or of your own skills)... and even when you’re not... there *will* be bugs in your code.  Don't panic!  We cover the attitudes and skills (not taught in most schools) to minimize your bugs, track them, find them, fix them, ensure they never recur, and deploy fixes to your users.\r\n'), ('website_url', 'https://conferences.oreilly.com/oscon/oscon2014/public/schedule/detail/33950'), ('speakers', [3471, 5199]), ('categories', ['Python'])])

In [22]:                                                                                             

 重新包装后的对象,本身的属性方法都可以用,当返回的为非JSON数据包含的数据类型时,返回数据。

因为if hasattr(self.__data, name): return getattr(self.__data, name)的存在,可以很方便的调用自己字典类型原来的方法。

 

处理无效属性名

前面写的类,没有对名称为Python关键字的属性做特殊处理。

In [22]: grad = FrozenJSON({'name':'Jim Bo','class':1982})                                           

In [23]: grad.name                                                                                   
Out[23]: 'Jim Bo'

In [24]: grad.class                                                                                  
  File "", line 1
    grad.class
             ^
SyntaxError: invalid syntax

 可以通过getattr方法读取属性。

In [25]: getattr(grad, 'class')                                                                      
Out[25]: 1982

 

当然也可以在初始化数据的时候,查找是否有关键字属性,或者其它不符合要求的属性。

from collections import abc
import keyword

from osconfeed import load

class FrozenJSON:

    def __init__(self, mapping):
        self.__data = {}
        # 扫描所有的keys,发现是关键字,添加一个_
        for key, value in mapping.items():
            if keyword.iskeyword(key):
                key += '_'
            self.__data[key] = value

       # 当对象没有该属性时候,进行调用。
    def __getattr__(self, name):
        # print(name)
        # 查寻对象属性__data是否有那么属性,有直接返回.主要为字典本省的一些属性
        if hasattr(self.__data, name):
            return getattr(self.__data, name)
        else:
            # 尝试读取name索引的value,并调用类方法。
            try:
                return FrozenJSON.build(self.__data[name])
            except KeyError as e:
                raise AttributeError(*e.args) from e

    @classmethod
    # 调用类方法对参数进行判断
    def build(cls, obj):
        '''
        JSON数据标准给格式就是字典与列表的集合类型
        '''
        # 如果为映射类型返回FrozenJSON实例
        if isinstance(obj, abc.Mapping):
            return cls(obj)
        # 如果为列表,递归将每一个元素传给build
        elif isinstance(obj, abc.MutableSequence):
            return [cls.build(item) for item in obj]
        # 其它情况返回该数据
        else:
            return obj

if __name__ == '__main__':
    l = load()
    feed = FrozenJSON(l)
    print(feed.Schedul)

 

In [26]: from explore1 import FrozenJSON                                                             

In [27]: grad = FrozenJSON({'name':'Jim Bo','class':1982})                                           

In [28]: grad.class_                                                                                 
Out[28]: 1982

 

一些不符合Python标准的命名方式也不能拿来获取属性比如:

In [29]: x = FrozenJSON({'2be': 'or not'})                                                           

In [30]: x.2be                                                                                       
  File "", line 1
    x.2be
      ^
SyntaxError: invalid syntax

 可以通过s.isidentifier()方法判断该命名是否为正确的Python变量名,如果不符合可以修改它或者直接报错。

 

 

使用__new__方式以灵活的方式创建对象。

这个章节让我对__new__有了更加充分的认识,了解到创建对象的时候,其中的不定长参数就是准备实例化里面的参数

__new__不许返回一个实例,返回的实例会作为第一个参数(self)传给__init__方法.

因为调用__init__方法时要传入实例,而且静止返回任何值,所有__init__方法其实是"初始化犯法"

而且__new___返回的实例如果不是该类的实例,解释器不会调用__init__方法。

class T_New:
    # 真正的构造一个类的过程,其中的参数也可以获取到。
    def __new__(cls, *args, **kwargs):
        print(f'this __new__ args  =>{args!r}, kwargs  =>{kwargs!r}')
        count_args = len(args)
        if count_args < 2:
            print(args, kwargs)
            return 'a'
        else:
            return super().__new__(cls)
    # 这里才会实例化类,给实例属性,如果实例不是该类的实例,不会初始化
    def __init__(self,*attr):
        print('my attr is',attr)
        self.attr = attr

    def __repr__(self):
        if hasattr(self, 'attr'):
            return str(self.attr)
        else:
            return self



if __name__ == '__main__':
    t = T_New('xb',)
    print(t)
    t1 = T_New('xb1','xb2')
    print(t1)

 

/usr/local/bin/python3.7 /Users/shijianzhong/study/Fluent_Python/第19章/t.py
this __new__ args  =>('xb',), kwargs  =>{}
('xb',) {}
a
this __new__ args  =>('xb1', 'xb2'), kwargs  =>{}
my attr is ('xb1', 'xb2')
('xb1', 'xb2')

 这是我自己写的测试代码。

下面书中按照__new__的方法取代前面的类方法写法。

from collections import abc
from osconfeed import load

class FrozenJSON:

    def __new__(cls, obj):
        # 判断传入的对象,如果映射类型,正常创建实例
        if isinstance(obj, abc.Mapping):
            return super().__new__(cls)
        # 如果是列表递归调用新建实例
        elif isinstance(obj, abc.MutableSequence):
            return [cls(item) for item in obj]
        # 如果其它返回输入值
        else:
            return obj

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

    def __getattr__(self, name):
        if hasattr(self.__data, name):
            return getattr(self.__data, name)
        else:
            try:
                # 尝试实例该数据,去测试该数据。
                return FrozenJSON(self.__data[name])
            except KeyError as e:
                raise AttributeError(*e.args) from e



if __name__ == '__main__':
    l = load()
    feed = FrozenJSON(l)
    # print(feed.Schedule.speakers[-1].name)

 非常厉害的一种考虑方式,避免了写类方法,直接调用__new__通过读取不同的数据传入类型判断实例输出对象。

 

使用shelve模块调整OSCON数据源的结构

shelve我前面也简单的使用过它的一些功能,它可以通过键值对的方式存储一些数据,跟新数据也比较方便。

书中通过shelve,存储了一些数据,方便后续的读取与查寻。

书中的实例还是比较简单的,通过一个shelve保存一份自己习惯使用的字典格式,最后的保存对象蛮好的,用了实例的属性赋值方式,这样最后面取出来的对是一个实例,可以通过实例来读取里面的属性。

还学到了可以通过python -i 的方式交互式的运行脚本,很实用的小技巧。

import warnings

import osconfeed

DB_NAME = 'data/schedule1_db'
CONFERENCE = 'conference.115'


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


def load_db(db):
    raw_data = osconfeed.load()
    warnings.warn('londing' + DB_NAME)
    for collection, rec_list in raw_data['Schedule'].items():
        record_type = collection[:-1]
        for record in rec_list:
            # 更新key
            key = '{}.{}'.format(record_type, record['serial'])
            record['serial'] = key
            # value保存为实例
            db[key] = Record(**record)

 运行情况:

shijianzhongdeMacBook-Pro:第19章 shijianzhong$ python3 -i schedule1.py 
>>> import shelve
>>> db = shelve.open(DB_NAME)
>>> if CONFERENCE not in db:
...     load_db(db)
... 
schedule1.py:16: UserWarning: londingdata/schedule1_db
  warnings.warn('londing' + DB_NAME)
>>> speaker = db['speaker.3471']
>>> type(speaker)

>>> speaker.name, speaker.twitter
('Anna Ravenscroft', 'annaraven')
>>> db.close()
>>> exit
Use exit() or Ctrl-D (i.e. EOF) to exit
>>> exit()

 

使用特性获取链接的记录

import warnings
import inspect

import osconfeed

DB_NAME = 'data/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:
            # 调用other的__eq__方法
            return NotImplemented


class MissingDatabaseError(RuntimeError):
    """Raised when a databases is required but was not set"""


class DbRecord(Record):

    __db = None

    @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(my_db)"'
                raise MissingDatabaseError(msg.format(cls.__name__))


    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)
        return self.__class__.fetch(key)

    @property
    def speakers(self):
        if not hasattr(self, '_speaker_objs'):
            spkr_serials = self.__dict__['speakers']
            fetch = self.__class__.fetch
            self._speaker_objs = [
                fetch('speaker.{}'.format(key))
                for key in spkr_serials
            ]
        return self._speaker_objs

    def __repr__(self):
        # 用于对象输出的显示
        if hasattr(self, 'name'):
            cls_name = self.__class__.__name__
            return '<{} {!r}>'.format(cls_name, self.name)
        else:
            return super().__repr__()


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]
        cls_name = record_type.capitalize()
        # 读取字典的key,看能否获取里面的类型,能获取的就用定义的类(就一个Event)
        # 不能获取的用DbRecord
        cls = globals().get(cls_name, DbRecord)
        # 针对不同的字典key,把几种字典分两种不同的类进行
        if inspect.isclass(cls) and issubclass(cls, DbRecord):
            factory = cls
        else:
            factory = DbRecord
        for record in rec_list:
            key = '{}.{}'.format(record_type, record['serial'])
            record['serial'] = key
            db[key] = factory(**record)

 

import shelve
>>> db = shelve.open(DB_NAME)
>>> if CONFERENCE not in db: load_db(db)
... 
schedule2.py:85: UserWarning: loadingdata/schedule2_db
  warnings.warn('loading' + DB_NAME)
>>> DbRecord.set_db(db)
>>> event = DbRecord.fetch('event.33950')
>>> event

>>> event.venue

>>> event.venue.
event.venue.category  event.venue.get_db(   event.venue.serial
event.venue.fetch(    event.venue.name      event.venue.set_db(
>>> event.venue.name
'Portland 251'

 书中的代码,确实写的很厉害,通过类初始化接收json数据,然后通过__repr__输出格式化后的对象,通过实例的属性调用具体的json数据。

因为书中的json数据太长,我看懂了整个代码的逻辑没仔细去研究json数据的格式,但通过类实例来接收对象,重新定义一个新的字典,通过实例的属性,来获取内部的数据,确实很高超,很厉害。

 

2、使用特性验证属性

 

LineItem类第一版:表示订单中商品的类

class LineItem:

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

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

 

shijianzhongdeMacBook-Pro:第十九章 shijianzhong$ python3 -i bulkfood_v1.py 
>>> raisins = LineItem('Golden raisns', 10, 6.95)
>>> raisins.subtoall()
69.5
>>> raisins.weight = -20
>>> raisins.subtoall()
-139.0
>>> 

 这个一个没有任何限制的订单,出现了负数问题。

 

LineItem类第2版:能验证值的特性

class LineItem:

def __init__(self, description, weight, price):
self.description = description
# 这个按照书中的说法叫特性的赋值方法了,不是属性赋值了。
self.weight = weight
self.price = price

def subtoall(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')

 

shijianzhong$ python3 -i bulkfood_v2.py 
>>> raisins = LineItem('Golden raisns', 10, 6.95)
>>> raisins.subtoall()
69.5
>>> raisins.weight = -1
Traceback (most recent call last):
  File "", line 1, in 
  File "bulkfood_v2.py", line 21, in weight
    raise ValueError('value must be > 0')
ValueError: value must be > 0

 

特性全解析

 

class LineItem:

    def __init__(self, description, weight, price):
        self.description = description
        # 这个按照书中的说法叫特性赋值了,不是属性赋值了。
        self.weight = weight
        self.price = price

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

    def get_weight(self):
        return self.__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)
        
    

 上面的经典写法,在Python2.4以后才出现了@装饰器的写法

property(fget=None, fset=None, fdel=None, doc=None)

所有的参数都是可选的,如果没有函数传给某个参数,那么得到的特性对象就不允许执行。

 

特性会覆盖实例属性

特性都是类属性,但是特性管理的其实是实例属性的存储。

在实例和所属的类有同名称属性时,实例属性会覆盖类属性。

In [31]: class Class: 
    ...:     data = 'the class data attr' 
    ...:     @property 
    ...:     def prop(self): 
    ...:         return 'the prop value' 
    ...:                                                                                                   

In [32]: obj = Class()                                                                                     

In [33]: vars(obj)                                                                                         
Out[33]: {}

In [34]: # ocj对象没有属性                                                                                 

In [35]: obj.data                                                                                          
Out[35]: 'the class data attr'

In [36]: # 调用类属性                                                                                      

In [37]: obj.data = 'bar'                                                                                  

In [38]: obj.__dict__                                                                                      
Out[38]: {'data': 'bar'}

In [39]: # 赋值调用对象的属性,读取对象的可读写,不可调用属性                                              

In [40]: obj.data                                                                                          
Out[40]: 'bar'

In [41]: Class.data                                                                                        
Out[41]: 'the class data attr'

In [42]: # 类属性不会覆盖                                                                                  

In [43]: Class.prop                                                                                        
Out[43]: 

In [44]: # 直接从Class读取prop特性,获取特性对象本身                                                       

In [45]: obj.prop                                                                                          
Out[45]: 'the prop value'

In [46]: # 从实例执行特性的读取方式                                                                        

In [47]: obj.prop = 'hello'                                                                                
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
 in 
----> 1 obj.prop = 'hello'

AttributeError: can't set attribute

In [48]: # 尝试属性赋值失败,因为没有设置赋值的特性                                                        

In [49]: obj.__dict__['prop'] = 'foo'                                                                      

In [50]: vars(obj)                                                                                         
Out[50]: {'data': 'bar', 'prop': 'foo'}

In [51]: obj.prop                                                                                          
Out[51]: 'the prop value'

In [52]: # 给obj的prop属性赋值,但通过执行该prop,执行的是特性                                             

In [53]: Class.prop = 'baz'                                                                                

In [54]: obj.prop                                                                                          
Out[54]: 'foo'

In [55]: # 通过类属性,删除了该特性,可以通过属性获取对象的该属性                                          

In [56]: obj.data                                                                                          
Out[56]: 'bar'

In [57]: Class.data                                                                                        
Out[57]: 'the class data attr'

In [58]: Class.data = property(lambda self: 'the "data" prov value')                                       

In [59]: # 特性覆盖了类属性data                                                                            

In [60]: obj.data                                                                                          
Out[60]: 'the "data" prov value'

In [61]: # 执行了特性                                                                                      

In [62]: del Class.data                                                                                    

In [63]: obj.data                                                                                          
Out[63]: 'bar'

In [64]:  

                                                                                                           

 前面的测试主要告诉我,obj.attr这样的表达式,不会从obj'开始寻找attr,而是从obj.__class__开始,而且仅当类中没有attr的特性时,Python才会在obj实例中寻找。

这条规则不仅使用与特性,还使用与一整类描述符。

 

 

定义一个特性工厂

特性工厂主要返回一个特性给类属性赋值,实例可以使用该特性。

# 通过传入的名称去寻找实例的内部属性,通过__dict__进行属性赋值。
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(fget=qty_getter, fset=qty_setter)



class LineItem:
    # 类属性赋值特性
    weight = quantity('weight')
    price = quantity('price')

    def __init__(self, description, weight, price):
        self.description = description
        # 这个按照书中的说法叫特性的赋值方法了,不是属性赋值了。
        self.weight = weight
        self.price = price

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

 

处理属性的删除操作

由于Python中用的比较少,而且实例也比较简单,步写了。

 

处理属性的重要属性和函数

 

影响属性处理方式的特殊属性。

__class__

对象所属类的引用(obj.__class__与type.__class__作用一样)

__dict__

一个映射,存储对象或类的可写属性。有__dict__属性的对象,任何时候都能随意设定新属性。

__slots__

类可以定义这个属性,限制实例能有哪些属性。__slots__属性的值时一个字符串数组的元祖,指明允许有的属性。如果__slots__中没有'__dict__',那么该类的实例没有__dict__属性,实例只允许有指定的名称的属性。

 

处理属性的内置函数

dir([object])

列出对象的大多数属性。官方文档说,dir函数的目的是交互式使用,因此没有提供完整的属性列表,只列出一组"重要的"属性名。dir函数能审查有或没有__dict__属性的对象。dir函数不会列处__dict__属性本身,但会列出其中的键。

dir函数也不会列出类的几个特殊属性,例如__mro__, __bases__和__name__。如果没有指定可选的object,dir函数会列出当前作用域的名称

 

getattr,hasattr,setattr,前面已经讲过.

 

vars([object])

返回object对象的__dict__属性。如果没有指定参数,那么vars()函数的作用与locals()函数一样:返回表示本地作用域的字典。

 

处理属性的特殊方法

特殊方法不会被同名实例属性遮盖。

通过obj.__dict__属性修改对象的属性不会激活特殊方法,另外的不管用点或者处理属性的函数,都会粗发特殊方法中的一个。

特殊方法为

__getattrrubute__

__delattr__

__dir__

__getattr__

__setattr__

 

本章我主要学到了特性,也就时方法转换为属性的强大,虽然为类属性,但实例使用该属性,具有第一优先权。

 

你可能感兴趣的:(流畅的python,Fluent Python 第十九章笔记 (动态属性和特性))