Python学习笔记36:动态属性和特性

Python学习笔记36:动态属性和特性

值得高兴的是,经过一段时间的学习,《Fluent Python》一书的内容只剩下最后一个部分了:元编程。

当然,我同时也发现这本书在中后部的内容难度陡然增加,但是随着书页的变薄,任何读者想必都难免会有喜悦和轻松之感。

闲话少说,进入今天的主题。

任何语言对面向对象的学习都是先介绍类,而类中最开始学的内容必然是属性。Python作为一门动态语言,相比静态语言,在类和对象的属性上面有更多“花样”,值得我们专门花时间总结一下。

属性

Python的类和对象中有一些特殊属性,可以帮助我们实现类似Java中的反射功能,直接“嗅探”一个未知类或者对象的内部结构。

特殊属性

__class__

这个属性其实之前多次使用过,它关联到一个实例的类对象,其和type函数获取到的对象是一致的。

class TestClass:
    def __init__(self) -> None:
        pass

tc = TestClass()
print(tc.__class__)
print(type(tc))
print(tc.__class__ == type(tc))
print(type(tc.__class__))
# 
# 
# True
# 

可以看到“类对象”实质上是一个type类型的实例,用于存储类定义中的相关信息。

__dict__

__dict__属性是一个字典,包含实例中的所有可变属性,我们可以直接操作这个属性来动态地给实例添加新的属性。

  • 之前我们讨论过,事实上Python不存在类似const的关键字,所有属性都是可变的,但是我们可以通过特性(property)来实现不可变属性,这一点在之后会详细说明。
  • 操作__dict__来修改实例属性似乎多此一举,因为我们可以使用常规的.操作符,但是在之后介绍完特性之后你就会明白其价值。
class TestClass:
    def __init__(self) -> None:
        pass


tc = TestClass()
print(tc.__dict__)
tc.newOpt = 'new'
print(tc.__dict__)
newOpts = {'opt1': 1, 'opt2': 2, 'opt3': 3}
tc.__dict__.update(newOpts)
print(tc.__dict__)
print(tc.newOpt)
print(tc.opt1)
print(tc.opt2)
print(tc.opt3)
# {}
# {'newOpt': 'new'}
# {'newOpt': 'new', 'opt1': 1, 'opt2': 2, 'opt3': 3}
# new
# 1
# 2
# 3

这个示例展示了通过.操作符和直接修改__dict__属性来给实例添加新属性,可以看到结果并无区别,本质上都是在修改__dict__字典。此外,使用__dict__还有个好处是我们可以通过调用字典的update方法来批量添加、覆盖实例的属性。

__slots__

之前在Python学习笔记26:符合Python风格的对象有介绍过__slots__,我们可以利用这个特殊的类属性优化对象的存储性能,将散列式的字典结构变成紧密排列的元组结构。

但我们也提到过,使用这一优化技术的后果就是无法再动态添加实例的属性:

class TestCls:
    __slots__=('opt1','opt2')
    def __init__(self) -> None:
        self.opt1 = 1
        self.opt2 = 2

tc = TestCls()
# tc.opt3 = 3
# Traceback (most recent call last):
#   File "D:\workspace\python\python-learning-notes\note36\test.py", line 8, in 
#     tc.opt3 = 3
# AttributeError: 'TestCls' object has no attribute 'opt3'
# print(tc.__dict__)
# Traceback (most recent call last):
#   File "D:\workspace\python\python-learning-notes\note36\test.py", line 13, in 
#     print(tc.__dict__)
# AttributeError: 'TestCls' object has no attribute '__dict__'

我们可以看到,在使用__slots__以后实例甚至就没有__dict__这个属性。

class TestCls:
    __slots__=('opt1','opt2','__dict__')
    def __init__(self) -> None:
        self.opt1 = 1
        self.opt2 = 2

tc = TestCls()
tc.opt3 = 3
print(tc.opt3)
print(tc.__dict__)
# 3
# {'opt3': 3}

如果我们将__dict__这个特殊属性加入__slots__,就可以正常添加属性了。

当然这么做可能没有实际意义,只是作为__slots____dict__机制的说明。

内置函数

同样的,之前的学习中其实我们已经使用过很多有关属性的内建函数,这里做一个汇总。

dir

这个内建函数会返回实例中大部分对开发者有用的属性和方法,包括从父类继承的部分。

class TestClass:
    def __init__(self) -> None:
        pass

tc = TestClass
print(dir(tc))
# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', 
# '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
getattr

getattr(obj,name[,default])的作用是从指定实例obj获取一个名称为name的属性,default是可选的。

class TestClass:
    def __init__(self) -> None:
        self.opt1 = 1
        self.opt2 = 2

tc = TestClass()
print(getattr(tc,'opt1'))
print(getattr(tc,'opt3',3))
print(getattr(tc,'opt3'))
# 1
# 3
# Traceback (most recent call last):
#   File "D:\workspace\python\python-learning-notes\note36\test.py", line 9, in 
#     print(getattr(tc,'opt3'))
# AttributeError: 'TestClass' object has no attribute 'opt3'

可以看到,如果属性不存在,在指定了default的前提下会返回default值,如果没有指定,会抛出AttributeError异常。

hasattr

可以用hasattr(obj,name)判断实例obj是否有名称为name的属性。

class TestClass:
    def __init__(self) -> None:
        self.opt1 = 1
        self.opt2 = 2

tc = TestClass()
print(hasattr(tc,'opt1'))
print(hasattr(tc,'opt3'))
# True
# False

有Python文档显示,hasattr是通过检查getattr是否抛出异常来判断的。

setattr

setattr(obj,name,value)用于设置实例obj的属性。

class TestClass:
    def __init__(self) -> None:
        self.opt1 = 1
        self.opt2 = 2

tc = TestClass()
setattr(tc, 'opt1', 111)
setattr(tc, 'opt3', 333)
print(tc.opt1)
print(tc.opt3)
# 111
# 333

如果没有该属性,将添加一个新的属性,这点和使用.操作符是一致的。

vars

vars([obj])会返回实例obj__dict__属性。

class TestClass:
    def __init__(self) -> None:
        self.opt1 = 1
        self.opt2 = 2

tc = TestClass()
print(vars(tc))
print(vars())
# {'opt1': 1, 'opt2': 2}
# {'__name__': '__main__', '__doc__': None, '__package__': '', '__loader__': None, '__spec__': None, '__file__': 'D:\\workspace\\python\\python-learning-notes\\note36\\test.py', '__cached__': None, '__builtins__': {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': 

如果没有参数,就会返回当前上下文加载的属性。内容很多,这里只展示部分。

特殊方法

我们可以在类中定义一些作用于属性的魔术方法,这些魔术方法将改变类实例对属性的操作行为。

其实我们在Python学习笔记27:类序列对象中简单介绍和使用过这类魔术方法了,这里同样做一个总结。

__getattribute__

这个魔术方法会改变从类实例获取属性的行为。

from typing import Any


class TestClass:
    def __init__(self) -> None:
        self.opt1 = 1
        self.opt2 = 2

    def __getattribute__(self, name: str) -> Any:
        try:
            attr = super().__getattribute__(name)
        except AttributeError:
            setattr(self, name, 1)
            return super().__getattribute__(name)
        else:
            return attr

tc = TestClass()
print(tc.opt2)
print(tc.opt3)
# 2
# 1

这里我们使用__getattribute__实现了如果试图读取一个不存在的实例属性,我们就创建,并且给其赋值为1。

  • 可能更好的方式是将这个默认值可以通过__init__方法指定。
  • 从命名上可以看出__getattribute__其实并不如__getattr__有用,这点在之后的处理JSON的示例中会看到。
__setattr__

__setattr__(self,name,value)魔术方法会改变类实例对属性的赋值行为:

class TestClass:
    def __init__(self) -> None:
        pass

    def __setattr__(self, name, value):
        newVlue = 'new_'+str(value)
        super().__setattr__(name, newVlue)

tc = TestClass()
tc.opt1 = 1
print(tc.opt1)
# new_1

注意,__setattr__内部不能使用setattr(obj,name,newVlue),而是要用父类的__setattr__方法实现对属性的赋值操作,因为前者会陷入无限递归。

__delattr__

__delattr__魔术方法是和del关键字相关的,当我们使用del关键字删除类实例的某个属性时候,如果定义了该魔术方法,就会调用,而非执行原本的操作。

class TestClass:
    def __init__(self) -> None:
        self.opt1 = 1
        self.opt2 = 2

    def __delattr__(self, name: str) -> None:
        value = getattr(self, name)
        super().__delattr__(name)
        newName = 'deled_'+str(name)
        setattr(self, newName, value)

tc = TestClass()
del tc.opt1
del tc.opt2
print(tc.__dict__)
# {'deled_opt1': 1, 'deled_opt2': 2}

这里通过设置__delattr__魔术方法,实现了在用户删除实例属性后,依然保留一个名称为del_XXX的属性作为备份,将“硬删除”操作变为了“软删除”,甚至可以在这基础上进行属性的还原操作。

__getattr__

可能很多人在看到这里都会困惑,不是已经有__getattribute__了吗,这个又是干什么用的?

通过官方文档我们可以知道,__getattribute__是所有对类实例属性的访问行为都会触发,而__getattr__只有在获取实例属性时候触发AttributeError的时候才会被调用,所以上面那个如果属性不存在就创建默认属性的行为更适合放在__getattr__中:

from typing import Any


class TestClass:
    def __init__(self, default=None) -> None:
        self.default = default

    def __getattr__(self, name):
        setattr(self, name, self.default)
        return self.__dict__[name]

    def __getattribute__(self, name: str) -> Any:
        return super().__getattribute__(name)

tc = TestClass()
tc.opt1 = 1
print(tc.opt2)
print(tc.__dict__)
# None
# {'default': None, 'opt1': 1, 'opt2': None}

其实这里并不需要创建__getattribute__方法,只是为了对比说明。在这里这个方法继承了基类方法的行为,什么也没做。

__getattr__中没有使用getattr或者__getattribute__,是为了防止可能会发生的无限递归。

__dir__

这个很好理解,就是调用dir函数时候触发的,给其提供一个返回值。

from collections.abc import Iterable
class TestClass:
    def __init__(self) -> None:
        self.opt1 = 1
        self.opt2 = 2
    
    def __dir__(self) -> Iterable[str]:
        res = list(super().__dir__())
        res.append('del_XXX')
        return res

tc = TestClass()
print(dir(tc))
# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', 
# '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'del_XXX', 'opt1', 'opt2']

这里使用res.append('del_XXX')添加了一个不存在的属性说明,可能并不合适。仅用于说明魔术方法的用途。

动态属性

下面我们展示如何使用之前介绍的属性相关魔术方法来实现动态属性。

处理JSON

我们来看一个处理JSON字符串的例子:

jsonStr = '''{
    "sites": [
    { "name":"菜鸟教程" , "url":"www.runoob.com" }, 
    { "name":"google" , "url":"www.google.com" }, 
    { "name":"微博" , "url":"www.weibo.com" }
    ]
}'''
import json
jsonObj = json.loads(jsonStr)
print(jsonObj)
print(jsonObj['sites'][0]['name'])
print(jsonObj.sites)
# 
# 'www.weibo.com'}]}
# 菜鸟教程
# Traceback (most recent call last):
#   File "D:\workspace\python\python-learning-notes\note36\test.py", line 12, in 
#     print(jsonObj.sites)
# AttributeError: 'dict' object has no attribute 'sites'

可以看到,使用json.loads处理后的json字符串转变成了一个字典,我们可以使用字典的方式去访问其中的值。

这里的示例JSON字符串来自于www.runoob.com

但假如我们不想使用[],而是想像访问对象属性一样访问里边的值呢?

from typing import Any
from collections.abc import Mapping
from collections.abc import MutableSequence


class JSONReader:
    def __init__(self, dictData:dict) -> None:
        self.__dictData = dict(dictData)

    def __getattr__(self, name: str) -> Any:
        if hasattr(self.__dictData, name):
            return getattr(self.__dictData, name)
        elif name in self.__dictData:
            attrValue = self.__dictData[name]
            if isinstance(attrValue, Mapping):
                return self.__class__(attrValue)
            elif isinstance(attrValue, MutableSequence):
                return [self.__class__(item) for item in attrValue]
            else:
                return attrValue
        else:
            raise AttributeError

jsonStr = '''{
    "sites": [
    { "name":"菜鸟教程" , "url":"www.runoob.com" }, 
    { "name":"google" , "url":"www.google.com" }, 
    { "name":"微博" , "url":"www.weibo.com" }
    ]
}'''
import json
jsonDict = json.loads(jsonStr)
jReader = JSONReader(jsonDict)
print(jReader.sites[0].name)
print(jReader.sites[1].url)
# 菜鸟教程
# www.google.com

注意,这里只能使用__getattr__,不能使用__getattribute__,否则会陷入无限递归。因为我们在__getattr__中使用了大量的.运算符来进行属性访问,如果使用的是后者,就会再次触发__getattribute__,然后又使用.运算符,又触发,以此往复。

这也就是为什么官方文档中说__getattr____setattr__名称对应,而__getattribute__反而命名更另类的原因。从上面这个示例的角度上看,__getattr__显然更有用。

此外就是对JSON字数据的处理中我们只考虑了字典和可变数组这两种数据类型,事实上JSON字符串也只包含这两种类型的数据。

现在通过这个精巧的自定义类我们就可以像使用对象属性那样读取JSON中的数据了,但是我们还可以进一步改进这段代码,让其更优雅一点。

这里我们需要使用到__new__

__new____init__

学习Python的时候我们习惯性地称呼__init__方法为构造方法,这是从其他编程语言中借鉴过来的概念。事实上这是不准确的,准确地讲__init__只是一个“初始化方法”。

这点从__init__的函数签名也能看出来:

class JSONReader:
    def __init__(self, dictData:dict) -> None:
        self.__dictData = dict(dictData)

这段代码中__init__的帮助文档是VSCode插件自动生成的,很明显地说明了一个事实:__init__方法本身并不会返回任何实例,而且当进入__init__方法的时候,对象实例self已经创建。

这都是真正的“构造方法”__new__的功劳。

__new__的用途是创建类实例并返回,这点和其它语言中的构造函数是一样的。不同的是,Python中的__new__不仅能创建当前类的对象实例,还能创建并返回其它类型。

如果__new__创建并返回的是当前类型,将会调用__init__方法进行初始化,但如果是返回的其它类型,将不会调用__init__

因为这个特点,Python中使用类名产生实例就可以相当灵活,根据情况,可以产生非自己类型的结果

对于之前的例子,我们在__getattr__方法中编写了大量代码对数据类型判断,并使用JSONReader进行包装或者产生一个JSONReader的列表,亦或者直接返回。

除了这种方式外,我们现在有了另一个选择:借助__new__来让JSONReader类自己“智能”地判断传给它的数据如何进行处理,并返回一个恰当的结果,而__getattr__就简单很多了,只要把数据丢给构造方法即可。

from typing import Any
from collections.abc import Mapping
from collections.abc import MutableSequence


class JSONReader:
    def __new__(cls, jsonData:Any) -> Any:
        if isinstance(jsonData, Mapping):
            return super().__new__(cls)
        elif isinstance(jsonData, MutableSequence):
            return [cls(item) for item in jsonData]
        else:
            return jsonData

    def __init__(self, dictData:Mapping) -> None:
        self.__dictData = dict(dictData)

    def __getattr__(self, name: str) -> Any:
        if hasattr(self.__dictData, name):
            return getattr(self.__dictData, name)
        elif name in self.__dictData:
            attrValue = self.__dictData[name]
            return self.__class__(attrValue)
        else:
            raise AttributeError

jsonStr = '''{
    "sites": [
    { "name":"菜鸟教程" , "url":"www.runoob.com" }, 
    { "name":"google" , "url":"www.google.com" }, 
    { "name":"微博" , "url":"www.weibo.com" }
    ]
}'''
import json
jsonDict = json.loads(jsonStr)
jReader = JSONReader(jsonDict)
print(jReader.sites[0].name)
print(jReader.sites[1].url)
# 菜鸟教程
# www.google.com

这里需要注意的是,在__new__中不能使用cls(jsonData)的方式创建实例,因为__new__本身就是cls的构造方法,cls(jsonData)会再次触发__new__,陷入无限递归调用中。

在我写这几段代码后我的体会是,如果你在面临这种问题,首先应该尝试使用父类的方法,比如super().__new__(cls),这里实质上是调用基类object__new__方法,需要传入一个参数cls,告诉其创建一个什么类型的实例。

Python是有意没有使用类似C++和Java中的new关键字,在Python的理念中,构造函数本身就是一个普通的可执行对象,和别的方法以及可执行实例是没有区别的,所以使用new反而会限制Python中类构造方法的灵活性,在Python中通过构造方法创建实例和通过工厂方法创建,是完全可以互相替换的,只不过因为类名使用首字母大写的驼峰形式而方法不是,命名上两者有区别而已。

特性

我们在Python学习笔记26:符合Python风格的对象中有简单介绍和使用过特性(property)。现在我们全面了解一下。

声明

class TestClass:
    def __init__(self) -> None:
        self.__opt1 = 1

    @property
    def opt1(self):
        return self.__opt1


tc = TestClass()
print(tc.opt1)
print(tc.__dict__)
tc.opt1 = 2
# 1
# {'_TestClass__opt1': 1}
# Traceback (most recent call last):
#   File "D:\workspace\python\python-learning-notes\note36\test.py", line 13, in 
#     tc.opt1 = 2
# AttributeError: can't set attribute

创建一个特性很简单,只要给普通的getter方法前加上@property装饰器即可,这样就可以简单创建一个只读的特性,就像示例代码展示的那样。

此外需要注意的是,特性(property)和属性(attribute)有很大区别,特性并不保存在__dict__属性中,而且特性可以只读,尝试对一个只读特性赋值就会产生一个AttributeError异常。

此外,特性也支持写和删除操作,声明方式与读类似:

class TestClass:
    def __init__(self) -> None:
        self.__opt1 = 1

    @property
    def opt1(self):
        return self.__opt1

    @opt1.setter
    def opt1(self, value):
        self.__opt1 = value

    @opt1.deleter
    def opt1(self):
        del self.__opt1


tc = TestClass()
print(tc.opt1)
print(tc.__dict__)
tc.opt1 = 2
print(tc.opt1)
del tc.opt1
print(tc.opt1)
# 1
# {'_TestClass__opt1': 1}
# 2
# Traceback (most recent call last):
#   File "D:\workspace\python\python-learning-notes\note36\test.py", line 24, in 
#     print(tc.opt1)
#   File "D:\workspace\python\python-learning-notes\note36\test.py", line 7, in opt1
#     return self.__opt1
# AttributeError: 'TestClass' object has no attribute '_TestClass__opt1'

设置特性的写和读的时候使用的装饰器是opt1.setteropt1.deleter,这是刚刚创建的只读特性的两个装饰器方法,利用这两个装饰器我们可以给只读特性增加写和删除操作。

需要注意的是同一个特性的三个操作的方法名要和特性名称一致,在上面都是opt1,如果不一致就可能在进行相应的操作时产生AttributeError异常。

工厂方法

除了通过上面的方式创建特性,还可以用下面的方式:

class TestClass:
    def __init__(self) -> None:
        self.__opt1 = 1

    def get_opt1(self):
        return self.__opt1

    def set_opt1(self, value):
        self.__opt1 = value

    def del_opt1(self):
        del self.__opt1
    opt1 = property(fget=get_opt1, fset=set_opt1, fdel=del_opt1)


tc = TestClass()
print(type(TestClass.opt1))
print(tc.opt1)
print(tc.__dict__)
tc.opt1 = 2
print(tc.opt1)
del tc.opt1
print(tc.opt1)
# 
# 1
# {'_TestClass__opt1': 1}
# 2
# Traceback (most recent call last):
#   File "D:\workspace\python\python-learning-notes\note36\test.py", line 23, in 
#     print(tc.opt1)
#   File "D:\workspace\python\python-learning-notes\note36\test.py", line 6, in get_opt1
#     return self.__opt1
# AttributeError: 'TestClass' object has no attribute '_TestClass__opt1'

事实上property是一个类,从输出可以看出,我们通过opt1 = property(fget=get_opt1, fset=set_opt1, fdel=del_opt1)是创建了一个property的实例,并将其赋值给命名为opt1的类变量。

特性具体的读写删除操作是通过函数式编程的方式通过参数传入property初始化方法的。

在使用上两者当然完全一致。

这种方式比上面直接通过装饰器“绑定”方法的方式更灵活,我们可以利用其构建一个满足具体使用需求的特性工厂方法,并使用工厂方法批量创建特性。

def propertyFactory(name):
    def getter(instance):
        return instance.__dict__[name]

    def setter(instance, value):
        if value > 0:
            instance.__dict__[name] = value
        else:
            raise ValueError("value must > 0")

    def deleter(instance):
        del instance.__dict__[name]
    return property(fget=getter, fset=setter, fdel=deleter)


class TestClass:
    opt1 = propertyFactory("opt1")
    opt2 = propertyFactory("opt2")
    opt3 = propertyFactory("opt3")

    def __init__(self, opt1, opt2, opt3) -> None:
        self.opt1 = opt1
        self.opt2 = opt2
        self.opt3 = opt3

    def __str__(self) -> str:
        return str((self.opt1, self.opt2, self.opt3))


tc1 = TestClass(1, 2, 3)
print(tc1)
tc2 = TestClass(0, 1, 0)
# (1, 2, 3)
# Traceback (most recent call last):
#   File "D:\workspace\python\python-learning-notes\note36\test.py", line 32, in 
#     tc2 = TestClass(0, 1, 0)
#   File "D:\workspace\python\python-learning-notes\note36\test.py", line 22, in __init__
#     self.opt1 = opt1
#   File "D:\workspace\python\python-learning-notes\note36\test.py", line 9, in setter
#     raise ValueError("value must > 0")
# ValueError: value must > 0

这里通过创建特性工厂方法propertyFactory创建了三个特性,而且这三个特性的写操作都会检查新值是否为大于零的数,如果不是就抛出ValueError异常。

这里需要注意的要点有:

  • 具体的setter/getter/deleter方法中要用到特性绑定的类的实例引用instance,但这个引用不需要声明到propertyFactory的参数列表中,而是会在实际使用特性的时候由解析器直接传入相应方法。
  • getter等方法中通过instance.__dict__[name]的方式访问真实属性,而非使用setattr等内建函数,这是因为setattr等内建函数会触发特性,这又可能导致无限递归。
  • 真实的属性instance.__dict__[name]的名称name与特性同名,这样做的好处是除非外部程序直接操作__dict__,否则是访问不到真实属性的,所有对name的访问都会由特性处理。
  • opt1 = propertyFactory("opt1")等特性创建语句在__init__方法之前,这是为了能在初始化方法中就使用特性:self.opt1 = opt1

特性和属性优先级

Python学习笔记26:符合Python风格的对象我们讨论过实例属性对类属性的覆盖问题。事实上特性、实例属性和类属性的确有类似于访问优先级的问题。

特性、实例属性和类属性的优先级依次降低,顺序是特性>实例属性>类属性

下面我们用实际代码来验证:

class TestClass:
    def __init__(self) -> None:
        self.__dict__['opt1'] = 1

    @property
    def opt1(self):
        return 42


tc = TestClass()
print(tc.opt1)
del TestClass.opt1
print(tc.opt1)
# 42
# 1

这里可以清楚地看到,在删除特性(特性本质就是类属性)后,使用.运算符访问属性就可以直接访问到__dict__中的属性值了。

这个示例必须是通过特性直接返回固定值,而非利用__dict__对真实值进行读写操作,原因是那样做是将实例的同名属性与特性等价,无法说明特性和属性的访问优先级差异。

关于实例属性和类属性的优先级,Python学习笔记26:符合Python风格的对象中有详细说明,这里不再赘述。

其实特性和普通的类属性谈不上优先级,因为如果是同名的话他们根本就是同一个类属性。

通过上面的讨论我们可以看到,在访问实例的属性的时候,解释器会先去查看类定义中是否存在特性,如果没有,再查看实例中有没有该属性,如果还没有,再查看类定义中是否有普通的类属性。

也就是说按特性>实例属性>类属性的先后顺序进行检索,这也就是一开始说的这三者的优先级差异。

文档

在python中,可以通过help内建函数来读取函数的帮助文档,比如:

help(dir)
# Help on built-in function dir in module builtins:

# dir(...)
#     dir([object]) -> list of strings

#     If called without an argument, return the names in the current scope.
#     Else, return an alphabetized list of names comprising (some of) the attributes
#     of the given object, and of attributes reachable from it.
#     If the object supplies a method named __dir__, it will be used; otherwise
#     the default dir() logic is used and returns:
#       for a module object: the module's attributes.
#       for a class object:  its attributes, and recursively the attributes
#         of its bases.
# -- More  -- 

对于普通函数,帮助文档就是函数声明下的那一行信息:

def my_test():
    '''this is a test function'''
    pass

help(my_test)
# Help on function my_test in module __main__:

# my_test()
#     this is a test function

对于特性,我们也可以设置相关帮助文档:

class TestClass:
    def __init__(self) -> None:
        self.__opt1 = 1

    def get_opt1(self):
        return self.__opt1
    opt1 = property(fget=get_opt1,doc='this is a property doc')

help(TestClass.opt1)
# Help on property:

#     this is a property doc

对于通过装饰器设置的特性,就更简单了:

class TestClass:
    def __init__(self) -> None:
        self.__opt1 = 1

    @property
    def opt1(self):
        '''this is a property doc'''
        return self.__opt1

help(TestClass.opt1)
# Help on property:

#     this is a property doc

这种情况下特性的帮助文档就是读操作方法的帮助文档。

好了,关于动态属性和特性的内容介绍完毕。

这部分内容难倒是不难,就是很繁琐细碎,我的老腰…

谢谢阅读,我要去歇一会了ORZ。

Python学习笔记36:动态属性和特性_第1张图片

你可能感兴趣的:(Python,python,动态属性,特性,元编程)