内网穿透

内网穿透原理图:

90-1.png
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

例子:

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__ 发生的状态******

'''

静态方法和类方法

类属性、实例属性

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

例子:

class Province(object):
    # 类属性
    country = '中国'
    def __init__(self, name):
        # 实例属性
        self.name = name

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

'''
山东省
'''

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

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

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

例子:

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 属性

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

例子:

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 属性可以用来实现只能读不能写的属性。 (想必这就是莽蛇语言的独到之处了吧)

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

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 属性时,经典类和新式类无区别:

class Foo:
    def get_bar(self):
        return 'laowang'

    BAR = property(get_bar)

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

'''
laowang
'''

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

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 方法:

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

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

class Foo:
    def __init__(self):
        pass
    def __call__(self, *args, **kwargs):
        print('__call__')

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

'''
__call__
'''

莽蛇语言魅力时刻。

with 与“上下文管理器”

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

普通版:

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

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

进阶版:

def m2():
    f = open("output.txt", "w")
    try:
        f.write("python 之禅")
    except IOError:
        print("oops error")
    finally:
        f.close()

缺点是写起来太麻烦。

高级版:

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 后面的值是函数的返回值。

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
90-2.png

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

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

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

账户管理

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

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

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

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

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 进入之后:

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'

主从配置

主机操作

在主机上配置账户:

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 的配置:

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 数据库是存在的):

sudo service mysql restart

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

show variables like 'log_%';

从机操作

修改 mysql 的配置:

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

添加:

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

重启服务:

sudo service mysql restart

配置主从库之间的通信:

90-3.png
90-3.png

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

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

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

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-4.png

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

90-5.png
90-5.png

迷你 web 框架

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

90-6.jpeg
90-6.jpeg

定义 WSGI 接口

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

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 框架传递的字典的一个实际例子(不需要掌握):

{
    '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,那如何处理呢?

#!/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
90-7.png

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

闭包

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

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. 对象能够完成最为复杂的功能,传递是很多数据+很多功能,因此传递是功能+数据

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

闭包中对变量的修改:

# 这个代码会报错
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 ,使用外部函数的变量。

# 可以正常运行
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 等功能。业务部门使用基础功能时,只需调用基础平台提供的功能即可。如下:

############### 基础平台提供的功能如下 ###############
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()

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

解决方案

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

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')

语法

基本用法

实际例子:

# 装饰器一定是内部有闭包的函数
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-----
'''

关于一些原理上的细节:

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)
因此会有输出
'''

传递参数

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
'''

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

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)
'''

一个更具体的例子:

# 定义函数:完成包裹数据
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. 缓存

被装饰的函数带返回值

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
'''

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

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 .

装饰器带参数

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

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
'''

类装饰器

前期提要:

class Test:
    def __call__(self, *args, **kwds):
        print('I am call')

t = Test()
t()

'''
I am call
'''

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

一个具体的例子:

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
'''

装饰后的注释问题

问题演示和解决方法:

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 基础上,作如下改变。

# 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
90-8.png

version 7

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

# 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)
# 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

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

准备数据的过程:

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

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

表中的数据:

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
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

日志

例子:

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
'''

写到文件:

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')

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

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 语法:

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 中是为了保持向后兼容性)

用法如下:

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

例子:

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
'''

一个更完整的例子:

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 属性

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

class Foo(Bar):
    pass

Python 做了如下的操作:

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

例子:

# 效果:将 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 是用了一个函数,接下来使用一个类:

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
90-10.png

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

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

实例

# 这个类的工作:将 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)
'''

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

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 中的继承:

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++ 笔记里的东西差不多,这里只放几个例子。

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 方法后报错

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

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++ 中的一致。