对于未接触过软件工程和编程范式的人来说,很容易写出这样的代码
from datetime import date, datetime
class User:
def __init__(self, name, birthday):
self.birthday = birthday
self.name = name
self.age = 2020 - birthday.year
some_user = User('Daniel',date(1995,1,1))
some_user.age
年龄是可以通过当前时间和生日动态计算出来的,重点在于属性的访问上,当代码作者意识到这种年龄计算的荒谬时很可能他已经写了很多some_user.age
在项目里了。如果这时新建方法get_age()
去替代原模式,对既有项目的破坏性会很大。
Python 对此提供了一个解决方法——动态属性。通过标记 @property
将函数标识为属性,通过标记@property_name.settr
动态修改属性。类比于依赖注入中的 get, set 方法。
from datetime import date, datetime
class User:
def __init__(self, name, birthday):
self.birthday = birthday
self.name = name
self._age = 0
@property
def age(self):
return datetime.now().year - self.birthday.year
@age.setter
def age(self, val):
self._age = val
some_user = User('Daniel', date(1995,1,1))
some_user.age
some_user.age = 18
print(some_user.age, some_user._age)
此时其他位置的.age
不用改动,通过动态属性对于age
的访问与修改,都可以自由设计逻辑了
这里对属性访问再多说一点。Python 中对象属性访问的入口是__getattribute__
方法,只要尝试访问某个属性就会被调用,无论是否真实存在。所以在实际编程中,除非特别确定否则不要覆盖__getattribute__
方法,容易破坏类的功能
class Student:
def __init__(self, name, std_id):
self.name = name
self.std_id = std_id
def __getattribute__(self, item):
return 'Hacked'
std = Student('Tom', 2002001)
print(std.name, std.std_id, std.class_id) # 访问入口被错误覆盖,无法正常访问
但属性访问不到确实是必须应对的问题,Python 对此提供了稍弱一点的方法__getattr__
,只有当属性访问失败时才会被调用,可以通过它来修正访问逻辑或调用格式
class Employee:
def __init__(self, name, info={}):
self.name = name
self.info = info
def __getattr__(self, item):
return self.info[item]
emp = Employee('Daniel',{'locate':'Queen Street','age':25})
print(emp.name, emp.age, emp.locate) # 错误访问被修正
动态属性可以方便我们设计访问和设置规则,但缺点是和具体属性是绑定在一起的,如果每个属性都添加一点类型检查逻辑,使用动态属性时会面临要编写大量重复代码,复用率很低。这时就要用到属性描述符了,将属性描述(类型检查,边界检查.....)独立封装
具体来说,Python 提供了两种属性描述符——数据描述符和非数据描述符。数据描述符要求类必须实现__get__
,__set__
,__delete__
中任意一个,非数据描述符要求实现__get__
,多用于一般函数方法上
import numbers
class IntField:
'''整数标识'''
__counter = 0
def __init__(self):
cls = self.__class__
prefix = cls.__name__
index = cls.__counter
self.storage_name = '_{}_{}'.format(prefix, index) # 保证每个描述符名称唯一
cls.__counter += 1
def __set__(self, instance, value):
if not isinstance(value, numbers.Integral):
print('Need integer')
elif value < 0:
print('Should be positive')
else:
setattr(instance, self.storage_name, value)
def __get__(self, instance, owner): # owner 为托管类
return getattr(instance, self.storage_name)
这里我们定义了一个整数的数据描述符。为了能在规定某个对象属性为整数时,既能做到类型绑定,也能对该绑定关系作出唯一标识。在初始化函数中我们构造了标识符字符串,storage_name
就是对象下的属性名称
__set__
的三个参数,self
是实例所属的类,instance
是该实例,value
是要设置的值,我们进行了类型、边界检查。setattr
是全局方法,等同于 obj.attr_name = val
运算
__get__
方法里,owner
是托管类,也就是使用了描述符此时想访问值的类。getattr
同是全局方法,等同于 obj.attr
class User:
age = IntField() # 将整型描述绑定到 age 属性上
def __init__(self, name, age):
self.age = age # 不用担心类型检查的问题
self.name = name
user = User('Daniel', 19)
user.age = 'abc' # 描述符限制,不会成功
user.__dict__ # 注意年龄的存储名称
有数据描述符时,属性访问规则发生了变化
如果user是某个类的实例,那么user.age
(以及等价的 getattr(user,'age')
)
首先调用__getattribute__
。如果类定义了__getattr__
方法,那么在__getattribute__
抛出AttributeError的时候就会调用到__getattr__
,而对于描述符__get__
的调用,则是发生在__getattribute__
内部的。
比如对于user=User()
,那么user.age
访问顺序如下:
__dict__
中,且age是数据描述符,那么调用其__get__
方法,否则__dict__
中,那么直接返回obj.dict__['age']
,否则__dict__
中__get__
方法,否则__dict__['age']
__getattr__
方法,调用__getattr__
方法,否则通常我们已经习惯了使用__init__
实例化对象,但实际执行过程中对象的创建分两步走:
__new__
创建对象,需要传入对象的类cls
(Python 解析器默认执行),期间可以自行设计修改创立逻辑,最后将对象返回__init__
初始化对象,如果__new__
没有返回对象则不执行class Build:
def __new__(cls, *arg, **kw):
print('Hacked')
return
def __init__(self):
print('Success')
build = Build()
class OtherBuild:
def __new__(cls, *arg, **kw):
print('Build object')
return super().__new__(cls)
def __init__(self):
print('Initialize object')
otherbuild = OtherBuild()
之前说到过 Python 里一切皆对象,那类本身作为对象是被谁创建的呢?答案是元类,Python 中用metaclass
指定元类,而 type 正是元类之一。所以创建类的一般过程如下:
metaclass
,那就使用该元类创建当前类,否则metaclass
,找到了就用相应元类创建当前类,否则type
创建当前类的类对象,全局唯一type
方法接收参数个数不同,行为不同。当使用 type(name, bases, dict)
形式时,会动态创建一个类出来,bases
是元组,存储基类,dict
字典存放属性和方法声明
def say(self): # 不要忘了 self 参数
print('Hello, I am created by type')
user = type('User', (), {'name':'abc','say':say}) # 和 class 创建 User 效果相同
print(user)
user().say()
有了元类就可以制定类的生成规则了,比如注入一些方法,规定必须实现的抽象方法等等,工程实践可以参照 Django 框架的 orm 源码。要注意元类在开发过程中不是必须的。
class Meta(type):
def __new__(cls, *args, **kwds):
print('Build class')
print(args,kwds)
return super().__new__(cls, *args, **kwds)
class Son(metaclass=Meta):
pass