来源:https://www.jisilu.cn/question/516756

曾经有个朋友看到了卡叔这些年的投资收益图,他惊讶的问:“卡叔,你到底是怎么做到如此平稳又高速的收益率的?是因为帅吗?”
一向谦虚的卡叔立即否认了他的说法。在认真思索后,卡叔还是答复了他的问题。卡叔说:“我觉得吧,我有这样的收益率,可能和我除了帅以外的几十项优点中的某几项有关。
”截至2025年12月31日,卡叔的本年度收益为36.68%,虽然今年这个收益率和很多优秀的股票投资者无法相提并论。但是对于偏重低风险品类的卡叔来说,我自认为是相当不错的成绩了。
如果从2014年算起,截至2025年12月31日,在长达12年的周期里,卡叔的股市资产合计取得了超过18倍的增值。
而拉开我的历史收益图,不管是历史收益还是当年收益,收益曲线都是一条缓慢向上的曲线。

阅读全文 »

注:这是2018年2月写的旧博文,转载到此。

Redis使用了一个称为“A simple event-driven programming library”的自制异步事件库(以下简称“AE”)。整个事件库的代码量少于1k行,是个优秀的C异步事件库学习材料。

源码结构

版本 Redis 4.0.8

redis的src目录下,ae开头的几个文件就是AE事件库的源码。

阅读全文 »

注:这是2018年2月写的旧博文,转载到此。

事先声明,标题没有把“Python”错打成“Cython”,因为要讲的就是名为“Cython”的东西。

Cython是让Python脚本支持C语言扩展的编译器,Cython能够将Python+C混合编码的.pyx脚本转换为C代码,主要用于优化Python脚本性能或Python调用C函数库。由于Python固有的性能差的问题,用C扩展Python成为提高Python性能常用方法,Cython算是较为常见的一种扩展方式。

我们可以对比一下业界主流的几种Python扩展支持C语言的方案:
有试用版水印,是因为穷T_T

ctypes是Python标准库支持的方案,直接在Python脚本中导入C的.so库进行调用,简单直接。swig是一个通用的让高级脚本语言扩展支持C的工具,自然也是支持Python的。ctypes没玩过,不做评价。以c语言程序性能为基准的话,cython封装后下降20%,swig封装后下降70%。功能方面,swig对结构体和回调函数都要使用typemap进行手工编写转换规则,typemap规则写起来略复杂,体验不是很好。cython在结构体和回调上也要进行手工编码处理,不过比较简单。

Cython简单实例

我们尝试用Cython,让Python脚本调用C语言写的打印“Hello World”的函数,来熟悉一下Cython的玩法。

1
2
/*filename: hello_world.h */
void print_hello_world();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*filename: hello_world.c */
#include <stdio.h>
#include "hello_world.h"

void print_hello_world()
{
printf("hello world...");
}

int main(int arch, char *argv[])
{
print_hello_world();
return (0);
}
1
2
3
4
5
6
7
#file: hello_world.pyx

cdef extern from "hello_world.h":
void print_hello_world()

def cython_print_hello_world():
print_hello_world()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#filename: Makefile
all: hello_world cython_hello_world

hello_world:
gcc hello_world.c -c hello_world.c
gcc hello_world.o -o hello_world

cython:
cython cython_hello_world.pyx

cython_hello_world: cython
gcc cython_hello_world.c -fPIC -c
gcc -shared -lpython2.7 -o cython_hello_world.so hello_world.o cython_hello_world.o

clean:
rm -rf hello_world hello_world.o cython_hello_world.so cython_hello_world.c cython_hello_world.o

用Cython扩展C,最重要的就是编写.pyx脚本文件。.pyx脚本是Python调用C的桥梁,.pyx脚本中即能用Python语法写,也可以用类C语法写。

1
2
3
4
5
6
$ make all    # 详细的编译过程可以看Makefile中的相关指令
$ python
>>> import cython_hello_world
>>> cython_hello_world.cython_print_hello_world()
hello world...
>>>

可以看到,我们成功的在Python解释器中调用了C语言实现的函数。

Cython的注意事项

所有工具/语言的简单使用都是令人愉快的,但是深入细节就会发现处处“暗藏杀机”。最近是项目需要扩展C底层库给Python调用,所以引入了Cython。实践过程中踩了很多坑,熬了很多夜T_T。遇到了以下几点需要特别注意的点:

  1. .pyx中用cdef定义的东西,除类以外对.py都是不可见的;
  1. .py中是不能操作C类型的,如果想在.py中操作C类型就要在.pyx中从python object转成C类型或者用含有set/get方法的C类型包裹类;
  1. 虽然Cython能对Python的str和C的“char *”之间进行自动类型转换,但是对于“char a[n]”这种固定长度的字符串是无法自动转换的。需要使用Cython的libc.string.strcpy进行显式拷贝;
  1. 回调函数需要用函数包裹,再通过C的“void *”强制转换后才能传入C函数。

1. .pyx中用cdef定义的类型,除类以外对.py都不可见

我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#file: invisible.pyx
cdef inline cdef_function():
print('cdef_function')

def def_function():
print('def_function')

cdef int cdef_value

def_value = 999

cdef class cdef_class:
def __init__(self):
self.value = 1

class def_class:
def __init__(self):
self.value = 1
1
2
3
4
5
#file: test_visible.py
import invisible

if __name__ == '__main__':
print('invisible.__dict__', invisible.__dict__)

输出的invisible模块的成员如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ python invisible.py
{
'__builtins__': <module '__builtin__' (built-in)>,
'def_class': <class invisible.def_class at 0x10feed1f0>,
'__file__': '/git/EasonCodeShare/cython_tutorials/invisible-for-py/invisible.so',
'call_all_in_pyx': <built-in function call_all_in_pyx>,
'__pyx_unpickle_cdef_class': <built-in function __pyx_unpickle_cdef_class>,
'__package__': None,
'__test__': {},
'cdef_class': <type 'invisible.cdef_class'>,
'__name__': 'invisible',
'def_value': 999,
'def_function': <built-in function def_function>,
'__doc__': None}

我们在.pyx用cdef定义的函数cdef_function、变量cdef_value都看不到了,只有类cdef_class能可见。所以,使用过程中要注意可见性问题,不要错误的在.py中尝试使用不可见的模块成员。

2. .py传递C结构体类型

Cython扩展C的能力仅限于.pyx脚本中,.py脚本还是只能用纯Python。如果你在C中定义了一个结构,要从Python脚本中传进来就只能在.pyx手工转换一次,或者用包裹类传进来。我们来看一个例子:

1
2
3
4
5
6
7
8
/*file: person_info.h */
typedef struct person_info_t
{
int age;
char *gender;
}person_info;

void print_person_info(char *name, person_info *info);
1
2
3
4
5
6
7
8
9
//file: person_info.c
#include <stdio.h>
#include "person_info.h"

void print_person_info(char *name, person_info *info)
{
printf("name: %s, age: %d, gender: %s\n",
name, info->age, info->gender);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#file: cython_person_info.pyx
cdef extern from "person_info.h":
struct person_info_t:
int age
char *gender
ctypedef person_info_t person_info

void print_person_info(char *name, person_info *info)

def cyprint_person_info(name, info):
cdef person_info pinfo
pinfo.age = info.age
pinfo.gender = info.gender
print_person_info(name, &pinfo)

因为“cyprint_person_info”的参数只能是python object,所以我们要在函数中手工编码转换一下类型再调用C函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
#file: test_person_info.py
from cython_person_info import cyprint_person_info

class person_info(object):
age = None
gender = None

if __name__ == '__main__':
info = person_info()
info.age = 18
info.gender = 'male'

cyprint_person_info('handsome', info)
1
2
$ python test_person_info.py
name: handsome, age: 18, gender: male

能正常调用到C函数。可是,这样存在一个问题,如果我们C的结构体字段很多,我们每次从.py脚本调用C函数都要手工编码转换一次类型数据就会很麻烦。还有更好的一个办法就是给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
31
32
33
34
35
36
#file: cython_person_info.pyx
from libc.stdlib cimport malloc, free
cdef extern from "person_info.h":
struct person_info_t:
int age
char *gender
ctypedef person_info_t person_info

void print_person_info(char *name, person_info *info)

def cyprint_person_info(name, person_info_wrap info):
print_person_info(name, info.ptr)


cdef class person_info_wrap(object):
cdef person_info *ptr

def __init__(self):
self.ptr = <person_info *>malloc(sizeof(person_info))

def __del__(self):
free(self.ptr)

@property
def age(self):
return self.ptr.age
@age.setter
def age(self, value):
self.ptr.age = value

@property
def gender(self):
return self.ptr.gender
@gender.setter
def gender(self, value):
self.ptr.gender = value

我们定义了一个“person_info”结构体的包裹类“person_info_wrap”,并提供了成员set/get方法,这样就可以在.py中直接赋值了。减少了在.pyx中转换数据类型的步骤,能有效的提高性能。

1
2
3
4
5
6
7
8
9
#file: test_person_info.py
from cython_person_info import cyprint_person_info, person_info_wrap

if __name__ == '__main__':
info_wrap = person_info_wrap()
info_wrap.age = 88
info_wrap.gender = 'mmmale'

cyprint_person_info('hhhandsome', info_wrap)
1
2
$ python test_person_info.py 
name: hhhandsome, age: 88, gender: mmmale

3. python的str传递给C固定长度字符串要用strcpy

正如在C语言中,字符串之间不能直接赋值拷贝,而要使用strcpy复制一样,python的str和C字符串之间也要用cython封装的libc.string.strcpy函数来拷贝。我们稍微修改上一个例子,让person_info结构体的gender成员为16字节长的字符串:

1
2
3
4
5
6
/*file: person_info.h */
typedef struct person_info_t
{
int age;
char gender[16];
}person_info;
1
2
3
4
5
6
#file: cython_person_info.pyx
cdef extern from "person_info.h":
struct person_info_t:
int age
char gender[16]
ctypedef person_info_t person_info
1
2
3
4
5
6
7
8
9
#file: test_person_info.py
from cython_person_info import cyprint_person_info, person_info_wrap

if __name__ == '__main__':
info_wrap = person_info_wrap()
info_wrap.age = 88
info_wrap.gender = 'mmmale'

cyprint_person_info('hhhandsome', info_wrap)
1
2
3
4
5
6
7
8
9
$ make
$ python test_person_info.py
Traceback (most recent call last):
File "test_person_info.py", line 7, in <module>
info_wrap.gender = 'mmmale'
File "cython_person_info.pyx", line 39, in cython_person_info.person_info_wrap.gender.__set__
self.ptr.gender = value
File "stringsource", line 93, in carray.from_py.__Pyx_carray_from_py_char
IndexError: not enough values found during array assignment, expected 16, got 6

cython转换和make时候是没有报错的,运行的时候提示“IndexError: not enough values found during array assignment, expected 16, got 6”,其实就是6字节长的“mmmale”赋值给了person_info结构体的“char gender[16]”成员。我们用strcpy来实现字符串之间的拷贝就ok了。

1
2
3
4
5
6
7
8
9
10
11
12
#file: cython_person_info.pyx
from libc.string cimport strcpy
…… ……
cdef class person_info_wrap(object):
cdef person_info *ptr
…… ……
@property
def gender(self):
return self.ptr.gender
@gender.setter
def gender(self, value):
strcpy(self.ptr.gender, value)
1
2
3
$ make
$ python test_person_info.py
name: hhhandsome, age: 88, gender: mmmale

赋值拷贝正常,成功将“mmmale”拷贝给了结构体的gender成员。

4. 用回调函数作为参数的C函数封装

C中的回调函数比较特殊,用户传入回调函数来定制化的处理数据。Cython官方提供了封装带有回调函数参数的例子

1
2
3
//file: cheesefinder.h
typedef void (*cheesefunc)(char *name, void *user_data);
void find_cheeses(cheesefunc user_func, void *user_data);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//file: cheesefinder.c
#include "cheesefinder.h"

static char *cheeses[] = {
"cheddar",
"camembert",
"that runny one",
0
};

void find_cheeses(cheesefunc user_func, void *user_data) {
char **p = cheeses;
while (*p) {
user_func(*p, user_data);
++p;
}
}
1
2
3
4
5
6
7
8
9
10
#file: cheese.pyx
cdef extern from "cheesefinder.h":
ctypedef void (*cheesefunc)(char *name, void *user_data)
void find_cheeses(cheesefunc user_func, void *user_data)

def find(f):
find_cheeses(callback, <void*>f)

cdef void callback(char *name, void *f):
(<object>f)(name.decode('utf-8'))
1
2
3
4
5
6
import cheese

def report_cheese(name):
print("Found cheese: " + name)

cheese.find(report_cheese)

关键的步骤就是在.pyx中定义一个和C的回调函数相同的回调包裹函数,如上的“cdef void callback(char *name, void *f)”。之后,将.py中的函数作为参数传递给包裹函数,并在包裹函数中转换成函数对象进行调用。

扩展阅读

更进一步的研究Cython可以参考官方文档和相关书籍:

版权声明:自由转载-非商用-非衍生-保持署名(创意共享4.0许可证

注:这是2018年2月写的旧博文,转载到此。

Redis 协议

Redis客户端和服务端之间使用一种名为RESP(REdis Serialization Protocol)的二进制安全文本协议进行通信。RESP设计的十分精巧,下面是一张完备的协议描述图:
redis protocol

#举个栗子
用SET命令来举例说明RESP协议的格式。

1
2
redis> SET mykey "Hello"
"OK"

实际发送的请求数据:

1
*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$5\r\nHello\r\n

实际收到的响应数据:

1
+OK\r\n

每种命令对应的回复类型,可以查询Redis官网的命令列表Command reference。更详细的协议说明请参考Redis官方协议规范Redis Protocol specification

参考

[1] 通信协议(protocol),http://redisdoc.com/topic/protocol.html

[2] Redis Protocol specification,https://redis.io/topics/protocol

版权声明:自由转载-非商用-非衍生-保持署名(创意共享4.0许可证

注:这是2020年1月写的旧博文,转载到此。

Anki是什么?

本文假定你是个Anki用户,并不会对Anki基础知识进行介绍。对Anki不熟悉的读者可以阅读Anki英文官网(Anki - powerful, intelligent flashcards),或Anki中国(Anki–近乎完美的记忆神器)的介绍内容。

正确使用牌组

“正确使用牌组”是官方 Anki 手册上对 Anki 牌组功能的使用建议,内容如下:

牌组被设计成将你的内容分成你想单独学习的大类,如英语、地理等等。 你可能会想创建许多小的牌组,以保持你的内容有条理,如“我的地理书第1章”,或“食品动词”,但这是不推荐的,有以下原因:

  • 许多小牌组意味着你最终会以可识别的顺序复习卡片。 无论是因为你依次点击每一个牌组(这是缓慢的),或你在一个单一的父级牌组上增加了一些牌组,你最终会看到所有的“第1章”或“食物动词”卡片在一起。 这使得回答卡片更容易,因为你可以从上下文猜测他们,从而导致较弱的记忆。 当你需要回忆单词或Anki外的短语时,你不会有充足的可被证明的相关内容。

  • Anki不是设计来处理许多牌组(超过几十个),它会慢下来,当你添加更多的–尤其是如果你在一个移动客户端的情况下。一些额外的牌组不会产生明显的差异,但是如果你有许多牌组,延误将开始增加。

使用标签和/或字段来分类内容,替代创建许多小的牌组,这是一个更好的主意。 例如替代创建一个“食物动词”,你可以把这些卡片添加到你的主要语言学习牌组,并用“食物”和“动词”来标记卡片。每个卡片可以有多个标签,这意味着你可以做的事情,如寻找所有的动词,或所有与食品有关的词汇,或所有的动词与食品有关。

对于那些喜欢保持非常有条理的,您可以添加字段到您的笔记分类的内容,如“书”,“页”等。 Anki支持特定字段的搜索,这意味着你可以做一个“图书搜索:‘我的书’页码:63”马上找到你要找的。

Anki的定制学习和筛选牌组特点使其特别强大,因为你可以从搜索条件创建临时牌组。这允许您在大多数时间(最佳内存)中将内容混合在一个单独的牌组上,同时也需要在特定的材料上创建临时牌组,例如在测试之前。一般的规则是,如果你总是希望能够单独学习一些内容,它应该是在一个正常的牌组上,如果你只是偶尔需要能够单独学习(测试,有压力时,等),标签/字段和过滤牌组更好。

由上可知,官方的建议是不要创建许多小分类牌组,使用几个单独学习的大类牌组即可。强迫症患者可以使用标签或字段来细分类内容,替代创建许多小分类牌组。

极简牌组结构

极简Anki牌组结构

牌组名称 用途
00-全部 存放“有效”卡片
01-迭代 存放“待修改”卡片
99-归档 存放“废弃”卡片
  • 有效:需要学习/复习/记忆的卡片;
  • 待修改:内容不好影响记忆效果,需要修改的卡片;
  • 废弃:内容过时,已经不需要记忆的卡片;

“00-全部”牌组

Anki 中的“有效”卡片最终都是要内化到大脑记忆的,那么卡片也就不需要分类了。用易于记忆的方式组织好卡片的内容,记忆到大脑中后,大脑这台高端分类机器会自动分类的。
过度分类最大的弊端是破坏了“间隔重复算法”的有效性。 因为不同类别的卡片记忆难度是不同的,人为分类之后,你会常常在不经意间选择记忆轻松简单的牌组,而不是靠“间隔重复算法”自动为你选择当前最需要复习的卡片。
所以,要记忆的“有效”卡片请全都放到“00-全部”牌组。如果控制不住自己要进行分类,那么问自己“分类通常是为了区分重要程度,既然有不太重要的卡片,直接归档/删除即可,何必要分类?”

“01-迭代”牌组

Anki 卡片内容组织方式是使用 Anki 的难点,卡片内容无法一步到位,通常需要多次的迭代修改。如果多次复习某张卡片时都难以回想起来,那么主要有两种可能的原因:

  1. 没有理解卡片对应的知识,靠死记硬背;
  2. 卡片的内容组织方式不当,大脑难以记忆。

解决第 1 点问题的方式因人而异,毕竟 Anki 只是记忆工具。如何系统的学习和理解新知识超出了 Anki 的范畴,这方面大家可以求助于讲解学习方法的相关书籍。

第 2 点问题。目前我还没找到“间隔重复”学习理论的专著,只有知乎上的 Anki 爱好者们和 SuperMemo 的博客有一些零星的文章。卡片内容组织方面,推荐阅读 SuperMemo 的一篇博文《有效学习:组织知识的20条原则》[英文][中文]。文章表达的核心观点就是卡片不能把一大堆内容堆砌在卡片中,期望通过间隔重复的方式来记忆,这样是行不通的。卡片的内容要遵循“最小信息原则”,一张卡片只记忆一个极其简单的知识点。

按我的理解,不管是用问答的形式还是填空形式,一张卡片最好在 5s 内能快速回想出答案,快速、高频、精准的刺激对应的脑回路,才能形成长期记忆。 问题和答案没啥强关联,想半天想不出答案,或者答案是好几百字的一大段文字等,都是无法记忆的不合格卡片,需要转移到“01-迭代”牌组进行优化拆分后,再回到“00-全部”牌组进行记忆。

“99-归档”牌组

做卡片不易,要耗费很多时间精力,有时候看到过时无用的卡片不舍得删,所以存在这个牌组。由于存放在这个牌组的卡片都是“废弃”的卡片,所以牌组学习新卡片和复习卡片的数量都设置为“0”,以免出现数字提醒干扰。

总结

Anki 虽好,可不要瞎折腾。花太多时间在牌组的结构上就属于没有意义的瞎折腾,钻研卡片的内容组织方式才是对使用 Anki 真正有意义的事情^_^。

参考

[1] Anki 2.0 用户手册,https://www.ankichina.net/Index/ankishouce
[2] 有效学习——组织知识的20个原则,https://www.jianshu.com/p/163462164a5b

版权声明:自由转载-非商用-非衍生-保持署名(创意共享4.0许可证

I used to learn english by Duolingo.

Duolingo App

But Duolingo only has a little speak pratice, so i always try to find an better english speaking app.

At last year, ChatGPT became more and more popular. I thought may be some english learning applications could used the chatgpt’s AI feature to make a AI tutor. I searched by google, then i found the application “Speak”.It’s amazing app! I can speak to AI tutor, just like a real person.

Speak App

I think it may be helping me to speak english fluently in six months.

阿里云的服务器忘记续费,导致之前用 Flask 搭的博客数据全没了。考虑了一下,还是用 Hexo 在 Github 上搭博客,本地和远端都有 Git 仓库保存全量数据,不容易丢失。虽然需要科学上网才能访问 Github,但是不会科学上网的人似乎也不需要看技术博客,所以网络问题影响也不大。

0%