引言
元类(metaclass)能够拦截Python的class语句,让系统每次定义类的时候,都能实现某些特殊的行为。 Python还内置了一种神奇而强大的特性,可以动态地定制属性访问操作。
#44 用纯属性与修饰器取代旧式的setter与getter方法
从其他编程语言转入Python的开发者,可能想在类里面明确地实现getter与setter方法。
class OldResistor:
def __init__(self, ohms):
self._ohms = ohms
def get_ohms(self):
return self._ohms
def set_ohms(self, ohms):
self._ohms = ohms
虽然这些setter与getter用起来很简单,但这并不符合Python的风格。
r0 = OldResistor(50e3)
print('Before:', r0.get_ohms())
r0.set_ohms(10e3)
print('After: ', r0.get_ohms())
Before: 50000.0
After: 10000.0
例如,想让属性值变大或变小,采用这些方法来写会特别麻烦。
r0.set_ohms(r0.get_ohms() - 4e3)
assert r0.get_ohms() == 6e3
在Python中没有必要明确定义setter与getter方法。而是应该从最简单的public属性开始写起,例如像下面这样:
class Resistor:
def __init__(self, ohms):
self.ohms = ohms
self.voltage = 0
self.current = 0
r1 = Resistor(50e3)
r1.ohms = 10e3
这样就很容易实现原地增减属性值。
r1.ohms += 5e3
将来如果想在设置属性时,实现特别的功能,那么可以先通过@property 修饰器来封装获取属性的那个方法,并在封装出来的修饰器上面通过setter属性来封装设置属性的那个方法。下面这个新类继承自刚才的Resistor 类,它允许我们通过设置voltage(电压)来改变current(电流)。为了正确实现这项功能,必须保证设置属性与获取属性所用的那两个方法都跟属性同名。
class VoltageResistance(Resistor):
def __init__(self, ohms):
super().__init__(ohms)
self._voltage = 0
@property
def voltage(self):
return self._voltage
@voltage.setter
def voltage(self, voltage):
self._voltage = voltage
self.current = self._voltage / self.ohms
按照这种写法,给voltage属性赋值会触发同名的setter方法,该方法会根据新的voltage计算本对象的current属性。
r2 = VoltageResistance(1e3)
print(f'Before: {r2.current:.2f} amps')
r2.voltage = 10
print(f'After: {r2.current:.2f} amps')
Before: 0.00 amps
After: 0.01 amps
为属性指定setter方法还可以用来检查调用方所传入的值在类型与范围上是否符合要求。例如,下面这个Resistor 子类可以确保用户设置的电阻值总是大于0的。
class BoundedResistance(Resistor):
def __init__(self, ohms):
super().__init__(ohms)
@property
def ohms(self):
return self._ohms
@ohms.setter
def ohms(self, ohms):
if ohms <= 0:
raise ValueError(f'ohms must be > 0; got {ohms}')
self._ohms = ohms
给这个类设置无效电阻值,程序会抛出异常。
r3 = BoundedResistance(1e3)
r3.ohms = 0
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-11-4faf6805d103> in <module>
1 r3 = BoundedResistance(1e3)
----> 2 r3.ohms = 0
<ipython-input-10-5ad6e7a3c812> in ohms(self, ohms)
10 def ohms(self, ohms):
11 if ohms <= 0:
---> 12 raise ValueError(f'ohms must be > 0; got {ohms}')
13 self._ohms = ohms
ValueError: ohms must be > 0; got 0
如果构造时所用的值无效,那么同样会触发异常。
BoundedResistance(-5)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-12-9fc5cf3426e1> in <module>
----> 1 BoundedResistance(-5)
<ipython-input-10-5ad6e7a3c812> in __init__(self, ohms)
1 class BoundedResistance(Resistor):
2 def __init__(self, ohms):
----> 3 super().__init__(ohms)
4
5 @property
<ipython-input-4-47bf6a706de5> in __init__(self, ohms)
1 class Resistor:
2 def __init__(self, ohms):
----> 3 self.ohms = ohms
4 self.voltage = 0
5 self.current = 0
<ipython-input-10-5ad6e7a3c812> in ohms(self, ohms)
10 def ohms(self, ohms):
11 if ohms <= 0:
---> 12 raise ValueError(f'ohms must be > 0; got {ohms}')
13 self._ohms = ohms
ValueError: ohms must be > 0; got -5
之所以会出现这种效果,是因为子类的构造器(BoundedResistance.__init__ )会调用超类的构造器(Resistor.__init__ ),而超类的构造器会把self.ohms 设置成-5 。 于是就会触发BoundedResistance 里面的@ohms.setter 方法,该方法立刻发现属性值无效,所以程序在对象还没有构造完之前,就会抛出异常。
我们还可以利用@property 阻止用户修改超类中的属性。
class FixedResistance(Resistor):
def __init__(self, ohms):
super().__init__(ohms)
@property
def ohms(self):
return self._ohms
@ohms.setter
def ohms(self, ohms):
if hasattr(self, '_ohms'):
raise AttributeError('Ohms is immutable')
self._ohms = ohms
构造好对象之后,如果试图给属性赋值,那么程序就会抛出异常。
r4 = FixedResistance(1e3)
r4.ohms = 2e3
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-14-7fbedb722fb3> in <module>
1 r4 = FixedResistance(1e3)
----> 2 r4.ohms = 2e3
<ipython-input-13-ba849a43c974> in ohms(self, ohms)
10 def ohms(self, ohms):
11 if hasattr(self, '_ohms'):
---> 12 raise AttributeError('Ohms is immutable')
13 self._ohms = ohms
AttributeError: Ohms is immutable
用@property 实现setter和getter时,还应该注意不要让对象产生反常行为。例如,不要在某属性的getter方法里面设置其他属性的值。
class MysteriousResistor(Resistor):
@property
def ohms(self):
self.voltage = self._ohms * self.current
return self._ohms
@ohms.setter
def ohms(self, ohms):
self._ohms = ohms
假如在获取属性的getter方法里修改了其他属性的值,那么用户查询这个属性时,就会觉得相当奇怪。无法理解为什么另外一个属性会在他查询这个属性时发生变换。
r7 = MysteriousResistor(10)
r7.current = 0.01
print(f'Before: {r7.voltage:.2f}')
r7.ohms
print(f'After: {r7.voltage:.2f}')
Before: 0.00
After: 0.10
最好的办法是,只在@property.setter 方法里面修改状态,而且只应该修改对象之中与当前属性有关的状态。同时还要注意不要产生让调用者感到意外的一些副作用,例如,不要动态地引入模块,不要运行速度较慢的辅助函数,不要做I/O等等。 类的属性用起来应该跟其他Python对象一样方便切快捷。如果确实要执行比较复杂或比较缓慢的操作,那么应该用普通的方法来做,而不是应该把这些操作放在获取及设置属性的这两个方法里面。
@property 最大的缺点是,通过它而编写的获取即属性设置方法只能由子类共享。与此无关的类不能共用这份逻辑。但是没关系,Python还支持描述符,我们可以利用这种机制把早前编写的属性获取与属性设置逻辑复用到其他许多地方。
#45 考虑用@property实现新的属性访问逻辑,不要急着重构原有的代码
Python内置的@property 修饰器使开发者很容易就能实现出灵活的逻辑,它还有一种更为高级的用法,也很常见。就是把简单的数值属性迁移成那种实时计算的属性。这个用法可以确保,按照旧写法来访问属性的代码依然有效。 @property 可以说是一种重要的缓冲机制,使开发者能够逐渐改善接口而不影响已经写好的代码。
例如,下面我们用普通的Python对象实现带有配额(quota)的漏桶(leaky bucket)。这个类可以记录当前的配额以及这份配额在多才时间内有效。
from datetime import datetime, timedelta
class Bucket:
def __init__(self, period):
self.period_delta = timedelta(seconds=period)
self.reset_time = datetime.now()
self.quota = 0
def __repr__(self):
return f'Bucket(quota={self.quota})'
漏桶算法要求在添加配额时,不能把已有的额度带到下一个时段。
def fill(bucket, amount):
now = datetime.now()
if (now - bucket.reset_time) > bucket.period_delta:
bucket.quota = 0
bucket.reset_time = now
bucket.quota += amount
如果想使用额度,那么首先必须确保漏桶当前所剩的配额是足够用的。
def deduct(bucket, amount):
now = datetime.now()
if (now - bucket.reset_time) > bucket.period_delta:
return False
if bucket.quota - amount < 0:
return False
bucket.quota -= amount
return True
现在我们来使用这个类。首先填额度:
bucket = Bucket(60)
fill(bucket, 100)
bucket
Bucket(quota=100)
然后根据自己的需要使用额度:
if deduct(bucket, 99):
print('Had 99 quota')
else:
print('Not enough for 99 quota')
bucket
Had 99 quota
Bucket(quota=1)
这样用下去,最终会遇到额度不够的情况。从这时开始,额度就不会再变了。
if deduct(bucket, 3):
print('Had 3 quota')
else:
print('Not enough for 3 quota')
bucket
Not enough for 3 quota
Bucket(quota=1)
这种实现方式有个问题,就是没办法知道第一次填充漏桶时,给它分配的额度。我们只知道额度会越用越少直到不够位置。 如果当前这段时间内的额度已经降到0,那么不管你想使用多少额度,deduct 函数都会返回False ,除非通过fill 函数再往里面补充额度。 所以,当dedcut 函数返回False 时,了解这究竟是因为Bucket 没有足够的额度可以扣减,还是说它一开始根本就没有分配到任何额度,很重要。
为了解决这个问题,可以修改这个类,把当前时间段内的初始额度与已经使用的额度明确记录下来。
class NewBucket:
def __init__(self, period):
self.period_delta = timedelta(seconds=period)
self.reset_time = datetime.now()
self.max_quota = 0
self.quota_consumed = 0
def __repr__(self):
return (f'NewBucket(max_quota={self.max_quota}) ', f'quota_consumed={self.quota_consumed}')
@property
def quota(self):
return self.max_quota - self.quota_consumed
同时,为了让用户能像使用原来的Bucket 类那样使用这个新类。我们下面用这个@property 方法根据刚才设计的那两个属性实时计算漏桶目前的水位。
class NewBucket:
def __init__(self, period):
self.period_delta = timedelta(seconds=period)
self.reset_time = datetime.now()
self.max_quota = 0
self.quota_consumed = 0
def __repr__(self):
return (f'NewBucket(max_quota={self.max_quota}) ', f'quota_consumed={self.quota_consumed}')
@property
def quota(self):
return self.max_quota - self.quota_consumed
然后,我们实现下面这个方法,用来处理quota属性的赋值操作。采用旧式的fill 与deduct 函数来增减额度的那些代码依然可以正常运作,因为那两个函数在修改额度时会触发这个新方法,笔者在的代码里对相关情况做了特殊处理。
class NewBucket:
def __init__(self, period):
self.period_delta = timedelta(seconds=period)
self.reset_time = datetime.now()
self.max_quota = 0
self.quota_consumed = 0
def __repr__(self):
return (f'NewBucket(max_quota={self.max_quota}) ', f'quota_consumed={self.quota_consumed}')
@property
def quota(self):
return self.max_quota - self.quota_consumed
@quota.setter
def quota(self, amount):
delta = self.max_quota - amount
if amount == 0 :
self.quota_consumed = 0
self.max_quota = 0
elif delta < 0:
assert self.quota_consumed == 0
self.max_quota = amount
else:
assert amount > 0
self.quota_consumed = delta
按照旧的用法来使用新的漏桶,依然可以得到正确的结果。
bert = NewBucket(60)
print('Initial', bucket)
fill(bucket, 100)
print('Filled', bucket)
if deduct(bucket, 99):
print('Had 99 quota')
else:
print('Not enough for 99 quota')
print('Now', bucket)
if deduct(bucket, 3):
print('Had 3 quota')
else:
print('Not enough for 3 quota')
print('Still', bucket)
Initial Bucket(quota=1)
Filled Bucket(quota=101)
Had 99 quota
Now Bucket(quota=2)
Not enough for 3 quota
Still Bucket(quota=2)
这个方案的最大好处是,原来根据Bucket.quota 所写的那些代码可以继续沿用,而且无须考虑Bucket 现在已经换成了新的NewBucket 。
#46 用描述符来改写需要复用的@property方法
Python内置的@property 机制的最大确点就是不方便复用。例如,我们要编写一个类来记录学生的家庭作业成绩,而且要确保设置的成绩位于0到100之间。
class Homework:
def __init__(self):
self._grade = 0
@property
def grade(self):
return self._grade
@grade.setter
def grade(self, value):
if not (0 <= value <= 100):
raise ValueError(f'Grade must be betwwen 0 and 100')
self._grade = value
受@property 修饰的属性用起来很简单。
galileo = Homework()
galileo.grade = 95
假设,我们还需要写一个类记录学生的考试成绩,而且要把每科的成绩分别记录下来。
class Exam:
def __init__(self):
self._writing_grade = 0
self._math_grade = 0
@staticmethod
def _check_grade(value):
if not (0 <= value <= 100):
raise ValueError(f'Grade must be betwwen 0 and 100')
这样很麻烦,因为每科的成绩都需要一套@property 方法,而且其中设置属性值的那个方法还必须调用_check_grade 验证新值。
class Exam:
def __init__(self):
self._writing_grade = 0
self._math_grade = 0
@staticmethod
def _check_grade(value):
if not (0 <= value <= 100):
raise ValueError(f'Grade must be betwwen 0 and 100')
@property
def writing_grade(self):
return self._writing_grade
@writing_grade.setter
def writing_grade(self, value):
self._check_grade(value)
self._writing_grade = value
@property
def math_grade(self):
return self._math_grade
@math_grade.setter
def math_grade(self, value):
self._check_grade(value)
self._math_grade = value
这样写不仅麻烦,而且无法复用。 在Python里,这样的功能最好通过描述符(descriptor)实现。描述符协议规定了程序应该如何处理属性访问操作。充当描述符的那个类能够实现__get__ 与__set__ 方法,这样其他类就可以共用这个描述符所实现的逻辑而无须把这套逻辑分别重写一遍。
下面重新定义Exam 类,这次我们采用类级别的属性来实现每科成绩的访问功能,这些属性指向下面这个Grade 类的实例,而这个Grade 类则实现刚才提到的描述符协议。
class Grade:
def __get__(self, instance, instance_type):
pass
def __set__(self, instance, value):
pass
class Exam:
math_grade = Grade()
writing_grade = Grade()
science_grade = Grade()
在解释Grade 类的工作原理之前,我们首先要知道,当程序访问Exam 实例的某个属性时,Python如何将访问操作派发到Exam 类的描述符属性上面。例如,如果要给Exam 实例的writing_grade 属性赋值:
exam = Exam()
exam.writing_grade = 40
那么,Python会把这次赋值操作转译为:
Exam.__dict__['writing_grade'].__set__(exam, 40)
获取这个属性时也一样:
exam.writing_grade
Python会转译为:
Exam.__dict__['writing_grade'].__get__(exam, Exam)
这样的转译效果是由object 的__getattribute__ 方法促成的。简单地说,就是当Eaxm 实例里面没有名为writing_grade 的属性时,Python会转而在类的层面查找,查询Eaxm 类里面有没有这样一个属性。如果有,而且还实现了__get__ 与__set__ 方法后,那么系统就认定你想通过描述符协议定义的这个属性的访问行为。
知道了这条规则之后,我们来尝试把Homework 类早前用@property 实现的成绩验证逻辑搬到Grade 描述符里面。
class Grade:
def __init__(self):
self._value = 0
def __get__(self, instance, instance_type):
return self._value
def __set__(self, instance, value):
if not (0 <= value <= 100):
raise ValueError(f'Grade must be betwwen 0 and 100')
self._value = value
这样写其实不对,而且会让程序出现混乱。但在同一个Exam 实例上面访问不同的属性是没有问题的。
class Exam:
math_grade = Grade()
writing_grade = Grade()
science_grade = Grade()
first_exam = Exam()
first_exam.writing_grade = 82
first_exam.science_grade = 99
print('Writing', first_exam.writing_grade)
print('Science', first_exam.science_grade)
Writing 82
Science 99
但是,在不同的Exam 实例上分别访问同一个属性却会看到奇怪的结果。
second_exam = Exam()
second_exam.writing_grade = 75
print(f'Second {second_exam.writing_grade} is right')
print(f'First {first_exam.writing_grade} is wrong;'
f'should be 82')
Second 75 is right
First 75 is wrong;should be 82
出现这个问题的原因在于,这些Eaxm 实例之中的writing_grade 属性实际上是在共享同一个Grade 实例。 为了解决此问题,我们必须把每个Exam 实例在这个属性上面的取值都记录下来。可以通过字典实现每个实例的状态保存。
class Grade:
def __init__(self):
self._values = {}
def __get__(self, instance, instance_type):
if instance is None:
return self
return self._values.get(instance, 0)
def __set__(self, instance, value):
if not (0 <= value <= 100):
raise ValueError(f'Grade must be betwwen 0 and 100')
self._values[instance] = value
这种实现方案很简单,而且能得到正确结果,但仍然有一个缺陷,就是会泄露内存。
为了解决这个问题,我们可以求助于Python内置的weakref 模块。该模块里有一种特殊的字典,名为WeakKeyDictionary ,它可以取代刚才实现_values 时所用的普通字典。 这个字典的特殊之处在于:如果运行时系统发现,指向Eaxm 实例的引用只剩一个,而这个引用又是由WeakKeyDictionary 的键所发起的,那么系统会将该引用从这个特殊的字典里删掉,于是指向那个Exam 实例的引用数量就会降为0。
from weakref import WeakKeyDictionary
class Grade:
def __init__(self):
self._values = WeakKeyDictionary()
def __get__(self, instance, instance_type):
if instance is None:
return self
return self._values.get(instance, 0)
def __set__(self, instance, value):
if not (0 <= value <= 100):
raise ValueError(f'Grade must be betwwen 0 and 100')
self._values[instance] = value
用这种字典改写Grade 描述符之后,Exam 就能正常运作了。
class Exam:
math_grade = Grade()
writing_grade = Grade()
science_grade = Grade()
first_exam = Exam()
first_exam.writing_grade = 82
second_exam = Exam()
second_exam.writing_grade = 75
print(f'First {first_exam.writing_grade} is right')
print(f'Second {second_exam.writing_grade} is right')
First 82 is right
Second 75 is right
#47 针对惰性属性使用__getattr__、getattribute__及__setattr
假设我们想把数据库中的记录表示为Python对象,数据库有它自己的模式(schema),而程序在把记录表示成对象时,必须知道数据库是按照什么样的模式来组织这些记录的。
这种动态的行为可以通过名为__getattr__ 的特殊方法来实现。如果类中定义了此方法,那么每当访问该类对象的属性,而且实例字典里又找不到这个属性时,系统就会触发__getattr__ 方法。
class LazyRecord:
def __init__(self):
self.exists = 5
def __getattr__(self, name):
value = f'Value for {name}'
setattr(self, name, value)
return value
data = LazyRecord()
print('Before: ', data.__dict__)
print('foo: ',data.foo)
print('After: ', data.__dict__)
Before: {'exists': 5}
foo: Value for foo
After: {'exists': 5, 'foo': 'Value for foo'}
下面我们通过子类给LazyRecord增加日志功能,用来观察程序在什么样的情况下才会调用__getattr__ 方法。
class LoggingLazyRecord(LazyRecord):
def __getattr__(self, name):
print(f'* Called __getattr__({name!r}), populating instance dictionary')
result = super().__getattr__(name)
print(f'* Returning {result!r}')
return result
data = LoggingLazyRecord()
print('exists: ', data.exists)
print('Fisrt foo: ', data.foo)
print('Second foo: ', data.foo)
exists: 5
* Called __getattr__('foo'), populating instance dictionary
* Returning 'Value for foo'
Fisrt foo: Value for foo
Second foo: Value for foo
exists 属性本来就在实例字典里,所以不会触发__getattr__ 。接下来,访问data.foo 。foo 属性不在实例字典里,因此触发了该方法。并把foo 属性设置到字典里。 第二次访问的时候,已经包含这个属性,就不会触发__getattr__ 。
假设我们还需要验证数据库系统的事务状态。即,用户每次访问某属性时,我们都要确保数据库里面的那条记录依然有效,而且相应的事务也处在开启状态。这个需求没有办法通过__getattr__ 实现,因为一旦对象的实例字典里包含了这个属性,那么程序就会直接从字典获取,而不会再触发__getattr__ 。
为了应对这种比较高级的用法,Python的object还提供了另一个挂钩,叫做__getattribute__ 。只要访问对象中的属性,就会触发这个方法。 于是,我们可以在这个方法里面检测全局的事务状态,但值得注意的是,这种写法开销很大。 下面就定义ValidtingRecord 类,让它实现我们说的方法,并在系统每次调用这个方法时,打印相关的日志消息。
class ValidatingRecord:
def __init__(self):
self.exists = 5
def __getattribute__(self, name):
print(f'* Calaled __getattribute__({name!r})')
try:
value = super().__getattribute__(name)
print(f'* Found {name!r}, returing {value!r}')
return value
except AttributeError:
value = f'Value for {name}'
print(f'* Setting {name!r} to {value!r}')
setattr(self, name, value)
return value
data = ValidatingRecord()
print('exists: ', data.exists)
print('Fisrt foo: ', data.foo)
print('Second foo: ', data.foo)
* Calaled __getattribute__('exists')
* Found 'exists', returing 5
exists: 5
* Calaled __getattribute__('foo')
* Setting 'foo' to 'Value for foo'
Fisrt foo: Value for foo
* Calaled __getattribute__('foo')
* Found 'foo', returing 'Value for foo'
Second foo: Value for foo
如果要访问的属性根本就不应该存在,那么可以在__getattr__ 方法里面拦截。无论是__getattr__ 还是__getattribute__ ,都应该抛出标准的AttributeError 表示属性不存在,或不适合存在的情况。
class MissingPropertyRecord:
def __getattr__(self, name):
if name == 'bad_name':
raise AttributeError(f'{name} is missing')
data = MissingPropertyRecord()
data.bad_name
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
/var/folders/h5/58f251ws7bsfdgld4kgcv_pr0000gn/T/ipykernel_3032/1713336196.py in <module>
5
6 data = MissingPropertyRecord()
----> 7 data.bad_name
/var/folders/h5/58f251ws7bsfdgld4kgcv_pr0000gn/T/ipykernel_3032/1713336196.py in __getattr__(self, name)
2 def __getattr__(self, name):
3 if name == 'bad_name':
----> 4 raise AttributeError(f'{name} is missing')
5
6 data = MissingPropertyRecord()
AttributeError: bad_name is missing
在编写通用的Python代码时,我们经常需要依靠内置的hasattr 函数判断属性是否存在,并通过内置的getattr 函数获取属性值。 这些函数会先在实例的__dict__ 字典里面查找,如果找不到,则会触发__getattr__ 。
data = LoggingLazyRecord()
print('Before: ', data.__dict__)
print('Has first foo: ', hasattr(data, 'foo'))
print('After: ',data.__dict__)
print('Has second foo: ', hasattr(data, 'foo'))
Before: {'exists': 5}
* Called __getattr__('foo'), populating instance dictionary
* Returning 'Value for foo'
Has first foo: True
After: {'exists': 5, 'foo': 'Value for foo'}
Has second foo: True
在运行上面那段代码的过程中,__getattr__ 只触发了一次。 如果实现的是__getattribute__ 方法,那么效果就不一样了,程序每次对实例做hasattr 与getattr 操作时,都会触发这个方法。
data = ValidatingRecord()
print('Has first foo: ', hasattr(data, 'foo'))
print('Has second foo: ', hasattr(data, 'foo'))
* Calaled __getattribute__('foo')
* Setting 'foo' to 'Value for foo'
Has first foo: True
* Calaled __getattribute__('foo')
* Found 'foo', returing 'Value for foo'
Has second foo: True
假设程序给Python对象赋值时,我们不想立刻更新数据库。这个功能可以通过__setattr__ 实现,它也是object提供的挂钩,可以拦截所有的属性赋值操作。 属性的赋值操作只需要这一个挂钩就行。只要给实例中的属性赋值,就会触发该方法。
class SavingRecord:
def __setattr__(self, name, value):
super().__setattr__(name, value)
下面我们从上面这个类中派生一个子类,让它的__setattr__ 方法把每一次属性赋值操作都记录下来。
class LoggingSavingRecord(SavingRecord):
def __setattr__(self, name, value):
print(f'* Called __setattr__({name!r}, {value!r})')
super().__setattr__(name, value)
data = LoggingSavingRecord()
print('Before: ', data.__dict__)
data.foo = 5
print('After: ', data.__dict__)
data.foo = 7
print('Finally: ', data.__dict__)
Before: {}
* Called __setattr__('foo', 5)
After: {'foo': 5}
* Called __setattr__('foo', 7)
Finally: {'foo': 7}
__getattribute__ 与__setattr__ 这样的方法有个问题,就是只要访问对象的属性,就会触发该方法。有时候,我们不希望出现这种效果。例如,我们想实现这样一个类,让它通过自制的字典而不是标准的__dict__ 来保存属性,当在这个类的实例上面访问属性时,那么该实例会从自己的_data 字典里面查找。
class BrokenDictionaryRecord:
def __init__(self, data):
self._data = {}
def __getattribute__(self, name):
print(f'* Called __getattribute__({name!r})')
return self._data[name]
可惜,这样就导致__getattribute__ 方法必须访问self._data 才行。如果直接访问,那么程序会一直递归下去,直到崩溃。
data = BrokenDictionaryRecord({'foo': 3})
因为在__getattribute__ 访问self._data 时,由于_data 是自身的一项属性,程序会触发__getattribute__ 来获取这项属性,这又会访问到self._data ,于是程序就一直递归下去。
为了解决这个问题,我们可以改用super().__getattribute__ 方法获取_data 属性,由于超类的__getattribute__ 是直接从实例的属性字典获取的,不会继续触发__getattribute__ ,这样就避开了递归。
class DictionaryRecord:
def __init__(self, data):
self._data = data
def __getattribute__(self, name):
print(f'* Called __getattribute__({name!r})')
data_dict = super().__getattribute__('_data')
return data_dict[name]
data = DictionaryRecord({'foo': 3})
print('foo: ', data.foo)
* Called __getattribute__('foo')
foo: 3
在__setattr__ 里面为这种对象实现属性修改逻辑时,也需要通过super().__setattr__ 来获取_data 字典。
#48 用__init__subclass__验证子类写得是否正确
元类最简单的一种用法是验证某个类定义得是否正确。元类提供了一套可靠的手段,只要根据这个元类来定义新类,就能用元类中的验证逻辑核查新类的代码写得是否正确。
一般来说,我们会在__init__ 方法里面检查新对象构造得是否正确。但有时,我们想尽早拦住这个错误。例如,当程序刚刚启动并把包含这个类的模块加载进来时,我们就想验证这个类写得对不对,此时便可利用元类来实现。
在讲解如何用自定义的元类验证子类之前,我们首先必须明白元类的标准用法。元类应该从type 之中继承。 在默认情况下,系统会把通过这个元类所定义的其他类发送给元类的__new__ 方法,让该方法知道那类的class语句时怎么写的。 下面就定义这样一个元类,如果用户通过这个元类来定义其他类,那么在那个类真正构造出来之前,我们可以先在__new__ 里面观察到它的写法并做出修改。
class Meta(type):
def __new__(meta, name, bases, class_dict):
print(f"* Running {meta}.__new__ for {name}")
print("Base:", bases)
print(class_dict)
return type.__new__(meta, name, bases, class_dict)
class MyClass(metaclass=Meta):
stuff = 123
def foo(self):
pass
class MySubclass(MyClass):
other = 567
def bar(self):
pass
* Running <class '__main__.Meta'>.__new__ for MyClass
Base: ()
{'__module__': '__main__', '__qualname__': 'MyClass', 'stuff': 123, 'foo': <function MyClass.foo at 0x7f7a308e5e50>}
* Running <class '__main__.Meta'>.__new__ for MySubclass
Base: (<class '__main__.MyClass'>,)
{'__module__': '__main__', '__qualname__': 'MySubclass', 'other': 567, 'bar': <function MySubclass.bar at 0x7f7a30b7b040>}
元类可以获知那个类的名称(name),类的所有超类(bases)以及class语句体中定义的所有类属性(class_dict)。 我们可以在元类的__new__ 方法里面添加一些代码,用来判断这个元类所定义的类的各项参数是否合理。例如,要用不同的类来表示边数不同的多边形。 如果把这些类都纳入同一套体系,那么可以定义这样一个元类,让该体系内的所有类都受它约束。
class ValidatePolygon(type):
def __new__(meta, name, bases, class_dict):
if bases:
if class_dict['sides'] < 3:
raise ValueError('Polygons need 3+ sides')
return type.__new__(meta, name, bases, class_dict)
class Polygon(metaclass=ValidatePolygon):
sides = None
@classmethod
def interior_angles(cls):
return (cls.sides - 2) * 180
class Triangle(Polygon):
sides = 3
class Rectangle(Polygon):
sides = 4
class Nonagon(Polygon):
sides = 9
assert Triangle.interior_angles() == 180
assert Rectangle.interior_angles() == 360
assert Nonagon.interior_angles() == 1260
如果试着定义边数小于3的多边形子类,那么刚把那个子类的class语句体写完,元类就会通过__new__ 方法察觉到这个问题。 这意味着,只要定义了无效的多边形子类,程序就无法正常启动。
print('Before class')
class Line(Polygon):
print('Before sides')
sides = 2
print('After sides')
print('After class')
Before class
Before sides
After sides
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
/var/folders/h5/58f251ws7bsfdgld4kgcv_pr0000gn/T/ipykernel_3997/1619289719.py in <module>
1 print('Before class')
2
----> 3 class Line(Polygon):
4 print('Before sides')
5 sides = 2
/var/folders/h5/58f251ws7bsfdgld4kgcv_pr0000gn/T/ipykernel_3997/504365032.py in __new__(meta, name, bases, class_dict)
4 if bases:
5 if class_dict['sides'] < 3:
----> 6 raise ValueError('Polygons need 3+ sides')
7
8 return type.__new__(meta, name, bases, class_dict)
ValueError: Polygons need 3+ sides
但是这种写法还是有点啰嗦。Python3.6引入了一种简化的写法,能够直接通过__init__subclass__ 这个特殊的类方法实现相同的功能,这样就用专门定义元类了。
class BetterPolygon:
sides = None
def __init_subclass__(cls):
super().__init_subclass__()
if cls.sides < 3:
raise ValueError('Polygons need 3+ sides')
@classmethod
def interior_angles(cls):
return (cls.sides - 2) * 180
class Hexagon(BetterPolygon):
sides = 6
assert Hexagon.interior_angles() == 720
现在代码简短多了。在__init_subclass__ 方法里面,我们直接通过cls 实例来访问类级别的sides 属性。
print('Before class')
class Point(BetterPolygon):
sides = 1
print('After class')
Before class
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
/var/folders/h5/58f251ws7bsfdgld4kgcv_pr0000gn/T/ipykernel_3997/3548689399.py in <module>
1 print('Before class')
2
----> 3 class Point(BetterPolygon):
4 sides = 1
5
/var/folders/h5/58f251ws7bsfdgld4kgcv_pr0000gn/T/ipykernel_3997/2055010121.py in __init_subclass__(cls)
5 super().__init_subclass__()
6 if cls.sides < 3:
----> 7 raise ValueError('Polygons need 3+ sides')
8
9 @classmethod
ValueError: Polygons need 3+ sides
但是这样实现有一个缺点,就是每个类只能定义一个元类。
class ValidateFilled(type):
def __new__(meta, name, bases, class_dict):
if bases:
if class_dict['color'] not in ('red', 'green'):
raise ValueError('Fill color must be supported')
return type.__new__(meta, name, bases, class_dict)
class Filled(metaclass=ValidateFilled):
color = None
如果想同时利用Filled 的元类与Polygon 元类做验证,那么程序就会给出奇怪的错误消息。
class RedPentagon(Filled, Polygon):
color = 'red'
sides = 5
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
/var/folders/h5/58f251ws7bsfdgld4kgcv_pr0000gn/T/ipykernel_3997/1182594629.py in <module>
----> 1 class RedPentagon(Filled, Polygon):
2 color = 'red'
3 sides = 5
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
要解决这个问题,我们可以创建一套元类体系,让不同层面上的元类分别完成各自的验证逻辑。
class ValidatePolygon(type):
def __new__(meta, name, bases, class_dict):
if not class_dict.get('is_root'):
if class_dict['sides'] < 3:
raise ValueError('Polygons need 3+ sides')
return type.__new__(meta, name, bases, class_dict)
class Polygon(metaclass=ValidatePolygon):
is_root = True
sides = None
class ValidateFilledPolygon(ValidatePolygon):
def __new__(meta, name, bases, class_dict):
if not class_dict.get('is_root'):
if class_dict['color'] not in ('red', 'green'):
raise ValueError('Fill color must be supported')
return super().__new__(meta, name, bases, class_dict)
class FilledPolygon(Polygon, metaclass=ValidateFilledPolygon):
is_root = True
color = None
同时,这也要求我们必须设计一个支持填充色的多边形类(FilledPolygon )。现在带有具体填充色与边数的多边形需要从这个FilledPolygon 里面继承。
class GreenPentagon(FilledPolygon):
color = 'green'
sides = 5
greenie = GreenPentagon()
assert isinstance(greenie, Polygon)
如果采用不支持的颜色。
class OrangePentagon(FilledPolygon):
color = 'orange'
sides = 5
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
/var/folders/h5/58f251ws7bsfdgld4kgcv_pr0000gn/T/ipykernel_3997/1696298101.py in <module>
----> 1 class OrangePentagon(FilledPolygon):
2 color = 'orange'
3 sides = 5
/var/folders/h5/58f251ws7bsfdgld4kgcv_pr0000gn/T/ipykernel_3997/1449684336.py in __new__(meta, name, bases, class_dict)
17 if not class_dict.get('is_root'):
18 if class_dict['color'] not in ('red', 'green'):
---> 19 raise ValueError('Fill color must be supported')
20
21 return super().__new__(meta, name, bases, class_dict)
ValueError: Fill color must be supported
ValidateFilledPolygon 元类继承自ValidatePolygon ,因此边数的错误也可以检查出来。
class RedLine(FilledPolygon):
color = 'red'
sides = 2
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
/var/folders/h5/58f251ws7bsfdgld4kgcv_pr0000gn/T/ipykernel_3997/3139824580.py in <module>
----> 1 class RedLine(FilledPolygon):
2 color = 'red'
3 sides = 2
/var/folders/h5/58f251ws7bsfdgld4kgcv_pr0000gn/T/ipykernel_3997/1449684336.py in __new__(meta, name, bases, class_dict)
19 raise ValueError('Fill color must be supported')
20
---> 21 return super().__new__(meta, name, bases, class_dict)
22
23 class FilledPolygon(Polygon, metaclass=ValidateFilledPolygon):
/var/folders/h5/58f251ws7bsfdgld4kgcv_pr0000gn/T/ipykernel_3997/1449684336.py in __new__(meta, name, bases, class_dict)
4 if not class_dict.get('is_root'):
5 if class_dict['sides'] < 3:
----> 6 raise ValueError('Polygons need 3+ sides')
7
8 return type.__new__(meta, name, bases, class_dict)
ValueError: Polygons need 3+ sides
虽然这样能实现验证,但却没办法组合。这个问题,同样可以通过__init_subclass__ 这个特殊的类方法来解决。 在多层的类体系中,只要内置的super() 函数来调用__init_subclass__ 方法,系统就会按照适当的解析顺序触发超类或平级类的__init__subclass__ 方法。
这种写法可以正确应对多重继承。
class Filled:
color = None
def __init_subclass__(cls):
super().__init_subclass__()
if cls.color not in ('red', 'green', 'blue'):
raise ValueError('Fills need a valid color')
现在,我们就来定义这样的子类。定义好之后,系统会触发其中一个超类的__init_subclass__ 方法,而那个方法又会通过super() 正确触发另一个超类的__init_subclass__ 方法。
class RedTriangle(Filled, BetterPolygon):
color = 'red'
sides = 3
ruddy = RedTriangle()
assert isinstance(ruddy, Filled)
assert isinstance(ruddy, BetterPolygon)
如果子类的边数不符合要求:
print('Before class')
class BlueLine(Filled, BetterPolygon):
color = 'blue'
sides = 2
print('After class')
Before class
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
/var/folders/h5/58f251ws7bsfdgld4kgcv_pr0000gn/T/ipykernel_3997/2550909414.py in <module>
1 print('Before class')
2
----> 3 class BlueLine(Filled, BetterPolygon):
4 color = 'blue'
5 sides = 2
/var/folders/h5/58f251ws7bsfdgld4kgcv_pr0000gn/T/ipykernel_3997/3501412681.py in __init_subclass__(cls)
3
4 def __init_subclass__(cls):
----> 5 super().__init_subclass__()
6 if cls.color not in ('red', 'green', 'blue'):
7 raise ValueError('Fills need a valid color')
/var/folders/h5/58f251ws7bsfdgld4kgcv_pr0000gn/T/ipykernel_3997/2055010121.py in __init_subclass__(cls)
5 super().__init_subclass__()
6 if cls.sides < 3:
----> 7 raise ValueError('Polygons need 3+ sides')
8
9 @classmethod
ValueError: Polygons need 3+ sides
__init_subclass__ 还可以处理更为复杂的情况,例如棱形继承。
class Top:
def __init_subclass__(cls):
super().__init_subclass__()
print(f'Top for {cls}')
class Left(Top):
def __init_subclass__(cls):
super().__init_subclass__()
print(f'Left for {cls}')
class Right(Top):
def __init_subclass__(cls):
super().__init_subclass__()
print(f'Right for {cls}')
class Bottom(Left, Right):
def __init_subclass__(cls):
super().__init_subclass__()
print(f'Bottom for {cls}')
Top for <class '__main__.Left'>
Top for <class '__main__.Right'>
Top for <class '__main__.Bottom'>
Right for <class '__main__.Bottom'>
Left for <class '__main__.Bottom'>
由于是通过super() 触发__init_subclass__ ,系统在处理Bottom 类的定义时,只会把Top 类的__init_subclss__ 执行一遍。
#49 用__init_subclass__记录现有的子类
元类还有个常见的用途,是可以自动记录程序之中的类型。利用这项功能,我们就能根据某个标识符反向查出它对应的类。
比如,我们想给Python对象做序列化处理,并将其表示成JSON格式的数据。要实现这个功能,首先得想法把对象转换成JSON字符串。
import json
class Serializable:
def __init__(self, *args):
self.args = args
def serialize(self):
return json.dumps({'args': self.args})
这样,我们可以把Point2D 这样简单而不可变得数据转化成JSON字符串。
class Point2D(Serializable):
def __init__(self, x, y):
super().__init__(x, y)
self.x = x
self.y = y
def __repr__(self):
return f'Point2D({self.x}, {self.y})'
point = Point2D(5, 3)
print('Object: ', point)
print('Serialized:', point.serialize())
Object: Point2D(5, 3)
Serialized: {"args": [5, 3]}
假设还需要实现反序列功能。
class Deserializable(Serializable):
@classmethod
def deserialize(cls, json_data):
params = json.loads(json_data)
return cls(*params['args'])
这样,就可以让简单的不可变数据同时具备序列化与反序列化功能。
class BetterPoint2D(Deserializable):
def __init__(self, x, y):
super().__init__(x, y)
self.x = x
self.y = y
def __repr__(self):
return f'Point2D({self.x}, {self.y})'
before = BetterPoint2D(5, 3)
print('Before: ', before)
data = before.serialize()
print('Serialized:', data)
after = BetterPoint2D.deserialize(data)
print('After: ', after)
Before: Point2D(5, 3)
Serialized: {"args": [5, 3]}
After: Point2D(5, 3)
缺点是,我们必须提前知道JSON字符串所表示的类型,然后才能还原成对象。 我们来改进它。
class BetterSerializable:
def __init__(self, *args):
self.args = args
def serialize(self):
return json.dumps({
'class': self.__class__.__name__,
'args': self.args,
})
def __repr__(self):
name = self.__class__.__name__
args_str = ', '.join(str(x) for x in self.args)
return f'{name}({args_str})'
然后,用一份字典把支持序列化与反序列化的类记录下来,凡是经过register_class 注册的类,其JSON数据都可以通过deserialize 函数还原成相应的对象。
registry = {}
def register_class(target_class):
registry[target_class.__name__] = target_class
def deserialize(data):
params = json.loads(data)
name = params['class']
target_class = registry[name]
return target_class(*params['args'])
为确保deserialize 函数能正确还原JSON数据,必须调用上面的注册方法。
class EvenBetterPoint2D(BetterSerializable):
def __init__(self, x, y):
super().__init__(x, y)
self.x = x
self.y = y
register_class(EvenBetterPoint2D)
这样就可以把任意的JSON字符串都还原为相应的对象,而不需要明确指出类名,因为deserialize 函数会从注册字典里面查询。
before = EvenBetterPoint2D(5, 3)
print('Before: ', before)
data = before.serialize()
print('Serialized:', data)
after = deserialize(data)
print('After: ', after)
Before: EvenBetterPoint2D(5, 3)
Serialized: {"class": "EvenBetterPoint2D", "args": [5, 3]}
After: EvenBetterPoint2D(5, 3)
如果忘记调用注册方法,就会出问题。
那如何优化呢?
可以通过元类实现,让元类自动调用注册方法。
class Meta(type):
def __new__(meta, name, bases, class_dict):
cls = type.__new__(meta, name, bases, class_dict)
register_class(cls)
return cls
class RegisteredSerializable(BetterSerializable, metaclass=Meta):
pass
用户只要把RegisteredSerializable 的子类定义完就好。
class Vector3D(RegisteredSerializable):
def __init__(self, x, y, z):
super().__init__(x, y, z)
self.x, self.y, self.z = x, y, z
before = Vector3D(10, -7, 3)
print('Before: ', before)
data = before.serialize()
print('Serialized:', data)
print('After: ', deserialize(data))
Before: Vector3D(10, -7, 3)
Serialized: {"class": "Vector3D", "args": [10, -7, 3]}
After: Vector3D(10, -7, 3)
还有简单的办法。通过__init_subclss__ 特殊类方法实现。
class BetterRegisteredSerializable(BetterSerializable):
def __init_subclass__(cls):
super().__init_subclass__()
register_class(cls)
class Vector1D(BetterRegisteredSerializable):
def __init__(self, magnitude):
super().__init__(magnitude)
self.magnitude = magnitude
before = Vector1D(6)
print('Before: ', before)
data = before.serialize()
print('Serialized:', data)
print('After: ', deserialize(data))
Before: Vector1D(6)
Serialized: {"class": "Vector1D", "args": [6]}
After: Vector1D(6)
#50 用__set_name__给类属性加注解
元类还有一个更有用的功能,可以在某个类真正投入使用之前,率先修改或注解这个类所定义的属性。这通常需要与描述符搭配使用。
例如,我们要定义一个新的类,来表示客户数据库中的每一行数据。这个类需要定义一些属性,与数据表中的各列相对应,每个属性都分别表示这行数据在这一列的取值。下面用描述符来实现这些属性,把她们和数据表中同名的列联系起来。
class Field:
def __init__(self, name):
self.name = name
self.internel_name = '_' + self.name
def __get__(self, instance, instance_type):
if instance is None:
return self
return getattr(instance, self.internel_name, '')
def __set__(self, instance, value):
setattr(instance, self.internel_name, value)
Field 描述符的name 属性指的是数据表中那一列的列名,所以,我们通过setattr 把每行数据在这个属性上的取值保存到那行数据自己的状态字典里面去。
下面定义Customer 类,每个Customer 都表示数据表中的一行数据,其中的四个属性分别对应于这行数据在那四列上面的取值。
class Customer:
first_name = Field('first_name')
last_name = Field('last_name')
prefix = Field('prefix')
suffix = Field('suffix')
这个类用起来很简单。
cust = Customer()
print(f'Before: {cust.first_name!r} {cust.__dict__}')
cust.first_name = 'Euclid'
print(f'After: {cust.first_name!r} {cust.__dict__}')
Before: '' {}
After: 'Euclid' {'_first_name': 'Euclid'}
这样写虽然没错,但是有点啰嗦。first_name = Field('first_name') 中出现了两个first_name 。
元类可以当作class语句的挂钩,只要class语句体定义完毕,元类就会看到它的写法并尽快做出应对。我们看如何修改。
class Meta(type):
def __new__(meta, name, bases, class_dict):
for key, value in class_dict.items():
if isinstance(value, Field):
value.name = key
value.internal_name = '_' + key
cls = type.__new__(meta, name, bases, class_dict)
return cls
下面定义一个基类,让该基类把刚才定义好的Meta 当成元类。凡是表示数据库某行的类都继承自该基类,以确保它们可以利用元类所提供的功能。
class DatabaseRow(metaclass=Meta):
pass
为了跟元类配合,Field 描述符需要稍加调整。
class Field:
def __init__(self):
self.name = None
self.internal_name = None
def __get__(self, instance, instance_type):
if instance is None:
return self
return getattr(instance, self.internal_name, '')
def __set__(self, instance, value):
setattr(instance, self.internal_name, value)
这样,代码就不像之前那么冗余了。
class BetterCustomer(DatabaseRow):
first_name = Field()
last_name = Field()
prefix = Field()
suffix = Field()
cust = BetterCustomer()
print(f'Before: {cust.first_name!r} {cust.__dict__}')
cust.first_name = 'Euler'
print(f'After: {cust.first_name!r} {cust.__dict__}')
Before: '' {}
After: 'Euler' {'_first_name': 'Euler'}
这个方法的缺点是,必须从DatabaseRow 继承。如果不能继承,那么代码就会无法运行。 这个问题可以通过给描述符定义__set_name__ 特殊来解决。如果某个类用这种描述符来定义字段,那么系统就会在描述符上面触发这个特殊方法。 下面我们将Meta.__new__ 之中的逻辑移动到Field描述符的__set_name__ 里面。
class Field:
def __init__(self):
self.name = None
self.internal_name = None
def __set_name__(self, owner, name):
self.name = name
self.internal_name = '_' + name
def __get__(self, instance, instance_type):
if instance is None:
return self
return getattr(instance, self.internal_name, '')
def __set__(self, instance, value):
setattr(instance, self.internal_name, value)
现在,我们可以直接在类里面通过Field描述符来定义字段,而不用继承某个基类,还能把元类给省掉。
class FixedCustomer:
first_name = Field()
last_name = Field()
prefix = Field()
suffix = Field()
cust = FixedCustomer()
print(f'Before: {cust.first_name!r} {cust.__dict__}')
cust.first_name = 'Mersenne'
print(f'After: {cust.first_name!r} {cust.__dict__}')
Before: '' {}
After: 'Mersenne' {'_first_name': 'Mersenne'}
#51 优先考虑通过类修饰器来提供可组合的扩充功能,不要使用元类
尽管元类允许我们用各种方式来定制其他类的创建逻辑,但有些情况未必能处理好。
例如,要写一个辅助函数来修饰类中的每个方法,把这些方法在执行时所用的参数、所返回的值以及所抛出的异常都打印出来。
from functools import wraps
def trace_func(func):
if hasattr(func, 'tracing'):
return func
@wraps(func)
def wrapper(*args, **kwargs):
result = None
try:
result = func(*args, **kwargs)
return result
except Exception as e:
result = e
raise
finally:
print(f'{func.__name__}({args!r}, {kwargs!r}) -> {result!r}')
wrapper.tracing = True
return wrapper
如果我们从标准的dict里面派生类下面这个子类。我们可以这么使用:
class TraceDict(dict):
@trace_func
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@trace_func
def __setitem__(self, *args, **kwargs):
return super().__setitem__(*args, **kwargs)
@trace_func
def __getitem__(self, *args, **kwargs):
return super().__getitem__(*args, **kwargs)
下面来验证一下。
trace_dict = TraceDict([('hi', 1)])
trace_dict['there'] = 2
trace_dict['hi']
try:
trace_dict['does not exist']
except KeyError:
pass
__init__(({'hi': 1}, [('hi', 1)]), {}) -> None
__setitem__(({'hi': 1, 'there': 2}, 'there', 2), {}) -> None
__getitem__(({'hi': 1, 'there': 2}, 'hi'), {}) -> 1
__getitem__(({'hi': 1, 'there': 2}, 'does not exist'), {}) -> KeyError('does not exist')
这样写有个缺点,需要在子类中把需要@trace_func 修饰的方法全重写一遍。
要解决这个问题,其中一个办法是通过元类自动修饰那个类的所有方法。
import types
trace_types = (
types.MethodType,
types.FunctionType,
types.BuiltinFunctionType,
types.BuiltinMethodType,
types.MethodDescriptorType,
types.ClassMethodDescriptorType)
class TraceMeta(type):
def __new__(meta, name, bases, class_dict):
klass = super().__new__(meta, name, bases, class_dict)
for key in dir(klass):
value = getattr(klass, key)
if isinstance(value, trace_types):
wrapped = trace_func(value)
setattr(klass, key, wrapped)
return klass
现在,只需要让子类继承dict,配置下元类就可以了。
class TraceDict(dict, metaclass=TraceMeta):
pass
trace_dict = TraceDict([('hi', 1)])
trace_dict['there'] = 2
trace_dict['hi']
try:
trace_dict['does not exist']
except KeyError:
pass
__new__((<class '__main__.TraceDict'>, [('hi', 1)]), {}) -> {}
__getitem__(({'hi': 1, 'there': 2}, 'hi'), {}) -> 1
__getitem__(({'hi': 1, 'there': 2}, 'does not exist'), {}) -> KeyError('does not exist')
这种方法确实有效,但如果子类所继承的那个超类本身已经指定了它自己的metaclass,那么会怎样呢
class OtherMeta(type):
pass
class SimpleDict(dict, metaclass=OtherMeta):
pass
class TraceDict(SimpleDict, metaclass=TraceMeta):
pass
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
/var/folders/h5/58f251ws7bsfdgld4kgcv_pr0000gn/T/ipykernel_7882/738434464.py in <module>
5 pass
6
----> 7 class TraceDict(SimpleDict, metaclass=TraceMeta):
8 pass
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
这样会发生冲突,理论上我们可以让TraceMeta继承OtherMeta,从而解决这个问题。
class TraceMeta(type):
def __new__(meta, name, bases, class_dict):
klass = type.__new__(meta, name, bases, class_dict)
for key in dir(klass):
value = getattr(klass, key)
if isinstance(value, trace_types):
wrapped = trace_func(value)
setattr(klass, key, wrapped)
return klass
class OtherMeta(TraceMeta):
pass
class SimpleDict(dict, metaclass=OtherMeta):
pass
class TraceDict(SimpleDict, metaclass=TraceMeta):
pass
trace_dict = TraceDict([('hi', 1)])
trace_dict['there'] = 2
trace_dict['hi']
try:
trace_dict['does not exist']
except KeyError:
pass
else:
assert False
__init_subclass__((), {}) -> None
__new__((<class '__main__.TraceDict'>, [('hi', 1)]), {}) -> {}
__getitem__(({'hi': 1, 'there': 2}, 'hi'), {}) -> 1
__getitem__(({'hi': 1, 'there': 2}, 'does not exist'), {}) -> KeyError('does not exist')
然后,如果TraceMeta不是我们写的,那么这种方法就无法满足了。
为此,我们可以换一种方法,就是使用类修饰器来实现。这种修饰器与函数修饰器相似,都通过@ 符号来施加,但加在类的上面。
def my_class_decorator(klass):
klass.extra_param = 'hello'
return klass
@my_class_decorator
class MyClass:
pass
print(MyClass)
print(MyClass.extra_param)
<class '__main__.MyClass'>
hello
现在就来实现这样一个类修饰器,它可以施加在类上面,让该类的所有方法与函数都能自动封装在trace_func之中。
def trace(klass):
for key in dir(klass):
value = getattr(klass, key)
if isinstance(value, trace_types):
wrapped = trace_func(value)
setattr(klass, key, wrapped)
return klass
我们把类修饰器运用到自定义的dict子类上面,这样它就有了与刚才那套元类方案相同的功能。
@trace
class TraceDict(dict):
pass
trace_dict = TraceDict([('hi', 1)])
trace_dict['there'] = 2
trace_dict['hi']
try:
trace_dict['does not exist']
except KeyError:
pass
else:
assert False
__new__((<class '__main__.TraceDict'>, [('hi', 1)]), {}) -> {}
__getitem__(({'hi': 1, 'there': 2}, 'hi'), {}) -> 1
__getitem__(({'hi': 1, 'there': 2}, 'does not exist'), {}) -> KeyError('does not exist')
另外,类修饰器也能施加到已经有metaclass的类上面。
class OtherMeta(type):
pass
@trace
class TraceDict(dict, metaclass=OtherMeta):
pass
trace_dict = TraceDict([('hi', 1)])
trace_dict['there'] = 2
trace_dict['hi']
try:
trace_dict['does not exist']
except KeyError:
pass
else:
assert False
__new__((<class '__main__.TraceDict'>, [('hi', 1)]), {}) -> {}
__getitem__(({'hi': 1, 'there': 2}, 'hi'), {}) -> 1
__getitem__(({'hi': 1, 'there': 2}, 'does not exist'), {}) -> KeyError('does not exist')
|