python基础使用示例

重温一些常用的python使用基础,示例简单明了。

python基础

Python 基础主要总结Python 常用内置函数;Python 独有的语法特性、关键词
nonlocal, global 等;内置数据结构包括:列表(list), 字典(dict), 集合(set), 元组(tuple)
以及相关的高级模块collections 中的Counter, namedtuple, defaultdict
heapq 模块。

求绝对值

1
2
abs(-996.25)
996.25

元素都为真

接受一个迭代器,如果迭代器的所有元素都为真,那么返回True,否则返回False

1
2
3
4
all([1, 3, 5, 0, 8])
False
all([1, 4, 5, 6])
True

元素至少一个为真

接受一个迭代器,如果迭代器里至少有一个元素为真,那么返回True,否则返回False

1
2
3
4
any([0, '', [], ()])
False
any([0, 'e', [], ()])
True

进制转换

1
2
3
4
5
6
7
8
9
# 十进制转二进制
bin(1000)
'0b1111101000'
# 十进制转八进制
oct(1000)
'0o1750'
# 十进制转十六进制
hex(1000)
'0x3e8'

判断真假

1
2
3
4
5
6
7
8
bool([[]])
True
bool('')
False
bool(0)
False
bool([0, 0])
True

字符串转字节

将一个字符串转换成字节类型

1
2
3
4
5
s = 'hello中国world'
bytes(s, encoding='utf8')
b'hello\xe4\xb8\xad\xe5\x9b\xbdworld'
s.encode(encoding='utf8')
b'hello\xe4\xb8\xad\xe5\x9b\xbdworld'

Asall与十进制转换

1
2
3
4
5
6
# 查看十进制整数对应的ASCII 字符
chr(90)
'Z'
# 查看某个ASCII 字符对应的十进制数
ord('Y')
89

转字典

1
2
3
4
5
6
7
8
9
10
dict(a='A', b='B')
{'a': 'A', 'b': 'B'}
dict()
{}
dict(a='A', b='B')
{'a': 'A', 'b': 'B'}
dict(zip(['a', 'b'], ['A', 'B']))
{'a': 'A', 'b': 'B'}
dict([('a', 'A'), ('b', 'B')])
{'a': 'A', 'b': 'B'}

查看对象所有方法

不带参数时返回当前范围内的变量、方法和定义的类型列表;带参数时返回参数的属性,
方法列表

1
2
3
4
5
6
7
8
9
10
11
12
class Stu():
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return '姓名:%s,年龄:%s' % (self.name, self.age)
...
obj = Stu('张宝民', 68)
print(obj)
姓名:张宝民,年龄:68
dir(obj)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'age', 'name']

取商和余数

1
2
divmod(86, 15)
(5, 11)

计算表达式

将字符串str 当成有效的表达式来求值并返回计算结果取出字符串中内容

1
2
eval('2+5-3*8')
-17

过滤器

在函数中设定过滤条件,迭代元素,保留返回值为True 的元素

1
2
3
4
5
lis = filter(lambda x:x>5, [1, 2, 8, 4, 5, 6, 0])
print(lis)
<filter object at 0x0000028CDCB8E9C8>
list(lis)
[8, 6]

字符串格式化

格式化输出字符串,format(value, formatspec) 实质上是调用了value 的*``format(
formatspec)` 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
print('我叫:{0}, 今年:{1}岁'.format('鱼大仙', 16))
我叫:鱼大仙, 今年:16

>>>"{} {}".format("hello", "world") # 不设置指定位置,按默认顺序
'hello world'

"{0} {1}".format("hello", "world") # 设置指定位置
'hello world'

"{1} {0} {1}".format("hello", "world") # 设置指定位置
'world hello world'
print("姓名:{name}, 年龄 {age}".format(name="张三", age=20))
姓名:张三, 年龄 20
>>>
# 通过字典设置参数
site = {"name": "张三", "age": 20}
print("姓名:{name}, 年龄 {age}".format(**site))
姓名:张三, 年龄 20
>>>
# 通过列表索引设置参数
my_list = ['张三', 20]
print("姓名:{0[0]}, 年龄 {0[1]}".format(my_list)) # "0" 是必须的
姓名:张三, 年龄 20

#传入对象
class AssignValue(object):
def __init__(self, value):
self.value = value
...
my_value = AssignValue(6)
print('value 为: {0.value}'.format(my_value)) # "0" 是可选的
value 为: 6
# 数字格式化
print("{:.2f}".format(3.1415926));
3.14
数字格式输出描述
3.1415926{:.2f}3.14保留小数点后两位
3.1415926{:+.2f}+3.14带符号保留小数点后两位
-1{:+.2f}-1.00带符号保留小数点后两位
2.71828{:.0f}3不带小数
5{:0>2d}05数字补零 (填充左边, 宽度为2)
5{:x<4d}5xxx数字补x (填充右边, 宽度为4)
10{:x<4d}10xx数字补x (填充右边, 宽度为4)
1000000{:,}1,000,000以逗号分隔的数字格式
0.25{:.2%}25.00%百分比格式
1000000000{:.2e}1.00e+09指数记法
13{:>10d}13右对齐 (默认, 宽度为10)
13{:<10d}13左对齐 (宽度为10)
13{:^10d}13中间对齐 (宽度为10)
11‘{:b}’.format(11) ‘{:d}’.format(11) ‘{:o}’.format(11) ‘{:x}’.format(11) ‘{:#x}’.format(11) ‘{:#X}’.format(11)1011 11 13 b 0xb 0XB进制

^, <**, **> 分别是居中、左对齐、右对齐,后面带宽度, : 号后面带填充的字符,只能是一个字符,不指定则默认是用空格填充。

+ 表示在正数前显示 **+**,负数前显示 **-**; (空格)表示在正数前加空格

b、d、o、x 分别是二进制、十进制、八进制、十六进制。

此外我们可以使用大括号 {} 来转义大括号,如下实例:

内存地址

1
2
>>> id(obj)
2804021782472

isinstance

判断object 是否为类classinfo 的实例,是返回true

1
2
>>> isinstance(obj, Stu)
True

对元素为字典的列表排序

我们很少直接对字典进行排序,而是对元素为字典的列表进行排序。

比如,存在下面的 students 列表,它的元素是字典:

1
2
3
4
5
6
7
8
students = [
{'name': 'john', 'score': 'B', 'age': 15},
{'name': 'jane', 'score': 'A', 'age': 12},
{'name': 'dave', 'score': 'B', 'age': 10},
{'name': 'ethan', 'score': 'C', 'age': 20},
{'name': 'peter', 'score': 'B', 'age': 20},
{'name': 'mike', 'score': 'C', 'age': 16}
]
  • 按 score 从小到大排序
1
2
3
4
5
6
7
>>> sorted(students, key=lambda stu: stu['score'])
[{'age': 12, 'name': 'jane', 'score': 'A'},
{'age': 15, 'name': 'john', 'score': 'B'},
{'age': 10, 'name': 'dave', 'score': 'B'},
{'age': 20, 'name': 'peter', 'score': 'B'},
{'age': 20, 'name': 'ethan', 'score': 'C'},
{'age': 16, 'name': 'mike', 'score': 'C'}]

需要注意的是,这里是按照字母的 ascii 大小排序的,所以 score 从小到大,即从 ‘A’ 到 ‘C’。

  • 按 score 从大到小排序
1
2
3
4
5
6
7
>>> sorted(students, key=lambda stu: stu['score'], reverse=True)  # reverse 参数
[{'age': 20, 'name': 'ethan', 'score': 'C'},
{'age': 16, 'name': 'mike', 'score': 'C'},
{'age': 15, 'name': 'john', 'score': 'B'},
{'age': 10, 'name': 'dave', 'score': 'B'},
{'age': 20, 'name': 'peter', 'score': 'B'},
{'age': 12, 'name': 'jane', 'score': 'A'}]
  • 按 score 从小到大,再按 age 从小到大
1
2
3
4
5
6
7
>>> sorted(students, key=lambda stu: (stu['score'], stu['age']))
[{'age': 12, 'name': 'jane', 'score': 'A'},
{'age': 10, 'name': 'dave', 'score': 'B'},
{'age': 15, 'name': 'john', 'score': 'B'},
{'age': 20, 'name': 'peter', 'score': 'B'},
{'age': 16, 'name': 'mike', 'score': 'C'},
{'age': 20, 'name': 'ethan', 'score': 'C'}]
  • 按 score 从小到大,再按 age 从大到小

    像之前一个项目里,知道内置函数可以多元素排序,不知道当时怎么就以为多元素排序方式只能一致,要么都正序要么都倒序,导致那块业务用pandas去解决了多元素排序的问题。

1
2
3
4
5
6
7
>>> sorted(students, key=lambda stu: (stu['score'], -stu['age']))
[{'age': 12, 'name': 'jane', 'score': 'A'},
{'age': 20, 'name': 'peter', 'score': 'B'},
{'age': 15, 'name': 'john', 'score': 'B'},
{'age': 10, 'name': 'dave', 'score': 'B'},
{'age': 20, 'name': 'ethan', 'score': 'C'},
{'age': 16, 'name': 'mike', 'score': 'C'}]

交集/并集/差集

Python 中的集合也可以看成是数学意义上的无序和无重复元素的集合,因此,我们可以对两个集合作交集、并集等。

看看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> s1 = {1, 2, 3, 4, 5, 6}
>>> s2 = {3, 6, 9, 10, 12}
>>> s3 = {2, 3, 4}
>>> s1 & s2 # 交集
set([3, 6])
>>> s1 | s2 # 并集
set([1, 2, 3, 4, 5, 6, 9, 10, 12])
>>> s1 - s2 # 差集
set([1, 2, 4, 5])
>>> s3.issubset(s1) # s3 是否是 s1 的子集
True
>>> s3.issubset(s2) # s3 是否是 s2 的子集
False
>>> s1.issuperset(s3) # s1 是否是 s3 的超集
True
>>> s1.issuperset(s2) # s1 是否是 s2 的超集
False

reduce

reduce 函数的使用形式如下:

1
reduce(function, sequence[, initial])

解释:先将 sequence 的前两个 item 传给 function,即 function(item1, item2),函数的返回值和 sequence 的下一个 item 再传给 function,即 function(function(item1, item2), item3),如此迭代,直到 sequence 没有元素,如果有 initial,则作为初始值调用。

也就是说:

1
reduece(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)

看一些例子,就能很快理解了。

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> reduce(lambda x, y: x * y, [1, 2, 3, 4])  # 相当于 ((1 * 2) * 3) * 4
24
>>> reduce(lambda x, y: x * y, [1, 2, 3, 4], 5) # ((((5 * 1) * 2) * 3)) * 4
120
>>> reduce(lambda x, y: x / y, [2, 3, 4], 72) # (((72 / 2) / 3)) / 4
3
>>> reduce(lambda x, y: x + y, [1, 2, 3, 4], 5) # ((((5 + 1) + 2) + 3)) + 4
15
>>> reduce(lambda x, y: x - y, [8, 5, 1], 20) # ((20 - 8) - 5) - 1
6
>>> f = lambda a, b: a if (a > b) else b # 两两比较,取最大值
>>> reduce(f, [5, 8, 1, 10])
10

map

map 函数的使用形式如下:

1
map(function, sequence)

解释:对 sequence 中的 item 依次执行 function(item),并将结果组成一个 List 返回,也就是:

1
[function(item1), function(item2), function(item3), ...]

看一些简单的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> def square(x):
... return x * x

>>> map(square, [1, 2, 3, 4])
[1, 4, 9, 16]

>>> map(lambda x: x * x, [1, 2, 3, 4]) # 使用 lambda
[1, 4, 9, 16]

>>> map(str, [1, 2, 3, 4])
['1', '2', '3', '4']

>>> map(int, ['1', '2', '3', '4'])
[1, 2, 3, 4]

再看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def double(x):
return 2 * x

def triple(x):
return 3 *x

def square(x):
return x * x

funcs = [double, triple, square] # 列表元素是函数对象

# 相当于 [double(4), triple(4), square(4)]
value = list(map(lambda f: f(4), funcs))

print value

# output
[8, 12, 16]

上面的代码中,我们加了 list 转换,是为了兼容 Python3,在 Python2 中 map 直接返回列表,Python3 中返回迭代器。

闭包

在 Python 中,函数也是一个对象。因此,我们在定义函数时,可以再嵌套定义一个函数,并将该嵌套函数返回,比如:

1
2
3
4
5
6
from math import pow

def make_pow(n):
def inner_func(x): # 嵌套定义了 inner_func
return pow(x, n) # 注意这里引用了外部函数的 n
return inner_func # 返回 inner_func

上面的代码中,函数 make_pow 里面又定义了一个内部函数 inner_func,然后将该函数返回。因此,我们可以使用 make_pow 来生成另一个函数:

1
2
3
4
5
>>> pow2 = make_pow(2)  # pow2 是一个函数,参数 2 是一个自由变量
>>> pow2
<function inner_func at 0x10271faa0>
>>> pow2(6)
36.0

我们还注意到,内部函数 inner_func 引用了外部函数 make_pow 的自由变量 n,这也就意味着,当函数 make_pow 的生命周期结束之后,n 这个变量依然会保存在 inner_func 中,它被 inner_func 所引用。

1
2
3
4
5
6
7
>>> del make_pow         # 删除 make_pow
>>> pow3 = make_pow(3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'make_pow' is not defined
>>> pow2(9) # pow2 仍可正常调用,自由变量 2 仍保存在 pow2 中
81.0

像上面这种情况,一个函数返回了一个内部函数,该内部函数引用了外部函数的相关参数和变量,我们把该返回的内部函数称为闭包(Closure)

在上面的例子中,inner_func 就是一个闭包,它引用了自由变量 n

装饰器的副作用

前面提到,使用装饰器有一个瑕疵,就是被装饰的函数,它的函数名称已经不是原来的名称了,回到最开始的例子:

1
2
3
4
5
6
7
8
def makeitalic(func):
def wrapped():
return "<i>" + func() + "</i>"
return wrapped

@makeitalic
def hello():
return 'hello world'

函数 hellomakeitalic 装饰后,它的函数名称已经改变了:

1
2
>>> hello.__name__
'wrapped'

为了消除这样的副作用,Python 中的 functools 包提供了一个 wraps 的装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from functools import wraps

def makeitalic(func):
@wraps(func) # 加上 wraps 装饰器
def wrapped():
return "<i>" + func() + "</i>"
return wrapped

@makeitalic
def hello():
return 'hello world'

>>> hello.__name__
'hello'

partial 函数

Python 提供了一个 functools 的模块,该模块为高阶函数提供支持,partial 就是其中的一个函数,该函数的形式如下:

1
functools.partial(func[,*args][, **kwargs])

这里先举个例子,看看它是怎么用的。

假设有如下函数:

1
2
def multiply(x, y):
return x * y

现在,我们想返回某个数的双倍,即:

1
2
3
4
5
6
>>> multiply(3, y=2)
6
>>> multiply(4, y=2)
8
>>> multiply(5, y=2)
10

上面的调用有点繁琐,每次都要传入 y=2,我们想到可以定义一个新的函数,把 y=2 作为默认值,即:

1
2
def double(x, y=2):
return multiply(x, y)

现在,我们可以这样调用了:

1
2
3
4
5
6
>>> double(3)
6
>>> double(4)
8
>>> double(5)
10

事实上,我们可以不用自己定义 double,利用 partial,我们可以这样:

1
2
3
from functools import partial

double = partial(multiply, y=2)

partial 接收函数 multiply 作为参数,固定 multiply 的参数 y=2,并返回一个新的函数给 double,这跟我们自己定义 double 函数的效果是一样的。

所以,简单而言,partial 函数的功能就是:把一个函数的某些参数给固定住,返回一个新的函数。

需要注意的是,我们上面是固定了 multiply 的关键字参数 y=2,如果直接使用:

1
double = partial(multiply, 2)

2 是赋给了 multiply 最左边的参数 x,不信?我们可以验证一下:

1
2
3
4
5
6
7
8
from functools import partial

def subtraction(x, y):
return x - y

f = partial(subtraction, 4) # 4 赋给了 x
>>> f(10) # 4 - 10
-6
小结
  • partial 的功能:固定函数参数,返回一个新的函数。
  • 当函数参数太多,需要固定某些参数时,可以使用 functools.partial 创建一个新的函数。

str & repr

先看一个简单的例子:

1
2
3
4
5
6
class Foo(object):
def __init__(self, name):
self.name = name

>>> print Foo('ethan')
<__main__.Foo object at 0x10c37aa50>

在上面,我们使用 print 打印一个实例对象,但如果我们想打印更多信息呢,比如把 name 也打印出来,这时,我们可以在类中加入 __str__ 方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Foo(object):
def __init__(self, name):
self.name = name
def __str__(self):
return 'Foo object (name: %s)' % self.name

>>> print Foo('ethan') # 使用 print
Foo object (name: ethan)
>>>
>>> str(Foo('ethan')) # 使用 str
'Foo object (name: ethan)'
>>>
>>> Foo('ethan') # 直接显示
<__main__.Foo at 0x10c37a490>

可以看到,使用 print 和 str 输出的是 __str__ 方法返回的内容,但如果直接显示则不是,那能不能修改它的输出呢?当然可以,我们只需在类中加入 __repr__ 方法,比如:

1
2
3
4
5
6
7
8
9
10
class Foo(object):
def __init__(self, name):
self.name = name
def __str__(self):
return 'Foo object (name: %s)' % self.name
def __repr__(self):
return 'Foo object (name: %s)' % self.name

>>> Foo('ethan')
'Foo object (name: ethan)'

可以看到,现在直接使用 Foo('ethan') 也可以显示我们想要的结果了,然而,我们发现上面的代码中,__str____repr__ 方法的代码是一样的,能不能精简一点呢,当然可以,如下:

1
2
3
4
5
class Foo(object):
def __init__(self, name):
self.name = name
def __repr__(self):
return 'Foo object (name: %s)' % self.name

这样,print打印也会执行__repr__

slots 魔法

在 Python 中,我们在定义类的时候可以定义属性和方法。当我们创建了一个类的实例后,我们还可以给该实例绑定任意新的属性和方法。

看下面一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
class Point(object):    
def __init__(self, x=0, y=0):
self.x = x
self.y = y

>>> p = Point(3, 4)
>>> p.z = 5 # 绑定了一个新的属性
>>> p.z
5
>>> p.__dict__
{'x': 3, 'y': 4, 'z': 5}

在上面,我们创建了实例 p 之后,给它绑定了一个新的属性 z,这种动态绑定的功能虽然很有用,但它的代价是消耗了更多的内存。

因此,为了不浪费内存,可以使用 __slots__ 来告诉 Python 只给一个固定集合的属性分配空间,对上面的代码做一点改进,如下:

1
2
3
4
5
6
class Point(object):
__slots__ = ('x', 'y') # 只允许使用 x 和 y

def __init__(self, x=0, y=0):
self.x = x
self.y = y

上面,我们给 __slots__ 设置了一个元组,来限制类能添加的属性。现在,如果我们想绑定一个新的属性,比如 z,就会出错了,如下:

1
2
3
4
5
6
7
8
>>> p = Point(3, 4)
>>> p.z = 5
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-648-625ed954d865> in <module>()
----> 1 p.z = 5

AttributeError: 'Point' object has no attribute 'z'

使用 __slots__ 有一点需要注意的是,__slots__ 设置的属性仅对当前类有效,对继承的子类不起效,除非子类也定义了 __slots__,这样,子类允许定义的属性就是自身的 slots 加上父类的 slots。

super 原理

super 的工作原理如下:

1
2
3
def super(cls, inst):
mro = inst.__class__.mro()
return mro[mro.index(cls) + 1]

其中,cls 代表类,inst 代表实例,上面的代码做了两件事:

  • 获取 inst 的 MRO 列表
  • 查找 cls 在当前 MRO 列表中的 index, 并返回它的下一个类,即 mro[index + 1]

当你使用 super(cls, inst) 时,Python 会在 inst 的 MRO 列表上搜索 cls 的下一个类。

现在,让我们回到前面的例子。

首先看类 C 的 __init__ 方法:

1
super(C, self).__init__()

这里的 self 是当前 C 的实例,self.class.mro() 结果是:

1
[__main__.C, __main__.A, __main__.B, __main__.Base, object]

可以看到,C 的下一个类是 A,于是,跳到了 A 的 __init__,这时会打印出 enter A,并执行下面一行代码:

1
super(A, self).__init__()

注意,这里的 self 也是当前 C 的实例,MRO 列表跟上面是一样的,搜索 A 在 MRO 中的下一个类,发现是 B,于是,跳到了 B 的 __init__,这时会打印出 enter B,而不是 enter Base。

整个过程还是比较清晰的,关键是要理解 super 的工作方式,而不是想当然地认为 super 调用了父类的方法。

陌生的 metaclass

Python 中的元类(metaclass)是一个深度魔法,平时我们可能比较少接触到元类,本文将通过一些简单的例子来理解这个魔法。

类也是对象

在 Python 中,一切皆对象。字符串,列表,字典,函数是对象,类也是一个对象,因此你可以:

  • 把类赋值给一个变量
  • 把类作为函数参数进行传递
  • 把类作为函数的返回值
  • 在运行时动态地创建类

看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Foo(object):
foo = True

class Bar(object):
bar = True

def echo(cls):
print cls

def select(name):
if name == 'foo':
return Foo # 返回值是一个类
if name == 'bar':
return Bar

>>> echo(Foo) # 把类作为参数传递给函数 echo
<class '__main__.Foo'>
>>> cls = select('foo') # 函数 select 的返回值是一个类,把它赋给变量 cls
>>> cls
__main__.Foo

熟悉又陌生的 type

在日常使用中,我们经常使用 object 来派生一个类,事实上,在这种情况下,Python 解释器会调用 type 来创建类。

这里,出现了 type,没错,是你知道的 type,我们经常使用它来判断一个对象的类型,比如:

1
2
3
4
5
6
7
8
9
10
11
class Foo(object):
Foo = True

>>> type(10)
<type 'int'>
>>> type('hello')
<type 'str'>
>>> type(Foo())
<class '__main__.Foo'>
>>> type(Foo)
<type 'type'>

事实上,type 除了可以返回对象的类型,它还可以被用来动态地创建类(对象)。下面,我们看几个例子,来消化一下这句话。

使用 type 来创建类(对象)的方式如下:

type(类名, 父类的元组(针对继承的情况,可以为空),包含属性和方法的字典(名称和值))

最简单的情况

假设有下面的类:

1
2
class Foo(object):
pass

现在,我们不使用 class 关键字来定义,而使用 type,如下:

1
Foo = type('Foo', (object, ), {})    # 使用 type 创建了一个类对象

上面两种方式是等价的。我们看到,type 接收三个参数:

  • 第 1 个参数是字符串 ‘Foo’,表示类名
  • 第 2 个参数是元组 (object, ),表示所有的父类
  • 第 3 个参数是字典,这里是一个空字典,表示没有定义属性和方法

在上面,我们使用 type() 创建了一个名为 Foo 的类,然后把它赋给了变量 Foo,我们当然可以把它赋给其他变量,但是,此刻没必要给自己找麻烦。

接着,我们看看使用:

1
2
3
4
>>> print Foo
<class '__main__.Foo'>
>>> print Foo()
<__main__.Foo object at 0x10c34f250>
有属性和方法的情况

假设有下面的类:

1
2
3
4
5
class Foo(object):
foo = True
def greet(self):
print 'hello world'
print self.foo

type 来创建这个类,如下:

1
2
3
4
5
def greet(self):
print 'hello world'
print self.foo

Foo = type('Foo', (object, ), {'foo': True, 'greet': greet})

上面两种方式的效果是一样的,看下使用:

1
2
3
4
5
6
7
8
>>> f = Foo()
>>> f.foo
True
>>> f.greet
<bound method Foo.greet of <__main__.Foo object at 0x10c34f890>>
>>> f.greet()
hello world
True
继承的情况

再来看看继承的情况,假设有如下的父类:

1
2
class Base(object):
pass

我们用 Base 派生一个 Foo 类,如下:

1
2
class Foo(Base):
foo = True

改用 type 来创建,如下:

1
Foo = type('Foo', (Base, ), {'foo': True})

什么是元类(metaclass)

元类(metaclass)是用来创建类(对象)的可调用对象。这里的可调用对象可以是函数或者类等。但一般情况下,我们使用类作为元类。对于实例对象、类和元类,我们可以用下面的图来描述:

1
2
3
4
5
6
7
8
9
类是实例对象的模板,元类是类的模板

+----------+ +----------+ +----------+
| | | | | |
| | instance of | | instance of | |
| instance +------------>+ class +------------>+ metaclass|
| | | | | |
| | | | | |
+----------+ +----------+ +----------+

我们在前面使用了 type 来创建类(对象),事实上,**type 就是一个元类**。

那么,元类到底有什么用呢?要你何用…

元类的主要目的是为了控制类的创建行为。我们还是先来看看一些例子,以消化这句话。

元类的使用

先从一个简单的例子开始,假设有下面的类:

1
2
3
4
class Foo(object):
name = 'foo'
def bar(self):
print 'bar'

现在我们想给这个类的方法和属性名称前面加上 my_ 前缀,即 name 变成 my_name,bar 变成 my_bar,另外,我们还想加一个 echo 方法。当然,有很多种做法,这里展示用元类的做法。

1.首先,定义一个元类,按照默认习惯,类名以 Metaclass 结尾,代码如下:

1
2
3
4
5
6
7
8
9
class PrefixMetaclass(type):
def __new__(cls, name, bases, attrs):
# 给所有属性和方法前面加上前缀 my_
_attrs = (('my_' + name, value) for name, value in attrs.items())

_attrs = dict((name, value) for name, value in _attrs) # 转化为字典
_attrs['echo'] = lambda self, phrase: phrase # 增加了一个 echo 方法

return type.__new__(cls, name, bases, _attrs) # 返回创建后的类

上面的代码有几个需要注意的点:

  • PrefixMetaClass 从 type 继承,这是因为 PrefixMetaclass 是用来创建类的

  • __new__
    
    1
    2
    3
    4
    5
    6
    7



    是在



    __init__
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20



    之前被调用的特殊方法,它用来创建对象并返回创建后的对象,对它的参数解释如下:

    - cls:当前准备创建的类
    - name:类的名字
    - bases:类的父类集合
    - attrs:类的属性和方法,是一个字典

    2.接着,我们需要指示 Foo 使用 PrefixMetaclass 来定制类。

    在 Python2 中,我们只需在 Foo 中加一个 `__metaclass__` 的属性,如下:

    ```python
    class Foo(object):
    __metaclass__ = PrefixMetaclass
    name = 'foo'
    def bar(self):
    print 'bar'

在 Python3 中,这样做:

1
2
3
4
class Foo(metaclass=PrefixMetaclass):
name = 'foo'
def bar(self):
print 'bar'

现在,让我们看看使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> f = Foo()
>>> f.name # name 属性已经被改变
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-774-4511c8475833> in <module>()
----> 1 f.name

AttributeError: 'Foo' object has no attribute 'name'
>>>
>>> f.my_name
'foo'
>>> f.my_bar()
bar
>>> f.echo('hello')
'hello'

可以看到,Foo 原来的属性 name 已经变成了 my_name,而方法 bar 也变成了 my_bar,这就是元类的魔法。

再来看一个继承的例子,下面是完整的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class PrefixMetaclass(type):
def __new__(cls, name, bases, attrs):
# 给所有属性和方法前面加上前缀 my_
_attrs = (('my_' + name, value) for name, value in attrs.items())

_attrs = dict((name, value) for name, value in _attrs) # 转化为字典
_attrs['echo'] = lambda self, phrase: phrase # 增加了一个 echo 方法

return type.__new__(cls, name, bases, _attrs)

class Foo(object):
__metaclass__ = PrefixMetaclass # 注意跟 Python3 的写法有所区别
name = 'foo'
def bar(self):
print 'bar'

class Bar(Foo):
prop = 'bar'

其中,PrefixMetaclass 和 Foo 跟前面的定义是一样的,只是新增了 Bar,它继承自 Foo。先让我们看看使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> b = Bar()
>>> b.prop # 发现没这个属性
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-778-825e0b6563ea> in <module>()
----> 1 b.prop

AttributeError: 'Bar' object has no attribute 'prop'
>>> b.my_prop
'bar'
>>> b.my_name
'foo'
>>> b.my_bar()
bar
>>> b.echo('hello')
'hello'

我们发现,Bar 没有 prop 这个属性,但是有 my_prop 这个属性,这是为什么呢?

原来,当我们定义 class Bar(Foo) 时,Python 会首先在当前类,即 Bar 中寻找 __metaclass__,如果没有找到,就会在父类 Foo 中寻找 __metaclass__,如果找不到,就继续在 Foo 的父类寻找,如此继续下去,如果在任何父类都找不到 __metaclass__,就会到模块层次中寻找,如果还是找不到,就会用 type 来创建这个类。

这里,我们在 Foo 找到了 __metaclass__,Python 会使用 PrefixMetaclass 来创建 Bar,也就是说,元类会隐式地继承到子类,虽然没有显示地在子类使用 __metaclass__,这也解释了为什么 Bar 的 prop 属性被动态修改成了 my_prop。

写到这里,不知道你理解元类了没?希望理解了,如果没理解,就多看几遍吧~

小结

  • 在 Python 中,类也是一个对象。
  • 类创建实例,元类创建类。
  • 元类主要做了三件事:
    • 拦截类的创建
    • 修改类的定义
    • 返回修改后的类
  • 当你创建类时,解释器会调用元类来生成它,定义一个继承自 object 的普通类意味着调用 type 来创建它。