python 语法杂记 -- 装饰器,类的特殊方法,常量类

最近在看 python 一些语法知识,虽然 python 代码写了不少,但是对于一些高级语法的了解还不够深入;因此本文主要记录了一些比较生疏的知识点,主要包括了装饰器,类的特殊方法,常量类这三个方面的知识。

装饰器

装饰器本质上是一个高阶函数,以被装饰的函数为参数,并返回一个包装后的函数给被装饰函数。

装饰器的一般使用形式如下:

1
2
3
@decorator
def func():
pass

等价于下面的形式:

1
2
3
def func():
pass
func = decorator(func)

装饰器可以定义多个,离函数定义最近的装饰器先被调用,比如:

1
2
3
4
@decorator_one
@decorator_two
def func():
pass

等价于:

1
2
3
4
def func():
pass

func = decorator_one(decorator_two(func))

对带参数的函数进行装饰

对带参数的函数进行装饰这个需求很常见,简单来说,装饰带参数的函数时,需要将参数传递给装饰器内部需要返回的函数 (也叫内嵌包装函数),也就是说内嵌包装函数的参数跟被装饰函数的参数对应,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
def makeitalic(func):
def wrapped(*args, **kwargs):
ret = func(*args, **kwargs)
return '<i>' + ret + '</i>'
return wrapped

@makeitalic
def hello(name):
return 'hello %s' % name

@makeitalic
def hello2(name1, name2):
return 'hello %s, %s' % (name1, name2)

可以看到,装饰器内部需要返回的函数 wrapped 带上了参数 (*args, **kwargs), 目的是为了适应可变参数。使用如下

1
2
3
4
>>> hello('python')
'<i>hello python</i>'
>>> hello2('python', 'java')
'<i>hello python, java</i>'

带参数的装饰器

上面的例子,我们增强了函数 hello 的功能,给它的返回加上了标签 <i>...</i>,现在,我们想改用标签 <b>...</b><p>...</p>。是不是要像前面一样,再定义一个类似 makeitalic 的装饰器呢?其实,我们可以可以使用带参数的装饰器,简单来说,就是在原来的装饰器基础上再封装一层函数,将标签作为参数,返回一个装饰器

1
2
3
4
5
6
7
8
def wrap_in_tag(tag):
def decorator(func):
def wrapped(*args, **kwargs):
ret = func(*args, **kwargs)
return '<' + tag + '>' + ret + '</' + tag + '>'
return wrapped

return decorator

现在,我们可以根据需要生成想要的装饰器了:

1
2
3
4
5
6
7
makebold = wrap_in_tag('b')  # 根据 'b' 返回 makebold 生成器`
@makebold
def hello(name):
return 'hello %s' % name

>>> hello('world')
'<b>hello world</b>'

上面的形式也可以写得更加简洁:

1
2
3
@wrap_in_tag('b')
def hello(name):
return 'hello %s' % name

这就是带参数的装饰器,其实就是在装饰器外面多了一层包装,根据不同的参数返回不同的装饰器。

私有成员

python 不像 C++ 有 private 之类的关键字,但是 可以在属性或方法的名称前面加上两个下划线 __, 来限制用户访问对象的属性或方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
In [1]: class Animal(object):
...: def __init__(self, name):
...: self.__name = name
...: def greet(self):
...: print ('Hello, I am %s.' % self.__name)
...:

In [2]: a = Animal("dog")

In [3]: a.__name
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-5-5d5520ef9fe0> in <module>()
----> 1 a.__name

AttributeError: 'Animal' object has no attribute '__name'

In [4]: a.greet()
Hello, I am dog.

类方法 vs 静态方法

python 中的类有两个特殊的方法:类方法和静态方法,两个方法主要有以下特点

  1. 两个方法均是属于类而不是属于对象的
  2. 两个方法都是通过内置的装饰器定义(@classmethod@staticmethod
  3. 类方法可以访问类属性,静态方法则不能

如下是类方法的一个例子

1
2
3
4
5
6
7
8
9
10
class A(object):
bar = 1
@classmethod
def class_foo(cls):
print 'Hello, ', cls
print cls.bar

>>> A.class_foo() # 直接通过类来调用方法
Hello, <class '__main__.A'>
1

在上面,我们使用了 classmethod 装饰方法 class_foo,它就变成了一个类方法,class_foo 的参数是 cls,代表类本身,当我们使用 A.class_foo() 时,cls 就会接收 A 作为参数。另外,被 classmethod 装饰的方法由于持有 cls 参数,因此我们可以在方法里面调用类的属性、方法,比如 cls.bar

上面的类方法是可以修改类的属性的,静态方法定义方式类似,但是不会改变类和实例状态;如下所示,静态方法没有 self 和 cls 参数,因此没法改变类的属性,可以把它看成是一个普通的函数,甚至可以把它写到类外面,但是有时候,类就是需要这么一类方法,如果写到外面,一是不利于类的完整性,二是不利于命名空间的整洁性。

1
2
3
4
5
6
7
8
9
10
class A(object):
@staticmethod
def static_foo():
print 'Hello'

>>> a = A()
>>> a.static_foo()
Hello
>>> A.static_foo()
Hello

那么,这两个方法该在什么时候使用呢?参考 class method vs static method in Python 如下

We generally use class method to create factory methods. Factory methods return class object ( similar to a constructor ) for different use cases. We generally use static methods to create utility functions.

如下是个比较形象的例子

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
# Python program to demonstrate 
# use of class method and static method.
from datetime import date

class Person:
def __init__(self, name, age):
self.name = name
self.age = age

# a class method to create a Person object by birth year.
@classmethod
def fromBirthYear(cls, name, year):
return cls(name, date.today().year - year)

# a static method to check if a Person is adult or not.
@staticmethod
def isAdult(age):
return age > 18

person1 = Person('mayank', 21)
person2 = Person.fromBirthYear('mayank', 1996)

print person1.age
print person2.age

# print the result
print Person.isAdult(22)

魔法方法

以双下划线 __ 包裹起来的方法,比如最常见的 __init__,这些方法被称为魔法方法(magic method)或特殊方法(special method), 这些方法可以给 Python 的类提供特殊功能,方便我们定制一个类。

__new__

在 Python 中,当我们创建一个类的实例时,类会先调用 __new__(cls[, ...]) 来创建并返回实例,然后 __init__ 方法再对该实例(self)中的变量进行初始化

关于 __new____init__ 有以下几点需要注意:

  1. __new__ 是在 __init__ 之前被调用的
  2. __new__ 是类方法,__init__ 是实例方法
  3. 重载 __new__ 方法,需要返回类的实例

一般情况下,我们不需要重载 __new__ 方法。但在某些情况下,我们想控制实例的创建过程,这时可以通过重载 __new__ 方法来实现。 比如说,下面的例子通过了 __new__ 来实现单例模式

1
2
3
4
5
6
7
8
class Singleton(object):
_instance = None
def __new__(cls, *args, **kw):
if not cls._instance:
cls._instance = super(Singleton, cls).__new__(cls, *args, **kw)
return cls._instance
class MyClass(Singleton):
a = 1

__str__ & __repr__

这两个方法主要是在直接打印类时候调用的,通过下面两个例子可以比较直观地看到如何使用

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
class Foo(object):
def __init__(self, name):
self.name = name
def __repr__(self):
return 'Foo object (name: %s)' % self.name

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

__iter__

在某些情况下,我们希望实例对象可被用于 for...in 循环,这时我们需要在类中定义 __iter__next(在 Python3 中是 __next__)方法,其中,__iter__ 返回一个迭代对象,next 返回容器的下一个元素,在没有后续元素时抛出 StopIteration 异常

如下是一个斐波那契数列的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Fib(object):
def __init__(self):
self.a, self.b = 0, 1

def __iter__(self): # 返回迭代器对象本身
return self

def next(self): # 返回容器下一个元素
self.a, self.b = self.b, self.a + self.b
return self.a

>>> fib = Fib()
>>> for i in fib:
... if i > 10:
... break
... print i
...
1
1
2
3
5
8

__getitem__ & __setitem__ & __delitem__

有时,我们希望可以使用 obj[n] 这种方式对实例对象进行取值,比如对斐波那契数列,我们希望可以取出其中的某一项,这时我们需要在类中实现 __getitem__ 方法,比如下面的例子:

1
2
3
4
5
6
7
8
9
10
class Fib(object):
def __getitem__(self, n):
a, b = 1, 1
for x in xrange(n):
a, b = b, a + b
return a

>>> fib = Fib()
>>> fib[0], fib[1], fib[2], fib[3], fib[4], fib[5]
(1, 1, 2, 3, 5, 8)

类似地,__setitem__ 用于设置值,__delitem__ 用于删除值,让我们看下面一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Point(object):
def __init__(self):
self.coordinate = {}

def __getitem__(self, key):
return self.coordinate.get(key)

def __setitem__(self, key, value):
self.coordinate[key] = value

def __delitem__(self, key):
del self.coordinate[key]
print 'delete %s' % key

def __len__(self):
return len(self.coordinate)

在上面,我们定义了一个 Point 类,它有一个属性 coordinate(坐标),是一个字典,让我们看看使用:

1
2
3
4
5
6
7
8
9
10
>>> p = Point()
>>> p['x'] = 2 # 对应于 p.__setitem__('x', 2)
>>> p['y'] = 5 # 对应于 p.__setitem__('y', 5)
>>> len(p) # 对应于 p.__len__
2
>>> p['x'] # 对应于 p.__getitem__('x')
2
>>> del p['x'] # 对应于 p.__delitem__('x')
>>> len(p)
1

__call__

我们一般使用 obj.method () 来调用对象的方法,那能不能直接在实例本身上调用呢?在 Python 中,只要我们在类中定义 __call__ 方法,就可以对实例进行调用,比如下面的例子:

1
2
3
4
5
class Point(object):
def __init__(self, x, y):
self.x, self.y = x, y
def __call__(self, z):
return self.x + self.y + z

使用如下:

1
2
3
4
5
>>> p = Point(3, 4)
>>> callable(p) # 使用 callable 判断对象是否能被调用
True
>>> p(6) # 传入参数,对实例进行调用,对应 p.__call__(6)
13 # 3+4+6

__slots__

__slots__ 跟前面的方法不太一样,因为这是一个类的属性,当我们创建了一个类的实例后,我们还可以给该实例绑定任意新的属性和方法,如下

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}

这样其实是违背了 OOP 的封装性的理念,而且会消耗更多的内存,为了禁止这一属性,可以使用 __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__ 设置了一个元组,来限制类能添加的属性。现在,如果想绑定一个新的属性,就会出错了

常量类

在 Python 中使用常量一般来说有以下两种方式:

  1. 通过命名风格来提醒使用者该变量代表的意义为常量,如常量名所有字母大写,用下划线连接各个单词,PEP8 给出的编程风格就是这样的
  2. 通过自定义的类实现常量功能。这要求符合命名全部为大写值一旦绑定便不可再修改 这两个条件。下面是一种较为常见的解决办法,将常量放到同一个文件中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# FileName:constant.py
class _const:
class ConstError(TypeError):
pass
class ConstCaseError(ConstError):
pass

def __setattr__(self, name, value):
if name in self.__dict__:
raise self.ConstError, "Can't change const value!"
if not name.isupper():
raise self.ConstCaseError, 'const "%s" is not all letters are capitalized' %name
self.__dict__[name] = value

import sys
sys.modules[__name__] = _const()

import constant
constant.MAX_COUNT = 10
constant.JOBS = 5
constant.PROCESSES = 8

简单解释一下,对象的所有属性及属性的值都存储在 __dict__ 中, 上面的 __setattr__ 方法在对象每次创建新常量的时候会判断常量是否已经被定义过,如果已经定义过则 raise error,从而确保了已经创建的常量不可修改。

sys.modules[__name__] = _const() 则确保了当上面的文件被 import 时,其 module 名称 (也就是 __name__ 的值,当文件被运行时 __name__ 的值为 __main__, 被 import 时 __name__ 的值则是 module 名称) 对应的是一个 _const() 对象,从而可以直接通过其创建常量。因此,使用的方法如下

1
2
import constant
print(constant.MAX_COUNT)

参考:

class method vs static method in Python Data model Python 之旅