Effective Python 筆記摘錄5.2

元類(Metaclasses)和屬性(Attributes)

元類經(jīng)常被提及,但是很少知道實(shí)際如何使用。簡(jiǎn)單地說(shuō),元類可以讓你攔截 Python 的class語(yǔ)句,并在每次定義類時(shí)提供特殊行為。

動(dòng)態(tài)屬性使您能夠覆蓋對(duì)象并導(dǎo)致意外的副作用。元類可以創(chuàng)建非常奇怪的行為,最好實(shí)現(xiàn)容易理解的代碼,而不要意外發(fā)生。

  • Item44: 使用原始的Attributes而不是Setter和Getter方法

通常都會(huì)實(shí)現(xiàn)類似Java的getter和setter來(lái)進(jìn)行內(nèi)部屬性的獲?。?/p>

class OldResistor:
    def __init__(self, ohms):
        self._ohms = ohms
    def get_ohms(self):
        return self._ohms
    def set_ohms(self, ohms):
        self._ohms = ohms

用起來(lái)簡(jiǎn)單但是并不Pythonic:

r0 = OldResistor(50e3)
print('Before:', r0.get_ohms())
r0.set_ohms(10e3)
print('After: ', r0.get_ohms())

>>>
Before: 50000.0
After: 10000.0

有些時(shí)候要做增量的操作的時(shí)候,比較復(fù)雜:

r0.set_ohms(r0.get_ohms() - 4e3)
assert r0.get_ohms() == 6e3

在python里面,不用明確定義getter和setter,首先從public的屬性開(kāi)始:

class Resistor:
    def __init__(self, ohms):
        self.ohms = ohms
        self.voltage = 0
        self.current = 0

r1 = Resistor(50e3)
r1.ohms = 10e3

對(duì)屬性進(jìn)行增量操作就顯得自然:

r1.ohms += 5e3

如果需要一些行為設(shè)置,可以用@property裝飾器。
下面的類繼承了Resistor,然后維護(hù)了自己的voltage。

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

這樣就可以直接以屬性進(jìn)行調(diào)用:

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可以執(zhí)行類型檢查和值校驗(yàn),比如只允許電阻大于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

賦值的時(shí)候不行。

r3 = BoundedResistance(1e3)
r3.ohms = 0
>>>
Traceback ...
ValueError: ohms must be > 0; got 0

構(gòu)建函數(shù)的時(shí)候也不行:

BoundedResistance(-5)

>>>
Traceback ...
ValueError: ohms must be > 0; got -5

因?yàn)檎{(diào)BoundedResistance.init的時(shí)候,調(diào)用了super().init,而super().init調(diào)用了self.ohms = ohms,此時(shí),@ohms.setter就會(huì)執(zhí)行并檢查數(shù)值。

甚至可以用@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

第一次初始化的時(shí)候,還沒(méi)有_ohms,以后訪問(wèn)的時(shí)候就有,所以會(huì)報(bào)錯(cuò):

r4 = FixedResistance(1e3)
r4.ohms = 2e3
>>>
Traceback ...
AttributeError: Ohms is immutable

不要在getter里面設(shè)置其它屬性:

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

行為怪異:

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

最好的方式就是在屬性的setter方法里面修改和對(duì)象相關(guān)的狀態(tài)。
@property 最大的缺點(diǎn)是屬性的方法只能由子類共享。更多可參見(jiàn)Item46。


  • Item45: 使用注解@property而不是重構(gòu)屬性

隨著時(shí)間推移,@property 還為改進(jìn)接口提供了重要的方案。
比如,現(xiàn)在用Python對(duì)象實(shí)現(xiàn)leaky bucket quota(漏桶配額)。
Bucket類表示還有多少配額剩余,還有配額的可用持續(xù)時(shí)間:

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

消費(fèi)配額的算法如下:

def deduct(bucket, amount):
    now = datetime.now()
    if (now - bucket.reset_time) > bucket.period_delta:
        return False # Bucket hasn't been filled this period
    if bucket.quota - amount < 0:
        return False # Bucket was filled, but not enough
    bucket.quota -= amount
    return True # Bucket had enough, quota consumed

填滿桶:

bucket = Bucket(60)
fill(bucket, 100)
print(bucket)

>>>
Bucket(quota=100)

消費(fèi)配額:

if deduct(bucket, 99):
    print('Had 99 quota')
else:
    print('Not enough for 99 quota')
print(bucket)

>>>
Had 99 quota
Bucket(quota=1)

如果不夠,配額水平保持不變:

if deduct(bucket, 3):
    print('Had 3 quota')
else:
    print('Not enough for 3 quota')
print(bucket)
>>>
Not enough for 3 quota
Bucket(quota=1)

這個(gè)實(shí)現(xiàn)的問(wèn)題是:我永遠(yuǎn)不知道存儲(chǔ)桶開(kāi)始的配額級(jí)別。
新的桶:

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})')

當(dāng)前的配額獲取就是最大配額減已經(jīng)消費(fèi)的配額:

@property
def quota(self):
    return self.max_quota - self.quota_consumed

另外設(shè)置配額:

@quota.setter
def quota(self, amount):
    delta = self.max_quota - amount
    if amount == 0:
        # Quota being reset for a new period
        self.quota_consumed = 0
        self.max_quota = 0
    elif delta < 0:
        # Quota being filled for the new period
        assert self.quota_consumed == 0
        self.max_quota = amount
    else:
        # Quota being consumed during the period
        assert self.max_quota >= self.quota_consumed
        self.quota_consumed += delta

重新再運(yùn)行一次實(shí)例:

bucket = 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 NewBucket(max_quota=0, quota_consumed=0)
Filled NewBucket(max_quota=100, quota_consumed=0)
Had 99 quota
Now NewBucket(max_quota=100, quota_consumed=99)
Not enough for 3 quota
Still NewBucket(max_quota=100, quota_consumed=99)

使用@property 在數(shù)據(jù)模型方面不斷取得進(jìn)展。
主要是服務(wù)頂層設(shè)計(jì)不變的情況下進(jìn)行的,但是當(dāng)?shù)臅r(shí)候反復(fù)使用@property的話,應(yīng)該考慮重構(gòu)這個(gè)類。


  • Item46: 對(duì)可重用的@property方法們,使用描述符(Descriptors)

比如現(xiàn)在有一個(gè)作業(yè)給分的實(shí)現(xiàn):

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(
                'Grade must be between 0 and 100')
        self._grade = value

(用了@property很容易實(shí)現(xiàn))

galileo = Homework()
galileo.grade = 95

當(dāng)需要給考試成績(jī)的時(shí)候,可能多個(gè)學(xué)科有各自的成績(jī),此時(shí)重用起來(lái)就比較麻煩:

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(
                'Grade must be between 0 and 100')

然后就是乏味的property步驟:

@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

如果要重用這個(gè)分?jǐn)?shù)檢查機(jī)制,需要每次都重寫這個(gè)樣板。
最好的操作是用描述符(descriptor protocol,定義了語(yǔ)言如何解釋屬性訪問(wèn)):

class Grade:
    def __get__(self, instance, instance_type):
        ...
    def __set__(self, instance, value):
        ...

class Exam:
    # Class attributes
    math_grade = Grade()
    writing_grade = Grade()
    science_grade = Grade()
exam = Exam()
# 當(dāng)調(diào)用這個(gè)的時(shí)候
exam.writing_grade = 40
# 等價(jià)于這個(gè)表達(dá)式
Exam.__dict__['writing_grade'].__set__(exam, 40)
# 同理
exam.writing_grade

Exam.__dict__['writing_grade'].__get__(exam, Exam)

訪問(wèn)getattribute的時(shí)候,如果實(shí)例變量沒(méi)有,則會(huì)用類變量。
如果實(shí)現(xiàn)了__get__和__set__方法,則假定要用描述符協(xié)議。

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(
                'Grade must be between 0 and 100')
        self._value = value

然而,這是錯(cuò)誤的,在單一類變量上面操作:

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
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

應(yīng)該對(duì)每個(gè)實(shí)例變量維護(hù)相應(yīng)的結(jié)果:

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(
                'Grade must be between 0 and 100')
        self._values[instance] = value

雖然實(shí)現(xiàn)容易,但是會(huì)造成內(nèi)存泄漏。_values會(huì)一直持有每個(gè)實(shí)例的引用。
為了解決,可以用weakref,讓python自己來(lái)管理引用,當(dāng)實(shí)例不再使用時(shí),字典會(huì)為空。

from weakref import WeakKeyDictionary
class Grade:
def __init__(self):
    self._values = WeakKeyDictionary()
def __get__(self, instance, instance_type):
    ...
def __set__(self, instance, value):
    ...

這樣,所有的東西都可以正常工作了:

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

  • Item47: 對(duì)于懶惰(Lazy)的屬性,使用getattr, getattribute, and setattr

比如用對(duì)象來(lái)表示數(shù)據(jù)庫(kù)里的記錄。代碼也必須知道數(shù)據(jù)庫(kù)的樣子,但是在 Python 中,將 Python 對(duì)象連接到數(shù)據(jù)庫(kù)的代碼不需要顯式指定記錄的模式,而是通用的。
Python 使用 __getattr__ 特殊方法使這種動(dòng)態(tài)行為成為可能。如果一個(gè)類定義了 __getattr__,則每次在對(duì)象的實(shí)例字典中找不到屬性時(shí)都會(huì)調(diào)用該方法:

class LazyRecord:
    def __init__(self):
        self.exists = 5
    def __getattr__(self, name):
        value = f'Value for {name}'
        setattr(self, name, value)
        return value

如果我訪問(wèn)了缺失的foo,會(huì)先調(diào)用__getattr__:

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'}

這里加了一些log語(yǔ)句來(lái)觀察,其中用了super()的__getattr__來(lái)獲得結(jié)果:

class LoggingLazyRecord(LazyRecord):
    def __getattr__(self, name):
        print(f'* Called __getattr__({name!r}), '
                f'populating instance dictionary')
        result = super().__getattr__(name)
        print(f'* Returning {result!r}')
        return result
data = LoggingLazyRecord()
print('exists: ', data.exists)
print('First foo: ', data.foo)
print('Second foo: ', data.foo)
>>>
exists: 5
* Called __getattr__('foo'), populating instance dictionary
* Returning 'Value for foo'
First foo: Value for foo
Second foo: Value for foo

可以看到確實(shí)調(diào)用了一次__getattr__。
這種懶加載的方式對(duì)無(wú)模式(schemaless)數(shù)據(jù)特別有用。
為了完成數(shù)據(jù)庫(kù)系統(tǒng)的事務(wù),比如:下次用戶訪問(wèn)某個(gè)屬性時(shí),想知道數(shù)據(jù)庫(kù)中對(duì)應(yīng)的記錄是否還有效,事務(wù)是否還處于打開(kāi)狀態(tài)。
Python有另一個(gè)對(duì)象的hook,叫__getattribute__。
每次對(duì)象屬性被訪問(wèn)的時(shí)候,都會(huì)調(diào)用。
需要注意的是,這樣的操作會(huì)產(chǎn)生大量開(kāi)銷并對(duì)性能產(chǎn)生負(fù)面影響。
比如這里,在方法中打log來(lái)觀察:

class ValidatingRecord:
    def __init__(self):
        self.exists = 5
    def __getattribute__(self, name):
        print(f'* Called __getattribute__({name!r})')
        try:
            value = super().__getattribute__(name)
            print(f'* Found {name!r}, returning {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('First foo: ', data.foo)
print('Second foo: ', data.foo)

>>>
* Called __getattribute__('exists')
* Found 'exists', returning 5
exists: 5
* Called __getattribute__('foo')
* Setting 'foo' to 'Value for foo'
First foo: Value for foo
* Called __getattribute__('foo')
* Found 'foo', returning 'Value for foo'
Second foo: Value for foo

找不到對(duì)應(yīng)屬性的時(shí)候拋出AttributeError的錯(cuò)誤。
比如例子:

class MissingPropertyRecord:
    def __getattr__(self, name):
        if name == 'bad_name':
            raise AttributeError(f'{name} is missing')
        ...

data = MissingPropertyRecord()
data.bad_name

>>>
Traceback ...
AttributeError: bad_name is missing

實(shí)現(xiàn)通用代碼少不了用hasattr來(lái)檢查屬性是否存在,還有g(shù)etattr來(lái)提取屬性的數(shù)值:

data = LoggingLazyRecord() # Implements __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

同樣觀察到,調(diào)用了一次__getattr__。

data = ValidatingRecord() # Implements __getattribute__
print('Has first foo: ', hasattr(data, 'foo'))
print('Has second foo: ', hasattr(data, 'foo'))
>>>
* Called __getattribute__('foo')
* Setting 'foo' to 'Value for foo'
Has first foo: True
* Called __getattribute__('foo')
* Found 'foo', returning 'Value for foo'
Has second foo: True

同樣觀察到,調(diào)用了兩次__getattribute__。

現(xiàn)在,可以用__setattr__(或者內(nèi)建的setattr方法)來(lái)做到:將值分配給Python對(duì)象時(shí),懶惰地將數(shù)據(jù)推回?cái)?shù)據(jù)庫(kù):

class SavingRecord:
    def __setattr__(self, name, value):
        # Save some data for the record
        ...
        super().__setattr__(name, value)

建一個(gè)打log的實(shí)例:

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}

假設(shè)我希望對(duì)我的對(duì)象進(jìn)行屬性訪問(wèn)以實(shí)際查找關(guān)聯(lián)字典中的鍵:

class BrokenDictionaryRecord:
    def __init__(self, data):
        self._data = {}
    def __getattribute__(self, name):
        print(f'* Called __getattribute__({name!r})')
        return self._data[name]

但是,程序會(huì)直接運(yùn)行到報(bào)錯(cuò):

data = BrokenDictionaryRecord({'foo': 3})
data.foo

>>>
* Called __getattribute__('foo')
* Called __getattribute__('_data')
* Called __getattribute__('_data')
* Called __getattribute__('_data')
...
Traceback ...
RecursionError: maximum recursion depth exceeded while
calling a Python object

主要是由于運(yùn)行了self._data導(dǎo)致又運(yùn)行了__getattribute__,導(dǎo)致了無(wú)限循環(huán)。
而是應(yīng)該從父類中獲取屬性,然后從這個(gè)值里面返回對(duì)應(yīng)的結(jié)果:

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

  • Item48: 用__init_subclass__驗(yàn)證子類

元類(MetaClass)通過(guò)繼承type來(lái)定義。在運(yùn)行時(shí)構(gòu)建類的類型。元類通過(guò)__new__來(lái)接收關(guān)聯(lián)類的內(nèi)容:

class Meta(type):
    def __new__(meta, name, bases, class_dict):
        print(f'* Running {meta}.__new__ for {name}')
        print('Bases:', 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

元類可以訪問(wèn)類的名稱,以及它的父類繼承自(bases)和中定義的所有類屬性

>>>
* Running <class '__main__.Meta'>.__new__ for MyClass
Bases: ()
{'__module__': '__main__',
'__qualname__': 'MyClass',
'stuff': 123,
'foo': <function MyClass.foo at 0x105a05280>}
* Running <class '__main__.Meta'>.__new__ for MySubclass
Bases: (<class '__main__.MyClass'>,)
{'__module__': '__main__',
'__qualname__': 'MySubclass',
'other': 567,
'bar': <function MySubclass.bar at 0x105a05310>}

主要的作用可以驗(yàn)證子類,比如驗(yàn)證是否是多邊形:

class ValidatePolygon(type):
    def __new__(meta, name, bases, class_dict):
        # Only validate subclasses of the Polygon class
        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 # Must be specified by subclasses
    @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

當(dāng)邊大于2的時(shí)候正常,但是當(dāng)邊為2的時(shí)候,報(bào)錯(cuò):

print('Before class')

class Line(Polygon):
    print('Before sides')
    sides = 2
    print('After sides')

print('After class')
>>>
Before class
Before sides
After sides
Traceback ...
ValueError: Polygons need 3+ sides

Python3.6引入了__init_subclass__來(lái)方便引入相同的特性:

class BetterPolygon:
    sides = None # Must be specified by subclasses
    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

代碼更短了,可以直接從cls獲取sides,而不用從class_dict['sides']獲得。

print('Before class')
class Point(BetterPolygon):
    sides = 1
    print('After class')
>>>
Before class
Traceback ...
ValueError: Polygons need 3+ sides

每個(gè)類只能指定一個(gè)元類。當(dāng)我想要第二個(gè)元類來(lái)驗(yàn)證顏色時(shí):

class ValidateFilled(type):
    def __new__(meta, name, bases, class_dict):
    # Only validate subclasses of the Filled class
        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 # Must be specified by subclasses

然后期望同樣的方式來(lái)驗(yàn)證:

class RedPentagon(Filled, Polygon):
    color = 'red'
    sides = 5
>>>
Traceback ...
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

只能通過(guò)比較復(fù)雜的繼承來(lái)修復(fù):

class ValidatePolygon(type):
    def __new__(meta, name, bases, class_dict):
    # Only validate non-root classes
        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 # Must be specified by subclasses

class ValidateFilledPolygon(ValidatePolygon):
    def __new__(meta, name, bases, class_dict):
    # Only validate non-root classes
        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 # Must be specified by subclasses

只能繼承FilledPolygon:

class GreenPentagon(FilledPolygon):
    color = 'green'
    sides = 5

greenie = GreenPentagon()
assert isinstance(greenie, Polygon)

驗(yàn)證顏色和驗(yàn)證邊:

class OrangePentagon(FilledPolygon):
    color = 'orange'
    sides = 5

>>>
Traceback ...
ValueError: Fill color must be supported
class RedLine(FilledPolygon):
color = 'red'
sides = 2

>>>
Traceback ...
ValueError: Polygons need 3+ sides

如果是用__init_subclass__來(lái)做:

class Filled:
    color = None # Must be specified by subclasses
    def __init_subclass__(cls):
        super().__init_subclass__()
        if cls.color not in ('red', 'green', 'blue'):
            raise ValueError('Fills need a valid color')

則不會(huì)破壞組合性(當(dāng)然,也可以像上面一樣定義多層的類繼承):

class RedTriangle(Filled, Polygon):
    color = 'red'
    sides = 3

ruddy = RedTriangle()
assert isinstance(ruddy, Filled)
assert isinstance(ruddy, Polygon)

以下是更多的測(cè)試:

print('Before class')
class BlueLine(Filled, Polygon):
    color = 'blue'
    sides = 2
    print('After class')

>>>
Before class
Traceback ...
ValueError: Polygons need 3+ sides
print('Before class')
class BeigeSquare(Filled, Polygon):
    color = 'beige'
    sides = 4
print('After class')
>>>
Before class
Traceback ...
ValueError: Fills need a valid color

甚至可以用它來(lái)做一些復(fù)雜的場(chǎng)景的繼承:

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'>

每個(gè)類只調(diào)用了一次Top.__init_subclass__。


  • Item49: 用__init_subclass__注冊(cè)類存在(Existence)

另一種公共的使用元類的方式是自動(dòng)注冊(cè)程序里的類型。當(dāng)反向搜索時(shí)(把簡(jiǎn)單的identifier映射回對(duì)應(yīng)的類),“注冊(cè)”是有用的。
比如,用JSON來(lái)序列化object。

import json
class Serializable:
    def __init__(self, *args):
        self.args = args
    def serialize(self):
        return json.dumps({'args': self.args})

可以成功序列化點(diǎn):

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]}

此時(shí),需要反序列化JSON,然后構(gòu)建點(diǎn):

class Deserializable(Serializable):
    @classmethod
    def deserialize(cls, json_data):
        params = json.loads(json_data)
        return cls(*params['args'])
class BetterPoint2D(Deserializable):
    ...
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)

問(wèn)題在于需要提前知道類的類型(BetterPoint2D,Point2D)。應(yīng)該是接收一個(gè)很大的JSON,然后分別都構(gòu)建出對(duì)應(yīng)的對(duì)象:

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})'

可以注冊(cè)類以及反序列化:

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'])

但是每次都要對(duì)類調(diào)用注冊(cè):

class EvenBetterPoint2D(BetterSerializable):
    def __init__(self, x, y):
        super().__init__(x, y)
        self.x = x
        self.y = y
register_class(EvenBetterPoint2D)

可以反序列化任何JSON串:

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)

但是忘記注冊(cè)就會(huì)有問(wèn)題:

class Point3D(BetterSerializable):
    def __init__(self, x, y, z):
        super().__init__(x, y, z)
        self.x = x
        self.y = y
        self.z = z
# Forgot to call register_class! Whoops!

反序列化了忘記注冊(cè)的類:

point = Point3D(5, 9, -4)
data = point.serialize()
deserialize(data)
>>>
Traceback ...
KeyError: 'Point3D'

元類可以來(lái)做到每次都注冊(cè)的需求:

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

這樣每次都能使得類得到注冊(cè):

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_subclass__就更好了:

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)

以上是用__init_subclass__來(lái)替代元類實(shí)現(xiàn)一些類注冊(cè)功能。


  • Item50: 用__set_name__注解類屬性

在這里,定義一個(gè)描述符類來(lái)將屬性連接到數(shù)據(jù)庫(kù)表的列名:

class Field:
    def __init__(self, name):
        self.name = name
        self.internal_name = '_' + self.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)

然后定義一個(gè)顧客類:

class Customer:
    # Class attributes
    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'}

但是顯得冗余,因?yàn)閒irst_name已經(jīng)可以表示了,為什么還要構(gòu)建出一個(gè)Field來(lái)保存同樣的信息?

class Customer:
    # Left side is redundant with right side
    first_name = Field('first_name')
...

此時(shí)可以用元類來(lái)處理:

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

讓元類來(lái)提取到key,賦值給相應(yīng)的Field。然后數(shù)據(jù)行繼承元類:

class DatabaseRow(metaclass=Meta):
    pass

最后,每個(gè)屬性用無(wú)參的init即可:

class Field:
    def __init__(self):
        # These will be assigned by the metaclass.
        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)

然后,實(shí)際使用時(shí),直接繼承:

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'}

但是,當(dāng)忘記繼承的時(shí)候,會(huì)出錯(cuò):

class BrokenCustomer:
    first_name = Field()
    last_name = Field()
    prefix = Field()
    suffix = Field()

cust = BrokenCustomer()
cust.first_name = 'Mersenne'

>>>
Traceback ...
TypeError: attribute name must be string, not 'NoneType'

Python3.6之后引入了__set_name__,可以代替元類的new來(lái)完成工作:

class Field:
    def __init__(self):
        self.name = None
        self.internal_name = None
    def __set_name__(self, owner, name):
        # Called on class creation for each descriptor
        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)

這樣就不用繼承元類也能完成工作了:

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'}
  • 元類使您能夠在完全定義類之前修改類的屬性
  • 描述符(descriptors)和元類(metaclasses)讓聲明式行為和運(yùn)行時(shí)自省(introspection)有力的組合
  • 在描述符類上定義 set_name 以允許它們考慮周圍的類及其屬性名稱。
  • 通過(guò)讓描述符將它們直接操作的數(shù)據(jù)存儲(chǔ)在類的實(shí)例字典中,避免內(nèi)存泄漏和 weakref 內(nèi)置模塊。

  • Item51: 對(duì)可組合的類擴(kuò)展,使用類裝飾器而不是元類

假如現(xiàn)在要追蹤各個(gè)函數(shù)的函數(shù)名,參數(shù)和返回值:

from functools import wraps

def trace_func(func):
    if hasattr(func, 'tracing'): # Only decorate once
        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}) -> '
        f'{result!r}')
    wrapper.tracing = True
    return wrapper

需要每個(gè)函數(shù)都打上decorator,比較麻煩:

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 # Expected
>>>
__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')

如果換種方式:

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

用元類也可以解決問(wèn)題:

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 # Expected
>>>
__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')

按理說(shuō)應(yīng)該每個(gè)繼承的類是可以的,實(shí)際卻會(huì)沖突:

class OtherMeta(type):
    pass
class SimpleDict(dict, metaclass=OtherMeta):
    pass
class TraceDict(SimpleDict, metaclass=TraceMeta):
    pass

>>>
Traceback ...
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

只能這么進(jìn)行:

class TraceMeta(type):
    ...
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

>>>
__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')
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

實(shí)際上,Python提供了類的decorator來(lái)使用:

def trace(klass):
    for key in dir(klass):
        value = getattr(klass, key)
        if isinstance(value, trace_types):
            wrapped = trace_func(value) # 將函數(shù)修改
            setattr(klass, key, wrapped) # 重新賦值函數(shù)
    return klass
@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
>>>
__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')

已經(jīng)有元類的類也可以用類的裝飾器:

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

>>>
__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')
  • 類裝飾器是一個(gè)簡(jiǎn)單的函數(shù),它接收一個(gè)類實(shí)例作為參數(shù)并返回一個(gè)新類或原始類的修改版本。
  • 當(dāng)你想用最少的樣板修改類的每個(gè)方法或?qū)傩詴r(shí),類裝飾器很有用。
  • 元類不容易組合在一起,而許多類裝飾器可以用來(lái)擴(kuò)展同一個(gè)類而不會(huì)發(fā)生沖突。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容