《Effective Python 2nd》——元类与属性_愤怒的可乐的博客-程序员宝宝

技术标签: 读书笔记  python  EffectivePython  

引言

元类(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属性的赋值操作。采用旧式的filldeduct函数来增减额度的那些代码依然可以正常运作,因为那两个函数在修改额度时会触发这个新方法,笔者在的代码里对相关情况做了特殊处理。

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__) # 此时并没有foo这个属性
print('foo:  ',data.foo)
print('After: ', data.__dict__) # 此时多了foo这个属性
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.foofoo属性不在实例字典里,因此触发了该方法。并把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() # 实现 __getattr__
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__方法,那么效果就不一样了,程序每次对实例做hasattrgetattr操作时,都会触发这个方法。

data = ValidatingRecord() # 实现了 __getattribute__
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):
        # Save some data for the record
        # ...
        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})
# data.foo
# RecursionError: maximum recursion depth exceeded while calling a Python object

因为在__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):
        # 只验证Polygon类的子类
        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  # Expected
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  # Expected
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  # Expected
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')
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/yjw123456/article/details/121938939

智能推荐

java线程:Atomic(原子的)_iteye_13622的博客-程序员宝宝

  一、何谓Atomic? Atomic一词跟原子有点关系,后者曾被人认为是最小物质的单位。计算机中的Atomic是指不能分割成若干部分的意思。如果一段代码被认为是Atomic,则表示这段代码在执行过程中,是不能被中断的。通常来说,原子指令由硬件提供,供软件来实现原子方法(某个线程进入该方法后,就不会被中断,直到其执行完成)  在x86 平台上,CPU提供了在指令执行期间对总线...

jQuery设置select的某个option选中_别团等shy哥发育的博客-程序员宝宝_option选中

jQuery设置select的某个option选中html部分 &lt;label class="col-sm-1 control-label text-right"&gt;商品类型:&lt;/label&gt; &lt;div class="col-sm-2"&gt; &lt;select class="form-control" id="type"&gt;&lt;/select&gt;

Ubuntu使用科大讯飞SDK_涂伟峰的博客-程序员宝宝

操作系统:Ubuntu16.04LTS最近公司的项目需要用到科大讯飞的离线语音合成,使用SDK时遇到了两个问题。1.编译c文件2.导入动态链接库 .so文件接下来详细说明一下流程。1.科大讯飞下载SDK         添加离线语音合成服务,并点亮图标        SDK下载下来是这样的         解压2.导入动态链接库     ...

使用gcc的-finstrument-functions选项进行函数跟踪_xk_一步一步来的博客-程序员宝宝_-finstrument-functions 作用

https://blog.csdn.net/jasonchen_gbd/article/details/44044899GCC Function instrumentation机制可以用来跟踪函数的调用关系,在gcc中对应的选项为“-finstrument-functions”。可查看gcc的man page来获取更详细信息。编译时如果为gcc加上“-finstrument-functions...

视频教程-从编程小白到量化宗师之路C02---BackTrader基础-Python_weixin_32875407的博客-程序员宝宝

从编程小白到量化宗师之路C02---BackTrader基础 20年企业以及...

Oracle导出SQL语句结果集为dmp文件_DBA狗剩儿的博客-程序员宝宝

文章目录前言一、views_as_tables二、测试日志前言在运维工作中遇到一个应用方提出的需求,需要将SQL语句查询出来的结果集导出为dmp文件。提示:以下是本篇文章正文内容,下面案例可供参考一、views_as_tablesOracle呢,从12C版本以后可通过views_as_tables的参数来实现我们的expdp导出视图的结果集,方法呢也很简单,也有一定的格式要求如下:1、导出多个视图格式:views_as_tables=v_cxl,v_wnn(注意:不加括号)否则会出现:-b

随便推点

安装pytorch (RTX2080 Ti+cuda10.1+cudnn7.5.0)_zqun817的博客-程序员宝宝

1、linux修改源镜像地址修改镜像文件:https://blog.csdn.net/maizousidemao/article/details/79127695cp /etc/apt/source.list /etc/apt/source.list.backupsudo gedit sources.list打开https://mirrors.tuna.tsinghua....

Ubuntu使用软连接更新python版本_Test_hh112的博客-程序员宝宝

参考地址:Ubuntu16.04下升级Python到3.6.51.增加ppa仓库sudo add-apt-repository ppa:jonathonf/python-3.62.升级apt索引,更新pythonsudo apt-get updatesudo apt-get install python3.63.更换系统默认的软链命令Python3到新的Python3.6sudo...

使用Docker在本地搭建Hadoop分布式集群_kmQtYHiB4G的博客-程序员宝宝_windows docker上搭建的hadoop集群本地能访问吗

原文:https://www.cnblogs.com/onetwo/p/6419925.html (原文的原文:http://www.cnblogs.com/felixzh/p/4992178.html)学习Hadoop集群环境搭建是Hadoop入门必经之路。搭建分布式集群通常有两个办法:要么找多台机器来部署(常常找不到机器) 或者在本地开多个虚拟机(开销很大,对宿主机器性能要求高,...

SegmentFault 技术周刊 Vol.28 - GitHub —— 你不得不上的交友网站_weixin_34174132的博客-程序员宝宝

相信每一个程序猿都听说过 GitHub 了,简单来说 GitHub 是一个主要提供基于 Git 来进行版本控制的项目托管服务的网站,她是世界上最大的开源软件社区,世界各地的程序猿在 GitHub 上开源自己的项目,关注和参与自己感兴趣的开源项目,嗯,同时 GitHub 还是世界上最大的同性交友网站。什么是 GitHub先来了解下 G...

dnsmasq配置dns实战_chuguanxian7753的博客-程序员宝宝

dnsmasq先去解析hosts文件, 再去解析/etc/dnsmasq.d/下的*.conf文件,并且这些文件的优先级要高于dnsmasq.conf,我们自定义的resolv.dnsmasq.conf中的DNS也被称为上游DNS,这是最后去查询解析的; 如果不想用hosts文件做解析,...

ChatGPT的使用基础_sinom21的博客-程序员宝宝

最近ChatGPT突然火出圈了,想去注册一个,BUT。这个难不到我们互联网人。我们可以进行更加科学的上网,当然需要全局的。如果还是不行,就使用chrome的隐私模式。注册完成之后,就提示需要输入手机号,现在有很多办法比如: 在线接受短信的虚拟号码 - SMS-Activate充个几块钱,购买印度的服务就可以了。实在是太热了,官方指南可以直接看。然后就能简单了,下面验证手机号码的时候,选择India,就收短信就好了。他在很认真的回答我的问题

推荐文章

热门文章

相关标签