属性作为类的重要组成部分,除了平时常用的读取和设置操作之外,还有很多隐藏的、高级的操作。比如属性的查找顺序、属性的类型检查、限制属性的动态添加等等。这一小节,就让我们深入理解属性的各种高级操作。
1、通过字符串操作属性和方法(反射)
通过反射我们可以把字符串映射到实例的属性或方法中,然后可直接进行调用、修改等操作,Python提供了四个函数,可以通过字符串操作属性和方法hasattr、getattr、setattr、delattr,需要注意属性和方法不能是私有的,如用_(下划线)开头的属性和方法,将无法对其进行操作。
def hasattr(*args, **kwargs): # real signature unknown # hasattr(obj, 'x') ① 通过"字符串"判断对象的属性或方法是否存在。"""Return whether the object has an attribute with the given name.This is done by calling getattr(obj, name) and catching AttributeError."""passdef getattr(object, name, default=None): # known special case of getattr ② 获取object对象的name属性"""getattr(object, name[, default]) -> valueGet a named attribute from an object; getattr(x, 'y') is equivalent to x.y. 相当于点操作符When a default argument is given, it is returned when the attribute doesn'texist; without it, an exception is raised in that case. 当name属性不存在时返回default,否则会引发AttributeError"""passdef setattr(x, y, v): # real signature unknown; restored from __doc__ ③ 通过"字符串"动态设置对象的属性或方法。"""Sets the named attribute on the given object to the specified value.setattr(x, 'y', v) is equivalent to ``x.y = v'' # 相当于点操作符"""passdef delattr(x, y): # real signature unknown; restored from __doc__ ④ 通过"字符串"删除对象的属性或方法。"""Deletes the named attribute from the given object.delattr(x, 'y') is equivalent to ``del x.y'' # 删除属性"""pass
发现其实效果都是与点操作符一样的直接访问或删除属性
class Input:def run(self, input):if hasattr(self, input): # 判断方法是否存在,然后调用相应的方法func = getattr(self, input)func()else:print('输入有误!')def sing(self):print("🎤~~~")def Swimming(self):print("游泳~~~~")if __name__ == '__main__':input = Input()input.run("sing")
上面通过反射调用了无参的方法,那有参方法该如何调用
import sysdef fun(name, age):print(name, age)if __name__ == '__main__':fun = getattr(sys.modules[__name__], 'fun')parameter_count = fun.__code__.co_argcount # 统计函数所需的参数个数v_list = [] # 创建一个空列表来存储参数值for i in range(0, parameter_count): # 遍历参数个数,赋值parameter = input('请输入第{}个参数:'.format(i + 1))v_list.append(parameter) # 讲值append进list中fun(*v_list) # 通过list拆包将参数传给带参函数
2、@property魔法
@property装饰器可以让调用者写出极短的代码,同时保证对参数进行必要的检查
2.1 实例方法变成同名属性
使用@property可以把一个实例方法变成其同名属性,以支持实例.号访问
class A:def __init__(self, name:str):self.name = name@property # info本来是函数,使用@property装饰之后,使用info时可以像属性一样使用了def info(self):return "我的名字是"+ self.nameif __name__ == "__main__":a = A("嘿嘿")print(a.name)print(a.info)

info定义成一个方法的形式,当加上@property后,可以通过a.info拿到该方法的返回值,不再需要用a.info()调用。
2.2 利用property校验属性的类型
Python中是没有无符号类型的,想要自己传给类的是一个正整数时,只能自己去校验,此时需要是要在方法上使用@属性名.setter装饰:
class Student:def __init__(self, name, age):self._name = None # 设置为私有属性,避免在类的外部直接对属性设置和修改self._age = None@propertydef name(self):return self._name # 返回私有属性值@name.setterdef name(self, value):if not isinstance(value, str): # isinstance检查value类型raise ValueError("Must be string")self._name = value # 设置私有属性值@propertydef age(self):return self._age@age.setterdef age(self, value):if isinstance(value, int) and value > 0:self._age = valueelse:raise ValueError("Must be positive number")if __name__ == "__main__":# n1 = Student(12, 10)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'])
