image.png属性作为类的重要组成部分,除了平时常用的读取和设置操作之外,还有很多隐藏的、高级的操作。比如属性的查找顺序、属性的类型检查、限制属性的动态添加等等。这一小节,就让我们深入理解属性的各种高级操作。

1、通过字符串操作属性和方法(反射)

通过反射我们可以把字符串映射到实例的属性或方法中,然后可直接进行调用、修改等操作,Python提供了四个函数,可以通过字符串操作属性和方法hasattr、getattr、setattr、delattr,需要注意属性和方法不能是私有的,如用_(下划线)开头的属性和方法,将无法对其进行操作。

  1. def hasattr(*args, **kwargs): # real signature unknown # hasattr(obj, 'x') ① 通过"字符串"判断对象的属性或方法是否存在。
  2. """
  3. Return whether the object has an attribute with the given name.
  4. This is done by calling getattr(obj, name) and catching AttributeError.
  5. """
  6. pass
  7. def getattr(object, name, default=None): # known special case of getattr ② 获取object对象的name属性
  8. """
  9. getattr(object, name[, default]) -> value
  10. Get a named attribute from an object; getattr(x, 'y') is equivalent to x.y. 相当于点操作符
  11. When a default argument is given, it is returned when the attribute doesn't
  12. exist; without it, an exception is raised in that case. 当name属性不存在时返回default,否则会引发AttributeError
  13. """
  14. pass
  15. def setattr(x, y, v): # real signature unknown; restored from __doc__ ③ 通过"字符串"动态设置对象的属性或方法。
  16. """
  17. Sets the named attribute on the given object to the specified value.
  18. setattr(x, 'y', v) is equivalent to ``x.y = v'' # 相当于点操作符
  19. """
  20. pass
  21. def delattr(x, y): # real signature unknown; restored from __doc__ ④ 通过"字符串"删除对象的属性或方法。
  22. """
  23. Deletes the named attribute from the given object.
  24. delattr(x, 'y') is equivalent to ``del x.y'' # 删除属性
  25. """
  26. pass

发现其实效果都是与点操作符一样的直接访问或删除属性

  1. class Input:
  2. def run(self, input):
  3. if hasattr(self, input): # 判断方法是否存在,然后调用相应的方法
  4. func = getattr(self, input)
  5. func()
  6. else:
  7. print('输入有误!')
  8. def sing(self):
  9. print("🎤~~~")
  10. def Swimming(self):
  11. print("游泳~~~~")
  12. if __name__ == '__main__':
  13. input = Input()
  14. input.run("sing")

上面通过反射调用了无参的方法,那有参方法该如何调用

  1. import sys
  2. def fun(name, age):
  3. print(name, age)
  4. if __name__ == '__main__':
  5. fun = getattr(sys.modules[__name__], 'fun')
  6. parameter_count = fun.__code__.co_argcount # 统计函数所需的参数个数
  7. v_list = [] # 创建一个空列表来存储参数值
  8. for i in range(0, parameter_count): # 遍历参数个数,赋值
  9. parameter = input('请输入第{}个参数:'.format(i + 1))
  10. v_list.append(parameter) # 讲值append进list中
  11. fun(*v_list) # 通过list拆包将参数传给带参函数

2、@property魔法

@property装饰器可以让调用者写出极短的代码,同时保证对参数进行必要的检查

2.1 实例方法变成同名属性

使用@property可以把一个实例方法变成其同名属性,以支持实例.号访问

  1. class A:
  2. def __init__(self, name:str):
  3. self.name = name
  4. @property # info本来是函数,使用@property装饰之后,使用info时可以像属性一样使用了
  5. def info(self):
  6. return "我的名字是"+ self.name
  7. if __name__ == "__main__":
  8. a = A("嘿嘿")
  9. print(a.name)
  10. print(a.info)

image.png
info定义成一个方法的形式,当加上@property后,可以通过a.info拿到该方法的返回值,不再需要用a.info()调用。

2.2 利用property校验属性的类型

Python中是没有无符号类型的,想要自己传给类的是一个正整数时,只能自己去校验,此时需要是要在方法上使用@属性名.setter装饰:

  1. class Student:
  2. def __init__(self, name, age):
  3. self._name = None # 设置为私有属性,避免在类的外部直接对属性设置和修改
  4. self._age = None
  5. @property
  6. def name(self):
  7. return self._name # 返回私有属性值
  8. @name.setter
  9. def name(self, value):
  10. if not isinstance(value, str): # isinstance检查value类型
  11. raise ValueError("Must be string")
  12. self._name = value # 设置私有属性值
  13. @property
  14. def age(self):
  15. return self._age
  16. @age.setter
  17. def age(self, value):
  18. if isinstance(value, int) and value > 0:
  19. self._age = value
  20. else:
  21. raise ValueError("Must be positive number")
  22. if __name__ == "__main__":
  23. # n1 = Student(12, 10)
  24. n2 = Student("Jim", -1)

实例化n2时,由于传递给类的age为负数,执行代码将会得到ValueError: Must be positive number异常,当使用@age.setter时,一定要先@property修改age属性

3、描述符

Django的models中通常会定义字段类型如:age=models.IntegerField(),会校验age设置的数据类型必须为Integer,使用描述符实现该功能

3.1 通过代码了解描述符

class StringPropertyDescriptor:
    def __init__(self, init_value=None):
        self.value = init_value

    def __get__(self, instance, cls): # 属性被访问时调用。instance是使用描述符实例的类的实例(student),cls就是这个类(Student)
        print('call __get__', instance, cls)
        if instance is None:
            return self
        else:
            return instance.__dict__[self.value]

    def __set__(self, instance, value): # 实例化类或者设置属性时,被调用
        print('call __set__', instance, value)
        if not isinstance(value, str):
            raise ValueError("Must be string")
        instance.__dict__[self.value] = value  # 使用实例字典操作

    def __delete__(self, instance): # 删除实例属性时,被调用
        print('call __set__', instance)
        del instance.__dict__[self.value]  # 使用实例字典操作


# 在这个类Student中使用描述符StringPropertyDescriptor
class Student:
    name = StringPropertyDescriptor()  # 【提醒!】Student的类属性设置成描述符的实例

    def __init__(self, name):
        self.name = name  # 【重要】类属性name赋值给实例属性name


if __name__ == '__main__':
    student = Student("小茗同学")  # 实例化Student类会调用__set__
    student.name = "xiaoming"  # 设置Student类name属性,会调用__set__
    print(Student.name)  # 访问Student类的类属性name,会调用__get__,返回描述符本身。
    print(student.name)  # 访问Student类的实例属性name,会调用Student.name.__get__(),返回"小茗同学"
    del student.name  # 删除Student类name属性,会调用__delete__
    student.name = 1000  # 输出ValueError("Must be string")
call __set__ <__main__.Student object at 0x7f97bd65eb90> 小茗同学
call __set__ <__main__.Student object at 0x7f97bd65eb90> xiaoming
call __get__ None <class '__main__.Student'>
<__main__.StringPropertyDescriptor object at 0x7f97bd384710>
call __get__ <__main__.Student object at 0x7f97bd65eb90> <class '__main__.Student'>
xiaoming
call __set__ <__main__.Student object at 0x7f97bd65eb90>
call __set__ <__main__.Student object at 0x7f97bd65eb90> 1000
Traceback (most recent call last):
  File "/Users/admin/PycharmProjects/pythonProject2/描述符.py", line 37, in <module>
    student.name = 1000  # 输出ValueError("Must be string")
  File "/Users/admin/PycharmProjects/pythonProject2/描述符.py", line 15, in __set__
    raise ValueError("Must be string")
ValueError: Must be string

当一个对象定义了set()或delete(),则被视为数据描述符,仅定义get()的描述符称为非数据描述符,描述符类中对实例的属性操作,都是通过属性字典dict进行的,从而实现对属性访问、修改和删除等额外操作。dict作为一个字典,完全独属于实例自己的属性,不包含从父类继承得到的属性。可以直接通过dict访问、设置、修改、删除属性,描述符类中通过instance.dict[self.value] = value 设置value属性,等效于通过类的对象实例self.value=value设置value属性。 student.name 等价StringPropertyDescriptor.get(name, student, Student)。
如果Student类增加一个address属性,也规定它必须是字符串,那么修饰Student类,增加代码比@property要容易

class Student:
    name = StringPropertyDescriptor("初始值")  # 【提醒!】Student的类属性设置成描述符的实例
    address = StringPropertyDescriptor("初始值")

    def __init__(self, name, address):
        self.name = name  # 【重要】类属性name赋值给实例属性name
        self.address = address

3.2 类装饰器实现StringPropertyDescriptor类的功能

class TypedAssertion:
    def __init__(self, name, expected_type):
        self.name = name
        self.expected_type = expected_type

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError('Expected ' + str(self.expected_type))
        instance.__dict__[self.name] = value

    def __delete__(self, instance):
        del instance.__dict__[self.name]

def type_decorator(**kwargs):
    def decorator(cls): # 对cls类进行装饰
        for name, expected_type in kwargs.items():
            # 给cls类添加属性类name,并给属性设置描述符实例
            setattr(cls, name, TypedAssertion(name, expected_type))
        return cls
    return decorator

if __name__ == '__main__':
    @type_decorator(brand=str, shares=int, price=float)
    class Stock:
        def __init__(self,brand, shares, price):
            self.brand = brand
            self.shares = shares
            self.price = price


    # s1 = Stock("nio", "14000", 10.2)
    s2 = Stock("nio", 14000, 10.2)

通过装饰器我们可以写出使描述符更加的优雅,不过不建议过度使用,如果只是对某个自定义的类的单个属性进行访问控制时,使用@property会更加直观和容易。关于描述符的Python官方文档请参考:https://docs.python.org/zh-cn/3/howto/descriptor.html

4、slots魔法

使用slots限制属性的动态添加功能,在创建大量对象的场合可以减少内存占用。
Python用一个字典来保存一个对象的实例属性,通过dict可以查看所有实例的属性,Python是动态语言,因此可以在代码运行时,设置任意的新属性,比如在实例化后给实例添加publisher属性

class Document:
    def __init__(self, title, author):
        self.title = title
        self.author = author

if __name__ == '__main__':
    doc = Document("title", "author")
    doc.publisher = "出版社"
    print(doc.publisher)

如何避免在运行时,设置任意的新属性,可以在类定义时,添加slots属性,规定这个类的对象能够包含的属性列表,当给类对象添加不在列表中的属性时,会报错。
在上面的类定义中增加添加slots属性,只能包含title、author属性。这样,在动态添加publisher属性时将会报AttributeError错误。不过, slot限制的是实例属性的添加,不限制类属性的添加。

class Document:

    __slots__ = ['title', 'author']

    def __init__(self, title, author):
        self.title = title
        self.author = author

if __name__ == '__main__':
    doc = Document("title", "author")
    doc.publisher = "出版社"
    print(doc.publisher)

对象的dict字典会占用大量内存,使用slot后,就不会存在dict了,在创建大量对象的场合就可以减少内存占用了,这也是设计slot的初衷。虽然可以减少内存的使用,但是我们也不用滥用它,除非你预计得到这个类会创建很多的实例,否则不要用它。因为当你用它之后,就不会存在实例属性的字典dict了,而Python的很多特性都依赖于这个字典实现的,因此可能会导致某些异常的问题。

5、属性的设置与读取底层原理

实例对象通过.点操作符进行属性的访问和设置。例如访问实例d的title属性:

d = Document("title", "author")
print(d.title)

点背后的原理是什么,为什么通过实例对象.属性名就可以拿到属性值

5.1 属性访问拦截器getattribute

访问类的属性时,其实都会经过当前类或父类的getattribute方法。此方法是属性访问拦截器。Python中只要定义的类继承了object的类,就默认存在属性拦截器,只不过是拦截后没有进行任何操作,而是直接返回。如果我们想在访问属性时做一些操作,如:查看权限,打印log日志,可以通过自定义类中改写getattribute方法来实现相关功能:

class Document:

    __slots__ = ['title', 'author']

    def __init__(self, title, author):
        self.title = title
        self.author = author

    def __getattribute__(self, item):
        print("属性拦截器,你正在访问属性{}".format(item))
        return super().__getattribute__(item)

if __name__ == '__main__':
    doc = Document("title", "author")
    print(doc.title)

5.2 属性访问拦截器getattr

Python中还提供了getattr,当访问某个对象不存在的属性时被调用。
通常当访问某一个对象不存在的属性时,getattribute会抛出AttributeError异常。但是,如果对象所属的类定义了getattr()方法后, 访问某一个对象不存在的属性时,则返回这个方法的返回值,避免了AttributeError异常。

class Student(object):

    def __init__(self):  # 只定义了name属性,没定义score属性
        self.name = 'mali'

    def __getattr__(self, attr):  # 当访问不存在的属性时,到这里找,比如找到score,则返回99,找不到则返回None
        print("遇到了一个不存在的属性")
        if attr == 'score':
            return 99


if __name__ == '__main__':
    s = Student()
    print(s.score)  # 99
    print(s.age)  # None
    print(s.name)
    print(s.__dict__)

代码执行结果

遇到了一个不存在的属性
99
遇到了一个不存在的属性
None
Michael
{'name': 'mali'}

可见当使用点号获取实例属性时,如果属性不存在就自动调用getattr方法。这里说的属性不存在说的是,属性name没有在实例instance的dict或它构造类的dict或者父类的dict中。当属性name可以通过正常机制追溯到时,getattr是不会被调用的。比如访问s.name就不会调用getattr方法。因为s.dict中包含这个属性:

{'name': 'mali'}

5.3 属性设置拦截器setattr

获取属性相对应的是设置属性,Python提供了setattr方法,当设置类实例属性时自动调用。

class Dict(dict):
    def __getattr__(self, key):
        try:
            print("调用原本没有定义的属性")
            return self[key]
        except KeyError:
            raise AttributeError(r"'Dict' object has no attribute '%s'" % key)

    def __setattr__(self, key, value):
        print("对实例的属性进行赋值")
        self.__dict__[key] = value  #【重要】不能直接给属性赋值,而通常的做法是使用__dict__属性。


d = Dict(a=1, b=2)  # 从类定义看,其实没有a,b这两个属性
print(d)
print(d['a'])  # 调用__getattr__
print(d.a) # 调用__getattr__
d.a = 100  # 对实例的属性进行赋值的时候调用__setattr__
print(d['a'])