Python 元编程(1)-动态属性和特性

在 Python 中,数据的属性和处理数据的方法统称属性(attribute)。其实,方法只是可调用的属性。除了这二者之外,我们还可以创建特性(property),在不改变类接口的前提下,使用存取方法(即读值方法和设值方法)修改数据属性。这与统一访问原则相符:

不管服务是由存储还是计算实现的,一个模块提供的所有服务都应该通过统一的方式使用。

Python 还提供了丰富的 API,用于控制属性的访问权限,以及实现动态属性。使用点号访问属性时(如 obj.attr),Python 解释器会调用特殊的方法(如__getattr__ 和 __setattr__)计算属性。用户自己定义的类可以通过\ _getattr_ 方法实现“虚拟属性”,当访问不存在的属性时(如 obj.no_such_attribute),即时计算属性的值。

1.使用动态属性转换数据

首先来看一个json文件的读取。书中给出了一个json样例。该json文件有700多K,数据量充足,适合本章的例子。文件的具体内容可以在http://www.oreilly.com/pub/sc/osconfeed上查看。首先先下载数据生成json文件。

def load():
    url='http://www.oreilly.com/pub/sc/osconfeed'
    JSON="osconfeed.json"
    if not os.path.exists(JSON):
        remote=urlopen(url)
        with open(JSON,'wb')as local:
            local.write(remote.read())
    with open(JSON)as fp:
        return json.load(fp)

我们要访问json数据里面的例子,该如何访问呢,一般情况是:

print feed['Schedule']['speakers'][-1]['name'] 

但是这种句法有个缺点,就是很冗长。能不能按照feed.Schedule.speakers[-1].name这种比较简洁的方式来访问呢。要实现这种访问。需要对数据做下重新处理。这里要用到__getattr__方法:代码如下:

class FrozenJSON:
    def __init__(self,mapping):
        self.__data=dict(mapping)   (1)
    def __getattr__(self,name):
        if hasattr(self.__data,name):
            return getattr(self.__data,name)   (2)
        else:
            return FrozenJSON.build(self.__data[name])  (3)

    @classmethod
    def build(cls,obj):
        if isinstance(obj,dict):      (4)
            return cls(obj)
        elif isinstance(obj,list):      (5)
            return [cls.build(item) for item in obj]
        else:                  (6)
            return obj

(1)构造一个字典,这样做确保传入的是字典
(2)确保没有此属性的时候调用__getattr__
(3)如果name是__data的属性,则返回那个属性。
(4)如果判定是字典,则返回该字典对象
(5)如果是列表,则将列表的每个元素递归的传给build方法,构建一个列表
(6)如果既不是列表也不是字典,则直接返回元素
这样实现我们就能按照前面的预期来访问元素了:raw_feed.Schedule.speakers[-1].name

new方法来创建对象

首先来介绍下__new__方法。我们通常都将__init__称为构造函数。其实在python中真正的构造函数应该是__new__。我们没有具体的去实现__new__方法。是因为从object类继承的实现已经足够了。来看一个例子:

class A(object):
    def __init__(self):
        print '__init__'
    def __new__(cls, *args, **kwargs):
        print '__new__'
        print cls
        return object.__new__(cls, *args, **kwargs)

if __name__=="__main__":
    a=A()

代码运行结果如下:


image.png

从结果可以看到首先是进入__new__,然后来生成一个对象的实例并返回。最后才是执行__init__。从这个例子可以看出在构造一个对象实例的时候,首先是进入__new__生成对象实例,然后再调用__init__方法进行初始赋值。那么我们用__new__方法来改造前面的FrozenJSON类。在前面的FrozenJSON实现中,build函数其实是不停的在递归各个字典对象,在递归过程中生成FronzenJSON实例进行处理。也就是第四步中的return cls(obj)。这里我们可以__new__来改造。

class FrozenJSON1(object):
    def __new__(cls, args):
        if isinstance(args,dict):
            return object.__new__(cls)
        elif isinstance(args,list):
            return [cls(item) for item in arg]
        else:
            return args
    def __init__(self,mapping):
        self.__data=dict(mapping)
    def __getattr__(self,name):
        if hasattr(self.__data,name):
            return getattr(self.__data,name)
        else:
            return FrozenJSON(self.__data[name])

上面代码部分中的__new__就是实现了build方法。在__getattr__中没有找到对应name属性时候,return FrozenJSON(self.__data[name])新建一个FrozenJSON对象进行往下递归。

2.使用特性验证属性

先来看一个经典的简单电商应用:

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

每个商品都有重量、单价和描述,用户可以拿到一个商品的售价。
上述代码中会有意外情况,就是商品重量或者单价是负数时,就会返回一个负的总价,这个情况就很糟糕。所以需要加入一点基本的校验:

class LineItem(object):
    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.setter
    def weight(self,value):
        if value <=0:
            raise ValueError('value must be > 0')
        else:
            self.__weight=value

去除重复的方法是抽象。抽象特性的定义有两种方式:使用特性工厂函数,或者使用描述符类。后者更灵活。
虽然内置的 property 经常用作装饰器,但它其实是一个类。在 Python 中,函数和类通常可以互换,因为二者都是可调用的对象,而且没有实例化对象的 new 运算符,所以调用构造方法与调用工厂函数没有区别。此外,只要能返回新的可调用对象,代替被装饰的函数,二者都可以用作装饰器。

不适用property装饰器的例子,经典的调用:

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

    def set_weight(self, value):
        if value > 0:
            self.__weight = value
        else:
            raise ValueError('value must be > 0')

    weight = property(get_weight, set_weight)

某些情况下,这种经典形式比装饰器句法好;稍后讨论的特性工厂函数就是一例。但是,在方法众多的类定义体中使用装饰器的话,一眼就能看出哪些是读值方法,哪些是设值方法,而不用按照惯例,在方法名的前面加上 get 和 set。

本节的主要观点是,obj.attr 这样的表达式不会从 obj 开始寻找 attr,而是从obj.class 开始,而且,仅当类中没有名为 attr 的特性时,Python 才会在 obj 实例中寻找。这条规则不仅适用于特性,还适用于一整类描述符——覆盖型描述符。

先寻找类属性,再寻找实例属性。

如果使用经典调用句法,为 property 对象设置文档字符串的方法是传入 doc 参数:

weight = property(get_weight, set_weight, doc='weight in kilograms')

使用装饰器创建 property 对象时,读值方法(有 @property 装饰器的方法)的文档字符串作为一个整体,变成特性的文档。

创建特性工厂函数
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
        self.weight = weight
        self.price = price

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

在真实的系统中,分散在多个类中的多个字段可能要做同样的验证,此时最好把quantity 工厂函数放在实用工具模块中,以便重复使用。最终可能要重构那个简单的工厂函数,改成更易扩展的描述符类,然后使用专门的子类执行不同的验证。

处理属性删除操作
class BlackKnight:

    def __init__(self):
        self.members = ['an arm', 'another arm', 'a leg', 'another leg']
        self.phrases = ["It's 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)))

影响属性处理方式的特殊属性,后面几节中的很多函数和特殊方法,其行为受下述 3 个特殊属性的影响。
__class__
  对象所属类的引用(即 obj.class 与 type(obj) 的作用相同)。Python 的某些特殊方法,例如 __getattr__,只在对象的类中寻找,而不在实例中寻找。
__dict__
  一个映射,存储对象或类的可写属性。有 __dict__ 属性的对象,任何时候都能随意设置新属性。如果类有 __slots__ 属性,它的实例可能没有 __dict__ 属性。参见下面对 __slots__ 属性的说明。
__slots__
  类可以定义这个这属性,限制实例能有哪些属性。__slots__ 属性的值是一个字符串组成的元组,指明允许有的属性。 如果 __slots__ 中没有 '__dict__',那么该类的实例没有 __dict__ 属性,实例只允许有指定名称的属性。

当读取实例属性的时候会覆盖类的属性。而在读取实例特性的时候,特性不会被实例属性覆盖,而依然是读取类的特性。除非类特性被销毁。需要根据具体情况选择需要的使用方式。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容

  • 一定要跑赢时间,才不枉人世间一遭走。 人的初始,都是一样从襁褓中的婴儿开始,初始的三年,经历了从呀呀学语到蹒跚走路...
    在楚生香阅读 332评论 0 0
  • 一、爱的理论前提:爱的理论必须以人的理论、人的生存理论为前提 人一生下来就从一个确定的环境被推到另一个充满不确定因...
    敏姐的简书阅读 945评论 0 0
  • 嘿,简友们,好久不见!今天难得抽出时间更文章,给大家介绍一下我的家乡,以及家乡的“红玛瑙”。在这之前先告...
    Sunny仪阅读 2,077评论 0 4
  • 漫呷格律 /黎峰 格律长在不同的故乡 就成为了某些私人订制 比如李白的静夜思 比如王维的寒梅和山东兄弟 杜甫和东坡...
    黎峰小峰峰阅读 686评论 13 21