Python 2 与 Python 3 的主要区别

对比选项Python 3Python 2
printprint("Hello")print "Hello"
整数除法除法得到的值是float类型除法得到的值是int类型
Unicode字符串默认是 Unicode 保存,需要通过b'string'指定为 bytes默认为 bytes 需通过u'string'指定为Unicode
变量泄露变量值不会改变全局变量的值会在 for-loop 的内部发生改变
兼容从 Python 2 迁移到 Python 3 并不困难Python 3 与 Python 2 不向后兼容

Python 2 中 x 的值在列表推导式中发生改变

>>> x = 'hello'
>>> print(x)
hello
>>> vowels = [x for x in 'aeiou']
>>> print(vowels)
['a', 'e', 'i', 'o', 'u']
>>> print(x)
u

Python 3 中 x 的值未发生改变

>>> x = 'hello'
>>> print(x)
hello
>>> vowels = [x for x in 'aeiou']
>>> print(vowels)
['a', 'e', 'i', 'o', 'u']
>>> print(x)
hello

Python 2 与 Python 3 均会泄露:

>>> i = 0
>>> print(i)
0
>>> for i in [1, 2, 3]:
...     pass
...
>>> print(i)
3

参考内容:
Python 2 vs Python 3: What’s the Difference Between Python 2.x and Python 3.x?
Leaked variables in list comprehension

Strings, Bytes, and Unicode in Python 2 and 3

Python 2 vs Python 3 String Handling

"Hello World"
Python 2a "str" object stored as bytes. Prefix it with "u" to get a "unicode" object which is stored as Unicode
Python 3a "str" object that stores Unicode. Prefix it with "b" to get a bytes object or use .encode.

Using the Unicode Sandwich model to handle Unicode

Unicode Sandwich model.png
当你的程序接收到 byte strings 后尽快将其解码为 Unicode,在你的程序中所有的对这些 strings 的操作都将会在 unicode 的形式下进行,最后将其编码回 byte strings 并输出。

Pragmatic Unicode - Unipain

Str vs Unicode
stra sequence of bytes
unicodea sequence of code points (unicode)

unicode .encode() -> bytes
bytes .decode() -> unicode

转换时的错误处理策略

strict(default)/replace/xmlcharrefreplace/ignore

my_unicode = u'Hi \u2119\u01b4\u2602\u210c\xf8\u1f24'
my_unicode.encode('ascii', 'replace')
>>> 'Hi ??????'

my_unicode.encode('ascii', 'xmlcharrefreplace')
>>> 'Hi ℙƴ☂ℌøἤ'

my_unicode.encode('ascii', 'ignore')
>>> 'Hi '

快速剔除字符串中的非ascii字符串: string.encode('utf-8').decode('ascii', 'ignore')

UTF-8编码字节含义

  • 对于UTF-8编码中的任意字节B,如果B的第一位为0,则B独立的表示一个字符(ASCII码);
  • 如果B的第一位为1,第二位为0,则B为一个多字节字符中的一个字节(非ASCII字符);
  • 如果B的前两位为1,第三位为0,则B为两个字节表示的字符中的第一个字节;
  • 如果B的前三位为1,第四位为0,则B为三个字节表示的字符中的第一个字节;
  • 如果B的前四位为1,第五位为0,则B为四个字节表示的字符中的第一个字节;

因此,对UTF-8编码中的任意字节,根据第一位,可判断是否为ASCII字符;根据前二位,可判断该字节是否为一个字符编码的第一个字节;根据前四位(如果前两位均为1),可确定该字节为字符编码的第一个字节,并且可判断对应的字符由几个字节表示;根据前五位(如果前四位为1),可判断编码是否有错误或数据传输过程中是否有错误。

GBK编码的中文可以依据读音进行正则检索,因为GBK编码是按读音排序的。(部分多音字)
UTF-8编码的中文可以依据部首进行正则检索,UTF-8不是按读音排序的,是按照部首排序的。

参考内容:
Strings, Bytes, and Unicode in Python 2 and 3
Pragmatic Unicode - Unipain
The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)
Python 3 Unicode and Byte Strings
UTF-8 - Wikipedia

构建占位图片服务

构建一个简单的占位图片服务,使用Form来验证图片的尺寸:

class ImageForm(forms.Form):
    # valid height and width
    height = forms.IntegerField(min_value=1, max_value=1000)
    width = forms.IntegerField(min_value=1, max_value=1000)

    def generate(self, image_format='PNG'):
        height = self.cleaned_data['height']
        width = self.cleaned_data['width']
        image = Image.new('RGB', (width, height))
        content = BytesIO()
        image.save(content, image_format)
        content.seek(0)
        return content


def placeholder(request, width, height):
    form = ImageForm({'height': height, 'width': width})
    if form.is_valid():
        image = form.generate()
        return HttpResponse(image, content_type='image/png')
    else:
        return HttpResponseBadRequest('Invalid Image Request')


urlpatterns = (
    url(r'^image/(?P<width>\d+)x(?P<height>\d+)/$', placeholder, name='placeholder'),
)

加入缓存

先设置缓存key,先尝试读取key对应的缓存,如缓存不存在则生成图片并写缓存:

class ImageForm(forms.Form):
    # valid height and width
    height = forms.IntegerField(min_value=1, max_value=1000)
    width = forms.IntegerField(min_value=1, max_value=1000)


    def generate(self, image_format='PNG'):
        height = self.cleaned_data['height']
        width = self.cleaned_data['width']
        key = '{}x{}.{}'.format(height, width, image_format)
        content = cache.get(key)
        if content is None:
            image = Image.new('RGB', (width, height))
            content = BytesIO()
            image.save(content, image_format)
            content.seek(0)
            cache.set(key, content, 60*60)
        return content

设置Etag缓存

上述缓存的操作发生在服务器端,服务器端对缓存进行管理操作。同时可以进一步设置Etag使服务器对相同的资源返回304 Not Modified响应,以此来节约服务器运算资源和带宽。

def generate_etag(request, width, height):
    content = 'placeholder: {} x {}'.format(width, height)
    return hashlib.sha1(content.encode('utf-8')).hexdigest()


@etag(generate_etag)
def placeholder(request, width, height):
    form = ImageForm({'height': height, 'width': width})
    if form.is_valid():
        image = form.generate()
        return HttpResponse(image, content_type='image/png')
    else:
        return HttpResponseBadRequest('Invalid Image Request')

测试代码

启动测试服务器:
python placeholder.py runserver
根据构建的url模式,访问图片占位符地址:http://127.0.0.1:8000/image/200x200/
状态码为200,原始响应头如下:

HTTP/1.0 200 OK
Date: Sun, 29 Aug 2021 07:04:54 GMT
Server: WSGIServer/0.1 Python/2.7.18
Content-Length: 320
ETag: "3720c8007695a4c03152ef519310374e908ff60f"
Content-Type: image/png

再次请求该地址,状态码为304,原始响应头如下:

HTTP/1.0 304 Not Modified
Date: Sun, 29 Aug 2021 07:06:54 GMT
Server: WSGIServer/0.1 Python/2.7.18
Content-Length: 0
ETag: "3720c8007695a4c03152ef519310374e908ff60f"

同时可以注意到第二次的Content-Length为0,服务器只有headers的返回,没有body内容,节约了服务器带宽资源。

完整演示代码文件placeholder.pyplaceholder.py.txt 下载后删除.txt后缀即可。

上述演示代码存在的问题

  • 图片宽度过小是,文字会溢出,可将文字起始位置设置为(0, 0)
  • generate 方法与 ImageForm 耦合到一起了,难以在其他的地方复用
  • 图片缓存部分的逻辑,嵌入到了generate方法中,两者再次产生耦合,后续如果需要修改相关功能,可能比较困难,同时如果也会增加测试该代码的难度
  • 无法从外部(如通过URL参数)对generate方法指定image_format参数,如需实现此功能,需要同时修改urlpatternsgenerate_etag装饰器的代码,同时需要在ImageForm对图片格式参数进行限制。

包含模板文件的完整项目演示文件 placeholder.zip

《轻量级Django》笔记

一个最小的Django项目文件,包含 views,Url模式和配置即可。

文件 hello.py

import sys

from django.conf import settings
from django.conf.urls import url
from django.http import HttpResponse


# views
def index(request):
    return HttpResponse('Hello World')


# URL patterns
urlpatterns = (
    url(r'^$', index),
)


# settings
settings.configure(
    DEBUG=True,
    SECRET_KEY='SECRET_KEY_HERE',
    ROOT_URLCONF=__name__,
    MIDDLEWARE_CLASSES=(
        'django.middleware.common.CommonMiddleware',
        # 'django.middleware.csrf.CsrfViewMiddleware',
        # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
    ),
)


if __name__ == '__main__':
    from django.core.management import execute_from_command_line
    execute_from_command_line(sys.argv)

测试运行

在命令行中启动:python hello.py runserver
运行的结果:

>python hello.py runserver
Performing system checks...

System check identified no issues (0 silenced).
August 28, 2021 - 12:40:39
Django version 1.11.29, using settings None
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

运行环境软件版本

Python version : 2.7.18
Django version : 1.11.29