高级编程#
1. 几种方法#
1.1. 实例方法#
当方法所实现功能与具体实例有关时,通常定义为实例方法。如下,在列表中存储待办事项。add_event()
方法是实例方法,因为不同的日程表对象会存储不同的待办事项。
class Calendar:
def __init__(self):
self.events = []
def add_event(self, event):
self.events.append(event)
def is_weekend(self, date):
return date.weekday() > 4
1.2. 静态方法#
因为 is_weekend()
不关心具体的日程表实例,即和 event
没有关系,通过删除其第一个参数 self
,并使用@staticmethod 装饰器定义,is_weekend()
成为了一个静态方法。
class Calendar:
def __init__(self):
self.events = []
def add_event(self, event):
self.events.append(event)
@staticmethod
def is_weekend(date):
return date.weekday() > 4
约定成俗的是,使用类名直接调用会更加地明确。在一些特殊场景,如,在类的实例方法定义中调用静态方法时,会使用实例进行调用。
from datetime import date
today = date.today()
Calendar.is_weekend(today)
静态方法并不常用,从语法上来说静态方法完全可以从 class 中提取出,单独作为一个函数存在。通常当某函数与某类 class 的功能有业务上的紧密或唯一联系时,才会以静态方法形式作为类的一部分存在。静态方法体中没有使用任何的类或者对象属性。
1.3. 类方法#
假设我们可以从本地文件中读取构建返回一个日程表实例,如何实现?
class Calendar:
def __init__(self):
self.events = []
def add_event(self, event):
self.events.append(event)
@staticmethod
def is_weekend(date):
return date.weekday() > 4
@staticmethod
def from_json(filename):
c = Calendar()
... # 读取文件、添加待办事项等操作,省略
return c # 构建日程表对象实例 c 并返回
从文件中读取信息构建实例,定义一个静态方法 from_json()
,以上程序在功能实现上暂时没有问题。
注意到 from_json()
中日程表实例的实现是 c = Calendar()
,而这在类的继承中会带来一个问题。
class WorkCalendar(Calendar):
pass
c = WorkCalendar.from_json("mycal.calendar")
c = WorkCalendar.from_json("mycal.calendar")
子类 WorkCalendar
调用 from_json()
,实际创建返回的是父类 Calendar
的对象实例,因为 from_json()
方法体中写死了 c = Calendar()
,而我们期待的是返回 WorkCalendar
类的对象实例。
使用场景:类方法十分地常用,尤其常作为替代构建函数(alternative constructors)使用,类方法名中通常带有 from_xx()
、make_xx()
、create_xx()
等,功能是使用不同的参数集构建对象。
类方法会把调用自己的类作为第一个方法参数 cls
。
class Calendar:
def __init__(self):
self.events = []
def add_event(self, event):
self.events.append(event)
@staticmethod
def is_weekend(date):
return date.weekday() > 4
@classmethod
def from_json(cls, filename):
c = cls()
... # 读取文件、添加待办事项等操作,省略
return c # 构建日程表对象实例 c 并返回
同静态方法的调用一样,通常直接使用类调用类方法,不过使用实例调用类方法也完全没有问题。在 Python 中,静态方法和类方法都是通过描述符协议(descriptor protocol)实现。
2. 迭代控制#
2.1. 索引与切片#
要表现得像列表那样按照下标取出元素,需要实现 __getitem__()
方法。但,__getitem__()
传入的参数可能是一个 int
,也可能是一个切片对象 slice
,故要做判断:
class Fib():
def __getitem__(self, n):
if isinstance(n, int): # n 是索引
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
if isinstance(n, slice): # n 是切片
start = n.start
stop = n.stop
if start is None:
start = 0
a, b = 1, 1
L = []
for x in range(stop):
if x >= start:
L.append(a)
a, b = b, a + b
return L
f = Fib()
print(f[0:5])
print(f[:10])
2.2. 迭代器#
魔法方法 |
类方法 |
异步方法 |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
若一个类想被用于 for
循环,就必须实现一个 __iter__()
方法,该方法返回一个迭代对象,然后,Python 的 for
循环就会不断调用该迭代对象的 __next__()
方法拿到循环的下一个值,直到遇到 StopIteration
错误时退出循环。
构建可迭代的对象和迭代器时经常会出现错误,原因是混淆了二者。要知道,可迭代的对象有个 __iter__
方法,每次都实例化一个新的迭代器;而迭代器要实现 __next__
方法,
返回单个元素,此外还要实现 __iter__
方法,返回迭代器本身。
class Fib:
def __init__(self):
self.a = 0
self.b = 1 # 初始化两个计数器 a,b
def __iter__(self):
return self # 实例本身就是迭代对象,故返回自己
def __next__(self):
self.a, self.b = self.b, self.a + self.b
if self.a > 1000:
raise StopIteration()
return self.a
for n in Fib():
print(n)
3. 上下文#
魔法方法 |
异步方法 |
---|---|
|
|
|
|
3.1. contextmanager
#
在 Python 中,读写文件这样的资源要特别注意,必须在使用完毕后正确关闭它们。正确关闭文件资源的一个方法是使用 try...finally
或简化为 with
:
with open('/path/to/file', 'r') as f:
f.read()
并不是只有 open()
函数返回的 f
对象才能使用 with
语句。实际上,任何对象,只要正确实现了上下文管理,就可用于 with
语句。实现上下文管理是通过 __enter__
和 __exit__
这两个方法实现的。这样我们就可把自己写的资源对象用于 with
语句:
class Query:
def __init__(self, name):
self.name = name
def query(self):
print(f'Query info about {self.name}...')
def __enter__(self):
print('Begin')
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type:
print('Error')
else:
print('End')
with Query('Bob') as q:
q.query()
这样仍然很繁琐,因此 Python 的标准库 contextlib
提供了更简单的写法,contextmanager
这个装饰器接受一个生成器,用 yield
语句把 with ... as var
把变量输出出去,然后,with
语句就可正常地工作了:
from contextlib import contextmanager
class Query:
def __init__(self, name):
self.name = name
def query(self):
print(f'Query info about {self.name}...')
@contextmanager
def create_query(name):
# __enter__
print('Begin')
q = Query(name)
yield q
# __exit__
print('End')
with create_query('Bob') as q:
q.query()
很多时候,我们希望在某段代码执行前后自动执行特定代码,也可用 @contextmanager
实现。例如:
from contextlib import contextmanager
@contextmanager
def tag(name):
print(f"<{name}>")
yield
print(f"</{name}>")
with tag("h1"):
print("hello")
print("world")
代码的执行顺序是:
with
语句首先执行yield
之前的语句,因此打印出;
yield
调用会执行with
语句内部的所有语句;最后执行
yield
之后的语句,打印出。
故,@contextmanager 让我们通过编写生成器,它的作用就是把任意对象变为上下文对象,并支持 with 语句。来简化上下文管理。
3.2. closing()
#
若一个对象没有实现上下文,我们就不能把它用于 with
语句。这个时候,可用 closing()
来把该对象变为上下文对象。例如,用 with
语句使用 urlopen()
:
from contextlib import closing
from urllib.request import urlopen
with closing(urlopen('https://www.python.org')) as page:
for line in page:
print(line)
closing
亦为一个经过 @contextmanager
装饰的生成器,它的作用就是把任意对象变为上下文对象,并支持 with 语句。
3.3. 嵌套上下文#
import contextlib
@contextlib.contextmanager
def test_context(name):
print(f'enter, my name is {name}')
yield
print(f'exit, my name is {name}')
with test_context('aaa'):
with test_context('bbb'):
print('========== in main ============')
# 等价于
with test_context('aaa'), test_context('bbb'):
print('========== in main ============')
4. 自定义类装饰器#
类装饰器是一个简单的函数,它接收一个类实例作为参数,并返回一个新的类或原类的修改版本。当你想用最少的模板来修改一个类的每个方法或属性时,类装饰器是很有用的。
4.1. 无参数装饰器#
基于类装饰器的实现,必须实现 __call__
和 __init__
两个内置函数。
__init__
:接收被装饰函数。__call__
:实现装饰逻辑。
class logger:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print(f"[INFO]: the function {self.func.__name__}() is running...")
return self.func(*args, **kwargs)
@logger
def say(something):
print(f"say {something}!")
say("hello")
# [INFO]: the function say() is running...
# say hello!
4.2. 参数装饰器#
带参数和不带参数的类装饰器有很大的不同。
__init__
:不再接收被装饰函数,而是接收传入参数。__call__
:接收被装饰函数,实现装饰逻辑。
class logger:
def __init__(self, level='INFO'):
self.level = level
def __call__(self, func): # 接受函数
def wrapper(*args, **kwargs):
print(
f"[{self.level}]: the function {func.__name__}() is running..."
)
func(*args, **kwargs)
return wrapper #返回函数
@logger(level='WARNING')
def say(something):
print("say {something}!")
say("hello")
# [WARNING]: the function say() is running...
# say hello!