内网穿透

内网穿透原理图:

90-1.png

但是这种技术妨碍了 ISP 收互联网企业的带宽费用,因此 ISP 不会喜欢这种技术。

代码:
https://github.com/dropsong/py_webServer

GIL(全局解释器锁)

Python 语言和 GIL 没有关系。仅仅是由于历史原因在 Cpython 虚拟机(解释器),难以移除 GIL。

GIL:全局解释器锁。每个线程在执行的过程都需要先获取 GIL,保证同一时刻只有一个线程可以执行代码。

线程释放 GIL 锁的情况: 在 IO 操作等可能会引起阻塞的 system call 之前,可以暂时释放 GIL,但在执行完毕后,必须重新获取 GIL,Python 3.x 使用计时器(执行时间达到阈值后,当前线程释放GIL)或 Python 2.x tickets 计数达到 100

Python 使用多进程是可以利用多核的 CPU 资源的。

多线程爬取比单线程性能有提升,因为遇到 IO 阻塞会自动释放 GIL 锁

私有化

  • xx: 公有变量
  • _x: 单前置下划线,私有化属性或方法,from somemodule import * 禁止导入,类对象和子类可以访问
  • __xx:双前置下划线,避免与子类中的属性命名冲突,无法在外部直接访问(名字重整所以访问不到)
  • __xx__:双前后下划线,用户名字空间的魔法对象或属性。例如:__init__不要自己发明这样的名字
  • xx_:单后置下划线,用于避免与 Python 关键词的冲突

模块导入问题

不可变类型的全局变量用 from 方式导入的时候,相当于一份拷贝,在一个文件中修改,对另一个文件中的值没有影响。

多继承的解决方案

总结:

  1. 使用 super()
  2. 使用 *args**kwargs

例子:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
print("******多继承使用 super().__init__ 发生的状态******")
class Parent(object):
def __init__(self, name, *args, **kwargs): # 为避免多继承报错,使用不定长参数,接受参数
print('parent 的 init 开始被调用')
self.name = name
print('parent 的 init 结束被调用')

class Son1(Parent):
def __init__(self, name, age, *args, **kwargs): # 为避免多继承报错,使用不定长参数,接受参数
print('Son1 的 init 开始被调用')
self.age = age
super().__init__(name, *args, **kwargs) # 为避免多继承报错,使用不定长参数,接受参数
print('Son1 的 init 结束被调用')

class Son2(Parent):
def __init__(self, name, gender, *args, **kwargs): # 为避免多继承报错,使用不定长参数,接受参数
print('Son2 的 init 开始被调用')
self.gender = gender
super().__init__(name, *args, **kwargs) # 为避免多继承报错,使用不定长参数,接受参数
print('Son2 的 init 结束被调用')

class Grandson(Son1, Son2):
def __init__(self, name, age, gender):
print('Grandson 的 init 开始被调用')
# 多继承时,相对于使用类名.__init__方法,要把每个父类全部写一遍
# 而 super 只用一句话,执行了全部父类的方法,这也是为何多继承需要全部传参的一个原因
# super(Grandson, self).__init__(name, age, gender)
super().__init__(name, age, gender)
print('Grandson 的 init 结束被调用')

print(Grandson.__mro__) #打印出来顺序是谁,将来调用的就是谁
gs = Grandson('grandson', 12, '男')
print('姓名:', gs.name)
print('年龄:', gs.age)
print('性别:', gs.gender)
print("******多继承使用 super().__init__ 发生的状态******\n\n")


'''
******多继承使用 super().__init__ 发生的状态******
(<class '__main__.Grandson'>, <class '__main__.Son1'>, <class '__main__.Son2'>, <class '__main__.Parent'>, <class 'object'>)
Grandson 的 init 开始被调用
Son1 的 init 开始被调用
Son2 的 init 开始被调用
parent 的 init 开始被调用
parent 的 init 结束被调用
Son2 的 init 结束被调用
Son1 的 init 结束被调用
Grandson 的 init 结束被调用
姓名: grandson
年龄: 12
性别: 男
******多继承使用 super().__init__ 发生的状态******

'''

静态方法和类方法

类属性、实例属性

实例属性属于对象,类属性属于类。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Province(object):
# 类属性
country = '中国'
def __init__(self, name):
# 实例属性
self.name = name

# 创建一个实例对象
obj = Province('山东省')
# 直接访问实例属性
print(obj.name)
# 直接访问类属性
Province.country

'''
山东省
'''

实例属性需要通过对象来访,类属性通过类访问。

类属性在内存中只保存一份,实例属性在每个对象中都要保存一份。

实例方法、静态方法、类方法

例子:

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

def ord_func(self):
""" 定义实例方法,至少有一个 self 参数 """
# print(self.name)
print('实例方法')

@classmethod
def class_func(cls):
""" 定义类方法,至少有一个 cls 参数 """
print('类方法')

@staticmethod
def static_func():
""" 定义静态方法 ,无默认参数"""
print('静态方法')

f = Foo("中国")

# 调用实例方法
f.ord_func()

# 调用类方法
Foo.class_func()

# 调用静态方法
Foo.static_func()

'''
实例方法
类方法
静态方法
'''

property 属性

一种懒人方法,可以不用写括号。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Foo:
def func(self):
print('I am func')

# 定义 property 属性
@property
def prop(self):
print('I am prop')

foo_obj = Foo()
foo_obj.func() # 调用实例方法
foo_obj.prop # 调用 property 属性

'''
I am func
I am prop
'''

property 属性可以用来实现只能读不能写的属性。 (想必这就是莽蛇语言的独到之处了吧)

但是也可以有别的方法去写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Goods:
@property
def price(self):
print('@property')

@price.setter
def price(self, value):
print('@price.setter')

@price.deleter
def price(self):
print('@price.deleter')

obj = Goods()
obj.price
obj.price = 123 # 自动执行 @price.setter 修饰的 price 方法,并将 123 赋值给方法的参数
del obj.price # 自动执行 @price.deleter 修饰的 price 方法

'''
@property
@price.setter
@price.deleter
'''

使用类属性的方式创建 property 属性时,经典类和新式类无区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Foo:
def get_bar(self):
return 'laowang'

BAR = property(get_bar)

obj = Foo()
reuslt = obj.BAR # 自动调用 get_bar 方法,并获取方法的返回值
print(reuslt)

'''
laowang
'''

上面这种写法可以拓展为:

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
class Foo(object):
def get_bar(self):
print("getter...")
return 'laowang'

def set_bar(self, value):
"""必须两个参数"""
print("setter...")
return 'set value' + value

def del_bar(self):
print("deleter...")
return 'laowang'

BAR = property(get_bar, set_bar, del_bar, "description...")

obj = Foo()
obj.BAR # 自动调用第一个参数中定义的方法:get_bar
obj.BAR = "alex" # 自动调用第二个参数中定义的方法:set_bar 方法,并将“alex”当作参数传入
desc = Foo.BAR.__doc__ # 自动获取第四个参数中设置的值:description...
print(desc)
del obj.BAR # 自动调用第三个参数中定义的方法:del_bar 方法

'''
getter...
setter...
description...
deleter...
'''

使用 property 升级 getter 和 setter 方法:

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

def getMoney(self):
return self.__money

def setMoney(self, value):
if isinstance(value, int):
self.__money = value
else:
print("error:不是整型数字")
# 定义一个属性,当对这个 money 设置值时调用 setMoney,当获取值时调用 getMoney

money = property(getMoney, setMoney)


a = Money()
a.money = 100 # 调用 setMoney 方法
print(a.money) # 调用 getMoney 方法

'''
100
'''

魔法属性 call

对象后面加括号,触发执行。

1
2
3
4
5
6
7
8
9
10
11
12
class Foo:
def __init__(self):
pass
def __call__(self, *args, **kwargs):
print('__call__')

obj = Foo() # 执行 __init__
obj() # 执行 __call__''

'''
__call__
'''

莽蛇语言魅力时刻。

with 与“上下文管理器”

场景:应用程序打开某些资源并执行完业务逻辑之后,必须做的一件事就是要关闭(断开)该资源。

普通版:

1
2
3
4
def m1():
f = open("output.txt", "w")
f.write("python 之禅")
f.close()

这样写有一个潜在的问题,如果在调用 write 的过程中,出现了异常进而导致后续代码无法继续执行,close 方法无法被正常调用,因此资源就会一直被该程序占用者释放。那么该如何改进代码呢?

进阶版:

1
2
3
4
5
6
7
8
def m2():
f = open("output.txt", "w")
try:
f.write("python 之禅")
except IOError:
print("oops error")
finally:
f.close()

缺点是写起来太麻烦。

高级版:

1
2
3
def m3():
with open("output.txt", "r") as f:
f.write("Python 之禅")

一种更加简洁、优雅的方式就是用 with 关键字。类似于 C++ 的 RAII 。

上下文管理器的实现和魔法属性 __enter__()__exit__() 有关,和 C++ 原理上差不多,但是碍于 Python 混乱的语法,稍微显得麻烦一点,此处略去。

实现上下文管理器的另外方式:
Python 还提供了一个 contextmanager 的装饰器,更进一步简化了上下文管理器的实现方式。通过 yield 将函数分割成两部分,yield 之前的语句在 __enter__ 方法中执行,yield 之后的语句在 __exit__ 方法中执行。紧跟在 yield 后面的值是函数的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from contextlib import contextmanager

@contextmanager
def my_open(path, mode):
try:
f = open(path, mode)
yield f
except Exception as result:
print(result)
finally:
f.close()

# 调用
with my_open('out.txt', 'w') as f:
f.write("hello , the simplest context manager")

MySQL 知识补充

绕个弯补充一下,之后会可能会用到。

SQL 注入

例子:

90-2.png

用字符串拼接的方式去进行 sql 是非常危险的。

解决方法,直接传到 execute() 中,不要分两步写:

1
2
params = [find_name]
count = cs1.execute('select * from goods where name=%s', params)

账户管理

在生产环境下操作数据库时,绝对不可以使用 root 账户连接,而是创建特定的账户,授予这个账户特定的操作权限。

MySQL 的账户可以分为以下几种:

  • 服务实例级账号:启动了一个 mysql,即为一个数据库实例;如果某用户如 root,拥有服务实例级分配的权限,那么该账号就可以删除所有的数据库、连同这些库中的表
  • 数据库级别账号:对特定数据库执行增删改查的所有操作
  • 数据表级别账号:对特定表执行增删改查等所有操作
  • 字段级别的权限:对某些表的特定字段进行操作
  • 存储程序级别的账号:对存储程序进行增删改查的操作

注册新用户及之后的一些操作:

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
mysql> select user from mysql.user;
+------------------+
| user |
+------------------+
| mysql.infoschema |
| mysql.session |
| mysql.sys |
| root |
+------------------+
4 rows in set (0.00 sec)

mysql> create user 'yanfei'@'localhost' identified by '123';
Query OK, 0 rows affected (0.02 sec)

mysql> grant select on akashi.* to 'yanfei'@'localhost';
Query OK, 0 rows affected (0.01 sec)

mysql> select user from mysql.user;
+------------------+
| user |
+------------------+
| mysql.infoschema |
| mysql.session |
| mysql.sys |
| root |
| yanfei |
+------------------+
5 rows in set (0.00 sec)

使用 sudo mysql -u yanfei -p 进入之后:

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
36
37
38
39
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| akashi |
| information_schema |
| performance_schema |
+--------------------+
3 rows in set (0.00 sec)

mysql> use akashi;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show tables;
+------------------+
| Tables_in_akashi |
+------------------+
| goods |
| miaochuan |
| user |
+------------------+
3 rows in set (0.00 sec)

mysql> select * from goods limit 5;
+----+-----------------------------+-----------+------------+----------+------------------+------------------------+
| id | name | cate_name | brand_name | price | is_show | is_saleoff |
+----+-----------------------------+-----------+------------+----------+------------------+------------------------+
| 1 | r510vc 15.6 英寸笔记本 | 笔记本 | 华硕 | 3399.000 | 0x01 | 0x00 |
| 2 | g150th 15.6 英寸游戏本 | 游戏本 | 雷神 | 8499.000 | 0x01 | 0x00 |
| 3 | x550cc 15.6 英寸笔记本 | 笔记本 | 华硕 | 2799.000 | 0x01 | 0x00 |
| 4 | x240 超极本 | 超级本 | 联想 | 4880.000 | 0x01 | 0x00 |
| 5 | u330p 13.3 英寸超极本 | 超级本 | 联想 | 4299.000 | 0x01 | 0x00 |
+----+-----------------------------+-----------+------------+----------+------------------+------------------------+
5 rows in set (0.00 sec)

mysql> insert into goods values(6, 非常厉害的笔记本, 笔记本 ,联想, 3333.0);
ERROR 1142 (42000): INSERT command denied to user 'yanfei'@'localhost' for table 'goods'

主从配置

主机操作

在主机上配置账户:

1
2
3
4
create user 'backup'@'192.168.19.129' identified by '123';
# 192.168.19.129 是从机的 IP
GRANT REPLICATION SLAVE ON *.* TO 'backup'@'192.168.19.129';
flush privileges;

修改 mysql 的配置:

1
2
3
4
5
6
7
sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf

log_bin = /var/log/mysql/mysql-bin.log
server-id = 1
expire_logs_days = 1
max_binlog_size = 100M
binlog_do_db = test

重启 mysql 服务(重启之前确保 test 数据库是存在的):

1
sudo service mysql restart

查看二进制日志是否生成:

1
show variables like 'log_%';

从机操作

修改 mysql 的配置:

1
sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf

添加:

1
2
3
server-id = 2
expire_logs_days = 10
max_binlog_size = 100M

重启服务:

1
sudo service mysql restart

配置主从库之间的通信:

90-3.png

从机上设置从库(129 机)与主库(130 机)进行通信。先把从机的 test 数据库清空,模板:

1
2
3
4
5
6
CHANGE MASTER TO
MASTER_HOST='master_host_name',
MASTER_USER='replication_user_name',#backup
MASTER_PASSWORD='replication_password', #123
MASTER_LOG_FILE='recorded_log_file_name', # mysql-bin.000056
MASTER_LOG_POS=recorded_log_position; # 155

作为一个可以参考的实例:

1
2
3
4
5
6
CHANGE MASTER TO
MASTER_HOST='192.168.19.130',
MASTER_USER='backup',
MASTER_PASSWORD='123',
MASTER_LOG_FILE='mysql-bin.000149',
MASTER_LOG_POS=155;

START SLAVE; 开启主从同步线程(关闭用 stop slave)。

show slave status\G ,一个可能的输出是:

90-4.png

针对多个数据库,设置为:

90-5.png

迷你 web 框架

本节实现一个 mini frame web 框架。功能类似一个迷你的 Django .

90-6.jpeg

定义 WSGI 接口

WSGI 接口定义非常简单,它只要求 Web 开发者实现一个函数,就可以响应 HTTP 请求。我们来看一个最简单的 Web 版本的“Hello World!”:

1
2
3
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
return 'Hello World!'

上面的 application() 函数就是符合 WSGI 标准的一个 HTTP 处理函数,它接收两个参数:

  • environ:一个包含所有 HTTP 请求信息的 dict 对象。
  • start_response:一个发送 HTTP 响应的函数。

application() 函数必须由 WSGI 服务器来调用。我们现在做的 web 服务器项目,就是一个既能解析静态网页,又能解析动态网页的服务器。

web 框架传递的字典的一个实际例子(不需要掌握):

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
{
'HTTP_ACCEPT_LANGUAGE': 'zh-cn',
'wsgi.file_wrapper': <built-infunctionuwsgi_sendfile>,
'HTTP_UPGRADE_INSECURE_REQUESTS': '1',
'uwsgi.version': b'2.0.15',
'REMOTE_ADDR': '172.16.7.1',
'wsgi.errors': <_io.TextIOWrappername=2mode='w'encoding='UTF-8'>,
'wsgi.version': (1,0),
'REMOTE_PORT': '40432',
'REQUEST_URI': '/',
'SERVER_PORT': '8000',
'wsgi.multithread': False,
'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'HTTP_HOST': '172.16.7.152: 8000',
'wsgi.run_once': False,
'wsgi.input': <uwsgi._Inputobjectat0x7f7faecdc9c0>,
'SERVER_PROTOCOL': 'HTTP/1.1',
'REQUEST_METHOD': 'GET',
'HTTP_ACCEPT_ENCODING': 'gzip,deflate',
'HTTP_CONNECTION': 'keep-alive',
'uwsgi.node': b'ubuntu',
'HTTP_DNT': '1',
'UWSGI_ROUTER': 'http',
'SCRIPT_NAME': '',
'wsgi.multiprocess': False,
'QUERY_STRING': '',
'PATH_INFO': '/index.html',
'wsgi.url_scheme': 'http',
'HTTP_USER_AGENT': 'Mozilla/5.0(Macintosh;IntelMacOSX10_12_5)AppleWebKit/603.2.4(KHTML,likeGecko)Version/10.1.1Safari/603.2.4',
'SERVER_NAME': 'ubuntu'
}

实战

如果请求是不是 html,是动态请求,后缀是 .py,那如何处理呢?

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#!/usr/bin/python
# author luke

import socket
import re
import multiprocessing
import time

class WSGIServer(object):
def __init__(self):
# 1. 创建套接字
self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# 2. 绑定
self.tcp_server_socket.bind(("", 7890))

# 3. 变为监听套接字
self.tcp_server_socket.listen(128)

def service_client(self, new_socket):
"""为这个浏览器返回数据"""

# 1. 接收浏览器发送过来的请求 ,即http请求
# GET / HTTP/1.1
# .....
request = new_socket.recv(1024).decode("utf-8")
# print(">>>"*50)
# print(request)

request_lines = request.splitlines()
print("")
print(">" * 20)
print(request_lines)

# GET /index.html HTTP/1.1
# get post put del
file_name = ""
ret = re.match(r"[^/]+(/[^ ]*)", request_lines[0])
if ret:
file_name = ret.group(1)
# print("*"*50, file_name)
if file_name == "/":
file_name = "/index.html"

# 2. 返回http格式的数据,给浏览器
if not file_name.endswith(".py"):
try:
f = open("./html" + file_name, "rb")
except:
response = "HTTP/1.1 404 NOT FOUND\r\n"
response += "\r\n"
response += "------file not found-----"
new_socket.send(response.encode("utf-8"))
else:
html_content = f.read()
f.close()
# 2.1 准备发送给浏览器的数据---header
response = "HTTP/1.1 200 OK\r\n"
response += "\r\n"
# 2.2 准备发送给浏览器的数据---boy
# response += "hahahhah"

# 将response header发送给浏览器
new_socket.send(response.encode("utf-8"))
# 将response body发送给浏览器
new_socket.send(html_content)
else:
# 2.2 如果是以.py结尾,那么就认为是动态资源的请求
header = "HTTP/1.1 200 OK\r\n"
header += "\r\n"

body = "hahahah %s " % time.ctime()

response = header + body
# 发送response给浏览器
new_socket.send(response.encode("utf-8"))




# 关闭套接
new_socket.close()

def run_forever(self):
"""用来完成整体的控制"""

while True:
# 4. 等待新客户端的链接
new_socket, client_addr = self.tcp_server_socket.accept()

# 5. 为这个客户端服务
p = multiprocessing.Process(target=self.service_client, args=(new_socket,))
p.start()

new_socket.close()

# 关闭监听套接字
self.tcp_server_socket.close()


def main():
"""控制整体,创建一个web 服务器对象,然后调用这个对象的run_forever方法运行"""
wsgi_server = WSGIServer()
wsgi_server.run_forever()


if __name__ == "__main__":
main()

效果:

90-7.png

上面的代码已经接下来的几个版本,参考代码:
https://github.com/dropsong/py_webServer/tree/master/web_mini_frame

闭包

可以在函数里面定义函数,例子:

1
2
3
4
5
6
7
8
9
10
11
12
def line6(k, b):
def create_line(x):
print(k*x+b)
return create_line

# l1 是 create_line 函数,函数中 k 的值是 1,b 的值是 2
l1 = line6(2, 3)
l1(3)

'''
9
'''

函数、匿名函数、闭包、对象当做实参时有什么区别?

  1. 匿名函数能够完成基本的简单功能,传递是这个函数的引用只有功能(lambda)
  2. 普通函数能够完成较为复杂的功能,传递是这个函数的引用只有功能
  3. 闭包能够将较为复杂的功能,传递是这个闭包中的函数以及数据,因此传递是功能+数据(相对于对象,占用空间少
  4. 对象能够完成最为复杂的功能,传递是很多数据+很多功能,因此传递是功能+数据

闭包引用了外部函数的局部变量,若外部函数的局部变量没有及时释放,会消耗内存。

闭包中对变量的修改:

1
2
3
4
5
6
7
8
9
10
11
# 这个代码会报错
def test1():
x = 200
def test2():
print("----1----x=%d" % x)
x = 100
print("----2----x=%d" % x)
return test2

t1 = test1()
t1()

因为我们打印 x 时,没有提前定义,需要在打印之前增加 nonlocal x ,使用外部函数的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 可以正常运行
def test1():
x = 200
def test2():
nonlocal x
print("----1----x=%d" % x)
x = 100
print("----2----x=%d" % x)
return test2

t1 = test1()
t1()

'''
----1----x=200
----2----x=100
'''

装饰器

需求场景

初创公司有 N 个业务部门,基础平台部门负责提供底层的功能,如:数据库操作、redis 调用、监控 API 等功能。业务部门使用基础功能时,只需调用基础平台提供的功能即可。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
############### 基础平台提供的功能如下 ###############
def f1():
print('f1')
def f2():
print('f2')
def f3():
print('f3')
def f4():
print('f4')
############### 业务部门 A 调用基础平台提供的功能 ###############
f1()
f2()
f3()
f4()
############### 业务部门 B 调用基础平台提供的功能 ###############
f1()
f2()
f3()
f4()

目前公司有条不紊的进行着,但是,以前基础平台的开发人员在写代码时候没有关注验证相关的问题,即:基础平台的提供的功能可以被任何人使用。现在需要对基础平台的所有功能进行重构,为平台提供的所有功能添加验证机制,即:执行功能前,先进行验证。

解决方案

遵循开放封闭原则,对拓展开放,对修改闭合。使用装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def w1(func):
def inner():
# 验证 1
# 验证 2
# 验证 3
func()
return inner

@w1
def f1():
print('f1')

@w1
def f2():
print('f2')

@w1
def f3():
print('f3')

@w1
def f4():
print('f4')

语法

基本用法

实际例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 装饰器一定是内部有闭包的函数
def set_func(func):
def call_func():
print('----permission check1----')
print('----permission check2----')
func()
return call_func

@set_func
def test1():
print('----test1-----')

test1()

'''
----permission check1----
----permission check2----
----test1-----
'''

关于一些原理上的细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def set_func(func):
print('--- 开始进行装饰 ---')
def call_func():
print('----permission check1----')
print('----permission check2----')
func()
return call_func

@set_func
def test1():
print('----test1-----')

'''
--- 开始进行装饰 ---
'''

'''
即使没有调用 test1() ,
这段代码也相当于执行了:
test1 = set_func(test1)
因此会有输出
'''

传递参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def set_func(func):
print('--- 开始进行装饰 ---')
def call_func(a):
print('----permission check1----')
print('----permission check2----')
func(a)
return call_func

@set_func
def test1(num):
print('----test1----- %d' % num)

test1(3)

'''
--- 开始进行装饰 ---
----permission check1----
----permission check2----
----test1----- 3
'''

多个装饰器装饰同一个函数

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
36
def add_first(func):
print("---开始进行装饰权限 1 的功能---")
def call_func(*args, **kwargs):
print("---这是权限验证 1----")
return func(*args, **kwargs)
return call_func

def add_second(func):
print("---开始进行装饰权限 2 的功能---")
def call_func(*args, **kwargs):
print("---这是权限验证 2----")
return func(*args, **kwargs)
return call_func

# 离函数越近的先装饰
@add_first
@add_second
def test1():
print('----test1-----')

# 后装饰的先执行
test1()

'''
---开始进行装饰权限 2 的功能---
---开始进行装饰权限 1 的功能---
---这是权限验证 1----
---这是权限验证 2----
----test1-----
'''

'''
一种解释:
s = add_second(test1)
f = add_first(s)
'''

一个更具体的例子:

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
# 定义函数:完成包裹数据
def makeBold(fn):
def wrapped():
return "<b>" + fn() + "</b>"
return wrapped

# 定义函数:完成包裹数据
def makeItalic(fn):
def wrapped():
return "<i>" + fn() + "</i>"
return wrapped

@makeBold
def test1():
return "hello world-1"

@makeItalic
def test2():
return "hello world-2"

@makeBold
@makeItalic
def test3():
return "hello world-3"

print(test1())
print(test2())
print(test3())

'''
<b>hello world-1</b>
<i>hello world-2</i>
<b><i>hello world-3</i></b>
'''

装饰器(decorator)功能有很多,具体来说:

  1. 引入日志,在执行某个函数前或者函数后记录日志
  2. 函数执行时间统计
  3. 执行函数前预备处理
  4. 执行函数后清理功能
  5. 权限校验等场景
  6. 缓存

被装饰的函数带返回值

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
from time import ctime, sleep

def timefun(func):
def wrapped_func():
print("%s called at %s" % (func.__name__, ctime()))
func()
return wrapped_func

@timefun
def foo():
print("I am foo")

@timefun
def getInfo():
return '----hahah---'

foo()
sleep(1)
foo()
ret=getInfo()
print(ret)

'''
foo called at Sun Oct 27 22:30:36 2024
I am foo
foo called at Sun Oct 27 22:30:37 2024
I am foo
getInfo called at Sun Oct 27 22:30:37 2024
None
'''

注意,上面的代码没有拿到返回值,应该修改为:

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
from time import ctime, sleep

def timefun(func):
def wrapped_func():
print("%s called at %s" % (func.__name__, ctime()))
return func() # modified
return wrapped_func

@timefun
def foo():
print("I am foo")

@timefun
def getInfo():
return '----hahah---'

foo()
sleep(1)
foo()
ret=getInfo()
print(ret)

'''
foo called at Sun Oct 27 22:35:25 2024
I am foo
foo called at Sun Oct 27 22:35:26 2024
I am foo
getInfo called at Sun Oct 27 22:35:26 2024
----hahah---
'''

一般情况下为了让装饰器更通用,都可以加上 return .

装饰器带参数

装饰器带参数,在原有装饰器的基础上,设置外部变量。

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
36
37
38
39
40
41
from time import sleep

def timefun_arg(pre="hello"):
def timefun(func):
def wrapped_func():
print("%s called at %s" % (func.__name__, pre))
return func()
return wrapped_func
return timefun

# 下面的装饰过程
# 1. 调用 timefun_arg("wangdao")
# 2. 将步骤 1 得到的返回值,即 time_fun 返回, 然后 time_fun(foo)
# 3. 将 time_fun(foo)的结果返回,即 wrapped_func
# 4. 让 foo = wrapped_fun,即 foo 现在指向 wrapped_func

@timefun_arg("wangdao")
def foo():
print("I am foo")

@timefun_arg("python")
def too():
print("I am too")

foo()
sleep(2)
foo()
too()
sleep(2)
too()

'''
foo called at wangdao
I am foo
foo called at wangdao
I am foo
too called at python
I am too
too called at python
I am too
'''

类装饰器

前期提要:

1
2
3
4
5
6
7
8
9
10
class Test:
def __call__(self, *args, **kwds):
print('I am call')

t = Test()
t()

'''
I am call
'''

装饰器函数其实是这样一个接口约束,它必须接受一个 callable 对象作为参数,然后返回一个 callable 对象。在 Python 中一般 callable 对象都是函数,但也有例外。只要某个对象重写了 __call__() 方法,那么这个对象就是 callable 的。

一个具体的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Test:
def __init__(self, func):
self.__func = func

def __call__(self, *args, **kwds):
print('I am call')
self.__func(*args, **kwds)

@Test
def foo(name):
print('I am foo {}'.format(name))

# t = Test(foo)
# t()

foo('akashi')

'''
I am call
I am foo akashi
'''

装饰后的注释问题

问题演示和解决方法:

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
36
37
38
39
40
41
def my_decorator(func):
def wper(*args, **kwargs):
'''decorator'''
print('Calling decorated function...')
return func(*args, **kwargs)
return wper

@my_decorator
def example():
"""Docstring"""
print('Called example function')

print(example.__name__, example.__doc__) #wper decorator

'''
wper decorator
'''


###############################


from functools import wraps
def my_new_decorator(func):
@wraps(func) # focus this
def wper(*args, **kwargs):
'''decorator'''
print('Calling decorated function...')
return func(*args, **kwargs)
return wper

@my_new_decorator
def example1():
"""I am example1"""
print('Called example1 function')

print(example1.__name__, example1.__doc__) # example Docstring

'''
example1 I am example1
'''

迷你 web 框架(续)

本节概要:
之前,去替换 html 模板中的变量时,替换的内容是随意写的,现在要替换的内容是从 mysql 数据库中查出来的,并把装饰器应用到框架里。

version 6

在之前的 version 5 基础上,作如下改变。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# mini_frame.py

#!/usr/bin/python
# author luke

import time
import re

URL_FUNC_DICT = dict()

def route(url):
def set_func(func):
URL_FUNC_DICT[url] = func
# def call_func(*args, **kwargs):
# return func(*args, **kwargs)
# return call_func
return set_func

@route("/index.py")
def index():
with open("./templates/index.html", encoding="utf-8") as f:
content = f.read()

#这是实际要从数据库里边查出来
my_stock_info = "哈哈哈,我是本月最佳员工。。。。"

content = re.sub(r"\{%content%\}", my_stock_info, content)

return content


@route("/center.py")
def center():
with open("./templates/center.html", encoding="utf-8") as f:
content = f.read()

my_stock_info = "这里是从mysql查询出来的数据。。。"

content = re.sub(r"\{%content%\}", my_stock_info, content)

return content


def application(environ, start_response):
# 由mini_frame框架添加响应码和头部
start_response('200 OK', [('Content-Type', 'text/html;charset=utf-8')])
file_name = environ['PATH_INFO']
# file_name = "/index.py"

return URL_FUNC_DICT[file_name]() # 加括号是为了执行

修改要点:

  • 使用字典替换 if 判断
  • 使用装饰器实现路由功能

伪静态、静态、动态

目前开发的网站其实都是动态网站,只是 URL 上有些区别,一般 URL 分为静态 URL、动态 URL、伪静态 URL 。

静态 URL 类似于 /news/2012-5-18/110.html ,一般称为真静态 URL,每个网页有真实的物理路径,也就是真实存在服务器里的。

  • 网站打开速度快
  • 网址结构比较友好,利于记忆
  • 如果是中大型网站,则产生的页面特别多,不好管理
  • 有利于 SEO

动态 URL 类似于 /NewsMore.asp?id=5 或者 /DaiKuan.php?id=17 ,带有?号的 URL,一般称为动态网址,每个 URL 只是一个逻辑地址,并不是真实物理存在服务器硬盘里的。

  • 适合中大型网站,修改页面很方便
  • 因为要进行运算,所以打开速度稍慢,不过这个可以忽略不计,服务器缓存技术可以解决该问题
  • URL 结构稍稍复杂,不利于记忆
  • 搜索引擎已经能够很好地理解动态 URL,所以对 SEO 没有什么减分的影响(特别复杂的 URL 结构除外)

伪静态 URL 类似于 /course/74.html ,和真静态 URL 类似。通过伪静态规则把动态 URL 伪装成静态网址,也是逻辑地址,不存在物理地址。

  • URL 比较友好,利于记忆
  • 适合大中型网站,是个折中方案
  • 设置麻烦,服务器要支持重写规则
  • 增加服务器负担,速度变慢(虽然可以忽略)
  • 可能造成动态 URL 和静态 URL 都被搜索引擎收录(可以用 robots 禁止掉动态地址)

关于伪静态规则,一个具体的例子:

90-8.png

version 7

实现伪静态,作如下修改。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# mini_frame.py
import re

URL_FUNC_DICT = dict()


def route(url):
def set_func(func):
# URL_FUNC_DICT["/index.py"] = index
URL_FUNC_DICT[url] = func

# def call_func(*args, **kwargs):
# return func(*args, **kwargs)
# return call_func

return set_func


@route("/index.html")
def index():
with open("./templates/index.html", encoding="utf-8") as f:
content = f.read()

my_stock_info = "哈哈哈哈 这是你的本月名称....."

content = re.sub(r"\{%content%\}", my_stock_info, content)

return content


@route("/center.html")
def center():
with open("./templates/center.html", encoding="utf-8") as f:
content = f.read()

my_stock_info = "这里是从mysql查询出来的数据。。。"

content = re.sub(r"\{%content%\}", my_stock_info, content)

return content


def application(env, start_response):
start_response('200 OK', [('Content-Type', 'text/html;charset=utf-8')])

file_name = env['PATH_INFO']
# file_name = "/index.py"

# if file_name == "/index.py":
# return index()
# elif file_name == "/center.py":
# return center()
# else:
# return 'Hello World! 我爱你中国....'
try:
func = URL_FUNC_DICT[file_name]
return func()
# return URL_FUNC_DICT[file_name]()
except Exception as ret:
return "产生了异常:%s" % str(ret)
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# web_server.py
import socket
import re
import multiprocessing
import time
# import dynamic.mini_frame
import sys


class WSGIServer(object):
def __init__(self, port, app, static_path):
# 1. 创建套接字
self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# 2. 绑定
self.tcp_server_socket.bind(("", port))

# 3. 变为监听套接字
self.tcp_server_socket.listen(128)

self.application = app
self.static_path = static_path

def service_client(self, new_socket):
"""为这个客户端返回数据"""

# 1. 接收浏览器发送过来的请求 ,即http请求
# GET / HTTP/1.1
# .....
request = new_socket.recv(1024).decode("utf-8")
# print(">>>"*50)
# print(request)
if request:
request_lines = request.splitlines()
print("")
print(">"*20)
print(request_lines)

# GET /index.html HTTP/1.1
# get post put del
file_name = ""
ret = re.match(r"[^/]+(/[^ ]*)", request_lines[0])
if ret:
file_name = ret.group(1)
# print("*"*50, file_name)
if file_name == "/":
file_name = "/index.html"

# 2. 返回http格式的数据,给浏览器
# 2.1 如果请求的资源不是以.html结尾,那么就认为是静态资源(css/js/png,jpg等)
if not file_name.endswith(".html"):
try:
f = open(self.static_path + file_name, "rb")
except:
response = "HTTP/1.1 404 NOT FOUND\r\n"
response += "\r\n"
response += "------file not found-----"
new_socket.send(response.encode("utf-8"))
else:
html_content = f.read()
f.close()
# 2.1 准备发送给浏览器的数据---header
response = "HTTP/1.1 200 OK\r\n"
response += "\r\n"
# 2.2 准备发送给浏览器的数据---boy
# response += "hahahhah"

# 将response header发送给浏览器
new_socket.send(response.encode("utf-8"))
# 将response ic.mini_frame.applicationbody发送给浏览器
new_socket.send(html_content)
else:
# 2.2 如果是以.py结尾,那么就认为是动态资源的请求

env = dict() # 这个字典中存放的是web服务器要传递给 web框架的数据信息
env['PATH_INFO'] = file_name
# {"PATH_INFO": "/index.py"}
# body = dynamic.mini_frame.application(env, self.set_response_header)
body = self.application(env, self.set_response_header)

header = "HTTP/1.1 %s\r\n" % self.status

for temp in self.headers:
header += "%s:%s\r\n" % (temp[0], temp[1])

header += "\r\n"

response = header+body
# 发送response给浏览器
new_socket.send(response.encode("utf-8"))


# 关闭套接
new_socket.close()

def set_response_header(self, status, headers):
self.status = status
self.headers = [("server", "mini_web v8.8")]
self.headers += headers


def run_forever(self):
"""用来完成整体的控制"""

while True:
# 4. 等待新客户端的链接
new_socket, client_addr = self.tcp_server_socket.accept()

# 5. 为这个客户端服务
p = multiprocessing.Process(target=self.service_client, args=(new_socket,))
p.start()

new_socket.close()


# 关闭监听套接字
self.tcp_server_socket.close()


def main():
"""控制整体,创建一个web 服务器对象,然后调用这个对象的run_forever方法运行"""
if len(sys.argv) == 3:
try:
port = int(sys.argv[1]) # 7890
frame_app_name = sys.argv[2] # mini_frame:application
except Exception as ret:
print("端口输入错误。。。。。")
return
else:
print("请按照以下方式运行:")
print("python3 xxxx.py 7890 mini_frame:application")
return

# mini_frame:application
ret = re.match(r"([^:]+):(.*)", frame_app_name)
if ret:
frame_name = ret.group(1) # mini_frame
app_name = ret.group(2) # application
else:
print("请按照以下方式运行:")
print("python3 xxxx.py 7890 mini_frame:application")
return

with open("./web_server.conf") as f:
conf_info = eval(f.read())

# 此时 conf_info是一个字典里面的数据为:
# {
# "static_path":"./static",
# "dynamic_path":"./dynamic"
# }


sys.path.append(conf_info['dynamic_path'])

# import frame_name --->找frame_name.py
frame = __import__(frame_name) # 返回值标记这 导入的这个模板
app = getattr(frame, app_name) # 此时app就指向了 dynamic/mini_frame模块中的application这个函数

# print(app)

wsgi_server = WSGIServer(port, app, conf_info['static_path'])
wsgi_server.run_forever()


if __name__ == "__main__":
main()

本次修改总结:
将字典里存放的 key 作了修改。另外模板里的 html 文件也作了相应修改(没有展示),将链接指向了 /index.html/center.html

version 8

从数据库查询数据,替换模板中的变量。

准备数据的过程:

1
2
3
create database stock_db charset=utf8;
use stock_db
source /home/zhiyue/Downloads/stock_db.sql

(这个 sql 文件不重要,我们这里只是演示)

表中的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> select * from focus;
+----+--------------------------+---------+
| id | note_info | info_id |
+----+--------------------------+---------+
| 2 | 你确定要买这个? | 36 |
| 3 | 利好 | 37 |
| 9 | | 88 |
| 10 | | 89 |
| 13 | | 1 |
+----+--------------------------+---------+
5 rows in set (0.00 sec)

mysql> select * from info limit 5;
+----+--------+--------------+--------+----------+-------+-------+------------+
| id | code | short | chg | turnover | price | highs | time |
+----+--------+--------------+--------+----------+-------+-------+------------+
| 1 | 000007 | 全新好 | 10.01% | 4.40% | 16.05 | 14.60 | 2019-07-18 |
| 2 | 000036 | 华联控股 | 10.04% | 10.80% | 11.29 | 10.26 | 2019-07-20 |
| 3 | 000039 | 中集集团 | 1.35% | 1.78% | 18.07 | 18.06 | 2019-06-28 |
| 4 | 000050 | 深天马A | 4.38% | 4.65% | 22.86 | 22.02 | 2017-07-19 |
| 5 | 000056 | 皇庭国际 | 0.39% | 0.65% | 12.96 | 12.91 | 2017-07-20 |
+----+--------+--------------+--------+----------+-------+-------+------------+
5 rows in set (0.00 sec)

现在,我们希望实现如下的效果:

90-9.png

代码参见:
https://github.com/dropsong/py_webServer/tree/master/web_mini_frame/version8

version 9

让路由支持正则,实现增删改功能。

https://github.com/dropsong/py_webServer/tree/master/web_mini_frame/version9

日志

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import logging

logging.basicConfig(level=logging.WARNING,
format='%(asctime)s - %(filename)s[line:%(lineno)d] -%(levelname)s: %(message)s')

# 开始使用 log 功能
logging.debug('这是 loggging debug message')
logging.info('这是 loggging info message')
logging.warning('这是 loggging a warning message')
logging.error('这是 an loggging error message')
logging.critical('这是 loggging critical message')

'''
2017-11-06 23:07:35,725 - log1.py[line:9] - WARNING: 这是 loggging a warning message
2017-11-06 23:07:35,725 - log1.py[line:10] - ERROR: 这是 an loggging err or message
2017-11-06 23:07:35,725 - log1.py[line:11] - CRITICAL: 这是 loggging critical message
'''

写到文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
import logging

logging.basicConfig(level=logging.WARNING,
filename='log.txt',
filemode='w',
format='%(asctime)s - %(filename)s[line:%(lineno)d] -%(levelname)s: %(message)s')

# 开始使用 log 功能
logging.debug('这是 loggging debug message')
logging.info('这是 loggging info message')
logging.warning('这是 loggging a warning message')
logging.error('这是 an loggging error message')
logging.critical('这是 loggging critical message')

既把日志输出到控制台,又写入文件:

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
import logging
# 第一步,创建一个 logger
logger = logging.getLogger()
logger.setLevel(logging.INFO) # Log 等级总开关

# 第二步,创建一个 handler,用于写入日志文件
logfile = './log.txt'
fh = logging.FileHandler(logfile, mode='a') # open 的打开模式这里可以进行参考
fh.setLevel(logging.DEBUG) # 输出到 file 的 log 等级的开关

# 第三步,再创建一个 handler,用于输出到控制台
ch = logging.StreamHandler()
ch.setLevel(logging.WARNING) # 输出到 console 的 log 等级的开关

# 第四步,定义 handler 的输出格式
formatter = logging.Formatter("%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s")
fh.setFormatter(formatter)
ch.setFormatter(formatter)

# 第五步,将 logger 添加到 handler 里面
logger.addHandler(fh)
logger.addHandler(ch)

# 日志
logger.debug('这是 logger debug message')
logger.info('这是 logger info message')
logger.warning('这是 logger warning message')
logger.error('这是 logger error message')
logger.critical('这是 logger critical message')

元类

C, C++ 中无此设计。

我们想做一个类,这个类是用代码生成的。

类也是对象

类的本质仍然是一个对象,于是可以做如下操作:

  1. 将它赋值给一个变量
  2. 拷贝它
  3. 为它增加属性
  4. 将它作为函数参数进行传递

下面展示自由的 Python 语法:

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
36
37
class ObjectCreator(object):
pass

my_object = ObjectCreator()
print(my_object)

print(ObjectCreator) # 你可以打印一个类,因为它其实也是一个对象

def echo(obj):
print(obj)

echo(ObjectCreator)

print(hasattr(ObjectCreator, 'new_attribute'))

ObjectCreator.new_attribute = 'foo' # 你可以为类增加属性
print(hasattr(ObjectCreator, 'new_attribute'))

# 把类名给一个变量
val = ObjectCreator
print(val)

print(type(1))
print(type(my_object))
print(type(ObjectCreator))

'''
<__main__.ObjectCreator object at 0x7f15b9e8b050>
<class '__main__.ObjectCreator'>
<class '__main__.ObjectCreator'>
False
True
<class '__main__.ObjectCreator'>
<class 'int'>
<class '__main__.ObjectCreator'>
<class 'type'>
'''

创建类出来的那个类,叫元类

使用 type 创建类

type 还有一种完全不同的功能,动态地创建类。

type 可以接受一个类的描述作为参数,然后返回一个类。(要知道,根据传入参数的不同,同一个函数拥有两种完全不同的用法是一件很傻的事情,但这在 Python 中是为了保持向后兼容性)

用法如下:

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

例子:

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
Test2 = type('Test2', (), {})
print(Test2)

Foo = type('Foo', (), {'bar':True})
print(Foo.bar)

# 继承
FooChild = type('FooChild', (Foo,), {})
print(FooChild.bar)

# 创建带有方法的类
def echo_bar(self):
print(self.bar)

#让 FooChild 类中的 echo_bar 属性,指向了上面定义的函数
FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar})

my_foo = FooChild()
my_foo.echo_bar()

'''
<class '__main__.Test2'>
True
True
True
'''

一个更完整的例子:

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
class A(object):
num = 100

def print_b(self):
print(self.num)

@staticmethod
def print_static():
print("----haha-----")

@classmethod
def print_class(cls):
print(cls.num)

B = type("B", (A,), {"print_b": print_b, "print_static": print_static,"print_class": print_class})

b = B()
b.print_b()
b.print_static()
b.print_class()

'''
100
----haha-----
100
'''

metaclass 属性

当我们写下如下的代码时:

1
2
class Foo(Bar):
pass

Python 做了如下的操作:

  1. Foo 中有 __metaclass__ 这个属性吗?如果是,Python 会通过 __metaclass__ 创建一个名字为 Foo 的类(对象)
  2. 如果 Python 没有找到 __metaclass__,它会继续在 Bar(父类)中寻找 __metaclass__ 属性,并尝试做和前面同样的操作。
  3. 如果 Python 在任何父类中都找不到 __metaclass__ ,它就会在模块层次中去寻找 __metaclass__ ,并尝试做同样的操作。
  4. 如果还是找不到 __metaclass__ ,Python 就会用内置的 type 来创建这个类对象。

例子:

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
# 效果:将 Foo 的属性 bar 改成了 BAR
def upper_attr(class_name, class_parents, class_attr):
#遍历属性字典,把不是__开头的属性名字变为大写
new_attr = {}
for name,value in class_attr.items():
if not name.startswith("__"):
new_attr[name.upper()] = value

#调用 type 来创建一个类,这里的返回值给了 Foo
return type(class_name, class_parents, new_attr)

class Foo(object, metaclass=upper_attr):
bar = 'bip'

print(hasattr(Foo, 'bar'))
print(hasattr(Foo, 'BAR'))

f = Foo()
print(f.BAR)

'''
False
True
bip
'''

上面的代码中 metaclass 是用了一个函数,接下来使用一个类:

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
36
class UpperAttrMetaClass(type):
# __new__ 是在__init__之前被调用的特殊方法
# __new__是用来创建对象并返回之的方法
# 而__init__只是用来将传入的参数初始化给对象
# 你很少用到__new__,除非你希望能够控制对象的创建
# 这里,创建的对象是类,我们希望能够自定义它,所以我们这里改写__new__
# 如果你希望的话,你也可以在__init__中做些事情
# 还有一些高级的用法会涉及到改写__call__特殊方法,但是我们这里不用
def __new__(cls, class_name, class_parents, class_attr):
# 遍历属性字典,把不是__开头的属性名字变为大写
new_attr = {}
for name, value in class_attr.items():
if not name.startswith("__"):
new_attr[name.upper()] = value

# 方法 1:通过'type'来做类对象的创建
return type(class_name, class_parents, new_attr)

# 方法 2:复用 type.__new__方法
# 这就是基本的 OOP 编程,没什么魔法
# return type.__new__(cls, class_name, class_parents, new_attr)

# python3 的用法
class Foo(object, metaclass=UpperAttrMetaClass):
bar = 'bip'

print(hasattr(Foo, 'bar'))
print(hasattr(Foo, 'BAR'))
f = Foo()
print(f.BAR)

'''
False
True
bip
'''

“元类就是深度的魔法,99%的用户应该根本不必为此操心。如果你想搞清楚究竟是否需要用到元类,那么你就不需要它。那些实际用到元类的人都非常清楚地知道他们需要做什么,而且根本不需要解释为什么要用元类。” (Python 界的领袖 Tim Peters)

元类实现 ORM

ORM 是 Django 的核心思想,“Object Relational Mapping”,即对象-关系映射,简称 ORM。(JAVA 也是 ORM)

一句话理解:创建一个实例对象,用创建它的类名当做数据表名,用创建它的类属性对应数据表的字段,当对这个实例对象操作时,能够对应 MySQL 语句。

需求场景

我们在之前的 Django 开发中已经看到,我们写的类属性“好像”变成了对象属性(这使我们写代码更方便了),但这是怎么回事呢?

90-10.png

所谓的 ORM 就是让开发者在操作数据库的时候,能够像操作对象时通过 xxxx.属性=yyyy 一样简单,这是开发 ORM 的初衷。

本节完成一个和 insert 类似的 ORM ,简单地展示一下原理。

实例

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 这个类的工作:将 User 类的类属性删除,然后增加一些东西返回
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
mappings = dict()
# 判断是否需要保存
for k, v in attrs.items():
# 判断是否是指定的 StringField 或者 IntegerField 的实例对象
if isinstance(v, tuple):
print('Found mapping: %s ==> %s' % (k, v)) # 日志
mappings[k] = v
# example:
# k: uid
# v: ('uid', "int unsigned")

# 删除这些已经在字典中存储的属性
for k in mappings.keys():
attrs.pop(k)

attrs['__mappings__'] = mappings # 保存属性和列的映射关系
attrs['__table__'] = name # 假设表名和类名一致
return type.__new__(cls, name, bases, attrs)


class User(metaclass=ModelMetaclass):
uid = ('uid', "int unsigned")
name = ('username', "varchar(30)")
email = ('email', "varchar(30)")
password = ('password', "varchar(30)")
# 我们要将上面的四个变成对象属性

def __init__(self, **kwargs):
for name, value in kwargs.items():
# 通过 setattr 可以给对象新增属性
setattr(self, name, value)
# 这种操作只有写框架会需要,平时不需要

def save(self):
fields = []
args = []
for k, v in self.__mappings__.items():
fields.append(v[0])
args.append(getattr(self, k, None))

sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join([str(i) for i in args]))
print('SQL: %s' % sql)


# 非常魔幻的写法。在 C++ 的构造函数中,你不会需要将 uid=, name= 这些写出来,
# 这里 Python 写出来,是直接传给了 __init__
u = User(uid=12345, name='Michael', email='test@orm.org', password='my-pwd')
print(u.__dict__) # 打印对象属性
u.save()

'''
Found mapping: uid ==> ('uid', 'int unsigned')
Found mapping: name ==> ('username', 'varchar(30)')
Found mapping: email ==> ('email', 'varchar(30)')
Found mapping: password ==> ('password', 'varchar(30)')
{'uid': 12345, 'name': 'Michael', 'email': 'test@orm.org', 'password': 'my-pwd'}
SQL: insert into User (uid,username,email,password) values (12345,Michael,test@orm.org,my-pwd)
'''

一点微小的改进(增加了类型校验):

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
mappings = dict()
# 判断是否需要保存
for k, v in attrs.items():
# 判断是否是指定的 StringField 或者 IntegerField 的实例对象
if isinstance(v, tuple):
print('Found mapping: %s ==> %s' % (k, v))
mappings[k] = v

# 删除这些已经在字典中存储的属性
for k in mappings.keys():
attrs.pop(k)

# 将之前的 uid/name/email/password 以及对应的对象引用、类名字
attrs['__mappings__'] = mappings # 保存属性和列的映射关系
attrs['__table__'] = name # 假设表名和类名一致
return type.__new__(cls, name, bases, attrs)

class User(metaclass=ModelMetaclass):
uid = ('uid', "int unsigned")
name = ('username', "varchar(30)")
email = ('email', "varchar(30)")
password = ('password', "varchar(30)")
# 当指定元类之后,以上的类属性将不在类中,而是在__mappings__属性指定的字典中存储
# 以上 User 类中有
# __mappings__ = {
# "uid": ('uid', "int unsigned")
# "name": ('username', "varchar(30)")
# "email": ('email', "varchar(30)")
# "password": ('password', "varchar(30)")
# }
# __table__ = "User"
def __init__(self, **kwargs):
for name, value in kwargs.items():
setattr(self, name, value)

def save(self):
fields = []
args = []
for k, v in self.__mappings__.items():
fields.append(v[0])
args.append(getattr(self, k, None))

args_temp = list()
for temp in args:
# 模拟做类型校验
if isinstance(temp, int):
args_temp.append(str(temp))
elif isinstance(temp, str):
args_temp.append("""'%s'""" % temp)

sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(args_temp))
print('SQL: %s' % sql)

u = User(uid=12345, name='Michael', email='test@orm.org', password='my-pwd')
print(u.__dict__)
u.save()

模拟 ORM 中的继承:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
mappings = dict()
# 判断是否需要保存
for k, v in attrs.items():
# 判断是否是指定的 StringField 或者 IntegerField 的实例对象
if isinstance(v, tuple):
print('Found mapping: %s ==> %s' % (k, v))
mappings[k] = v

# 删除这些已经在字典中存储的属性
for k in mappings.keys():
attrs.pop(k)

# 将之前的 uid/name/email/password 以及对应的对象引用、类名字
attrs['__mappings__'] = mappings # 保存属性和列的映射关系
attrs['__table__'] = name # 假设表名和类名一致
return type.__new__(cls, name, bases, attrs)
# return super().__new__(cls, name, bases, attrs)


class Model(object, metaclass=ModelMetaclass):
def __init__(self, **kwargs):
for name, value in kwargs.items():
setattr(self, name, value)

def save(self):
fields = []
args = []
for k, v in self.__mappings__.items():
fields.append(v[0])
args.append(getattr(self, k, None))

args_temp = list()
for temp in args:
# 模拟做类型校验
if isinstance(temp, int):
args_temp.append(str(temp))
elif isinstance(temp, str):
args_temp.append("""'%s'""" % temp)

sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(args_temp))
print('SQL: %s' % sql)


class User(Model):
uid = ('uid', "int unsigned")
name = ('username', "varchar(30)")
email = ('email', "varchar(30)")
password = ('password', "varchar(30)")


u = User(uid=12345, name='Michael', email='test@orm.org', password='my-pwd')
print(u.__dict__)
u.save()

'''
Found mapping: uid ==> ('uid', 'int unsigned')
Found mapping: name ==> ('username', 'varchar(30)')
Found mapping: email ==> ('email', 'varchar(30)')
Found mapping: password ==> ('password', 'varchar(30)')
{'uid': 12345, 'name': 'Michael', 'email': 'test@orm.org', 'password': 'my-pwd'}
SQL: insert into User (uid,username,email,password) values (12345,'Michael','test@orm.org','my-pwd')
'''

接口类与抽象类

没什么好说的,和 C++ 笔记里的东西差不多,这里只放几个例子。

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
class payment:
def pay(self,money):
e=Exception('缺少编写 pay 方法')
raise e #手动抛异常

class Alipay(payment):
# 这里类的方法不是一致的 pay,导致后面调用的时候找不到 pay
def paying(self, money):
print('支付宝支付了')

# 支付函数,总体负责支付,对应支付的对象和要支付的金额
def pay(payment, money):
payment.pay(money)

p = Alipay() # 不报错
pay(p, 200)

'''
Traceback (most recent call last):
File "/home/zhiyue/Documents/1030work/test.py", line 16, in <module>
pay(p, 200)
File "/home/zhiyue/Documents/1030work/test.py", line 13, in pay
payment.pay(money)
File "/home/zhiyue/Documents/1030work/test.py", line 4, in pay
raise e #手动抛异常
^^^^^^^
Exception: 缺少编写 pay 方法
'''

# Alipay 没有 pay 方法,走到父类的 pay 方法后报错

然而这样的代码还是可以改进,我们希望在编写代码的时候就能发现问题,而不是调用的时候才报错,为此引出抽象类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from abc import abstractmethod, ABCMeta

class Payment(metaclass= ABCMeta):
@abstractmethod
def pay(self,money):
pass

class Alipay(Payment):
def paying(self, money):
print('支付宝支付了')

p = Alipay()

'''
TypeError: Can't instantiate abstract class Alipay with abstract method pay
'''

原因和 C++ 中的一致。