Python中的属性
访问类和实例中的属性
Python3中的类及其实例都各自有一个__dict__属性,该属性用来保存类或者对象的全部属性。
def get_class_attribute(cls):
common_dict_keys = dict(__module__=None,
__dict__=None,
__weakref__=None,
__doc__=None,
__init__=None).keys()
return {k: v for k, v in cls.__dict__.items() if k not in common_dict_keys}
class Book:
title = "BOOK"
author = "AUTHOR"
book1 = Book()
print(get_class_attribute(Book))
print(vars(book1))
print(Book.title, book1.title)
print(Book.author, book1.author)
"""output
{'title': 'BOOK', 'author': 'AUTHOR'}
{}
BOOK BOOK
AUTHOR AUTHOR
"""
上例中,我们新建了一个类Book,并实例化它赋值给book1作为Book的实例化对象。如果我们调用Book.dict,会返回Book类的全部属性,这里我们通过调用get_class_attribute函数来过滤掉Book类的一些魔法方法后看到,第一个print输出了我们定义在Book类中的属性title和author。
第二个print调用了vars方法,并将book1实例作为参数,vars调用book1.__dict__属性,该方法返回了book1实例的全部属性,我们可以看到此时返回了一个空的字典,说明book1还没有实例对象。
第三和第四个print我们通过"."的方式分别通过调用类的属性和实例的属性观察结果,发现book1没有属性title和author,因此返回了类的属性title和author。
下面的例子描述了调用实例对象的过程
class Book:
title = "BOOK"
author = "AUTHOR"
def __init__(self):
pass
def __getattribute__(self, item):
attr = super().__getattribute__(item)
print("__getattribute__{}".format(item), end=': ')
return attr
def __getattr__(self, item):
return 2
book1 = Book()
print(book1.title)
print(book1.author)
print(book1.time)
"""output
__getattribute__BOOK: BOOK
__getattribute__AUTHOR: AUTHOR
2
"""
当调用实例对象book1的属性时,Python解释器会调用该对象的魔方方法__getattribute__,我们自定义实现了该魔方方法方便调试观察。当调用book1.title时,book1的__getattribute__方法会拦截到这次调用,再通过父类object的__getattribute__进行处理返回属性,object中的__getattribute__会先查询book1是否有属性title,如果没有则返回Book类的属性title。
我们最后调用了一个book1和Book中都不存在的属性time,此时__getattribute__依然会拦截这次调用,但当执行attr = super().getattribute(item)时会抛出异常,因此后面的print(“getattribute …”)将不会执行,如果不定义__getattr__魔法方法,则object中的__getattr__属性将会执行,并抛出AttributeError: ‘Book’ object has no attribute ‘time’,我们重载这个方法可以拦截到这次调用,最后book1.time获取到值“2”。因此__getattr__方法,会在__getattribute__产生异常时触发,如果我们用在__getattribute__里做了try/except的拦截时,__getattr__则永远不会被触发。
此时如果调用以下代码,会发现Book类多了比原来多了两个函数的属性,因为类中定义的函数也是类的属性,下面的代码将__getattribute__和__getattr__两个函数独立出来,并用类属性定义的方式来描述,这种方法和在类中定义函数的方式实现是一样的,直观的表明了类中的函数也是类的属性。
def __getattribute__(self, item):
attr = object.__getattribute__(self, item)
print("__getattribute__{}".format(item), end=': ')
return attr
def __getattr__(self, item):
return 2
class Book:
title = "BOOK"
author = "AUTHOR"
__getattribute__ = __getattribute__
__getattr__ = __getattr__
def __init__(self):
pass
book1 = Book()
print(list(get_class_attribute(Book).keys()))
print(book1.title)
print(book1.author)
print(book1.time)
"""output
['title', 'author', '__getattribute__', '__getattr__']
__getattribute__BOOK: BOOK
__getattribute__AUTHOR: AUTHOR
2
"""
设置类和实例的属性
在上一节中,我们在Book类中定义了类属性title, author以及同样作为属性的两个函数,我们也可以在Book类定义外设置Book类的属性。
book1 = Book()
print(book1.title)
print(book1.time)
Book.time = "2018"
print(book1.time)
"""output
__getattribute__title: BOOK
2
__getattribute__time: 2018
"""
通过class.attr的方式定义类属性,回顾上文,当定义Book类属性time后__getattr__将不会触发,book1.time返回了类属性time。
下面我们单独为book1实例类设置属性。
book1 = Book()
book1.page = "300"
print(book1.page)
print(vars(book1))
"""output
__getattribute__page: 300
__getattribute____dict__: {'page': '300'}
"""
为book1设置属性page后,book1中的__dict__存储了属性page, 如果调用Book.page将抛出Attribute异常,因为Book类作为一种对象并没有重写__getattr__方法,如要像上文所述增加的__getattr__将需要为Book提供一个实现该方法的元类(metaclass),下面为修改代码,元类的具体细节不在本篇阐述。
class Book(metaclass=type("meta",
(type,),
dict(__getattr__ = __getattr__))):
title = "BOOK"
author = "AUTHOR"
__getattribute__ = __getattribute__
__getattr__ = __getattr__
def __init__(self):
pass
book1 = Book()
book1.page = 300
print(book1.page)
print(vars(book1))
print(Book.page)
"""output
__getattribute__page: 300
__getattribute____dict__: {'page': '300'}
2
"""
类描述器
在上文中,对实例对象属性的访问或是设置,都是从instance.__dict__中获取,并没有对属性进行控制,例如若在设置book1.page = 300时进行控制,若大于300时则抛出异常,上文中对实例对象的赋值并没有实现这个控制,下文将要讲述的类描述器可以实现对对象属性的访问和赋值控制。
描述器
描述器是一个类,它可以实现__get__, __set__, __delete__方法分别控制属性的访问、赋值和删除操作,当某个类定义了一个描述器对象的类属性,调用这个类对象的此属性,会触发相应描述器的__get__, __set__, __delete__方法。例如,对Book类中的属性page进行赋值控制,定义了一个描述器类Unsigned并实现__set__方法,并在Book类中定义属性page为该描述器对象。
class Unsigned:
def __init__(self, name):
self.name = name
def __set__(self, instance, value):
if value > 0:
instance.__dict__[self.name] = value
else:
raise ValueError('size must > 0')
class Book:
title = "BOOK"
author = "AUTHOR"
page = Unsigned("page")
book1 = Book()
book1.page = 300
## raise ValueError when execute 'book1.page = -20'.
若想实现访问控制需要实现描述器中的__get__方法,例如想在访问book1.page时增加5。
class Unsigned:
def __init__(self, name):
self.name = name
def __set__(self, instance, value):
if value > 0:
instance.__dict__[self.name] = value
else:
raise ValueError('size must > 0')
def __get__(self, instance, owner):
if instance is None:
return self
else:
return instance.__dict__[self.name] + 5
class Book:
title = "BOOK"
author = "AUTHOR"
page = Unsigned("page")
book1 = Book()
book1.page = 300
print(book1.page)
print(vars(book1))
"""output
305
{'page': 300}
"""
我们发现,在__set__方法中,instance指的是对象book1,value则为300,我们通过instance.dict[self.name] = value,为实例book1增加了属性’page’。目前为止,Book类拥有了属性page(值为Unsigned描述器类的对象),以及book1拥有了属性page(值为300), 因此按照描述器章节前面所述,调用book1.page时,book1的__dict__属性有值为300的属性page,应当返回300,可这里却触发了描述器的__get__返回了305。这里需要理解Python编译器处理点访问对象属性的过程,下面通过流程图和伪代码进行描述。
def get_attr_pesud(obj, attr):
obj_cls = type(obj)
if hasattr(obj_cls, attr):
descipter = getattr(obj_cls, attr)
descipter_cls = type(descipter)
if hasattr(descipter_cls, '__set__'):
if hasattr(descipter_cls, '__get__'):
return descipter.__get__(obj, obj_cls)
if hasattr(obj, attr):
return obj.__dict__[attr]
if hasattr(descipter_cls, '__get__'):
return descipter.__get__(obj, obj_cls)
if hasattr(obj, attr):
return obj.__dict__[attr]
raise AttributeError('')
同样,book1.page = 300是为book1实例对象属性赋值,我们也同样看看Python编译器的处理细节。
def set_obj_attr_pesud(obj, attr, value):
obj_cls = type(obj)
if hasattr(obj_cls, attr):
descipter = getattr(obj_cls, attr)
descipter_cls = type(descipter)
if hasattr(descipter_cls, '__set__'):
descipter.__set__(obj, value)
return
obj.__dict__[attr] = value
下面看看几种实现描述器的例子
实现了__get__和__set__方法的描述器
当使用实现了这两个方法的描述器控制实例属性,根据上面的原则,在访问实例属性时,实际上触发了__get__函数从中获取属性值,而当为实例属性赋值时则会触发__set__函数,一个经典的用法是为实例创建一个只读属性。
class ReadOnly:
def __init__(self, init_value):
self.init_value = init_value
def __set__(self, instance, value):
error = "Can't set {} attribute.".format(self.init_value)
raise AttributeError(error)
def __get__(self, instance, owner):
if instance is None:
return self
else:
return self.init_value
class Book:
title = "BOOK"
author = "AUTHOR"
page = Unsigned("page")
publish = ReadOnly("xinhua")
book1 = Book()
print(book1.publish)
book1.__dict__['publish'] = 3
print(vars(book1))
print(book1.publish)
del Book.publish
print(book1.publish)
"""output
xinhua
{'publish': 3}
xinhua
3
"""
按照上文中介绍的实例属性的访问原理,即使我们用__dict__的方式强制为book1添加了属性publish,但因为ReadOnly实现了__set__和__get__,因此调用book1.publish依然会直接触发ReadOnly中的__get__而非获取到book1.__dict__中的属性,当然如果用del删除了Book类中的publish属性,则book1.publish则返回book1.__dict__的属性。
因此若想实现一个只读属性,需要通过一个实现了__get__和__set__方法的描述器,若只实现__set__,则可以通过book1.__dict__的方式修改book1的属性。
只实现__set__方法的描述器
这类描述器通常用来验证属性的赋值操作。
class Unsigned:
def __init__(self, name):
self.name = name
def __set__(self, instance, value):
if value > 0:
instance.__dict__[self.name] = value
else:
raise ValueError('size must > 0')
class Book:
title = "BOOK"
author = "AUTHOR"
page = Unsigned("page")
所有的book1.page = something的赋值操作都会直接触发Unsigned描述器的__set__方法。
只实现__get__方法的描述器
这类描述器通常可以用来处理耗时的工作并进行缓存,可以理解为延迟加载,下面用例子进行说明。
class Cache:
def __init__(self, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
else:
### do expensive computation then the result is a list
print('Cache __get__.')
result = [1, 2, 3, 4]
instance.__dict__[self.name] = result
return result
class Book:
title = "BOOK"
author = "AUTHOR"
data = Cache('data')
book1 = Book()
print(book1.data)
print(vars(book1))
print(book1.data)
"""output
Cache __get__.
[1, 2, 3, 4]
{'data': [1, 2, 3, 4]}
[1, 2, 3, 4]
"""
按照上文介绍的属性访问原理,当第一调用book1.data访问属性时,由于data的描述器Cache没有__set__方法,因此会先从book1.__dict__中找data属性,此时没有找到,接着就会触发Cache描述器的__get__方法,在这里可以完成一些耗时的工作,并将结果保存到book1.__dict__的data属性中,最后返回。因此再次调用book1.data时,由于book1.__dict__此时拥有了data属性,直接返回了结果,并没有触发Cache的__get__方法。
函数是只实现了__get__的描述器对象
class Book:
title = "BOOK"
author = "AUTHOR"
def read(self):
print('read')
book1 = Book()
print(Book.read)
print(book1.read)
book1.read()
Book.read(book1)
Book.read.__get__(book1, Book)()
"""output
<function Book.read at 0x10cc727b8>
<bound method Book.read of <__main__.Book object at 0x10b9456a0>>
read
read
"""
当在类Book中定义了函数read时,Book拥有了属性read,它是一个只实现了__get__方法的描述器对象。通过Book.read返回的是一个普通函数function(这其实是该描述器的自身对象),通过book1.read返回的是个绑定方法,这些都是通过描述器中的__get__方法实现。这种绑定可以理解为下面代码:
class read:
def __call__(self, instance=None, *args, **kwargs):
print('read:', instance.title)
def __get__(self, instance, owner):
if instance is None:
return self
else:
return lambda *args, **kwargs: self.__call__(instance, *args, **kwargs)
class Book:
title = "BOOK"
author = "AUTHOR"
read = read()
book1 = Book()
print(Book.read)
print(book1.read)
book1.read()
Book.read(book1)
Book.read.__get__(book1, Book)()
"""output
<__main__.read object at 0x10eab46a0>
<function read.__get__.<locals>.<lambda> at 0x10fde0510>
read: BOOK
read: BOOK
read: BOOK
"""
因为函数是个描述器对象,需要实现__call__方法使之可调用,instance可以理解为在类中定义函数的’self’, __get__方法通过绑定instance的方式使self生效。
property
property是Python中内置的一种描述器。
class Book:
title = "BOOK"
author = "AUTHOR"
@property
def page(self):
return self._page
@page.setter
def page(self, value):
if value < 500:
raise ValueError('value is not valid')
self._page = value
book1 = Book()
book1.page = 300
当调用book1.page会抛出异常,property实现了描述器的三种方法,当不设置page.setter时,则page为只读属性,当调用为page赋值时,property中的__set__会抛出异常。property的__init__函数接收四个参数
property(fget=None, fset=None, fdel=None, doc=None)
我们也可以不用装饰器,显示的调用property创建属性。
class Book:
title = "BOOK"
author = "AUTHOR"
def get_page(self):
return self._page
def set_page(self, value):
if value < 500:
raise ValueError('value is not valid')
self._page = value
page = property(get_page, set_page)
book1 = Book()
book1.page = 300
总结
类和实例对象的属性
实例和类的属性访问和赋值原理
用描述器控制属性
函数是描述器(只实现了__get__)
property是描述器,实现__get__, __set__, __delete__