Effective Python
Table of Contents
用 python 的方式来思考
第一条 确认 python 使用的版本
了解 CPython, JPython, IronPython, PyPy 等流行的 python 运行环境。
python –version
可以通过运行 python3 来运行 python 3
import sys
print sys.version
第二条 遵循 pep8 规范
可以利于多人协作,后续修改工作更加的容易
指南:www.python.org/dev/peps/pep-0008
- 使用 space 来缩进
- 每行字符数不超过 79
- 文件中的函数和类之间 两个空行
- 同一类中,各个方法之间用一个空行隔开
- 赋值是左右都要有空格 a = b; 而不是 a=b;
函数命名
- 函数,变量,属性 用小写
- 受保护的用单个下划线开头 _leadingunderscore
- 私有实例属性 用两个下划线 _doubleleadingunderscore
- 类和异常 全部采用大写字母来拼写 ALLCAPS
- 类中的实例方法应该把首个参数命名为 self
- 类方法的首个参数命名为 cls
表达式和语句
- 采用内联形式的否定词 例如 if a is not b 而不是 if not a is b
- 不要通过检验长度的方法来判断 list 是否为空,直接使用 if not somelist;
- import 总应该放到文件的开头
- import 应该按顺序划分成三个部分,分别表示标准库模块,第三方模块,自用模块 ,每一部分按模块的字母顺序排列
pylint
是流行的 Python 源码静态分析工具,可以自动检测代码是否符合 PEP 8 风格,还可以找出常见的错误
了解 bytes, str, unicode 的区别
python3 有两种表示字符序列的类型:bytes 和 str,前者包含 8 位原始的二进制值;后者包含 Unicode 字符。
python2 也有两种,str 和 unicode。它们分别对应上面的两种情况。
在 python2 中:
type("你好") ⇒ <type 'str'>
type(u"你好") ⇒ <type 'unicode'>
在 python3 中:
type("你好") ⇒ <class 'str'>
type(u"你好") ⇒ <class 'str'>
我们在 Python 代码中经常出现两种使用场景:
- 使用 UTF-8 编码的字符串
操作没有特定编码的 Unicode 字符
所以,写了两个辅助函数用于转换: 在 Python 3 中,接受 str 或 bytes,
def to_str(bytes_or_str): if isinstance(bytes_or_str, bytes): value = bytes_or_str.decode('utf-8') else: value = bytes_or_str return value def to_bytes(bytes_or_str): if isinstance(bytes_or_str, str): value = bytes_or_str.encode('utf-8') else: value = bytes_or_str return value
在 Python 2 中同理。
在 Python 3 中,str 于 bytes 在任何情况下绝不等价,而 Python 2 却不是。
另外一个问题是在 Python3 中,使用 open 函数获取文件句柄,这个句柄默认采用 UTF-8 编码,而在 Python 2 中默认是二进制形式。 所以我们在 Python 2 中可以这样,但 3 中不行:
with open('file', 'w') as f: f.write()
我们要这样:
with open('file', 'wb') as f: f.write()
读取也一样,要用 'rb' 模式。
用辅助函数取代复杂表达式
表达式如果变得复杂应该考虑将其拆成小块。
了解切割序列方法
a = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
fist four: a[:4]
last four: a[-4:]
切割列表时,start end 越界也不会出现问题。
在单次切片操作,不要同时指定 start, end 和 stride
Python 还支持 somelist[start:end:stride] stride 是步长。
a = ['red', 'orange', 'yellow', 'green', 'blue', 'purple']
odds = a[::2]
evens = a[1::2]
有一种技巧,就是翻转字符串:
x = b'mongoose'
y = x[::-1]
不要同时指定 start,end,stride 否则程序会变得非常难理解,可以分两步切割。
用列表推到来取代 map 和 filter
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
squares = [x**2 for x in a]
squares = map(lambda x: x ** 2, a)
even_squares = [x**2 for x in a if x % 2 == 0]
alt = map(lambda x: x**2, filter(lambda x: x % 2 == 0, a))
对于字典结构,我们也可以用:
chile_ranks = {'ghost': 1, 'habanero': 2, 'cayenne': 3}
rank_dict = {rank: name for name, rank in chile_ranks.items()}
chile_len_set = {len(name) for name in rank_dict.values()}
不要使用含有两个以上表达式的列表推导
flat = [x for row in matrix for x in row]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
squared = [[x**2 for x in row] for row in matrix]
[[1, 4, 9], [16, 25, 36], [49, 64, 81]]
像下面这种就不是很好了:
b = [x for x in a if x > 4 if x % 2 == 0]
我们可以搭配 if for 或辅助函数去处理。
用生成器表达式来改写数据量较大的列表推导
当输入的数据量较大时,列表推导可能会因为占用太多的内存而出问题, 所以我们这时要使用生成器表达式。
# 这时迭代器
value = [len(x) for x in open('my_file.txt')]
# 这个就是生成器
it = (len(x) for x in open('my_file.txt'))
print(next(it))
生成器表达还有个好处,就是可以相互组合。
c = [1 , 2 , 3]
d = (x for x in c)
e = (y**2 for y in d)
next(e)
next(e)
串在一起的生成器表达式执行速度很快。
尽量用 enumerate 取代 range
flavor_list = ['vanilla', 'chocolate', 'pecan', 'strawberry']
for flavor in flavor_list:
print('%s is delicious' % flavor)
for i, flavor in enumerate(flavor_list):
print('%d: %s' % (i + 1, flavor))
# 还可以指定开始计数使用的值
for i, flavor in enumerate(flavor_list, 1):
print('%d: %s' % (i, flavor))
用 zip 函数同时遍历两个迭代器
在 Python3 中,zip 可以把两个或以上的迭代器封装成生成器,以便稍后求值。
names = ['Cecilia', 'Lise', 'Marie']
letters = [len(n) for n in names]
longest_name = None
max_letters = 0
for name, count in zip(names, letters):
if count > max_letters:
longest_name = name
max_letters = count
print(longest_name)
如果在 Python 2 中用 zip 迭代数据量比较大的迭代器时,会占用大量内存并导致程序崩溃。 这时应该用 itertools 中的 izip 函数。
如果提供的迭代器长度不等,zip 会自动终止。 例如
names.append('Rosalind')
for name, count in zip(names, letters):
print(name)
不要在 for 和 while 后面写 else
for/else 结构中,会使得整个循环执行完立即运行 else。
try/except/else,中 else 是如果 try 没有失败,就执行 else。
for/else 中奇怪的是,如果 for 结构中意外的结束,会导致不执行 else。
合理利用 try/except/else/finally 结构中的每个代码块
finally
try/finally 可以将异常向上传播,而且在异常发生时可以执行清理工作。 这种有一个常见的用途,就是确保程序能可靠的关闭文件句柄。
handle = open('random_data.txt') # May raise IOError try: data = handle.read() # May raise UnicodeDecodeError finally: handle.close() # Always runs after try:
else
try/except/else 结构可以清晰的描述哪些异常会有自己代码处理,哪些异常传播到上一级。 如果 try 块没有发生异常,就执行 else 块。
def load_json_key(data, key): try: result_dict = json.loads(data) # May raise ValueError except ValueError as e: raise KeyError from e else: return result_dict[key] # May raise KeyError
混合使用
import json UNDEFINED = object() def divide_json(path): handle = open(path, 'r+') # May raise IOError try: data = handle.read() # May raise UnicodeDecodeError op = json.loads(data) # May raise ValueError value = ( op['numerator'] / op['denominator']) # May raise ZeroDivisionError except ZeroDivisionError as e: return UNDEFINED else: op['result'] = value result = json.dumps(op) handle.seek(0) handle.write(result) # May raise IOError return value finally: handle.close() # Always runs
这几块相互配合非常的有用。
函数
尽量用异常来表示特殊情况,而不要返回 None
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
return None
这种写法会给人一种误导,我们可以这样
def divide(a, b):
try:
return True, a / b
except ZeroDivisionError:
return False, None
success, result = divide(x, y)
if not success:
print('Invalid inputs')
另一种方法,要抛出异常
def divide(a, b):
try:
return a / b
except ZeroDivisionError as e:
raise ValueError('Invalid inputs') from e
这样调用者就不得不处理这个异常了
try:
result = divide(x, y)
except ValueError:
print('Invalid inputs')
else:
print('Result is %.1f' % result)
了解如何在闭包里使用外围作用域中的变量
闭包是一种定义在某个作用域中的函数。
class Sorter(object):
def __init__(self, group):
self.group = group
self.found = False
def __call__(self, x):
if x in self.group:
self.found = True
return (0, x)
return (1, x)
sorter = Sorter(group)
numbers.sort(key=sorter)
assert sorter.found is True
print('Found:', found)
print(numbers)
考虑用生成器改写直接返回列表的函数 ✓
下面的代码,用 append 方法将这些词的首字母索引加入到列表中,
def index_words(text):
result = []
if text:
result.append(0)
for index, letter in enumerate(text):
if letter == ' ':
result.append(index + 1)
return result
这段代码有些问题。其一,写的太过于拥挤,有很多无用的信息。其二,如果数据量很大的话,在返回之前,将所有的信息放在列表里有可能耗尽内存资源。
其实,这里使用生成器来写更好。生成器是使用 yield 表达式的函数。调用生成器时,它并不真正运行,而是返回迭代器。生成器传给 yield 的每个值都会有迭代器返回给调用者。
def index_words_iter(text):
if text:
yield 0
for index, letter in enumerate(text):
if letter == ' ':
yield index + 1
result = list(index_words_iter(address))
在参数上面迭代要小心
def normalize(numbers):
total = sum(numbers)
result = []
for value in numbers:
percent = 100 * value / total
result.append(percent)
return result
如果传递一个列表的参数是可以的。现在我们要扩大应用范围,从文件中读取。
def read_visits(data_path):
with open(data_path) as f:
for line in f:
yield int(line)
it = read_visits('my_numbers.txt')
percentages = normalize(it)
print(percentages)
但是现在却没有结果,原因就是迭代器只能产生一轮结果。
解决的办法是我们可以复制一份数据,但这样不好,如果数据大了就会有问题。
下面我们实现一个迭代器协议的容器类。 我们只需要让自己的类实现 iter 方法即可。
class ReadVisits(object):
def __init__(self, data_path):
self.data_path = data_path
def __iter__(self):
with open(self.data_path) as f:
for line in f:
yield int(line)
visits = ReadVisits(path)
percentages = normalize(visits)
print(percentages)
现在我们就可以这样了
visits = [15, 35, 80]
normalize_defensive(visits) # No error
visits = ReadVisits(path)
normalize_defensive(visits) # No error
用数量可变的位置参数
def log(message, *values): # The only difference
if not values:
print(message)
else:
values_str = ', '.join(str(x) for x in values)
print('%s: %s' % (message, values_str))
log('My numbers are', 1, 2)
log('Hi there') # Much better
对生成器使用 *
def my_generator():
for i in range(10):
yield i
def my_func(*args):
print(args)
it = my_generator()
my_func(*it)
在 def 中使用 *args, 可令函数接受数量可变的位置参数。 对迭代器使用 * 可能导致程序耗尽内存。
用关键字参数表达可选的行为
def remainder(number, divisor):
return number % divisor
remainder(20, divisor=7)
remainder(divisor=7, number=20)
def flow_rate(weight_diff, time_diff, period=1):
return (weight_diff / time_diff) * period
用 None 和文档字符串来描述具有动态默认值的参数
def log(message, when=datetime.now()):
print('%s: %s' % (when, message))
log('Hi there!')
sleep(0.1)
log('Hi again!')
上面显示的结果一样,因为 datatime.now()只是执行了一次。 参数的默认值,会在每个模块加载的时候求出。
def log(message, when=None):
"""Log a message with a timestamp.
Args:
message: Message to print.
when: datetime of when the message occurred.
Defaults to the present time.
"""
when = datetime.now() if when is None else when
print('%s: %s' % (when, message))
改成这样就可以了
用只能以关键字形式指定的参数来确保代码明晰
# 这种是使用位置参数来指定
result = safe_division(1.0, 10**500, True, False)
# 这种使用关键字形式来指定参数
safe_division_b(1.0, 10**500, ignore_overflow=True)
在 python 3 里,我们使用 * 来结束位置参数
def safe_division_c(number, divisor, *,
ignore_overflow=False,
ignore_zero_division=False):
try:
return number / divisor
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
else:
raise
在 Python 2 中,我们可以这样
def safe_division_d(number, divisor, **kwargs):
ignore_overflow = kwargs.pop('ignore_overflow', False)
ignore_zero_div = kwargs.pop('ignore_zero_division', False)
if kwargs:
raise TypeError('Unexpected **kwargs: %r' % kwargs)
try:
return number / divisor
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_div:
return float('inf')
else:
raise
类与继承
尽量用辅助类来维护程序的状态,而不要用字典和元组
不要使用包含字典的字典,这种结构维护会很麻烦。这时我们应该将其拆成类。
class Subject(object):
def __init__(self):
self._grades = []
def report_grade(self, score, weight):
self._grades.append(Grade(score, weight))
def average_grade(self):
total, total_weight = 0, 0
for grade in self._grades:
total += grade.score * grade.weight
total_weight += grade.weight
return total / total_weight
class Student(object):
def __init__(self):
self._subjects = {}
def subject(self, name):
if name not in self._subjects:
self._subjects[name] = Subject()
return self._subjects[name]
def average_grade(self):
total, count = 0, 0
for subject in self._subjects.values():
total += subject.average_grade()
count += 1
return total / count
简单的接口应该接受函数,而不是类实例
如下,用 λ 表达式充当 key 挂钩,根据名字的长度来排序
names = ['Socrates', 'Archimedes', 'Plato', 'Aristotle']
names.sort(key=lambda x: len(x))
print(names)
这里用到了 defaultdict 函数。如果使用 defaultdict,只要你传入一个默认的工厂方法,那么请求一个不存在的 key 时, 便会调用这个工厂方法使用其结果来作为这个 key 的默认值。
def increment_with_report(current, increments):
added_count = 0
def missing():
nonlocal added_count # Stateful closure
added_count += 1
return 0
result = defaultdict(missing, current)
for key, amount in increments:
result[key] += amount
return result, added_count
current = {'green': 12, 'blue': 3}
increments = [
('red', 5),
('blue', 17),
('orange', 9),
]
result, count = increment_with_report(current, increments)
这里我们还可以通过定义新的类解决:
class CountMissing(object):
def __init__(self):
self.added = 0
def missing(self):
self.added += 1
return 0
counter = CountMissing()
result = defaultdict(counter.missing, current) # Method reference
上面是用过辅助类来改写带状态的闭包。但是还是不够清晰。 这里有一个 call 的特殊方法,它能使相关对象能够像函数那样得到调用。
class BetterCountMissing(object):
def __init__(self):
self.added = 0
def __call__(self):
self.added += 1
return 0
counter = BetterCountMissing()
result = defaultdict(counter, current)
以 @classmethod 形式的多态去通用地构建对象 ✓
多态使得类能满足相同的接口或继承自相同的类,但却有着各自不同的功能。 为了实现 MapReduce 流程,我们需要定义公共基类来表示输入数据。
class InputData(object):
def read(self):
raise NotImplementedError
InputData 具体子类
class PathInputData(InputData):
def __init__(self, path):
super().__init__()
self.path = path
def read(self):
return open(self.path).read()
另外,我们还需要定义 Worker
class Worker(object):
def __init__(self, input_data):
self.input_data = input_data
self.result = None
def map(self):
raise NotImplementedError
def reduce(self, other):
raise NotImplementedError
class LineCountWorker(Worker):
def map(self):
data = self.input_data.read()
self.result = data.count('\n')
def reduce(self, other):
self.result += other.result
import os
def generate_inputs(data_dir):
for name in os.listdir(data_dir):
yield PathInputData(os.path.join(data_dir, name))
def create_workers(input_list):
workers = []
for input_data in input_list:
workers.append(LineCountWorker(input_data))
return workers
from threading import Thread
def execute(workers):
threads = [Thread(target=w.map) for w in workers]
for thread in threads: thread.start()
for thread in threads: thread.join()
first, rest = workers[0], workers[1:]
for worker in rest:
first.reduce(worker)
return first.result
def mapreduce(data_dir):
inputs = generate_inputs(data_dir)
workers = create_workers(inputs)
return execute(workers)
下面是测试函数
from tempfile import TemporaryDirectory
import random
def write_test_files(tmpdir):
for i in range(100):
with open(os.path.join(tmpdir, str(i)), 'w') as f:
f.write('\n' * random.randint(0, 100))
with TemporaryDirectory() as tmpdir:
write_test_files(tmpdir)
result = mapreduce(tmpdir)
print('There are', result, 'lines')
上面的代码能够完美的运行,但是不够通用。
class GenericInputData(object):
def read(self):
raise NotImplementedError
@classmethod
def generate_inputs(cls, config):
raise NotImplementedError
class PathInputData(GenericInputData):
def __init__(self, path):
super().__init__()
self.path = path
def read(self):
return open(self.path).read()
@classmethod
def generate_inputs(cls, config):
data_dir = config['data_dir']
for name in os.listdir(data_dir):
yield cls(os.path.join(data_dir, name))
class GenericWorker(object):
def __init__(self, input_data):
self.input_data = input_data
self.result = None
def map(self):
raise NotImplementedError
def reduce(self, other):
raise NotImplementedError
@classmethod
def create_workers(cls, input_class, config):
workers = []
for input_data in input_class.generate_inputs(config):
workers.append(cls(input_data))
return workers
class LineCountWorker(GenericWorker):
def map(self):
data = self.input_data.read()
self.result = data.count('\n')
def reduce(self, other):
self.result += other.result
def mapreduce(worker_class, input_class, config):
workers = worker_class.create_workers(input_class, config)
return execute(workers)
with TemporaryDirectory() as tmpdir:
write_test_files(tmpdir)
config = {'data_dir': tmpdir}
result = mapreduce(LineCountWorker, PathInputData, config)
print('There are', result, 'lines')
用 super 初始化父类
class MyBaseClass(object):
def __init__(self, value):
self.value = value
class MyChildClass(MyBaseClass):
def __init__(self):
MyBaseClass.__init__(self, 5)
这种方法对于简单的继承是可行的,但是在许多的情况下会出问题。
如果子类有多重继承以及出现砖石型继承就有很大的问题。
class TimesFiveCorrect(MyBaseClass):
def __init__(self, value):
super(TimesFiveCorrect, self).__init__(value)
self.value *= 5
class PlusTwoCorrect(MyBaseClass):
def __init__(self, value):
super(PlusTwoCorrect, self).__init__(value)
self.value += 2
class GoodWay(TimesFiveCorrect, PlusTwoCorrect):
def __init__(self, value):
super(GoodWay, self).__init__(value)
只在使用 Mix-in 组件制作工具类时进行多重继承
在开发时我们要尽量避免使用多重继承,若一定要利用多重继承带来的便利性,我们就考虑编写 mix-in 类。
mix-in 是一种小型的类,它只定义了其他类可能需要提供的一套附加方法。 例如,我们要把内存中的 python 对象转换为字典形式,以便将其序列化。
class ToDictMixin(object):
def to_dict(self):
return self._traverse_dict(self.__dict__)
# 具体实现,我们只需要用 hasattr 函数动态的访问属性、用 isinstance 动态的检查对象类型,并用 __dict__ 来访问实例内部的字典
def _traverse_dict(self, instance_dict):
output = {}
for key, value in instance_dict.items():
output[key] = self._traverse(key, value)
return output
def _traverse(self, key, value):
if isinstance(value, ToDictMixin):
return value.to_dict()
elif isinstance(value, dict):
return self._traverse_dict(value)
elif isinstance(value, list):
return [self._traverse(key, i) for i in value]
elif hasattr(value, '__dict__'):
return self._traverse_dict(value.__dict__)
else:
return value
下面演示了如何将 max-in 把二叉树表示为字典
class BinaryTree(ToDictMixin):
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
tree = BinaryTree(10,
left=BinaryTree(7, right=BinaryTree(9)),
right=BinaryTree(13, left=BinaryTree(11)))
print(tree.to_dict)
mix-in 的功能是可以随时安插通用的功能,并且能在必要的时候覆盖它们。
class BinaryTreeWithParent(BinaryTree):
def __init__(self, value, left=None,
right=None, parent=None):
super().__init__(value, left=left, right=right)
self.parent = parent
# 覆盖原来的方法
def _traverse(self, key, value):
if (isinstance(value, BinaryTreeWithParent) and
key == 'parent'):
return value.value # Prevent cycles
else:
return super()._traverse(key, value)
多个 mix-in 可以相互组合,例如一个通用的 JSON 序列化方法。
class JsonMixin(object):
@classmethod
def from_json(cls, data):
kwargs = json.loads(data)
return cls(**kwargs)
def to_json(self):
return json.dumps(self.to_dict())
class DatacenterRack(ToDictMixin, JsonMixin):
def __init__(self, switch=None, machines=None):
self.switch = Switch(**switch)
self.machines = [
Machine(**kwargs) for kwargs in machines]
class Switch(ToDictMixin, JsonMixin):
def __init__(self, ports=None, speed=None):
self.ports = ports
self.speed = speed
class Machine(ToDictMixin, JsonMixin):
def __init__(self, cores=None, ram=None, disk=None):
self.cores = cores
self.ram = ram
self.disk = disk
serialized = """{
"switch": {"ports": 5, "speed": 1e9},
"machines": [
{"cores": 8, "ram": 32e9, "disk": 5e12},
{"cores": 4, "ram": 16e9, "disk": 1e12},
{"cores": 2, "ram": 4e9, "disk": 500e9}
]
}"""
deserialized = DatacenterRack.from_json(serialized)
roundtrip = deserialized.to_json()
多用 public 属性,少用 privte 属性
编译器无法严格保证 private 的私密性
继承 collections.abc 以实现自定义的容器类型
如果要定制的子类比较简单,那就可以直接从 python 的容器类型,如 list 或 dict 入手
class FrequencyList(list):
def __init__(self, members):
super().__init__(members)
def frequency(self):
counts = {}
for item in self:
counts.setdefault(item, 0)
counts[item] += 1
return counts
foo = FrequencyList(['a', 'b', 'a', 'c', 'b', 'a', 'd'])
print('Length is', len(foo))
foo.pop()
print('After pop:', repr(foo))
print('Frequency:', foo.frequency())
如果想要编写自制的容器类型,可以从 collections.abc 模块入手。
元类及属性
谈到 python 特性时,元类经常被提到,但很少有人能理解它的实际用途。
metaclass 只是模糊的描述了一种高于类,而又超乎类的概念。
用纯属性取代 get 和 set 方法
不要编写 get set 方法。
class Resistor(object):
def __init__(self, ohms):
self.ohms = ohms
self.voltage = 0
self.current = 0
r1 = Resistor(50e3)
r1.ohms = 10e3
如果想在设置属性的时候实现特殊的行为,可以改用@property 和 setter 方法。
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
r2 = VoltageResistance(1e3)
print('Before: %5r amps' % r2.current)
# 会执行 setter 方法
r2.voltage = 10
print('After: %5r amps' % r2.current)
@property 要遵循最小惊讶原则,不要产生奇怪的副作用。
考虑用 @property 来代替属性重构
@property 还有一种高级的用法,就是可以把简单的数值属性迁移为实时计算(on-the-fly calculation).
class Bucket(object):
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 ('Bucket(max_quota=%d, quota_consumed=%d)' %
(self.max_quota, self.quota_consumed))
@property
def quota(self):
return self.max_quota - self.quota_consumed
@property 可以为现有的实例属性添加功能
用描述符来改写需要复用的 @property 方法 ✓
@property 有个明显的缺点就是不便于复用
描述符可以提供 get set 方法。
用 getattr, getattribute 和 setattr 实现按需生成的属性 ✓
class LazyDB(object):
def __init__(self):
self.exists = 5
def __getattr__(self, name):
value = 'Value for %s' % name
setattr(self, name, value)
return value
data = LazyDB()
print('Before:', data.__dict__)
print('foo: ', data.foo)
print('After: ', data.__dict__)
当系统查询找不到查询的属性,而且定义了 getattr, 就会调用这个方法。 而 getattribute 方法就是查到了,也会调用
用元类来验证子类
首先,定义元类,我们要继承 type, python 默认会把那些类的 class 语句体中所含的相关内容,发送给元类的 new 方法。
class Meta(type):
def __new__(meta, name, bases, class_dict):
print(meta, name, bases, class_dict)
return type.__new__(meta, name, bases, class_dict)
# 这是 python2 写法
class MyClassInPython2(object):
__metaclass__ = Meta
stuff = 123
def foo(self):
pass
# python 3
class MyClassInPython2(object, metaclass=Meta):
stuff = 123
def foo(self):
pass
class ValidatePolygon(type):
def __new__(meta, name, bases, class_dict):
# Don't validate the abstract Polygon class
if bases != (object,):
if class_dict['sides'] < 3:
raise ValueError('Polygons need 3+ sides')
return type.__new__(meta, name, bases, class_dict)
class Polygon(object, metaclass=ValidatePolygon):
sides = None # Specified by subclasses
@classmethod
def interior_angles(cls):
return (cls.sides - 2) * 180
class Triangle(Polygon):
sides = 3
print(Triangle.interior_angles())
用元类来注册子类
用元类来注解类的属性
我们可以在类完全定义好前,率先修改类的属性