在 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()
代码运行结果如下:
从结果可以看到首先是进入__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__ 属性,实例只允许有指定名称的属性。
当读取实例属性的时候会覆盖类的属性。而在读取实例特性的时候,特性不会被实例属性覆盖,而依然是读取类的特性。除非类特性被销毁。需要根据具体情况选择需要的使用方式。