但是很多时候我们需要为 ViewSet 中的某个 Action 指定不同的 Serializer。 比如在 UserViewSet中有一个修改密码的 Action:change_password,我们需要单独为它指定一个 ChangePasswordSerializer。
根据官方文档,我们可以这样做,重写 ViewSet 类的 get_serializer_class 方法,自己实现判断逻辑返回需要的 Serializer。
1 | def get_serializer_class(self): |
这样在 change_password 方法中我们就可以直接通过 self.get_serializer() 得到 ChangePasswordSerializer 对象了。
但是这不是一个优雅的解决方法,我们应该劲量避免代码中出现硬编码!!!
为了优雅的解决这个问题,我们需要实现一个方法装饰器和一个 Mixin 类。
首先是一个装饰器,用于为 Action 指定 Serializer。
1 | def action_serializer(serializer): |
接下来是一个 Mixin 类,重写 get_serializer_class 方法,返回使用了 action_serializer 装饰器修饰的 Action 的 Serializer 类即可。
1 | class ActionSerializerMixin(object): |
这样在我们的 UserViewSet 中,代码就可以简化成这样了,十分优雅。
1 |
|
其实还有一个更简单的方法
但是我暂时没有仔细看APIView类的源码,不确定在实例化ViewSet以后修改serializer_class属性会不会造成什么问题。
这个方法就是直接在装饰器中修改ViewSet对象的serializer_class属性,这样就只需要向action方法添加一个装饰器就能实现我们想要的功能了。
1 | from functools import wraps |
问题描述
《审美的历程》课上有n位学生,帅老师展示了m幅画,其中有些是梵高的作品,另外的都出自五岁小朋友之手。老师请同学们分辨哪些画的作者是梵高,但是老师自己并没有答案,因为这些画看上去都像是小朋友画的……老师只想知道,有多少对同学给出的答案完全相反,这样他就可以用这个数据去揭穿披着皇帝新衣的抽象艺术了(支持帅老师^_^)。
答案完全相反是指对每一幅画的判断都相反。
输入格式
第一行两个数n和m,表示学生数和图画数;
接下来是一个n*m的01矩阵A:
如果aij=0,表示学生i觉得第j幅画是小朋友画的;
如果aij=1,表示学生i觉得第j幅画是梵高画的。
输出格式
输出一个数ans:表示有多少对同学的答案完全相反。
样例输入
3 2
1 0
0 1
1 0
样例输出
2
样例说明
同学1和同学2的答案完全相反;
同学2和同学3的答案完全相反;
所以答案是2。
数据规模和约定
对于50%的数据:n<=1000;
对于80%的数据:n<=10000;
对于100%的数据:n<=50000,m<=20。
这道题困扰了我很久,原理上看不难,但是注意有时长限制。
时间限制:1.0s
尝试了很多次都无法拿到满分,问题都出在运行超时上。
最后总结出三个关键点:
以下是我最终的代码:
得分:100
CPU使用时间:453ms
内存使用:27.44MB
1 | import java.util.*; |
在修改自定义User模型的信息后,点击保存按钮提交Form时,提示email字段不能为空。
这是因为我们的UserAdmin类继承自django.contrib.auth.admin.UserAdmin,在父类UserAdmin的form中将email字段定义为了required,即必填字段。
通过继承django.contrib.auth.forms.UserChangeForm类,并在__init__方法中对email字段属性进行修改,可以实现我们想要的效果。
1 | from django.contrib.auth.forms import UserChangeForm |
这样在保存时就不会提示email字段不能为空了。
但是测试中发现,如果不填写email字段内容,保存后发现email字段的值变成了空字符串,而不是None,但是我们在自定义User模型中指定了email字段null=True,即我们期望的它应该是一个空值None。
经过调试后发现,在django.contrib.admin.options.ModelAdmin类的_changeform_view方法代码中,调用form.is_valid()方法后,email字段的值发生了变化。
1 | # django.contrib.admin.options.ModelAdmin |
于是顺藤摸瓜,找到django.forms.models.BaseModelForm类中_post_clean方法,发现在此方法中调用了Model实例的full_clean方法。
1 | # django.forms.models.BaseModelForm |
继续跟进发现在自定义User模型的父类AbstractUser中,实现了clean方法,并且对email字段的值进行了修改。
1 | # django.contrib.auth.models.AbstractUser |
这里调用的normalize_email是django.contrib.auth.base_user.BaseUserManager中实现的一个静态方法。
1 | # django.contrib.auth.base_user |
所以我们只需要自己实现一个UserManager类并继承BaseUserManager类,并重写normalize_email方法。
1 | from django.contrib.auth.models import BaseUserManager |
这样,当提交Form中缺少email值时,就能正确的把email字段值保存为None了。
]]>现在我的项目在原有网站基础上需要另外提供一套API,只需要简单的一点拓展就可以在现有session和auth应用基础上实现基于Token的认证。
在自己的app下创建middleware.py文件,写一个自己的class,继承django.contrib.session.middleware模块下的SessionMiddleware类,并且重写process_request方法。
1 | from django.conf import settings |
在处理请求时,先尝试从请求头中对应字段获取Token,即Session Key,并通过Session引擎获取Session对象。
如果请求头中没有包含Token字段,则调用父类方法。
最后,在登陆API的view中调用auth模块login方法后,从request中获取Session Key放入到响应体中即可。
这里因为是用了 REST-Framework 的 ViewSet,所以需要通过request.stream获取原始Django的HttpRequest对象。
1 |
|
听起来很有意思,打算使用 Python 的 selenium 包配合无界面浏览器 PhantomJS 来实现。
要注意较新版本的 selenium 已经不再支持 PhantomJS,需安装较老版本的 selenium。
模拟登陆使用 requests 包的 Session 类,然后将 cookie 添加到 WebDriver 里。
1 | session = requests.session() |
需要截图的页面内有两个 iframe,而 iframe 内容是由页面上点击某个菜单链接时动态加载的。
所以我们在页面加载结束以后执行 JS 语句模拟点击菜单链接,网页引入了 JQuery,可以很方便的使用。
1 | driver.get(CONTENT_URL) |
此时 iframe 开始加载,但是如果马上执行网页截图的话,iframe 可能还没有完成加载,所以我们等待想要截图的内容加载完成以后再执行截图。
这里可以方便的使用 selenium 提供的 WebDriverWait 类,用以阻塞式地循环判断条件是否成立,直到条件成立才会继续执行下面的代码。
1 | # 在代码中引入 WebDriverWait 类 |
当条件成立时,执行网页截图,返回图片内容的字节数组。
1 | screenshot = driver.get_screenshot_as_png() |
最后,将图片内容写入到响应体中返回,完工。
]]>在 Android 5.0 (API 21) 系统手机上测试时 App 抛出了空指针异常,而在其他测试机(均等于或高于 Android 6.0)上运行正常。
经排查发现时 Fragment 中的 OnAttach 方法没有被调用。
原来 Android 在 API 23 以后重载了 Fragment 中的 OnAttach 方法。
之前的
1 |
|
被重载为了
1 |
|
而我在继承 Fragment 的类中只重写了第二个方法,也就是 API 23 以后的方法。
所以我们复写的 OnAttach 方法在 API 21 的手机上没有被调用。
只需要复写 onAttach(Activity activity) 方法,并且添加一句 SDK 版本判断。
1 |
|
问题解决。
在一部华为手机上(Huawei GR3 HWTAG-L6753)上测试时 App 发生了 Crash,奇怪的是查看文件没有生成 CrashLog 文件。
于是连上 ADB 查看 Logcat,发现输出的全是
W/AEE : some logs have been lost
根据 StackOverflow 上的回答 android - Huawei, logcat not showing the log for my app? - Stack Overflow
Dial
*#*#2846579#*#*
and you will see a hidden menu. Go to the Project Menu > Background Setting > Log setting and define the log availability (log switch) and level (log level setting).
在拨号界面拨号 *#*#2846579#*#*
进入隐藏菜单开启 Log 输出以后,就能正常查看 Logcat 信息了。
(经测试开启后不需要重启手机,评论中有人提到部分手机重启手机将重置此设置)
自己写了一个静态地图控件,遇到了需要判断触摸点是否位于多边形指定区域内的问题。
网上资料很多,主流方法是利用光投射算法。Point in polygon - Wikipedia
CSDN上有一篇文章把原理讲的非常详细 点在多边形内算法——判断一个点是否在一个复杂多边形的内部
参考 StackOverFlow 上一个回答的写法,翻译成 JAVA 如下:
1 | /** |
为了方便使用,我将它封装到了我的 Polygon 类里。
1 | public class Polygon { |
在这里将动态生成的 Excel 文档保存到服务器,并重定向到静态文件链接显然不是一个合适的做法。
所以我们需要直接将 openpyx 生成的 Excel 文档写入到 Django 的 HttpResponse 对象响应体中。
查阅 openpyxl 的文档以后,并没有找到有用的信息,所以直接从源码入手。
在这篇文章中,我将把源码分析的过程详细的展现出来,并总结出完美的解决方案。
我们先从 openpyxl.workbook.workbook 模块下的 Workbook 类入手,找到平时我们用来保存 Excel 文档的方法。
1 | def save(self, filename): |
可以看到在一般情况下调用这个方法时,实际上会调用的是 save_workbook 这个函数,这个函数在 openpyxl.writer.excel 模块中定义。
1 | from openpyxl.writer.excel import save_workbook |
那我们就要来看看这个模块里还定义了什么东西。
打开这个文件,突然眼前一亮,这里还定义了一个叫做 save_virtual_workbook 的函数,作者在注释里写到 “Return an in-memory workbook, suitable for a Django response.” ,看来这就是我们要找的东西。
1 | def save_virtual_workbook(workbook,): |
这个函数将 Excel 文档写入到内存,返回一个字节数组。
Django 的 HttpResponse 类构造方法可以接受一个字节数组作为响应体,所以我们可以这样写。
1 | from django.http import HttpResponse |
这样就可以很简单的实现将 Excel 文档写入到响应体中了。
指定中文文件名时遇到问题可以参考我之前的一篇文章 Django 返回流文件时使用中文文件名的问题
查阅 Django 文档,发现在 HttpResponse 类的说明下有这样一句:“you can use response as a file-like object.”,并给了一段示例代码。看来 HttpResponse 对象可以直接作为一个 file-like 对象,像对文件对象一样进行写入操作。
Excel 文档的 .xlsx 文件实际上是一个 ZIP 格式的压缩包,在 openpyxl.writer.excel 模块的 save_workbook 函数中是使用 Python 自带的 zipfile 模块来创建的文件。
1 | archive = ZipFile(filename, 'w', ZIP_DEFLATED, allowZip64=True) |
查阅 zipfile 模块文档知道 ZipFile 类构造方法的第一个参数可以接受一个文件路径字符串、path-like 对象或是一个 file-like 对象。
太好了,看来我们可以直接把 Django 的 HttpResponse 对象传入到 ZipFile 的构造方法,让 ZipFile 对象直接往 HttpResponse 的响应体中写入内容。
我们当然可以在调用 openpyxl.writer.excel 模块的 save_workbook 函数时,直接将 HttpResponse 对象传入,就像这样。
1 | from django.http import HttpResponse |
但是继续来读 openpyxl 的源码,发现在调用 save_workbook 函数时,创建了一个 ExcelWriter 对象,并调用了它的 save 方法。
1 | def save(self, filename): |
在 save 方法的最后,调用了我们传入的 HttpResponse 对象的 close 方法,而查阅 Django 源码发现在 HttpResponse 的基类 HttpResponseBase 的 close 方法处注释着,该方法应该由 WSGI 服务器在处理完请求以后调用。
1 | # The WSGI server must call this method upon completion of the request. |
所以为了优雅,我们还是把调用 close 方法的工作交还给 WSGI 服务器来做吧。
我们来自己实现一个 save_workbook 函数,用于将 openpyxl 的 Workbook 生成 Excel 文档,并写入到 Django 的 HttpResponse 对象响应体中。
1 | from openpyxl.writer.excel import ExcelWriter |
这样,我们只需要在创建 HttpResponse 对象后,调用我们自己实现的 save_workbook_response 函数,将 openpyxl 的 Workbook 对象和 HttpResponse 对象一起传入,就可以生成 Excel 文档并写入到响应体中了。
1 | from django.http import HttpResponse |
使用 requestAnimationFrame 将会把动画帧渲染周期交给浏览器统一调度,实现更好的性能优化。
可是微信小程序并没有支持这个 Api,在模拟器上测试可以正常运行,可在真机上运行时会报错,没有这个方法。
所以我们只得想办法自己来模拟这个 Api 所做的工作。
requestAnimationFrame 能够将原本零散的帧渲染序列进行梳理,使得页面上所有动画帧都在同一周期进行渲染,以最大化利用系统资源。
一般浏览器的渲染周期为 60 次每秒,即每次间隔大约 16 毫秒。因此我们只需要写一段代码,来将渲染周期控制在 16 毫秒,就能够模拟出 requestAnimationFrame 的效果了。
1 | var lastFrameTime = 0; |
现在就可以像使用 requestAnimationFrame 和 cancelAnimationFrame 一样使用 doAnimationFrame 和 abortAnimationFrame 了。
]]>在测试的过程中发现了一个问题,无论我怎么在响应头中添加 Content-Disposition 指定文件名,在 Chrome 浏览器中访问的时候返回的文件名始终是按 url 的最后一段自动生成的文件名,而将文件名改成英文以后又是正常的。
猜测应该是字符串编码的问题,http 响应头中字符应该按照 url 百分号编码。
修改为以下代码后,终于能够正常显示中文文件名了。
1 | # Python3 |
Python2 中的 quote 方法引入位置有所不同,代码如下:
1 | # Python2 |
计划页面有两个 canvas,一个用来实时绘制上半部分的天气动画,一个用来绘制中间的气温趋势折线图。
上半部分天气动画在不同的天气下有不同的效果,比如不同强度的降雨会有相应的雨滴从上面落下,阴天多云的时候顶部会有云朵在漂浮,多云转晴的话后面再加上个太阳。
降雨的动画不难,无非就是随机生成一些雨滴对象,每一帧实时更新位置即可。
关键在云朵的动画上,我们需要生成随机宽高的椭圆形云朵,并且云朵沿着随机椭圆轨迹运动。
根据高中数学学习的关于椭圆方程的知识,我们知道,中心点位于点 (h,k) 的椭圆方程为:
a 为半长轴,b 为半短轴。在这里可以简单理解成 a 为椭圆宽的 1/2,b 为椭圆高的 1/2。
可是仅通过这个方程我们还没办法写出动画算法,因为无法计算出 x,y 在任意时刻的值。
还好,维基百科给了我们另一个公式:
这是椭圆的参数方程,其中 t 为椭圆中心点到椭圆上任意一点所确定的直线与x
轴的夹角,可以通过三角函数计算出在任意角度时点 (x,y) 的坐标。
我们先封装一个生成指定范围内随机数的方法,方便后面使用。
1 | function random(min, max) { |
好了,现在我们来写一个云的类,就叫做 Cloud。
按照我们的设想,云应该是一个绕椭圆轨迹运动的椭圆。所以我们需要给 Cloud 对象传入一个运动轨迹中点,然后让 Cloud 对象在初始化的时候随机生成一些变量,然后在每一帧的时候更新自己的位置并且绘制到 canvas 上。
1 | function Cloud() { } |
代码很简单,我们给了 Cloud 类三个方法。
在实例化 Cloud 对象的时候,调用 init 方法,传入云朵所围绕运动的椭圆轨迹中点坐标,并且随机生成一系列的随机变量。
在每一帧的时候调用 Cloud 对象的 draw 方法,通过角度 t 计算出新的绘制坐标,并在 canvas 上绘制一个云朵。
然后 Cloud 对象内部调用自己的 update 方法,根据自己的角速度变量计算出下一帧的角度 t。
在 draw 方法内,我们调用了一个名叫 fillEllipse 的方法,按照设想,我们可以传入椭圆的中点坐标、a 和 b 来绘制出一个椭圆。
现在我们来用代码实现这个方法。
这里我首先想到的是利用之前的椭圆参数方程,沿着椭圆绘制出一条路径,然后填充路径即可。(后来发现这是一个坑,详见后文)
1 | function fillEllipse(ctx, x, y, a, b) { |
代码很简单,先粗略的计算出一个合适的角度增量 Δt,然后循环计算角度 t 从 0 增加到 2π 过程中的每一个点的坐标,绘制出一条路径,然后填充路径。
效果如下:
这时遇到了问题,在微信开发工具上模拟的时候效果很不错,可是当放到真机上测试的时候发现,为什么云的运动速度变得异常的缓慢。
根据我们之前的绘制原理,每一次的增量移动是在每一帧的时候完成的,而不是根据时间增量所计算的。所以移动的速度会直接和帧率挂钩,当帧率大的时候,位置增量更新的就更快,反之就会更慢。
我在绘制的时候是采用的每绘制一帧间隔 16 毫秒,即约 60 帧每秒。
为了查看实际的帧率,我在每一帧的绘制方法里面加上了这样一句:
1 | let now = new Date() |
这样通过每一帧绘制的实际间隔时间,计算出当前的帧率,然后把这个数字绘制在 canvas 上。(高频率的输出不应使用 console.log(),会非常影响性能。)
结果如下:
可以看到,实际的帧率在 20-30 帧每秒之间浮动,远远低于设定的 60 帧每秒。
CPU 使用率和内存占用也是出乎意料的非常高。
查看前面的代码,不难发现,问题一定是出在了绘制椭圆的那个方法上。
每秒 60 次循环计算出椭圆上各个点的位置并绘制路径,计算量非常之大,并且大量的路径数据添加到上下文中,导致内存占用也开始飙升。
显然,这个方法一定是不可行的!
查阅小程序的开发文档以后,其中一个 canvas 的 Api 让我看到了希望。
canvasContext.scale(scaleWidth, scaleHeight)
定义:在调用scale方法后,之后创建的路径其横纵坐标会被缩放。多次调用scale,倍数会相乘。
参数:scaleWidth 和 scaleHeight 分别为横纵坐标缩放的倍率。
我们只需要计算出椭圆在横纵坐标上相较于正圆的缩放倍率,调用 scale 方法后,再绘制一个正圆,不就可以画出需要的椭圆形状了吗。
而绘制正圆的方法非常简单,用 arc 方法直接绘制一个完整的圆形路径即可。
代码如下:
1 | function fillEllipseByScale(ctx, x, y, a, b) { |
这样每次绘制椭圆只会向 canvas 上下文中添加少量的动作,计算量也大大降低。
我们再用同一台手机运行,测试一下实际帧率。
可以看到,这次帧率稳定在了 63 帧,与我们所设置的帧绘制间隔一致了,CPU 使用率和内存占用也降低到了之前的一半,性能有了很大的提升。
]]>为了方便管理,我决定写一个 Shell 脚本来控制 uWSGI 服务器的启动和停止。
这样以后项目中如果需要同时启动别的进程,也可以通过简单修改 Shell 脚本来实现对整个项目的一键控制。
首先把 uWSGI 服务器的各项选项参数写入到 ini 文件,并设置守护进程。
然后编写 Shell 脚本如下(dino 为项目简称):
1 |
|
将 Shell 脚本保存为 dino.sh,并且创建软连接到 /usr/bin/dino
尝试运行脚本:
]]>首先是添加常用的用户,以 Git 为例:
装机自带了 Git,先 yum 更新一下:
1 | yum update -y git |
接下来新建一个名为 git 的用户:
1 | useradd -d /home/git -m git |
这里的 -d 选项指定了此用户的主目录为 /home/git , -m 选项表示如果目录不存在则新建目录。
然后就要开始对 git 用户进行一些安全相关的设置。
因为只需要在常用的电脑上操作远程仓库,所以开启 SSH 的 RSA 公钥认证登陆,并关闭 git 用户的密码登陆功能。
修改 /etc/ssh/sshd_config 文件,这里我使用的是 vim 文本编辑器。
1 | HostKey /etc/ssh/ssh_host_rsa_key # 主机私钥文件位置 |
找到并修改以上 4 条设置,如果被#号注释掉了,删除前面的#号。
然后再文件末尾添加:
1 | Match User git, Group git |
这样就开启了 SSH 的 RSA 公钥认证,并禁用了 git 用户的密码认证登陆功能。
认证公钥文件为用户主目录下的 .ssh/authorized_keys 。
接下来 cd 到 git 用户的主目录,创建 .ssh 目录和 authorized_keys 文件,并设置权限。
1 | cd /home/git |
然后编辑 /home/git/.ssh/authorized_keys 文件,将需要登陆的电脑的 RSA 公钥写入进来,每行一个。
并重启 ssh 服务:
1 | /etc/init.d/sshd restart |
最后,
因为 git 用户仅用于使用 git 服务器上的管理远程仓库,所以我们应该让 git 用户的终端指向 git-shell。
1 | usermod -s /usr/bin/git-shell git |
现在就可以轻松的在自己的电脑上使用 git 管理服务器上的远程仓库了~
]]>