元類(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ā)生沖突。