【Python】 边边角角笔记 - 那些记不住却有用的细枝末节

1. 判断当前运行平台

import sys


## Usage one:
if sys.platform == 'win32':
    DEFAULT_BROWSER_NAME = 'phantomjs.exe'
elif sys.platform == 'linux':
    DEFAULT_BROWSER_NAME = 'phantomjs'

## Usage two:
if sys.platform == 'win32':
    pass
elif sys.platform == 'linux':
    '''fix the input() issue
     terminal show ^H when pressing [Backspace]
    '''
    import readline  

1.1 python 在 Windows - Cygwin 上运行的 sys.platform 值

$ python
Python 3.4.5 (default, Oct 10 2016, 14:41:48)
[GCC 5.4.0] on cygwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>>
>>> import sys
>>> sys.platform
'cygwin'
>>>
>>>
>>>
>>> exit()


2. Two path(PATH) of python-program

2.1 运行环境 PATH

(注意这个是用大写表示的)

在解释器中练习:

我博客所有环境都默认假设在 *nix 下工作。在 Windows 的例子应当会特别说明。
我们可以根据 $, #; >; %; 来区分平台。

$ echo $PATH
...:/usr/bin:/usr/local/bin:...
$ pwd
/home/<USER>/projdir
$ PATH=`pwd`:$PATH  # 1. 通过 shell 环境,添加当前工作路径到 PATH 中
## 注:`pwd`:$PATH 这种混合写法未必在每个 LINUX 发行版都支持,
## 另外,如果使用 zsh,fish,这类 shell,需要它的支持才可用。
## Ubuntu 的 bash(默认shell环境) 亲测支持。
$ echo $PATH
/home/<USER>/projdir:...:/usr/bin:/usr/local/bin:...
$ python3
>>> import os
>>> os.environ.get("PATH", None)
'/home/<USER>/projdir:...:/usr/bin:/usr/local/bin:...'
>>>
>>> ## 2. 在 python 代码中添加路径到 PATH:
>>> PATH = os.environ.get("PATH", None)
>>> os.environ["PATH"] = "/usr/lib/chromium-browser:" + PATH 
>>> 
>>> os.environ.get("PATH", None)
'/usr/lib/chromium-browser:/home/<USER>/projdir:...:/usr/bin:.......'
>>>
>>> exit()
$

2.2 sys.path

修改这个变量可以控制程序的包导入路径。

$ python3
>>> import sys
>>> sys.path
['', '/usr/lib/python3X.zip', '/usr/lib/python3.X', ...]
>>>
>>> # 添加自己写的包路径:
>>> sys.path.append("/path/to/your/package")
>>> import your.package
>>>
>>> exit()

2.2.1 项目中引入当前项目文件夹 abspathsys.path

假设目录结构:

$ pwd && tree .
/<path>/<to>/<proj>/src
.
├── somelib
│   └── utils.py
├── application
│   ├── exceptions.py
│   ├── __main__.py
│   ├── module
│   │   ├── module.py
...

$

这样的目录结构可以这样运行项目:

$ pwd
/<path>/<to>/<proj>/src
$ python -m application  # 通过 runit 运行 application/__main__.py
...

$

其中 somelib 是比较“通用”的库,层级和 application 同级(因为可能有不同的 application 使用到它);属于 project 的源码第一层。

当以上述方式运行代码的时候,因为 Python 总是会将 pwd 路径(以当前例子是 /<path>/<to>/<proj>/src)自动加入 sys.path 中。所以通过以上文件目录结构的方式运行,则不用考虑包路径的问题。

而在 __main__.py 中引入 application/ 下的 *.py 文件,或者是 application/ 下的文件夹(python 包),则需要使用


from . import exceptions
from .module import module

即,要有 . 开头,以相对导入的方式;因为这种方式相当于 application/ 本身也是一个“包”,因此对于“包”内部的模块,可以使用相对导入的方式。

2.2.2 Jupyter notebook 中引入

jupyter 中没有 __file__,如果要获得 __file__ 所在的文件夹,可以通过 os.path.abspath('') 取得。


3. Python 解释器

3.1 得知运行“自己”的 Python 解释器是哪个

$ python
>>> import sys
>>> sys.executable
/<path>/<to>/pythonX

# ====
$ /usr/bin/pytho3
>>> import sys
>>> sys.executable
/usr/bin/python3


.


一些坑

虽然我热爱 Python,但是不得不说,它确实存在一些坑。
之前遇到过一些,但是似乎是自己不注意?比较无关紧要。
但是今天遇到的一个坑,确确实实把我震惊了,花了不少时间才注意到(多线程下的函数外部变量非全局变量的失误使用)。

这个坑虽然是我编程的错误,但是我当然是希望在运行阶段就直接抛出错误,或者运行到该行代码,就出现错误,程序退出。但是在这个情况下,不会抛出错误,这个就是比较坑的原因。这里指的错误在本节“一些坑”的第一个小节(多层函数调用仍然可以使用最上层函数的变量)就是?。

多层函数调用仍然可以使用最上层函数的变量

我的代码稍微复杂一点点,也不算很复杂。但是我更愿意使用一个简单的例子说明。
问题复制:

# 复制 坑 的 demo.py
from selenium import webdriver
import sys

def callFunc():
    page_source = browser.page_source
    try:
       with open("damo_download.html", 'w') as fp:
           fp.write(page_source)
    except IOError as e:
        print(e)  # 在这个 demo 中,错误处理不是重点。

def main():
    try:
        url = sys.argv[1]
        browser.get(url)
    except Exception as e:
        print(e) # not care on my demo

    callFunc()

if __name__ == '__main__':
    if __debug__:
        browser = webdriver.Chrome(executable_path="/usr/lib/chromium-browser/chromedriver")
    else:
        browser = webdriver.PhantomJS()
    main()

以往我写博客,代码如果不是从真正的代码里面粘贴过来,如果是手动抽象敲出来的话,一般都懒得试,觉得意思表达到了就行,运行的话有点儿细节改动自己稍微改改就能跑。不过这一次我把这段代码确认跑了一遍再放上来。确认如果你从我的这篇文章代码复制下去实验,能够看到该看到的现象。

运行上面的 demo 代码:

$ ## 要运行上面的 demo.py,确保你已经安装 selenium 和 chromedriver
$ ## 使用 Ubuntu 的话,chromedriver 可以直接通过 APT 下载安装(你可以先 search 一下)。
$ python demo.py "http://localhost"
$
$ ls
damo_download.html  demo.py
$
$ ### 使用你的浏览器,将 *.html 拖动过去打开,发现确实是下载(保存)下来了。

是的,这段代码你可能看不出什么猫腻出来,因为一般不会这么写。
而且,就算这么写了,因为代码很短,变量很少,所以你知道这一切都是怎么回事儿。

但是,对于 python 这个“自动”上层作用域查找变量的机制,如果你的编码失误的话,有时候会造成意想不到的 bug。
考虑以下这一种情况:

# 复制 坑 的 demo2.py
from selenium import webdriver
import sys

import queue

browserQ = queue.Queue()
MAX_BROWSER_RUN = 2

def callFunc():
    [...]

def main():
    [...]

if __name__ == '__main__':
  for i in range(MAX_BROWSER_RUN):
       '''setup browsers '''
       if __debug__:
           '''Chromium in Linux
               make sure install chromium-chromedriver.
               it can be found and installed by 'apt'
           '''
           browser = webdriver.Chrome(
               executable_path="/usr/lib/chromium-browser/chromedriver"
           )
           browser.set_window_size(240, 240)  # not work well
           browserQ.put(browser)
       else:
           [...]
   main()

很明显,这篇博客,这一节的主题不是来讲多线程 和 Queue的,所以上面的代码是直接运行不了的,但是我相信它能够表达出我想表达的意思。关于使用多线程和 Queue 来并发获取网络资源以及节省主机不断开关浏览器的资源消耗,未来我会有一篇单独的博客讲讲我的方案。

让我来解释一下上面的代码,
它创建了一个 Queue 实例,用来放置 selenium.webdriver 的实例 browser。

这只是我用来避免程序不断开关浏览器,这未必是完美的方案,但是目前我们知道它就是这么工作的。

>>> ## 如果我假设上面的 if __name__ =='__main__': 下的代码是在解释器中一行一行执行过的,
>>> ## 那么现在我们有了一个 browserQ, 现在我来简单解释一下它是怎么工作的:
>>> thisBrowser1 = browserQ.get()
>>> thisBrowser1.get("https://www.baidu.com")  # 这里两次 get
>>> thisBrowser2 = browserQ.get()              # 会看到启动的两个浏览器
>>> thisBrowser2.get("https://bing.com")       # 会分别请求一次代码中的网站
>>> browserQ.put(thisBrowser1)
>>> browserQ.put(thisBrowser2)

那么上面解释的代码我目前是这么使用的:

# 复制 坑 的 demo2.py

[...]

import random

def callFunc(req_url):
    while True:
        try:
            thisThreadBrowser = browserQ.get()
            break
        except: # 暂时无可用浏览器在队列中,等待。
            sleep(2)
            pass
    if __debug__:
        sys.stdout.write("str(thisThreadBrowser) {}\n".format((str(thisThreadBrowser))))

    thisThreadBrowser.get(req_url)
 
    time.sleep(1)
    page_source = browser.page_source

    with open("{}.html".format(str(random.randint(1000,9999))), 'w') as htmlfp:
        '''这个命名完全是为了方便 '''
        htmlfp.write(pagesource)

    browserQ.put(thisThreadBrowser)  # !!!!!

现在,我们的程序拼图还剩下最后一块,在 main() 中模拟请求多张网页:

# 复制 坑 的 demo2.py

[...]
import threading

def callFunc(...):[...]

def main():
    req_urls = ["https://www.baidu.com", "http://localhost", "https://bing.com"]
    
    threads = []
    for url in req_urls:
        new_thread = threading.Thread(target=callFunc, args=(url, ))
        threads.append(new_thread)

    for i in range(len(req_urls)):
        threads[i].start()
    
    for i in range(len(req_urls)):
        threads[i].join()
     
if __name__ == "__main__":
    [...] # 这里就是启动了 MAX_BROWSER_RUN 个浏览器,并且放入 browserQ 队列。
    main()
    while True: # 这段代码只是顺便关闭一下浏览器。
        try:
            browser = browserQ.get(timeout=3)
            browser.quit()
        except queue.Empty:
            sys.exit(0)

可以看到,我们根据 req_urls 有多少个 url 就创建了多少个线程,线程就是运行 callFunc 函数;
而这个函数就是从我们的可用的浏览器队列中取出来一个控制浏览器的实例,来使用它获取一下它的 req_url 任务,然后把 page source 保存到一个随机文件名的 html 中,最后用完浏览器就归还给浏览器队列。

所以,运行程序的时候,我们可以知道现象会有一次 2 个(MAX_BROWSER_RUN 是 2)浏览器同时请求了两张页面,最后有一次只有一个浏览器请求了一张页面。

运行完程序之后,你的 current work directory 会有三个 *.html 文件。

如果你知道怎么调调代码的话,把我上面三个片段调整一下,就可以运行了,需要改动的地方不是很多(甚至拼成上述的 demo2.py 可能就能直接用)。这样就能打开三个 *.html 看看内容是什么了。

现在,这个 坑 的复制工作已经费了很大力气完成了,该揭晓这个坑到底是什么。

如果你有心把上面三段代码合并一下,调了调我手打可能存在的错误,并且点开了 *.html 查看,就会发现,
what the fuuuuck! 三张页面居然是一样的!
并且会注意到,明明两个浏览器确实请求三张不同的网页呀!!!!

当然,可能你也注意到了,也可能没有注意到,虽然代码是可以按逻辑运行起来的,但是上面的代码存在一处失误,
就是 def callFunc(req_url): 部分的 第 21 行!。我使用 page_source = browser.page_source

此刻你应该知道我本节重点要说什么了,这一点问题在大多数编译型的语言中都不会出现,我们的失误会在编译过程中就出错了。因为那些语言函数内的变量必须要传递进去才能使用。而不像 python 会“自动”向上查找。
python 让它能够正常地工作了。但是结果却不是我们想要的。我们更希望在这种情况下,它能够给与一个错误。

现在我们来考虑一下解决方法,或者说编程习惯怎么能够逃过这些坑。

第一种,就是在 callFunc 中,不再多定义一个 thisThreadBrowser 这样的变量名,直接使用 browser 这样就覆盖了原本 browser 绑定的实例。
太多的不同但又相似的变量,在代码中是有干扰性的。

我可以确认这一点是我自己的编程习惯问题,虽然很多变量相似,但是使用的位置不同,我喜欢为其赋予一个新的名字。
我这样做好不好,我自己还没有一个答案。

这个一直使用一个相同的变量名的方案好不好呢? 因为个人水平有限,目前无法给出答案。

第二种,就是对于:

	browser = webdriver.Chrome(
	    executable_path="/usr/lib/chromium-browser/chromedriver"
	)
	browser.set_window_size(240, 240)  # not work well
	browserQ.put(browser)

这种情况下的 browser 变量,它实际上是一个明确的“临时变量”,我们放在浏览器队列之后,其实已经不想通过 browser 这个变量名来控制浏览器了。而是要通过 browserQ.get() 返回绑定到一个变量名上来使用。
所以,我个人从这次踩坑学到了一个使用 del 理由和场景。那么这段代码可以这么调整:

for ... in ...:
	tmp_browser = webdriver.Chrome(executable_path="/usr/lib/chromium-browser/chromedriver")
	tmp_browser.set_window_size(240, 240)  # not work well
	browserQ.put(tmp_browser)
del tmp_browser

这样就可以确保我们不会在编程过程中,因为代码加入新的功能而漏改了,误用了后来变为临时变量的变量

回忆一下两个版本,第一次,我们代码的逻辑是线性的,browser 都是同一个,一直在使用它。
第二次,我们要提高并发性,更短的时间内获得到更多的页面,于是创建了多个浏览器实例放在 browserQ 中,这个时候,原来的 browser 变量性质已经变了,是个临时的变量。然后在其它 function 中继续调整代码使支持这个多线程版本的时候,callFunc 中使用 thisThreadBrowser = browserQ.get() 来获取一个绑定到一个可以控制浏览器的实例上,但是原本的代码中,有一行 page_source = browser.page_source 忘记改了。造成了所有下载下来的页面都是同一个的“惨案”。



异常(Exception)

精确捕获异常

背景

使用 requests 库的 .get(url) 方法时,没有捕获异常,和处理,当 web server 没有启动时,即端口没有服务器监听,所以这个 requests.get() 会产生类似如下异常信息:

Exception: HTTPConnectionPool(host='localhost', port=8010): \
Max retries exceeded with url: [the url] \
(Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x761afa10>: \
Failed to establish a new connection: [Errno 111] Connection refused',))

如果是一个长时间运行的程序(一个 daemon 进程),
那么我们很可能会捕获所有异常报告出来:

try:
    r = requests.get(url)
    [...]
except [which exception you already know]:
    [...]
except Exception as e:
    print("Exception: {}".format(e), file=sys.stderr)

如果 Exception 上面的捕获没有考虑到 url 指向的 server 没有启动的情况,那么打印出来的错误消息就如同我上面贴出的
Exception: HTTPConnectionPool(... 一致了。

那么现在看了错误的消息,我们定位出了是 server 没有启动(或者说,这么 url server 不存在)的原因,应该如何精确地使用 except [the exception]: ... 来在代码中考虑进去这种情况的发生?

换句话说,在还不够熟悉 requests 库的情况下,我们怎么知道 requests 库中定义的该种异常类是什么?一定要去翻 requests 的源码 吗?

当然不用,去翻源码找出一个异常的定义名称是不科学的。

Solution

这个时候就要使用上 python 的调试技巧/方法来告诉我们,调用了 requests.get(url) 之后,它给我们抛出的异常的类名是什么:

    try:
        r = requests.get(url)
        [...]
    except [which exception you already know]:
        [...]
    except Exception as e:
+       import traceback; traceback.print_exc();
        print("Exception: {}".format(e), file=sys.stderr)

这段代码和上面没有太多不同,我已经使用 diff 的方式指出,我们增加了一行 import traceback; traceback.print_exc();
当然,我们是要在 Exception 这个“捕获全部异常”的位置下面需要更加精确的异常情况。

这个时候,我们能够等到更多的输出信息:

[...略...]
During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/.../python3.X/site-packages/urllib3/util/connection.py", line 79, in create_connection
    raise err
  File "./[your_program].py", line 154, in run
    r = requests.get(loopArticles_url)
  File "/.../python3.X/site-packages/requests/api.py", line 72, in get
    return request('get', url, params=params, **kwargs)
[...略...]
  File "/.../python3.X/site-packages/requests/adapters.py", line 513, in send
    raise ConnectionError(e, request=request)
requests.exceptions.ConnectionError: HTTPConnectionPool(host='localhost', port=8010): \
Max retries exceeded with url: [the url] (Caused by NewConnectionError(\
'<urllib3.connection.HTTPConnection object at 0x76135230>: Failed to establish a new connection: \
[Errno 111] Connection refused',))
  File "/.../python3.X/site-packages/urllib3/util/connection.py", line 69, in create_connection
    sock.connect(sa)
Exception: HTTPConnectionPool(host='localhost', port=8010): \
Max retries exceeded with url: [the url] (Caused by NewConnectionError(\
'<urllib3.connection.HTTPConnection object at 0x76135230>: Failed to establish a new connection: \
[Errno 111] Connection refused',))

实际输出的信息比上述更多,我为了可读性做了一些轻微的省略和换行。
我们可以看到其中一行 requests.exceptions.ConnectionError: HTTPConnectionPool(...
这个 ConnectionError 是不是我们这个情况下要找的呢?
答案是对的,除了你在使用一个实在不按常规出牌的库,不然 python 的库一般都会将该库自定义的异常类放在 exceptions.py 文件中。
所以,当你看到这个顺序的信息:

  requests.exceptions.ConnectionError
     包       模块       类
requests/ || exceptions.py || class ConnectionError([继承自定义的更上一层异常,或者是继承 Exception])

就可以基本确认这个“精确的“异常的名称是 ConnectionError
然后捕获它,处理它即可:

    try:
        r = requests.get(url)
        [...]
    except [which exception you already know]:
        [...]
+   except ConnectionError as e_conn:
+       [...你针对这个异常情况的处理...]
+       print("ConnectionError: {}".format(e_conn), file=sys.stderr)  # 如果是 daemon 进程,可以考虑使用 logging 模块 
    except Exception as e:
        print("Exception: {}".format(e), file=sys.stderr)


特殊方法 (魔术方法)

本人不太喜欢将其称为 魔术方法。


__new__()

用法/用途 - N/A

n/a

TypeError: object() takes no parameters after defining __new__

转自 Stackoverflow ?

Q: I really don’t get where is the error in this little code :

class Personne:
    def __init__(self, nom, prenom):
        print("Appel de la méthode __init__")
        self.nom = nom
        self.prenom = prenom

    def __new__(cls, nom, prenom):
        print("Appel de la méthode __new__ de la classe {}".format(cls))
        return object.__new__(cls, nom, prenom)

personne = Personne("Doe", "John")

This code presented above is giving me the error :

Traceback (most recent call last):
  File "/home/bilal/Lien vers python/21_meta_classes/1_instanciation.py", line 21, in <module>
    personne = Personne("Doe", "John")
  File "/home/bilal/Lien vers python/21_meta_classes/1_instanciation.py", line 14, in __new__
    return object.__new__(cls, nom, prenom)
TypeError: object() takes no parameters

A: from Blckknght
In Python 3.3 and later, if you’re overriding both __new__ and __init__, you need to avoid passing any extra arguments to the object methods you’re overriding. If you only override one of those methods, it’s allowed to pass extra arguments to the other one (since that usually happens without your help).

So, to fix your class, change the __new__ method like so:

def __new__(cls, nom, prenom):
    print("Appel de la méthode __new__ de la classe {}".format(cls))
    return object.__new__(cls) # don't pass extra arguments here!

Comments:

FWIW, the OP’s code actually works in Python3.2, though the error indeed occurs in Python3.3+ – plamut Jan 13 '16 at 22:02


Thanks bro, it seems that it is working. please, tell me, how does python pass the rest of the arguments to init , can you explain the process to me (if you have time of course) ??!! – Sidahmed Jan 13 '16 at 22:03


The call to __init__ isn’t made by object.__new__, but rather by type.__call__ (bound to the class object). So object.__new__ doesn’t need to see the same arguments that your __init__ function expects. – Blckknght Jan 13 '16 at 22:09


@Blckknght do you know why it works that way in Python3.3+? Is it described somewhere? I could not find description of this behavior in the doc? – Pawel KolodziejOct 28 '17 at 12:27


I think I’ve an answer now. It’s hidden in comment in typeobject.c: github.com/python/cpython/blob/…Pawel Kolodziej Oct 28 '17 at 18:22



26 个字母 - 内置函数 ordchr

我们知道在 ASCII 码表中,26 个字母是一个接一个按序排列的。那么,如何不用在代码中一个一个字母的在键盘上找,然后敲下如下代码?呢?

#!/usr/bin/env python3

import [...]

LETTERS = 'abcdefghijklmnopqrstuvwxyz'  # hard-code

[...]

if __name__ == '__main__':
    main()

为了有“身临其境”的感觉,上面我特地写出一个较完整的 *.py 文件代码样式。


What?!
Oh, no, no, no !
n/a
不要以为我那么傻,上面的 LETTERS 可不是我一个个在键盘上敲出来的,那是我 Ctrl+Shift+C, Ctrl+V 出来的!

如果你知道!
在 C/C++ 中,可以使用

char g_letters[27] = {'\0'};

void init(void) {
    short i = 0;
    for (i=0; i<26; i++) {
        g_letters[i] = 'a' + i;
    }
}

其中,第 6 行实际发生的情况是:
g_letters[i] = (char)((short)'a' + i);
因为 C/C++ 是弱类型语言。可以隐式转换类型,所以“int 和 char 类型只是按照 ASCII 码表就直接等价转换了”。

正是因为弱类型的这种特性,实际上会有 20% (这个比例是按我感觉)左右的 “很诡异”的 bug 就是隐式转换产生的。
要嘛就是写对了,写错了就一定会是“诡异”的,而且有些 bug 不会立马暴露出来。

上面说到,a -> z 字母在 ASCII 上是一个接一个顺序排列的,我们只需要对 a 的“位置” +1 就能够得到 b,(上面“如果你知道”)C/C++ 中就可以直接通过这种方式('a' + 1)来得到其它字母。但是在 Python 中,你不能这么干!因为 Python 是一门强类型的语言1,所以你使用 "a" + 1 这样的代码在 Python 中是无法运行通过的。

有一次有个朋友在 Django 交流群里问了这么一个问题:
printf("%s\n", "su2owegi"+3); 的输出是什么?
有朋友把 printf 看成了 print,以为是 Python 代码,但是说不清楚
(当然,python 中也有 %s 的写法,只不过需要再使用一个 %,而非 ,
实际如果这是 Python 代码的话:
print("%s\n" % "su2owegi"+3)
运行就会报错。而不是不知道

我们应该感谢强类型语言的这种特性,实际上它会帮我们避免许多的 bug。

那么在 Python 中该怎么做呢?
"a" + 1 无法执行成功的原因是因为 "a" 无法转换为 ASCII 码表上 ‘a’ 对应的序号。所以我们只要从 "a" 得到它对应的序号就可以了。
这里可以使用 Python 的内置函数 ord 来实现:

>>> ord("a")
97

然后 97 + 1 对应的是 ‘b’ 的序号,我们要再通过一种方式将序号(整数类型)转换为它对应的 ASCII 字符 – 使用内置函数 chr 实现:

>>> chr(98)
'b'

最后,我们的 LETTER 可以这样得到:

LETTERS = [chr(_) for _ in range(ord('a'), (ord('z') + 1))]
# or LETTERS = ''.join([chr(_) for _ in range(ord('a'), (ord('z') + 1))])

至此,你已经知道了在 Python 中得到一个26全字母“列表”的正确姿势。



做一个 Pythonista

改善超长字符串的可读性2

单行代码的长度不宜太长。比如 PEP8 里就建议每行字符数不得超过 79。现实世界里,大部分人遵循的单行最大字符数在 79 到 119 之间。如果只是代码,这样的要求是比较容易达到的,但假设代码里需要出现一段超长的字符串呢?

这时,除了使用斜杠 \ 和加号 + 将长字符串拆分为好几段以外,还有一种更简单的办法:使用括号将长字符串包起来,然后就可以随意折行了

def main():
    logger.info(("There is something really bad happened during the process. "
                 "Please contact your administrator."))

另一个例子:

>>> user_agent = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                  "AppleWebKit/537.36 (KHTML, like Gecko) "
                  "Chrome/76.0.3809.132 Safari/537.36")
>>> print("user_agent({}): '{}'".format(type(user_agent), user_agent))
user_agent(<class 'str'>): 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36'
>>>

当多级缩进里出现多行字符串时

日常编码时,还有一种比较麻烦的情况。就是需要在已经有缩进层级的代码里,插入多行字符串字面量。因为多行字符串不能包含当前的缩进空格,所以,我们需要把代码写成这样:

def main():
    if user.is_active:
        message = """Welcome, today's movie list:
- Jaw (1975)
- The Shining (1980)
- Saw (2004)"""

但是这样写会破坏整段代码的缩进视觉效果,显得非常突兀。要改善它有很多种办法,比如我们可以把这段多行字符串作为变量提取到模块的最外层。不过,如果在你的代码逻辑里更适合用字面量的话,你也可以用标准库 textwrap 来解决这个问题:

from textwrap import dedent

def main():
    if user.is_active:
        # dedent 将会缩进掉整段文字最左边的空字符串
        message = dedent("""\
            Welcome, today's movie list:
            - Jaw (1975)
            - The Shining (1980)
            - Saw (2004)""")


Reference




  1. 关于强类型语言:说来也比较搞笑,之前有一次面试 Python,面试官竟然问出了“你对于 Python 是一门弱类型语言怎么看?”,我丝毫不怀疑其 C++ 水平(游戏公司招 Python 程序员做“用户活动”的后台服务),但是他真的谈不上懂 Python,如果他真的懂的话,那么那也就能理解强弱类型语言和静动态语言两者的不同。并且会理解为什么静态语言需要“声明”这一行为,而动态语言不需要(似乎 Golang 在这方面有着灵活的方式/技巧,但是我确实不懂 Golang)。而且,熟练使用动态语言的动态特性,就会在不知不觉中领会“鸭子类型”的精髓。 ↩︎

  2. Python 工匠:使用数字与字符串的技巧 ? ↩︎

发布了164 篇原创文章 · 获赞 76 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/qq_29757283/article/details/83689149