relationship loading technology in SQLAlchemy


准备工作

In [1]:
from sqlalchemy import create_engine, Column, Integer, ForeignKey, String
from sqlalchemy.ext.declarative import as_declarative
from sqlalchemy.orm import relationship, sessionmaker

engine = create_engine('sqlite:///:memory', echo=True)

@as_declarative(bind=engine)
class Base:
    id = Column(Integer, primary_key=True)


class User(Base):
    __tablename__ = 'users'
    
    username = Column(String)

    addresses = relationship('Address', lazy='select')


class Address(Base):
    __tablename__ = 'addresses'
    
    user_id = Column(Integer, ForeignKey('users.id'))
    street = Column(String)
    
    user = relationship('User')


Session = sessionmaker(bind=engine)
session = Session()

if __name__ == '__main__':
    Base.metadata.drop_all()
    Base.metadata.create_all()
    
    # 创建测试数据
    user1 = User(username='小明')
    user2 = User(username='小红')
    address1 = Address(street='皇后大道')
    address1.user = user1
    address2 = Address(street='长安大道')
    address2.user = user2
    
    address3 = Address(street='皇后大道')
    address3.user = user1
    address4 = Address(street='长安大道')
    address4.user = user2

    session.add(address1)
    session.add(address2)
    session.commit()
2020-01-12 22:40:56,473 INFO sqlalchemy.engine.base.Engine SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
2020-01-12 22:40:56,474 INFO sqlalchemy.engine.base.Engine ()
2020-01-12 22:40:56,475 INFO sqlalchemy.engine.base.Engine SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
2020-01-12 22:40:56,475 INFO sqlalchemy.engine.base.Engine ()
2020-01-12 22:40:56,476 INFO sqlalchemy.engine.base.Engine PRAGMA main.table_info("users")
2020-01-12 22:40:56,476 INFO sqlalchemy.engine.base.Engine ()
2020-01-12 22:40:56,478 INFO sqlalchemy.engine.base.Engine PRAGMA main.table_info("addresses")
2020-01-12 22:40:56,478 INFO sqlalchemy.engine.base.Engine ()
2020-01-12 22:40:56,479 INFO sqlalchemy.engine.base.Engine 
DROP TABLE addresses
2020-01-12 22:40:56,480 INFO sqlalchemy.engine.base.Engine ()
2020-01-12 22:40:56,488 INFO sqlalchemy.engine.base.Engine COMMIT
2020-01-12 22:40:56,490 INFO sqlalchemy.engine.base.Engine 
DROP TABLE users
2020-01-12 22:40:56,491 INFO sqlalchemy.engine.base.Engine ()
2020-01-12 22:40:56,497 INFO sqlalchemy.engine.base.Engine COMMIT
2020-01-12 22:40:56,498 INFO sqlalchemy.engine.base.Engine PRAGMA main.table_info("users")
2020-01-12 22:40:56,498 INFO sqlalchemy.engine.base.Engine ()
2020-01-12 22:40:56,499 INFO sqlalchemy.engine.base.Engine PRAGMA temp.table_info("users")
2020-01-12 22:40:56,500 INFO sqlalchemy.engine.base.Engine ()
2020-01-12 22:40:56,500 INFO sqlalchemy.engine.base.Engine PRAGMA main.table_info("addresses")
2020-01-12 22:40:56,501 INFO sqlalchemy.engine.base.Engine ()
2020-01-12 22:40:56,502 INFO sqlalchemy.engine.base.Engine PRAGMA temp.table_info("addresses")
2020-01-12 22:40:56,502 INFO sqlalchemy.engine.base.Engine ()
2020-01-12 22:40:56,503 INFO sqlalchemy.engine.base.Engine 
CREATE TABLE users (
	id INTEGER NOT NULL, 
	username VARCHAR, 
	PRIMARY KEY (id)
)


2020-01-12 22:40:56,504 INFO sqlalchemy.engine.base.Engine ()
2020-01-12 22:40:56,513 INFO sqlalchemy.engine.base.Engine COMMIT
2020-01-12 22:40:56,514 INFO sqlalchemy.engine.base.Engine 
CREATE TABLE addresses (
	id INTEGER NOT NULL, 
	user_id INTEGER, 
	street VARCHAR, 
	PRIMARY KEY (id), 
	FOREIGN KEY(user_id) REFERENCES users (id)
)


2020-01-12 22:40:56,514 INFO sqlalchemy.engine.base.Engine ()
2020-01-12 22:40:56,520 INFO sqlalchemy.engine.base.Engine COMMIT
2020-01-12 22:40:56,525 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2020-01-12 22:40:56,526 INFO sqlalchemy.engine.base.Engine INSERT INTO users (username) VALUES (?)
2020-01-12 22:40:56,527 INFO sqlalchemy.engine.base.Engine ('小明',)
2020-01-12 22:40:56,528 INFO sqlalchemy.engine.base.Engine INSERT INTO users (username) VALUES (?)
2020-01-12 22:40:56,528 INFO sqlalchemy.engine.base.Engine ('小红',)
2020-01-12 22:40:56,529 INFO sqlalchemy.engine.base.Engine INSERT INTO addresses (user_id, street) VALUES (?, ?)
2020-01-12 22:40:56,530 INFO sqlalchemy.engine.base.Engine (1, '皇后大道')
2020-01-12 22:40:56,531 INFO sqlalchemy.engine.base.Engine INSERT INTO addresses (user_id, street) VALUES (?, ?)
2020-01-12 22:40:56,531 INFO sqlalchemy.engine.base.Engine (2, '长安大道')
2020-01-12 22:40:56,532 INFO sqlalchemy.engine.base.Engine COMMIT

relationship的加载机制

lazyload

lazyload的机制是对象查询出来以后, 不会主动去获取和它关联的对象, 而是在获取关联对象时才会去查询, 通过relationship.lazy='select'设置

In [7]:
users = session.query(User).all()
for user in users:
    print(user.addresses)

session.expire_all()

print('-------------------------------------')
    
addresses = session.query(Address).all()
for address in addresses:
    print(address.user)
    
session.expire_all()
2020-01-12 22:43:06,152 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.username AS users_username 
FROM users
2020-01-12 22:43:06,153 INFO sqlalchemy.engine.base.Engine ()
2020-01-12 22:43:06,154 INFO sqlalchemy.engine.base.Engine SELECT addresses.id AS addresses_id, addresses.user_id AS addresses_user_id, addresses.street AS addresses_street 
FROM addresses 
WHERE ? = addresses.user_id
2020-01-12 22:43:06,155 INFO sqlalchemy.engine.base.Engine (1,)
[<__main__.Address object at 0x7f9631f7b630>]
2020-01-12 22:43:06,156 INFO sqlalchemy.engine.base.Engine SELECT addresses.id AS addresses_id, addresses.user_id AS addresses_user_id, addresses.street AS addresses_street 
FROM addresses 
WHERE ? = addresses.user_id
2020-01-12 22:43:06,157 INFO sqlalchemy.engine.base.Engine (2,)
[<__main__.Address object at 0x7f9631f7b6d8>]
-------------------------------------
2020-01-12 22:43:06,159 INFO sqlalchemy.engine.base.Engine SELECT addresses.id AS addresses_id, addresses.user_id AS addresses_user_id, addresses.street AS addresses_street 
FROM addresses
2020-01-12 22:43:06,159 INFO sqlalchemy.engine.base.Engine ()
2020-01-12 22:43:06,160 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.username AS users_username 
FROM users 
WHERE users.id = ?
2020-01-12 22:43:06,161 INFO sqlalchemy.engine.base.Engine (1,)
<__main__.User object at 0x7f964d2c09b0>
2020-01-12 22:43:06,162 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.username AS users_username 
FROM users 
WHERE users.id = ?
2020-01-12 22:43:06,163 INFO sqlalchemy.engine.base.Engine (2,)
<__main__.User object at 0x7f9631fc52b0>

现在有两个用户, 这里会执行三条sql, 第一条sql查询所有的user,

在遍历user的过程中, 当尝试获取address时, 会查询对应的address.

select在数据量大的时候性能很差, 最好是在每次只查一条数据的时候使用.

假设user数量为n, 查询到所有的user和user对应的address, 需要执行n+1条sql.

需要注意的时如果查询address时去获取user, 那么同一个user只会获取一次, 不会多次获取.

这里的address有4条记录, 属于两个user, 这里就只额外查询了两次address对应的user

eagerload

eagerload在获取对象时, 会将它所关联的对象也一并查询出来, eagerload有三种joined, subquery, selectin

In [3]:
from sqlalchemy.orm import joinedload, selectinload, subqueryload

users = session.query(User).options(joinedload(User.addresses)).all()
print('------------------------joined load finished------------------------')

users = session.query(User).options(subqueryload(User.addresses)).all()
print('------------------------subquery load finished------------------------')

users = session.query(User).options(selectinload(User.addresses)).all()
print('------------------------selectin load finished------------------------')

session.expire_all()
2020-01-12 22:40:56,555 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.username AS users_username, addresses_1.id AS addresses_1_id, addresses_1.user_id AS addresses_1_user_id, addresses_1.street AS addresses_1_street 
FROM users LEFT OUTER JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id
2020-01-12 22:40:56,556 INFO sqlalchemy.engine.base.Engine ()
------------------------joined load finished------------------------
2020-01-12 22:40:56,558 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.username AS users_username 
FROM users
2020-01-12 22:40:56,559 INFO sqlalchemy.engine.base.Engine ()
2020-01-12 22:40:56,561 INFO sqlalchemy.engine.base.Engine SELECT addresses.id AS addresses_id, addresses.user_id AS addresses_user_id, addresses.street AS addresses_street, anon_1.users_id AS anon_1_users_id 
FROM (SELECT users.id AS users_id 
FROM users) AS anon_1 JOIN addresses ON anon_1.users_id = addresses.user_id ORDER BY anon_1.users_id
2020-01-12 22:40:56,562 INFO sqlalchemy.engine.base.Engine ()
------------------------subquery load finished------------------------
2020-01-12 22:40:56,564 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.username AS users_username 
FROM users
2020-01-12 22:40:56,564 INFO sqlalchemy.engine.base.Engine ()
2020-01-12 22:40:56,566 INFO sqlalchemy.engine.base.Engine SELECT addresses.user_id AS addresses_user_id, addresses.id AS addresses_id, addresses.street AS addresses_street 
FROM addresses 
WHERE addresses.user_id IN (?, ?) ORDER BY addresses.user_id
2020-01-12 22:40:56,566 INFO sqlalchemy.engine.base.Engine (1, 2)
------------------------selectin load finished------------------------

这里通过options来覆盖默认的lazy设置, 也可以在创建model的时候定义.

这里可以看到获取到users的时候, 对于三种load方式, 都已经加载好了user对应的address

  • joinedload方式只执行了一条sql, 但是因为一个用户会有多个address, 所以需要对重复的记录作合并

  • subquery方式执行了两条sql, 第一条查询所有的user, 第二条通过构造子查询和address作join操作

  • selectin的方式于subquery类似, 区别在于第二条sql, 利用第一步获取的user id构造一个in查询

需要注意的时selectin方式对id的数量有限制, 默认会分成500个作为一批, 分批去做查询

一般来说, 如果是一对一的话, joined的方式最佳, 如果一对多的话使用selectin方式最好, 可以避免一次加载大量的数据.

noload

顾名思义, 这是用来禁止加载关联的对象, 根据行为可以分为noload和raiseload

In [4]:
from sqlalchemy.orm import raiseload, noload



users = session.query(User).options(noload(User.addresses)).all()
for user in users:
    print(user.addresses)
print('------------------------noload finished------------------------')

users = session.query(User).options(raiseload(User.addresses)).all()
for user in users:
    print(user.addresses)
print('------------------------raiseload finished------------------------')

addresses = session.query(Address).options(noload(Address.user)).all()
for address in addresses:
    print(address.user)
print('------------------------noload finished------------------------')

addresses = session.query(Address).options(raiseload(Address.user)).all()
for address in addresses:
    print(address.user)
print('------------------------raiseload finished------------------------')

session.expire_all()
2020-01-12 22:40:56,574 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.username AS users_username 
FROM users
2020-01-12 22:40:56,575 INFO sqlalchemy.engine.base.Engine ()
[]
[]
------------------------noload finished------------------------
2020-01-12 22:40:56,577 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.username AS users_username 
FROM users
2020-01-12 22:40:56,577 INFO sqlalchemy.engine.base.Engine ()
[]
[]
------------------------raiseload finished------------------------
2020-01-12 22:40:56,578 INFO sqlalchemy.engine.base.Engine SELECT addresses.id AS addresses_id, addresses.user_id AS addresses_user_id, addresses.street AS addresses_street 
FROM addresses
2020-01-12 22:40:56,579 INFO sqlalchemy.engine.base.Engine ()
None
None
------------------------noload finished------------------------
2020-01-12 22:40:56,580 INFO sqlalchemy.engine.base.Engine SELECT addresses.id AS addresses_id, addresses.user_id AS addresses_user_id, addresses.street AS addresses_street 
FROM addresses
2020-01-12 22:40:56,581 INFO sqlalchemy.engine.base.Engine ()
None
None
------------------------raiseload finished------------------------

raiseload使用options的方式时获取属性不会报错, 定义在model中的时候才会报错, 这时获取属性会返回空列表或者None,

Elasticsearch analysis总结


Analysis

Analysis是es中来对文本作处理的过程, 简单地说就是把句子分成一个个token, 具体由analyzer来执行这一过程.

Index time analysis

为了能高效地检索数据, ES会在数据存储前预先对文本做分词, 对每个token建立一个倒排索引.

对应的analyzer是 Index time analyzer

Search time analysis

查询ES中匹配关键字的文档时, 也要对搜索的关键字做处理, 将其转化成更小的单元token.

对应的analyzer是 Search time analyzer

Analyzer

analyzer由以下三部分组成

  • Tokenizer 对文本作分词, 将句子切分成更小的单元token
  • Character Filter 分词前对文本作预处理, 可以用来过滤掉无效字符等等
  • Token Filter 分词完成后, 对切分好的token做进一步的处理

一个analyzer有且只能有一个tokenizer, 可以有多个CharFilter和TokenFilter

文本处理的顺序

先由Character Filter对文本做预处理, 然后把结果传给Tokenizer分词, 最后Token Filter对token做进一步的处理

analyzer的优先级

Index time analyzer

  1. 优先使用 field 中定义的analyzer
  2. 其次使用 index 中定义的名为 default 的analyzer
  3. 最后使用全局默认的 standard analyazer

Search time analyzer

  1. 优先使用query中定义的analyzer
  2. field 中定义的search_analyzer
  3. field 定义的analyzer
  4. index 中定义的 default_search analyzer
  5. index 中定义的 default analyzer
  6. standard analyzer

Tokenizer

将一段文字切分成许多token并输出

同时还做了以下几件事情

  • 记录切分出来的token的位置或者顺序 (用于短语和临近搜索)
  • 记录每个token的起始和终止位置 (用于对匹配的文字做高亮)
  • 记录每个token的类型, 比如 <ALPHANUM>, <HANGUL>, <NUM>, 不区分的话返回<word>

Character Filter

对文本做预处理, 可以增加, 删除, 替换文本中的字符

Token Filter

对分词的结果做进一步处理, 包括修改token(更改大小写), 删除token(去掉停止词), 添加token(添加同义词)

在pycharm中使用http request的方式上传文件


基础知识

HTTP协议在 rfc1867 的提议中添加了在提交表单时上传文件的功能.

要通过http实现文件上传功能, 需要满足以下几个条件:

  1. 设置Content-Type

    Content-Type: multipart/form-data; boundary="Boundary";
    boundary是一段标识码, 用来在body中分割不同的数据, multipart在这里表示body中可以有多种类型的数据

  2. 设置Request Body

    body中按照 –Boundary 来区分不同的数据, Bounday就是前面指定的标示码, 比如要添加一个txt文件:

    --Boundary
    Content-Disposition: form-data; name="file"; filename="test.pdf"
    
    this is just a text file for test
    --Boundary
    Content-Disposition: form-data; name="type"; 
    
    report
    --Boundary
    ...
    --Boundary
    
    

    ```

    Content-Disposition 在请求中用来在multipart消息体的子部分中给出相应字段的信息,
    HTTP协议中, 使用multipart时, 只支持form-data, 以及可选的name和filename三个参数
    第一个参数是固定的form-data, 附加的参数之间用 ; 分割

    一个完整的请求如下:

    POST /api/v1/files
    Host: http://localhost:8088
    Content-Type: multipart/form-data; boundary=WebAppBoundary; charset=UTF-8
    
    --WebAppBoundary
    Content-Disposition: form-data; name="file"; filename="test.txt"
    
    this is just a text file for test
    --WebAppBoundary
    Content-Disposition: form-data; name="type"; 
    
    report
    --WebAppBoundary
    ...
    --WebAppBoundary
    

在PyCharm中使用http-request上传文件

新建一个.http后缀的文件, 内容如下:

POST /api/v1/files
Host: http://localhost:8088
Content-Type: multipart/form-data; boundary=WebAppBoundary; charset=UTF-8

--WebAppBoundary
Content-Disposition: form-data; name="file"; filename="text.txt"

< ./test.txt
--WebAppBoundary

区别只是在将文件的内容替换成指定文件路径, 使用 < 来将指定文件的内容追加到请求体中

在Tornado中解析上传文件的请求

  import tornado.ioloop
  import tornado.web

  class MainHandler(tornado.web.RequestHandler):
      def get(self):
	  print(self.request.files)
	  self.write("success")

  def make_app():
      return tornado.web.Application([
	  (r"/api/v1/files", MainHandler),
      ])

  if __name__ == "__main__":
      app = make_app()
      app.listen(8088)
      tornado.ioloop.IOLoop.current().start() 

在tornado的handler中, self.request.files表示的就是multipart格式的请求体中,
用boundry分割的多个文件, 没有指定filename的不会追加到files中, 而是会添加到self.request的body_arguments中

python中的文件IO之一


python 中有三种I/O, 分别是text I/O, binary I/O, raw I/O,
属于任意一种类型所对应的的对象都叫做file object, 也叫做stream, file-like object.

创建一个file object的通用方法是用open函数, 可以用来创建text I/O和binary I/O.
open函数的参数mode代表对文件操作的类型, 按照文件内容有二进制和纯文本,
按照操作类型可以分为以下几种, 摘自官方文档

  • 'r' open for reading (default)
  • 'w' open for writing, truncating the file first
  • 'x' create a new file and open it for writing
  • 'a' open for writing, appending to the end of the file if it exists
  • 'b' binary mode
  • 't' text mode (default)
  • '+' open a disk file for updating (reading and writing)

Text I/O

In [34]:
%%writefile myfile.txt
"这是一个测试的文本文件"
"hello can you hear me"
Overwriting myfile.txt
In [8]:
import io

# create a text stream
file1 = open('myfile.txt', 'r', encoding='utf8')

# create a in memory text stream
file2 = io.StringIO('texts stored in memory')

assert isinstance(file1, io.TextIOBase)
assert isinstance(file2, io.TextIOBase)

text stream对象的基类是TextIOBase, 我们重点关注以下几个方法

  • read(size=-1)
    按照size读取文本内容, 到达文件末尾时停止, size为负数或None时读取全部内容

  • readline(size=-1)
    按照size读取文本内容, 遇到换行符时或到达文件末尾时停止,
    size为负数或None或者读取一行全部内容, 如果已经在文件末尾, 返回空字符串

  • seek(offset, whence=SEEK_SET)
    改变文件指针的位置, 默认从0开始移动, offset是移动的偏移量, offset是指字节的大小, 一个中文字符是三个字节.
    whence可以是0, 1, 2代表从开始位置, 当前位置, 结束位置, 只有whence=0的时候, offset才可以有0以外的值

  • tell()
    返回当前的文件指针位置

  • write(s)
    写入文本

In [50]:
f = open('myfile.txt', 'r', encoding='utf8')
print("现在文件指针位置在%d" % f.tell())
print("让我们从开始位置读%d个字符: '%s'" % (10, f.read(10)))
print("现在文件指针位置在%d" % f.tell())
print("再从位置%d开始向后读取%d个字符: '%s'" % (f.tell(), 5, f.read(5)))
print("现在文件指针位置在%d" % f.tell())
print("最后从位置%d读取一行, size为%d: '%s'" % (f.tell(), 10, f.readline()))
print("现在文件指针位置在%d" % f.tell())
print("看看现在还剩下什么: '%s'" % f.read())
f.close()
现在文件指针位置在0
让我们从开始位置读10个字符: '这是一个测试的文本文'
现在文件指针位置在30
再从位置30开始向后读取5个字符: '件
hel'
现在文件指针位置在37
最后从位置37读取一行, size为10: 'lo can you hear me
'
现在文件指针位置在56
看看现在还剩下什么, ''

通过上面的例子可以看出:

  • 每次使用f.read() 或者 f.readline() 之后文件的指针都会移动, 具体视size的大小而定
  • f.read()或者f.readline() 每次都是从文件指针的位置开始向后读取size大小的字符
  • f.read(size) size指的是字符的大小, 而不是每个字所占的字节的大小
  • f.tell() 返回的位置是按照字节大小来计算的, 一个中文占三个字节, 一个字母占一个字节
  • 换行符也算一个字符, readline和read 返回的行文本包含了行尾的换行符
  • 因为文件指针一直在移动, 所以无法再读取已经读取过的内容, 除非手动移动指针的位置

下面来试试seek方法

In [61]:
f = open('myfile.txt', 'r', encoding='utf8')
print(f.read(1).encode('utf8'))
print("现在文件指针位置在%d" % f.tell())

f.seek(0)
print("现在文件指针位置在%d" % f.tell())

f.seek(1)
print("现在文件指针位置在%d" % f.tell())
try:
    f.read(1)
except UnicodeDecodeError:
    print("编码错误")
b'\xe8\xbf\x99'
现在文件指针位置在3
现在文件指针位置在0
现在文件指针位置在1
编码错误

前面说过f.tell()返回的位置是按照字节大小来计数, f.seek()也是按照字节数来移动文件指针, 而如果文本中有字符的字节数不为1的话, f.seek()可能会移动到某个字符的中间位置, 这时就会无法识别当前是什么字符, 这个例子中最后的f.seek(1) 之后指针所在位置的字节是\xbf, 无法与后续的字节构成一个合法的字符, 抛出UnicodeDecodeError.

读完本文, 希望你能够回答以下问题.

  1. 如何优雅的读写文件
    • 如何追加内容到文件的开始
    • 如何追加内容到文件的指定行
  2. 如何优化读写速度
    • 操作文件指针
    • 使用进程/线程锁确保数据一致性

Python元编程入门


Python元编程-入门

最近准备着手阅读sqlalchemy的源码,一个ORM框架离不开元编程,因此整理了一下python中元编程的相关知识。话不多说,下面进入正题。
P.S. 文中代码都是基于python3.6环境

基本概念

大家都知道python中的对象都是通过类的__new__方法创建,__init__方法进行初始化,
同时Python中一切皆对象, 类不用说也是一个对象,那么类又是如何创建的呢,这就引出了元类(metaclass)的概念。
我们使用type函数可以知道一个实例是通过哪个类实例化的,如果type的参数是一个类呢?

In [10]:
# example 1.1
print(type(object))
# using instance to verify
assert isinstance(object, type)
<class 'type'>

这里可以看出object的类型是type,也就是说type创建了object类,这里的type就是一个元类。
元类就是创建类的类,类实例化会创建一个实例,类似的,元类实例化就会创建一个类。

type是所有元类的基类,也是它自己的基类。

我们再来看一下type是如何创建类的。

以下摘录自官方文档 https://docs.python.org/3.6/library/functions.html#type

class type(name, bases, dict)
With one argument, return the type of an object. The return value is a type object and generally the same object as returned by object.__class__.

...

With three arguments, return a new type object. This is essentially a dynamic form of the class statement. The name string is the class name and becomes the __name__ attribute; the bases tuple itemizes the base classes and becomes the __bases__ attribute; and the dict dictionary is the namespace containing definitions for class body and is copied to a standard dictionary to become the __dict__ attribute. For example, the following two statements create identical type objects:

简单来说,如果参数是三个的时候,参数和生成的类的属性有如下对应关系

  • name  -> __name__
  • bases -> __bases__
  • dict     -> __dict__
In [1]:
# example 1.2
type('when using type with a single argument, it returns the class of argument')

MyObject = type('MyObject', (object,), {'a': 1})
assert isinstance(MyObject, type)
MyObject.__name__, MyObject.__bases__, MyObject.__dict__
Out[1]:
('MyObject',
 (object,),
 mappingproxy({'a': 1,
               '__module__': '__main__',
               '__dict__': <attribute '__dict__' of 'MyObject' objects>,
               '__weakref__': <attribute '__weakref__' of 'MyObject' objects>,
               '__doc__': None}))

元类的继承

In [19]:
# example 2.1

# 创建一个元类
class Meta(type):
    c = 1
    def __new__(mcs, *args, **kwargs):
        print("I'm creating a class using metaclass {}".format(mcs.__name__))
        cls = super().__new__(mcs, *args, **kwargs)       
        print('Now I got a class named {name}'.format(name=cls.__name__))
        print(cls.a, cls.hello)
        return cls
    
    def __init__(cls, *args, **kwargs):
        print("I'm initializing class {} in metaclass {}".format(
            cls.__name__, type(cls).__name__))
        super().__init__(*args, **kwargs)
        cls.b = 2 # 给类绑定变量
        print(cls.__name__, cls.__bases__, cls.__dict__)
    
    def assign_to_class(cls):
        print('I got a method from metaclass')

# 用定义的元类构造一个类
class MyObject(object, metaclass=Meta):
    a = 1
    def __new__(cls, *args, **kwargs):
        self = super().__new__(cls, *args, **kwargs)       
        return self
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
    @classmethod
    def name(cls):
        return cls.__name__
    
    @property
    def hello(self):
        return 'hello'
MyObject.a, MyObject.b, MyObject.c, MyObject.assign_to_class(), 
    
I'm creating a class using metaclass Meta
Now I got a class named MyObject
1 <property object at 0x10e82f958>
I'm initializing class MyObject in metaclass Meta
MyObject (<class 'object'>,) {'__module__': '__main__', 'a': 1, '__new__': <staticmethod object at 0x10e82b320>, '__init__': <function MyObject.__init__ at 0x10e80c1e0>, 'name': <classmethod object at 0x10e82b240>, 'hello': <property object at 0x10e82f958>, '__dict__': <attribute '__dict__' of 'MyObject' objects>, '__weakref__': <attribute '__weakref__' of 'MyObject' objects>, '__doc__': None, 'b': 2}
I got a method from metaclass
Out[19]:
(1, 2, 1, None)

通过以上示例可以看出,MyObject类生成的时候,先调用元类Meta的__new__,此时MyObject中的实例方法,类方法,类变量已经被绑定,然后是__init__,在元类的init方法中可以将类当做实例一样,可以给类绑定变量,方法等等。
MyObject是Meta的一个实例,因此像对象调用类定义的方法和属性一样,MyObject也可以调用Meta的属性和方法

元类可以用来实现很多“黑魔法”,form表单验证,orm模型都可以用它来实现。因为元类拥有对类的生杀大权,所以很容易写出难以定位的bug,如果不是经验丰富的老司机,尽量要避免使用。

最后总结一下,元类是类的类,类是元类的实例,type函数是一切元类的基类,也是自己的基类。 面向对象的本质就是给数据绑定方法,在python中完全可以不经过继承,直接通过type来创建类,同时为它绑定方法。

后续在读sqlalchemy源码的过程中会继续深入讨论元编程的应用。

Learning Cython - Basics


在jupyter notebook中学习cython

首先导入cython

In [1]:
%load_ext Cython
In [1]:
%%cython
# distutils: language=c 

def test(short test, char arg[]):
    return test, arg

print(test(1.1, b'ab'))
UsageError: Cell magic `%%cython` not found.
In [8]:
%%cython
# distutils: language=c
cdef test(short test):
    for i in range(10):
        test = test + i
    return test
    
print(test(12))

ElasticSearch Guide


前言

elasticsearch是一个java开发的企业级的实时的分布式全文搜索和分析引擎, 基于lucene, 可以快速搭建搜索引擎, 提供丰富的查询API, 正好最近在搞搜索引擎, 总结了一些elastic使用和架构方面的知识.

主要分几个部分:

  1. 基本概念, indice, document, shard, cluster
  2. 基础知识, 包括倒排索引, 分析器等
  3. elaticsearch的结构, indices, shard,

基本概念

数据库入门--关系模型与三范式


基础知识

范式

数据库表设计的参考标准, 主要有三大范式

范式之间是层层递进的关系,满足更高一级的范式必定满足低一级的范式

其他的范式还有BCNF,4NF,5NF等, 一般来说设计的时候满足三范式就够了

1NF
数据库中的所有字段都是不可再分的基本数据项, 关系数据库的基础, 不满足1NF的数据库不是关系数据库
2NF
非主属性 1完全函数依赖 2主码 3, 不能 部分函数依赖 4于主码
3NF
非主属性 都要与 主码 直接相关, 而不是间接相关, 消除 传递函数依赖 5

注意事项:

第二范式和第三范式的区别在于有没有分出两张表

实际应用中一般只能满足1NF, 2NF, 为了性能很难满足3NF

案例分析

下面介绍一下, 设计表的时候如何一步步演化到第三范式

下面是一张学生成绩表

学生 课名 分数
1022211101 , 李晓明 经济系 , 王强 高等数学 95

这张表明显不符合第一范式, 因为学生和系都是可以拆分的

拆分之后, 就满足第一范式了

学号 姓名 系名 系主任 课名 分数
1022211101 李晓明 经济系 王强 高等数学 95
1022211101 李晓明 经济系 王强 大学英语 85
1022211101 李晓明 经济系 王强 普通化学 75
1022211102 张丽 经济系 王强 高等数学 80
1022211102 张丽 经济系 王强 大学英语 82
1022211102 张丽 经济系 王强 普通化学 82

现在所有字段都是最小的数据项, 满足第一范式

这张表中主格是(学号, 课名), 非主属性为(姓名, 系名, 系主任, 分数)

系名和系主任只完全函数依赖于学号, 部分函数依赖于主格, 不满足2NF

现在存在的问题

数据冗余
每一个学生的姓名,系名,系主任这些数据重复多次
插入异常
一个系没有学生,则无法将系名与系主任的数据单独添加
删除异常
删除一个系中所有学生的数据,导致这个系也不存在了
修改异常
如果有学生需要转系,需要修改该学生所有数据中系与系主任的信息

为了解决这些问题,必须在第一范式的基础上拆分表来满足第二范式

成绩表
学号 课名 分数
1022211101 高等数学 95
1022211101 大学英语 85
1022211101 普通化学 75
1022211102 高等数学 80
1022211102 大学英语 82
1022211102 普通化学 82

主格:(学号, 课名), 非主属性(分数,), 满足3NF

学生表
学号 姓名 系名 系主任
1022211101 李晓明 经济系 王强
1022211102 张丽 经济系 王强

主格:(学号), 非主属性(姓名, 系名, 系主任), 满足2NF

但是系主任完全函数依赖于系名, 系名完全函数依赖于学号, 存在传递函数依赖, 不满足3NF

现在数据冗余和修改异常的问题得到了改善, 但是插入异常和删除异常依然存在

这时候就需要继续提高标准, 对学生表继续拆分, 使学生表满足第三范式

学生表
学号 姓名 系名
1022211101 李晓明 经济系
1022211102 张丽 经济系

主格:(学号), 非主属性:(姓名, 系名), 满足3NF

院系表
系名 系主任
经济系 王强

主格:(系名), 非主属性:(系主任), 满足3NF

这时候可以看到数据冗余减少到了最少, 同时删除,修改,插入异常都消除了

约束

主键约束 (Primay Key Coustraint)
唯一性, 非空性
唯一约束 (Unique Constraint)
唯一性, 建议不为空
默认约束 (Default Constraint)
字段的默认值, 可以为null
外键约束 (Foreign Key Constraint)
两表间的关系约束.
非空约束 (Not Null Constraint)
字段不能为null
检查约束 (Check Constraint)
限制字段的值范围

主键一般是自增长的id, 唯一约束一般是业务上的唯一约束;

编码

字符编码(Character Set)和排序规则(Collation)

可以在服务器, 数据库, 表, 字段四个层面设置字符编码

优先级: 字段 > 表 > 数据库 > 服务器

一个Character Set对应多个Collation, 不同的Collation性能和准确性有差异

索引

事务

规范

以mysql举例说明, 参考了阿里云和腾讯云社区的文章

命名

主要包括字段和表的命名, 未作特殊说明的表示对于表和字段都适用

  • 采用26个字母以及下划线, 单词之间统一用下划线分割, 一般不用数字
  • 字段名和表名全部小写, 禁止出现大写
  • 慎用数据库关键字, 如name, 最好结合其他单词使用, 如username
  • 名称要易于理解, 不宜过长, 最好不要超过三个单词
  • 单数形式表示名词, 如employee, 而不是employees
  • 必须有描述信息, 说明表和列的含义与用途
  • 表名使用名词, 字段可以使用名词和动宾短语
  • 尽量避免字段名中包含表名
  • 字段的名称中不要包含数据类型
  • 字段命名使用完整名词, 不要使用缩写
  • 日期类型字段推荐以“DATE”结尾的名字命名
  • 时间类型的字段推荐以“TIME”结尾的名字命名
  • 明细表命名推荐使用主表加dtl(detail缩写)来表示

类型

  • 字段最好都要有默认值, 不要为null, null字段查询难以优化, null字段的复合索引无效
  • 用尽量少的存储空间来存储, 优先级int> varchar, char, varchar(10) > varchar(100)
  • 固定长度的字段使用char, 可变长度的字段使用varchar
  • 主键使用int类型自增长的ID, int长度不能设置太短

编码

  • 使用utf8字符集同时考虑是否大小写敏感

Footnotes:

1

所有不属于主码的属性集合

2

设X,Y是两组集合, 如果知道了X也就确定了Y, 同时对于X的任何一个真子集X'无法确定Y, 则Y完全函数依赖于X

3

唯一标识一个实体的一个或者多个属性的集合, 可以完全决定所有的其他属性, 也叫主键, 主关键字

4

设X,Y是两组集合, 如果知道了X也就确定了Y, 同时存在X的一个真子集X'可以确定Y, 则Y部分函数依赖于X

5

设X,Y,Z是三组集合, 如果知道了X就可以确定Y, 知道了Y就确定Z, 则Z传递函数依赖于X