Effective Python 筆記摘錄5.2

元類(Metaclasses)和屬性(Attributes)

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

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

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

通常都會實現(xiàn)類似Java的getter和setter來進行內(nèi)部屬性的獲取:

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

用起來簡單但是并不Pythonic:

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里面,不用明確定義getter和setter,首先從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裝飾器。
下面的類繼承了Resistor,然后維護了自己的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

這樣就可以直接以屬性進行調(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í)行類型檢查和值校驗,比如只允許電阻大于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
>>>
Traceback ...
ValueError: ohms must be > 0; got 0

構建函數(shù)的時候也不行:

BoundedResistance(-5)

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

因為調(diào)BoundedResistance.init的時候,調(diào)用了super().init,而super().init調(diào)用了self.ohms = ohms,此時,@ohms.setter就會執(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

第一次初始化的時候,還沒有_ohms,以后訪問的時候就有,所以會報錯:

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

不要在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

行為怪異:

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方法里面修改和對象相關的狀態(tài)。
@property 最大的缺點是屬性的方法只能由子類共享。更多可參見Item46。


  • Item45: 使用注解@property而不是重構屬性

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

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

消費配額:

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)

這個實現(xiàn)的問題是:我永遠不知道存儲桶開始的配額級別。
新的桶:

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

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

@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:
        # 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

重新再運行一次實例:

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ù)模型方面不斷取得進展。
主要是服務頂層設計不變的情況下進行的,但是當?shù)臅r候反復使用@property的話,應該考慮重構這個類。


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

比如現(xiàn)在有一個作業(yè)給分的實現(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很容易實現(xiàn))

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

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

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()
# 當調(diào)用這個的時候
exam.writing_grade = 40
# 等價于這個表達式
Exam.__dict__['writing_grade'].__set__(exam, 40)
# 同理
exam.writing_grade

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

訪問getattribute的時候,如果實例變量沒有,則會用類變量。
如果實現(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

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

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

應該對每個實例變量維護相應的結果:

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

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

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: 對于懶惰(Lazy)的屬性,使用getattr, getattribute, and setattr

比如用對象來表示數(shù)據(jù)庫里的記錄。代碼也必須知道數(shù)據(jù)庫的樣子,但是在 Python 中,將 Python 對象連接到數(shù)據(jù)庫的代碼不需要顯式指定記錄的模式,而是通用的。
Python 使用 __getattr__ 特殊方法使這種動態(tài)行為成為可能。如果一個類定義了 __getattr__,則每次在對象的實例字典中找不到屬性時都會調(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

如果我訪問了缺失的foo,會先調(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語句來觀察,其中用了super()的__getattr__來獲得結果:

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

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

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

找不到對應屬性的時候拋出AttributeError的錯誤。
比如例子:

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

實現(xiàn)通用代碼少不了用hasattr來檢查屬性是否存在,還有getattr來提取屬性的數(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方法)來做到:將值分配給Python對象時,懶惰地將數(shù)據(jù)推回數(shù)據(jù)庫:

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

建一個打log的實例:

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}

假設我希望對我的對象進行屬性訪問以實際查找關聯(lián)字典中的鍵:

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

但是,程序會直接運行到報錯:

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

主要是由于運行了self._data導致又運行了__getattribute__,導致了無限循環(huán)。
而是應該從父類中獲取屬性,然后從這個值里面返回對應的結果:

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__驗證子類

元類(MetaClass)通過繼承type來定義。在運行時構建類的類型。元類通過__new__來接收關聯(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

元類可以訪問類的名稱,以及它的父類繼承自(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>}

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

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

當邊大于2的時候正常,但是當邊為2的時候,報錯:

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__來方便引入相同的特性:

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

每個類只能指定一個元類。當我想要第二個元類來驗證顏色時:

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

然后期望同樣的方式來驗證:

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

只能通過比較復雜的繼承來修復:

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)

驗證顏色和驗證邊:

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__來做:

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

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

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

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

以下是更多的測試:

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

甚至可以用它來做一些復雜的場景的繼承:

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

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


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

另一種公共的使用元類的方式是自動注冊程序里的類型。當反向搜索時(把簡單的identifier映射回對應的類),“注冊”是有用的。
比如,用JSON來序列化object。

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

可以成功序列化點:

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

此時,需要反序列化JSON,然后構建點:

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)

問題在于需要提前知道類的類型(BetterPoint2D,Point2D)。應該是接收一個很大的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})'

可以注冊類以及反序列化:

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

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

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)

但是忘記注冊就會有問題:

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!

反序列化了忘記注冊的類:

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

元類可以來做到每次都注冊的需求:

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

這樣每次都能使得類得到注冊:

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__來替代元類實現(xiàn)一些類注冊功能。


  • Item50: 用__set_name__注解類屬性

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

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)

然后定義一個顧客類:

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

但是顯得冗余,因為first_name已經(jīng)可以表示了,為什么還要構建出一個Field來保存同樣的信息?

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

此時可以用元類來處理:

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

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

class DatabaseRow(metaclass=Meta):
    pass

最后,每個屬性用無參的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)

然后,實際使用時,直接繼承:

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

但是,當忘記繼承的時候,會出錯:

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來完成工作:

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

  • Item51: 對可組合的類擴展,使用類裝飾器而不是元類

假如現(xiàn)在要追蹤各個函數(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

需要每個函數(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

用元類也可以解決問題:

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

按理說應該每個繼承的類是可以的,實際卻會沖突:

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

只能這么進行:

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

實際上,Python提供了類的decorator來使用:

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')
  • 類裝飾器是一個簡單的函數(shù),它接收一個類實例作為參數(shù)并返回一個新類或原始類的修改版本。
  • 當你想用最少的樣板修改類的每個方法或屬性時,類裝飾器很有用。
  • 元類不容易組合在一起,而許多類裝飾器可以用來擴展同一個類而不會發(fā)生沖突。
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容