<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>渡鸦的随手记</title><description>正于此地，愿你找到需要的知识</description><link>https://blog.ravenote.me/</link><language>zh_CN</language><item><title>群晖NAS部署Tailscale组网及HTTPS配置实战</title><link>https://blog.ravenote.me/posts/synology_tailscale_and_https/</link><guid isPermaLink="true">https://blog.ravenote.me/posts/synology_tailscale_and_https/</guid><description>基于Tailscale为群晖NAS构建安全内网访问，适用于DSM 7.x环境</description><pubDate>Wed, 31 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;为了提升群晖NAS的安全性（特别是EOL型号），或是在没有公网IP的情况下实现远程访问，我们可以选择使用Tailscale进行组网。&lt;/p&gt;
&lt;h2&gt;部署Tailscale&lt;/h2&gt;
&lt;p&gt;对于非国行版本的群晖，可以直接在套件中心搜索并安装Tailscale。对于国行版本的群晖，则需要前往&lt;a href=&quot;https://pkgs.tailscale.com/stable/#spks&quot;&gt;Tailscale Packages&lt;/a&gt;下载对应DSM版本及CPU架构的spk安装包（如果不确定架构，可在&lt;a href=&quot;https://github.com/SynoCommunity/spksrc/wiki/Synology-and-SynoCommunity-Package-Architectures&quot;&gt;Synology and SynoCommunity Package Architectures&lt;/a&gt;查询），随后在套件中心进行手动安装。&lt;/p&gt;
&lt;p&gt;我们可以通过计划任务来进行自动更新。在DSM中打开控制面板，找到计划任务，新增一个用户自定义脚本的计划任务。将执行账号设置为root，计划周期设为每月一次，脚本内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tailscale update --yes
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
根据官方建议，即使是通过套件中心安装的Tailscale也建议通过计划任务进行自动更新&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于DSM7，由于其对软件包行为引入了更严格的限制，导致Tailscale没有创建TUN设备的权限，所以说默认情况下仅允许外部设备连接到NAS，但NAS自己运行的应用程序无法通过Tailscale发起网络连接。如果有需要的话，在DSM中打开控制面板，找到计划任务，新建一个触发任务，事件选择开机，账号设置为root，脚本内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/var/packages/Tailscale/target/bin/tailscale configure-host; synosystemctl restart pkgctl-Tailscale.service
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
确保你正在运行Tailscale v1.22.2或更高版本&lt;/p&gt;
&lt;p&gt;升级Tailscale软件包后需要再次运行上述命令&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;配置HTTPS并自动化&lt;/h2&gt;
&lt;p&gt;进入Tailscale管理面板，在DNS选项卡下找到HTTPS Certificates，点击Enable HTTPS&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
启用此功能前，请确保MagicDNS已经打开&lt;/p&gt;
&lt;p&gt;启用后，机器名称和Tailnet DNS名称将公开可见（不影响安全性）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;然后我们在DSM中打开控制面板，找到终端机和SNMP，在这里开启SSH。&lt;/p&gt;
&lt;p&gt;通过SSH连接上群晖之后，切换到root账户（小小偷个懒），执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tailscale cert [Your Domain]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令会生成两个文件，想办法把它们下载下来（办法有很多，比如将其移动到某个共享文件夹后通过File Station下载，或者使用SCP命令）。&lt;/p&gt;
&lt;p&gt;然后在DSM中打开控制面板，找到安全性，然后切换到证书选项卡，点击新建，根据引导导入证书，其中，描述填&lt;code&gt;Tailscale&lt;/code&gt;，中间证书不用管，留空。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
务必注意这里描述必须是Tailscale，否则后继配置自动化更新后脚本将找不到证书路径。&lt;/p&gt;
&lt;p&gt;自动化更新脚本仅在DSM7下进行过测试。实际使用前建议先在root权限下检查jq命令是否可以找到路径。找不到的情况下，可以参考&lt;code&gt;/usr/syno/etc/certificate/_archive/INFO&lt;/code&gt;的结构调整jq查询语句，或直接通过sha256比对找到固定的证书目录（可使用&lt;code&gt;find /usr/syno/etc/certificate/_archive -type f -name cert.pem -exec sha256sum {} \;&lt;/code&gt;快速列出所有cert.pem的sha256，即crt文件的sha256）。&lt;/p&gt;
&lt;p&gt;如果修改脚本使用固定目录，日后在删除并重新导入证书等情况下路径会变，到时候记得改。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;导入完成后，记得点击设置，将相关服务使用的证书切换为刚刚导入的Tailscale证书。其实这时候HTTPS已经配置好了，但是每当证书过期时，我们都需要重新手动干一遍上面的操作，很是麻烦，尤其是2028年起证书有效期会被调整为45天&lt;/p&gt;
&lt;p&gt;编写如下脚本，记得把&lt;code&gt;[Your Domain]&lt;/code&gt;替换为你的Tailscale完整域名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
DOMAIN=&quot;[Your Domain]&quot;
CERT_DIR=$(jq -r --arg desc &quot;Tailscale&quot; &apos;to_entries[] | select(.value.desc == $desc) | .key&apos; /usr/syno/etc/certificate/_archive/INFO)
CERT_ARCHIVE_PATH=&quot;/usr/syno/etc/certificate/_archive/${CERT_DIR}&quot;

if [ -f &quot;${CERT_ARCHIVE_PATH}/cert.pem&quot; ]; then
    EXPIRY_DATE=$(openssl x509 -enddate -noout -in &quot;${CERT_ARCHIVE_PATH}/cert.pem&quot; | cut -d= -f2)
    EXPIRY_EPOCH=$(date -d &quot;${EXPIRY_DATE}&quot; +%s)
    CURRENT_EPOCH=$(date +%s)
    DAYS_REMAINING=$(( (EXPIRY_EPOCH - CURRENT_EPOCH) / 86400 ))
else
    exit 1
fi

if [ ${DAYS_REMAINING} -gt 14 ]; then
    exit 0
fi

tailscale cert &quot;${DOMAIN}&quot;

if [ ! -f &quot;${DOMAIN}.crt&quot; ]; then
    exit 1
fi

cp &quot;${DOMAIN}.crt&quot; &quot;${CERT_ARCHIVE_PATH}/cert.pem&quot;
mv &quot;${DOMAIN}.key&quot; &quot;${CERT_ARCHIVE_PATH}/privkey.pem&quot;
mv &quot;${DOMAIN}.crt&quot; &quot;${CERT_ARCHIVE_PATH}/fullchain.pem&quot;

chmod 400 &quot;${CERT_ARCHIVE_PATH}/cert.pem&quot;
chmod 400 &quot;${CERT_ARCHIVE_PATH}/privkey.pem&quot;
chmod 400 &quot;${CERT_ARCHIVE_PATH}/fullchain.pem&quot;

chown root:root &quot;${CERT_ARCHIVE_PATH}/cert.pem&quot;
chown root:root &quot;${CERT_ARCHIVE_PATH}/privkey.pem&quot;
chown root:root &quot;${CERT_ARCHIVE_PATH}/fullchain.pem&quot;

synosystemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将以上内容保存为&lt;code&gt;refresh_tailscale_cert.sh&lt;/code&gt;，放到任意一个共享文件夹内，然后在DSM中打开控制面板，找到计划任务，新增一个用户自定义脚本的计划任务。将执行账号设置为root，计划周期设为每周一次，脚本内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /[Your Volume]/[Your Shared Folder]
bash refresh_tailscale_cert.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不知道&lt;code&gt;/[Your Volume]/[Your Shared Folder]&lt;/code&gt;到底应该是什么，可以在DSM中右键你的脚本并查看属性。&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.xfelix.com/2020/04/issue-synology-lets-encrypt-cert-by-acme-sh-docker/&quot;&gt;Issue Synology Let&apos;s Encrypt Cert by acme.sh docker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Python入门到进阶：类与对象，以及面向对象编程</title><link>https://blog.ravenote.me/posts/python_classes_and_oop/</link><guid isPermaLink="true">https://blog.ravenote.me/posts/python_classes_and_oop/</guid><description>介绍Python中类与对象的相关知识，以及面向对象编程的核心概念</description><pubDate>Wed, 18 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;面向对象-序&lt;/h2&gt;
&lt;p&gt;当需求变得复杂时，单纯使用面向过程的思想编程会导致代码可读性和可维护性下降。假设你正在开发一个图书管理程序。最初，你可能会用这种最直接的方式记录书籍信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;book1_title = &quot;Python编程从入门到实践&quot;
book1_author = &quot;埃里克·马瑟斯&quot;
book1_isbn = &quot;978-7115613639&quot;
book1_stock = 5

book2_title = &quot;流畅的Python&quot;
book2_author = &quot;卢西亚诺·拉马略&quot;
book2_isbn = &quot;978-1492056355&quot;
book2_stock = 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很快你发现，每新增一本书，都要重复定义一堆变量。于是你灵机一动，把每本书的数据都抽离出来变成一个列表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;books = [
    [&quot;Python编程从入门到实践&quot;, &quot;埃里克·马瑟斯&quot;, &quot;978-7115613639&quot;, 5],
    [&quot;流畅的Python&quot;, &quot;卢西亚诺·拉马略&quot;, &quot;978-1492056355&quot;, 3]
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这样每次查询书的信息都要依赖&lt;code&gt;books[0][0]&lt;/code&gt;、&lt;code&gt;books[2][3]&lt;/code&gt;等索引，这很不直观，要是单独看这段代码，你完全不知道这些索引都会访问什么数据。所幸，你想起了Python还有个东西叫字典：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;books = [
    {
        &quot;title&quot;: &quot;Python编程从入门到实践&quot;,
        &quot;author&quot;: &quot;埃里克·马瑟斯&quot;,
        &quot;isbn&quot;: &quot;978-7115613639&quot;,
        &quot;stock&quot;: 5,
    },
    {
        &quot;title&quot;: &quot;流畅的Python&quot;,
        &quot;author&quot;: &quot;卢西亚诺·拉马略&quot;,
        &quot;isbn&quot;: &quot;978-1492056355&quot;,
        &quot;stock&quot;: 3,
    },
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好了，这下你感觉已经够用了，于是开始写功能：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def borrow(book: dict) -&amp;gt; None:
    if book[&quot;stock&quot;] &amp;gt; 0:
        book[&quot;stock&quot;] -= 1
        print(f&quot;{book[&apos;title&apos;]} 借阅成功！剩余库存：{book[&apos;stock&apos;]}&quot;)
    else:
        print(&quot;库存不足！&quot;)


def show_info(book: dict) -&amp;gt; None:
    print(
        f&quot;《{book[&apos;title&apos;]}》，作者：{book[&apos;author&apos;]}，ISBN：{book[&apos;isbn&apos;]}，库存：{book[&apos;stock&apos;]}&quot;
    )


def find_book(isbn: str) -&amp;gt; dict | None:
    for book in books:
        if book[&quot;isbn&quot;] == isbn:
            return book
    return None


book = find_book(&quot;978-1492056355&quot;)
if book:
    show_info(book)
    borrow(book)
else:
    print(&quot;未找到该书！&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不错，已经迈出第一步了。但是一个成熟的图书管理系统不仅要管理书籍，还要记录谁借了哪本书，需要追踪每本书的借阅记录，包括借阅时间和归还时间，如果书籍逾期未还，需要计算罚款，需要支持查询某用户的借阅状态。如果继续面向过程，你会发现你得再加个用户字典，用户字典里面还要套一个借过什么书的字典列表，然后书籍字典里面也得套上被谁借走了的字典列表、借阅历史字典列表，代码里会塞满一堆循环查找、条件判断和嵌套操作，牵一发而动全身。这种代码能跑是没错，但是可读性以及可维护性就比较低了。&lt;/p&gt;
&lt;p&gt;这时候你可能会想，要是能把数据和功能捆绑在一起就好了，这时候就需要类出场了。&lt;/p&gt;
&lt;h2&gt;约定&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本文基准Python版本为3.12，所有高于Python3.12的特性都需要注明。&lt;/li&gt;
&lt;li&gt;出于时效性考虑，所有参考资料必须标注年份。&lt;/li&gt;
&lt;li&gt;出于时效性考虑，如果参考资料与当前时间相差超过2年，则必须进行实践验证。&lt;/li&gt;
&lt;li&gt;代码块默认代表&lt;code&gt;main.py&lt;/code&gt;文件的内容。&lt;/li&gt;
&lt;li&gt;以&lt;code&gt;Output:&lt;/code&gt;开头的注释代表该行代码的输出。&lt;/li&gt;
&lt;li&gt;以&lt;code&gt;Raise:&lt;/code&gt;开头的注释代表该行代码将会抛出的异常。&lt;/li&gt;
&lt;li&gt;本文默认读者已经了解基本的Python面向过程编程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;基础概念&lt;/h2&gt;
&lt;p&gt;以防有人还没有接触过这些概念，在正式开始之前，我们先来对一些基本概念做一些讲解。&lt;/p&gt;
&lt;h3&gt;魔术方法&lt;/h3&gt;
&lt;p&gt;所有被双下划线所包围的方法都是魔术方法，如最常见的&lt;code&gt;__init__()&lt;/code&gt;。这里先不展开讲魔术方法，等到后继章节再讲。需要注意的是，永远不要用以双下画线开头和结尾的名字定义你自己的方法，虽然现在可能没有特殊的用途，但不保证以后没有。&lt;/p&gt;
&lt;h3&gt;装饰器&lt;/h3&gt;
&lt;p&gt;这个也在后继章节讲。简而言之，装饰器可以用来修改函数或方法的功能，一般形式为&lt;code&gt;@xxxx&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;命名空间&lt;/h3&gt;
&lt;p&gt;假设我们要找一个叫&quot;张伟&quot;的朋友，但是全国一共有294282个张伟（2019），我们怎么确定哪个张伟才是我们要找的人？所以说除了名称以外，我们还需要用更多信息去确定这个人——比如住在哪个城市、在哪家公司工作等。&lt;/p&gt;
&lt;p&gt;类似地，如果在多个地方定义了同名的方法或变量，Python解释器并不能确定你要调用的是哪一个。因此，命名空间这一概念应运而生。命名空间给名称分组，相同的名称在不同命名空间中指向不同对象，这样写代码时只需要在方法名或变量名前加上模块名，Python解释器就知道具体要调用哪一个了。&lt;/p&gt;
&lt;p&gt;我们再举一个计算机系统中的例子，一个文件夹(目录)中可以包含多个文件夹，每个文件夹中不能有相同的文件名，但不同文件夹中的文件可以重名。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/1.png&quot; alt=&quot;python_classes_and_oop_1&quot; /&gt;
（图源 菜鸟教程）&lt;/p&gt;
&lt;p&gt;以下为代码示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# File: module1.py
def echo():
    return &quot;来自模块1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# File: module2.py  
def echo():
    return &quot;来自模块2&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# File: main.py
import module1
import module2
print(module1.echo())  # Output: 来自模块1
print(module2.echo())  # Output: 来自模块2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Python中，我们通过&lt;code&gt;.&lt;/code&gt;操作符来访问对象的属性/方法。例如，在&lt;code&gt;module1.echo()&lt;/code&gt;中，&lt;code&gt;module1&lt;/code&gt;是一个模块对象，&lt;code&gt;echo&lt;/code&gt;是它的一个方法，通过&lt;code&gt;.&lt;/code&gt;调用。通过&lt;code&gt;module1.echo()&lt;/code&gt;和&lt;code&gt;module2.echo()&lt;/code&gt;，我们明确的告诉了Python解释器要调用哪个模块中的方法。&lt;/p&gt;
&lt;h3&gt;作用域与闭包&lt;/h3&gt;
&lt;p&gt;现在我们来讲讲Python的作用域。顾名思义，作用域是指名称能够起作用的区域，也就是”在哪里可以使用这个名称“。Python的作用域遵循LEGB规则，按以下顺序查找变量：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Local&lt;/strong&gt;：当前函数的局部作用域。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enclosing&lt;/strong&gt;：包含当前函数的外部函数的作用域（如果有嵌套函数）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global&lt;/strong&gt;：当前模块的全局作用域。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Builtin&lt;/strong&gt;：Python内置的作用域。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;更直观的可以直接看图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2.png&quot; alt=&quot;python_classes_and_oop_2&quot; /&gt;
（图源 菜鸟教程）&lt;/p&gt;
&lt;p&gt;查找过程从内层到外层单向逐级进行，内层同名变量会遮蔽外层变量。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x = &quot;全局变量&quot;
def main():
    x = &quot;局部变量&quot;
    print(x)

main()  # Output: 局部变量
print(x)  # Output: 全局变量
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每次调用函数时，都会创建其自身的局部作用域。传递给函数的参数会被复制到函数的形参中，这些形参仅能在函数的局部作用域内访问。函数完成执行后，由于垃圾回收机制，局部变量会从内存中释放：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def add(a, b):
    print(f&quot;函数内部: a = {a}&quot;)
    return a + b


result1 = add(2, 3)  # Output: 函数内部: a = 2
result2 = add(5, 7)  # Output: 函数内部: a = 5

print(a)  # Raise: NameError: name &apos;a&apos; is not defined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以可以说作用域具有一种“封闭性”。相信各位可能就已经意识到了，命名空间就是对作用域的一种抽象，利用作用域将名称“封在里面”的特性，实现分组的目的。&lt;/p&gt;
&lt;p&gt;当在一个函数内部定义另一个函数时，内部函数属于外部函数的局部作用域：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def outer():
    print(&quot;外部函数执行&quot;)
    
    def inner():
        print(&quot;内部函数执行&quot;)
    
    inner()

outer()
inner()  # Raise: NameError: name &apos;inner&apos; is not defined.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在外部函数的局部作用域中调用是没有问题的，但我们试图从全局作用域调用这个内部函数就会抛出一个&lt;code&gt;NameError&lt;/code&gt;，因为&lt;code&gt;inner&lt;/code&gt;未在全局作用域中定义。&lt;/p&gt;
&lt;p&gt;不过，有一种方法可以在局部作用域之外访问这个函数。我们可以返回内部函数对象而不是调用它，然后将返回的函数分配给一个变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def outer():
    def inner():
        return &quot;内部函数被调用了&quot;
    
    return inner

func = outer()
print(func())   # Output: 内部函数被调用了
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数&lt;code&gt;outer&lt;/code&gt;已经执行完毕，其局部作用域理应被销毁，为什么函数&lt;code&gt;inner&lt;/code&gt;还能正常工作？因为只要还有对该对象的引用，Python解释器就会将其保留在内存中。&lt;/p&gt;
&lt;p&gt;那如果内部函数引用到了外部函数作用域内的变量，会怎么样？下面是一个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def power(exponent):
    def inner(base):
        return base ** exponent
    return inner

power_of_two = power(2)
power_of_three = power(3)

print(power_of_two(5))  # Output: 25
print(power_of_three(5))  # Output: 125
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;理论上，外层函数&lt;code&gt;power&lt;/code&gt;的局部作用域在函数执行完毕后应该销毁，其中的局部变量（如&lt;code&gt;exponent&lt;/code&gt;）也应该随之消失。然而，实际运行时，内层函数&lt;code&gt;power_of_two&lt;/code&gt;仍能访问&lt;code&gt;exponent&lt;/code&gt;，且结果正确。这是因为Python为这种情况做了特殊处理。不在函数的局部作用域中定义，而在其外部作用域中定义的变量被称为非局部变量。如果内部函数引用了外部函数的变量，Python会将这些自由变量（在函数中使用但不在该函数内定义的变量）保存在一个特殊的属性&lt;code&gt;__closure__&lt;/code&gt;中，这些变量被称为自由非局部变量（如例子中的&lt;code&gt;exponent&lt;/code&gt;）。这样即使在包含它的函数的局部作用域被销毁，它仍保留在内存中。这种会携带其所在的环境的函数被称为闭包。&lt;/p&gt;
&lt;p&gt;虽然闭包中可以读取到非局部变量，但试图直接赋值只会创建局部变量，不会修改非局部变量。这时候就需要用到&lt;code&gt;nonlocal&lt;/code&gt;了。它的作用就是告诉Python解释器，某个变量引用的是最近的非局部作用域中已经绑定的变量，而不是想要创建一个新的局部变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def main():
    x = 1
    def a():
        nonlocal x
        x = 2
    def b():
        x += 3  # Raise: UnboundLocalError: cannot access local variable &apos;x&apos; where it is not associated with a value
    print(x)  # Output: 1
    a()
    print(x)  # Output: 2
    b()
    print(x)

main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有人可能会疑惑，为什么&lt;code&gt;x += 3&lt;/code&gt;这种操作会抛出&lt;code&gt;UnboundLocalError&lt;/code&gt;，这是因为&lt;code&gt;x += 3&lt;/code&gt;等价于&lt;code&gt;x = x + 3&lt;/code&gt;，Python首先需要读取x的值才能进行后继运算，但由于直接赋值只会创建局部变量，所以x被标记为局部变量，而作为局部变量的x并没有被赋值，最后就会抛出&lt;code&gt;UnboundLocalError&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当然，不涉及变量的重绑定（如操作可变对象）的话就可以直接用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def main():
    x = []

    def a():
        x.append(1)

    print(x)  # Output: []
    a()
    print(x)  # Output: [1]


main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想要修改全局作用域的变量，我们可以用&lt;code&gt;global&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x = 1
def a():
    global x
    x = 2

print(x)  # Output: 1
a()
print(x)  # Output: 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;初探类与对象&lt;/h2&gt;
&lt;p&gt;面向对象编程的核心在于抽象——将现实世界的事物提炼为对象，以对象为基本单元封装数据和行为，实现程序功能。&lt;/p&gt;
&lt;p&gt;想象一下我们日常生活中与各种物品的交互方式。看电视只需要用遥控器控制开关、音量和频道，完全不需要理解内部电路如何工作。面向对象编程也是如此，将相关的数据和操作这些数据的方法打包在一起成为类。调用者只需要知道提供了哪些接口，而不需要关心内部实现细节。&lt;/p&gt;
&lt;p&gt;我们使用这些类来创建对象，这被称为实例化，被创建出来的对象被称为类的实例。很多语言中的类都被比作蓝图。而在Python中，比起蓝图这个比喻，类更像一个工厂，实例则是工厂生产的产品。通常情况下，除非有特殊需求，我们使用的是这些产品（实例），而非工厂（类）本身。&lt;/p&gt;
&lt;p&gt;在Python中，我们使用&lt;code&gt;class&lt;/code&gt;关键字来定义一个类。比如我们定义一个类&lt;code&gt;Book&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Book:
    pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要实例化这个类，我们只需调用它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my_book = Book()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将创建类&lt;code&gt;Book&lt;/code&gt;的新实例并将这个实例对象分配给局部变量&lt;code&gt;my_book&lt;/code&gt;。我们可以用魔术方法&lt;code&gt;__class__&lt;/code&gt;来查询实例所属的类，通过魔术方法&lt;code&gt;__name__&lt;/code&gt;来查询所属类的名称。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(x.__class__)  # Output: &amp;lt;class &apos;__main__.Book&apos;&amp;gt;
print(x.__class__.__name__)  # Output: Book
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个空荡荡的工厂没什么用，我们需要它能生产出带有具体信息的产品。比如，每本书都应该有自己标题、作者和isbn号：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Book:
    def __init__(self, title, author, isbn, stock):
        self.title = title
        self.author = author
        self.isbn = isbn
        self.stock = stock

python_crash_course = Book(&quot;Python编程从入门到实践&quot;, &quot;埃里克·马瑟斯&quot;, &quot;978-7115613639&quot;, 5)
fluent_python = Book(&quot;流畅的Python&quot;, &quot;卢西亚诺·拉马略&quot;, &quot;978-1492056355&quot;, 3)

print(python_crash_course.title)  # Output: Python编程从入门到实践
print(fluent_python.title)  # Output: 流畅的Python
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;__init__()&lt;/code&gt;是一个在创建新实例时被自动调用的魔术方法，免去了手动调用的麻烦。&lt;/p&gt;
&lt;p&gt;每个实例都拥有自己独立的数据副本。&lt;code&gt;python_crash_course&lt;/code&gt;的&lt;code&gt;title&lt;/code&gt;和&lt;code&gt;fluent_python&lt;/code&gt;的&lt;code&gt;title&lt;/code&gt;是互不影响的。这些与特定实例绑定的属性，我们称之为实例属性。通常情况下，我们可以使用&lt;code&gt;__dict__&lt;/code&gt;查看一个由开发者自定义类创建的实例存储的所有属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(python_crash_course.__dict__)  # Output: {&apos;title&apos;: &apos;Python编程从入门到实践&apos;, &apos;author&apos;: &apos;埃里克·马瑟斯&apos;, &apos;isbn&apos;: &apos;978-7115613639&apos;, &apos;stock&apos;: 5}
print(fluent_python.__dict__)  # Output: {&apos;title&apos;: &apos;流畅的Python&apos;, &apos;author&apos;: &apos;卢西亚诺·拉马略&apos;, &apos;isbn&apos;: &apos;978-1492056355&apos;, &apos;stock&apos;: 3}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在类中定义的函数，我们称之为方法。当一个方法需要访问或修改实例自身的属性时，它就应该是一个实例方法。实例方法的第一个参数必须用来接收实例对象本身，按照约定，这个参数应该叫&lt;code&gt;self&lt;/code&gt;。让我们给类Book添加一个显示书籍信息的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Book:
    def __init__(self, title, author, isbn, stock):
        self.title = title
        self.author = author
        self.isbn = isbn
        self.stock = stock

    def get_book_info(self):
        return f&quot;《{self.title}》，作者：{self.author}（ISBN：{self.isbn}），库存：{self.stock}本&quot;


fluent_python = Book(&quot;流畅的Python&quot;, &quot;卢西亚诺·拉马略&quot;, &quot;978-1492056355&quot;, 3)
print(fluent_python.get_book_info())  # Output: 《流畅的Python》，作者：卢西亚诺·拉马略（ISBN：978-1492056355），库存：3本
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有时候，某些属性或行为是属于整个类的，而不是某个特定实例。例如，一个图书馆需要知道馆藏图书一共有多少种，这个信息不属于任何一本书，而是属于整个&lt;code&gt;Book&lt;/code&gt;这个类。这种在所有实例之间共享的属性，就是类属性。&lt;/p&gt;
&lt;p&gt;与此对应，如果一个方法操作的是类属性而不是实例属性，我们通常会将其定义为类方法。类方法使用&lt;code&gt;@classmethod&lt;/code&gt;装饰器来标识，并且它的第一个参数必须用来接收类对象本身，按照约定，这个参数应该叫&lt;code&gt;cls&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Book:
    _all_isbns = set()

    def __init__(self, title, author, isbn, stock):
        self.title = title
        self.author = author
        self.isbn = isbn
        self.stock = stock
        self._add_isbn(isbn)

    def get_book_info(self):
        return f&quot;《{self.title}》，作者：{self.author}，库存：{self.stock}本&quot;

    @classmethod
    def get_store_info(cls):
        return f&quot;欢迎来到图书馆！当前馆藏图书共{len(cls._all_isbns)}种。&quot;

    @classmethod
    def _add_isbn(cls, isbn: str):
        cls._all_isbns.add(isbn)

python_crash_course = Book(&quot;Python编程从入门到实践&quot;, &quot;埃里克·马瑟斯&quot;, &quot;978-7115613639&quot;, 5)
fluent_python = Book(&quot;流畅的Python&quot;, &quot;卢西亚诺·拉马略&quot;, &quot;978-1492056355&quot;, 3)


print(Book.get_store_info())  # Output: 欢迎来到图书馆！当前馆藏图书共2种。
print(fluent_python.get_store_info())  # Output: 欢迎来到图书馆！当前馆藏图书共2种。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以通过类直接调用类方法。当然，通过实例调用也行，效果一样，但不推荐。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;self&lt;/code&gt;和&lt;code&gt;cls&lt;/code&gt;只是一种约定俗成的命名，关键在于它们必须是第一个参数。只要保证位置是在第一个无论叫什么都是可以运行的，比如你哪一天突发奇想把&lt;code&gt;self&lt;/code&gt;和&lt;code&gt;cls&lt;/code&gt;反过来用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Book:
    _all_isbns = set()

    def __init__(cls, title, author, isbn, stock):
        cls.title = title
        cls.author = author
        cls.isbn = isbn
        cls.stock = stock
        cls._add_isbn(isbn)

    @classmethod
    def get_store_info(self):
        return f&quot;欢迎来到图书馆！当前馆藏图书共{len(self._all_isbns)}种。&quot;

    @classmethod
    def _add_isbn(self, isbn: str):
        self._all_isbns.add(isbn)

    def get_book_info(cls):
        return f&quot;《{cls.title}》，作者：{cls.author}（ISBN：{cls.isbn}），库存：{cls.stock}本&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那确实也能跑，不过项目一旦要被其他人接手那你得更能跑。所谓“约定俗成”的意义就在于下一个接手代码的人不至于需要反应一下才能明白这参数是干啥的。&lt;/p&gt;
&lt;p&gt;当一个方法既不需要用实例属性也不需要用类属性，但功能上与这个类相关时，这时候应该使用静态方法。比如我们添加一个验证isbn有效性的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Book:
    _all_isbns = set()

    def __init__(self, title, author, isbn, stock):
        if not self._is_valid_isbn(isbn):
            raise ValueError(&quot;ISBN无效&quot;)
        self.title = title
        self.author = author
        self.isbn = isbn
        self.stock = stock
        self._add_isbn(isbn)

    def get_book_info(self):
        return f&quot;《{self.title}》，作者：{self.author}，库存：{self.stock}本&quot;

    @classmethod
    def get_store_info(cls):
        return f&quot;欢迎来到图书馆！当前馆藏图书共{len(cls._all_isbns)}种。&quot;

    @classmethod
    def _add_isbn(cls, isbn: str):
        cls._all_isbns.add(isbn)

    @staticmethod
    def _is_valid_isbn(isbn: str) -&amp;gt; bool:
        clean = isbn.replace(&quot;-&quot;, &quot;&quot;).replace(&quot; &quot;, &quot;&quot;)
        if len(clean) != 13 or not clean.isdigit():
            return False

        total = sum(int(d) * (1 if i % 2 == 0 else 3) for i, d in enumerate(clean))
        return total % 10 == 0

fluent_python = Book(&quot;流畅的Python&quot;, &quot;卢西亚诺·拉马略&quot;, &quot;123-4567890123&quot;, 3)  # Raise: ValueError: ISBN无效
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类属性可以通过类方法赋值，也可以直接通过类赋值，但是不能在实例上赋值。实例上可以读取类属性，但是试图赋值就会创建一个新的实例属性而不是修改类属性(操作可变对象除外)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Book:
    _all_isbns = set()
    test = 1

    def __init__(self, title, author, isbn, stock):
        self.title = title
        self.author = author
        self.isbn = isbn
        self.stock = stock
        self._add_isbn(isbn)

    @classmethod
    def _add_isbn(cls, isbn: str):
        cls._all_isbns.add(isbn)

python_crash_course = Book(&quot;Python编程从入门到实践&quot;, &quot;埃里克·马瑟斯&quot;, &quot;978-7115613639&quot;, 5)
print(Book.__dict__) # {&apos;__module__&apos;: &apos;__main__&apos;, &apos;_all_isbns&apos;: {&apos;978-7115613639&apos;}, &apos;test&apos;: 1, &apos;__init__&apos;: &amp;lt;function Book.__init__ at 0x0000020EF37AD1C0&amp;gt;, &apos;_add_isbn&apos;: &amp;lt;classmethod(&amp;lt;function Book._add_isbn at 0x0000020EF37AD260&amp;gt;)&amp;gt;, &apos;__dict__&apos;: &amp;lt;attribute &apos;__dict__&apos; of &apos;Book&apos; objects&amp;gt;, &apos;__weakref__&apos;: &amp;lt;attribute &apos;__weakref__&apos; of &apos;Book&apos; objects&amp;gt;, &apos;__doc__&apos;: None}
print(python_crash_course.__dict__) # {&apos;title&apos;: &apos;Python编程从入门到实践&apos;, &apos;author&apos;: &apos;埃里克·马瑟斯&apos;, &apos;isbn&apos;: &apos;978-7115613639&apos;, &apos;stock&apos;: 5}
python_crash_course._all_isbns.add(&quot;978-1492056355&quot;)
python_crash_course.test = 123
print(Book.__dict__) # {&apos;__module__&apos;: &apos;__main__&apos;, &apos;_all_isbns&apos;: {&apos;978-7115613639&apos;, &apos;978-1492056355&apos;}, &apos;test&apos;: 1, &apos;__init__&apos;: &amp;lt;function Book.__init__ at 0x0000020EF37AD1C0&amp;gt;, &apos;_add_isbn&apos;: &amp;lt;classmethod(&amp;lt;function Book._add_isbn at 0x0000020EF37AD260&amp;gt;)&amp;gt;, &apos;__dict__&apos;: &amp;lt;attribute &apos;__dict__&apos; of &apos;Book&apos; objects&amp;gt;, &apos;__weakref__&apos;: &amp;lt;attribute &apos;__weakref__&apos; of &apos;Book&apos; objects&amp;gt;, &apos;__doc__&apos;: None}
print(python_crash_course.__dict__) # {&apos;title&apos;: &apos;Python编程从入门到实践&apos;, &apos;author&apos;: &apos;埃里克·马瑟斯&apos;, &apos;isbn&apos;: &apos;978-7115613639&apos;, &apos;stock&apos;: 5, &apos;test&apos;: 123}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;通过类继承实现复用&lt;/h3&gt;
&lt;p&gt;在编写类时，并非总是要从头开始。如果要编写的类是一个既有的类的特殊版本，可使用继承。当一个类继承另一个类时，将自动获得后者的所有属性和方法。原有的类被称为父类，而新类被称为子类。子类不仅继承了父类的所有属性和方法，还可定义自己的属性和方法。&lt;/p&gt;
&lt;p&gt;比如现在有一个交通工具类，我们想要再写一个汽车类，这时候就可以继承。继承语法非常直接，在定义类时把基类名称放括号里面就行了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Vehicle:
    def __init__(self, brand):
        self.brand = brand

    def drive(self):
        print(&quot;交通工具正在行驶&quot;)

class Car(Vehicle):
    def __init__(self, brand):
        super().__init__(brand)

    def honk(self):
        print(&quot;嘀嘀&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;super()&lt;/code&gt;可以让我们能够调用父类的方法。这行代码调用&lt;code&gt;Vehicle&lt;/code&gt;类的&lt;code&gt;__init__()&lt;/code&gt;方法，从而让&lt;code&gt;Car&lt;/code&gt;实例包含这个方法定义的所有属性。&lt;/p&gt;
&lt;p&gt;子类可以提供一个与父类方法同名的新实现，这个过程称为重写。这允许子类根据自身的特性，定制化地实现继承来的行为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Car(Vehicle):
    def __init__(self, brand):
        super().__init__(brand)

    def drive(self):
        print(f&quot;{self.brand}汽车正在行驶&quot;)

    def honk(self):
        print(&quot;嘀嘀&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设我们不小心把&lt;code&gt;drive&lt;/code&gt;写成了&lt;code&gt;driv&lt;/code&gt;，即一个父类中不存在的方法，那我们试图调用子类的&lt;code&gt;drive&lt;/code&gt;时，实际上会直接调用父类的&lt;code&gt;drive&lt;/code&gt;。如果想要让类型检查器发现这个错误，可以使用&lt;code&gt;@override&lt;/code&gt;注解来明确表示方法的覆盖关系：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from typing import override

class Vehicle:
    def __init__(self, brand):
        self.brand = brand

    def drive(self):
        print(&quot;交通工具正在行驶&quot;)


class Car(Vehicle):
    def __init__(self, brand):
        super().__init__(brand)

    @override
    def driv(self):
        print(f&quot;{self.brand}汽车正在行驶&quot;)

    def honk(self):
        print(&quot;嘀嘀&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Python允许一个类同时继承多个父类。比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Radio:
    def play_music(self, name):
        print(f&quot;收音机正在播放：{name}&quot;)

class MusicCar(Car, Radio):
    pass

my_music_car = MusicCar(&quot;不知名&quot;)
my_music_car.drive()  # 来自 Car
my_music_car.play_music(&quot;大鱼 - 周深&quot;)  # 来自 Radio
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当一个类有多个父类，并且这些父类可能拥有同名方法时，Python会遵循一个明确且可预测的顺序来查找方法（包括&lt;code&gt;super()&lt;/code&gt;）。这个顺序就是MRO。一般来说我们不需要了解Python是怎么确认顺序的，我们可以用&lt;code&gt;__mro__&lt;/code&gt;来直接查看排序结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(MusicCar.__mro__) # Output: (&amp;lt;class &apos;__main__.MusicCar&apos;&amp;gt;, &amp;lt;class &apos;__main__.Car&apos;&amp;gt;, &amp;lt;class &apos;__main__.Vehicle&apos;&amp;gt;, &amp;lt;class &apos;__main__.Radio&apos;&amp;gt;, &amp;lt;class &apos;object&apos;&amp;gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从执行结果来看，这个顺序是MusicCar-&amp;gt;Car-&amp;gt;Vehicle-&amp;gt;Radio-&amp;gt;object&lt;/p&gt;
&lt;p&gt;我们可以使用&lt;code&gt;isinstance()&lt;/code&gt;判断一个对象是否是某个类或其子类的实例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Vehicle:
    def __init__(self, brand):
        self.brand = brand

    def drive(self):
        print(&quot;交通工具正在行驶&quot;)


class Car(Vehicle):
    def __init__(self, brand):
        super().__init__(brand)

    def drive(self):
        print(f&quot;{self.brand}汽车正在行驶&quot;)


class Bike(Vehicle):
    def __init__(self, brand):
        super().__init__(brand)

    def drive(self):
        print(f&quot;{self.brand}自行车正在行驶&quot;)

my_bike = Bike(&quot;不知名&quot;)
my_car = Car(&quot;不知名&quot;)

print(isinstance(my_bike, Bike))  # Output: True
print(isinstance(my_bike, Vehicle))  # Output: True
print(isinstance(my_car, Vehicle))  # Output: True
print(isinstance(my_car, Bike))  # Output: False
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;我们真的需要继承吗？&lt;/h3&gt;
&lt;p&gt;继承建立的是一种“属于”的关系，例如汽车属于交通工具。然而我们会遇到“包含”的关系，比如前例中&lt;code&gt;MusicCar&lt;/code&gt;包含一个收音机。实际上这时候与其继承，不如直接把其他类的实例用作属性来进行一个组装。下面是一个具体的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Radio:
    def play_music(self, name):
        print(f&quot;收音机正在播放：{name}&quot;)

class Car:
    def __init__(self, brand):
        self.brand = brand
        self.radio = Radio()

    def drive(self):
        print(f&quot;{self.brand}汽车正在行驶&quot;)

    def play_music(self, name):
        self.radio.play_music(name)

my_car = Car(&quot;不知名&quot;)
my_car.drive()  # Output: 不知名汽车正在行驶
my_car.play_music(&quot;大鱼 - 周深&quot;) # Output: 收音机正在播放：大鱼 - 周深
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际应用中到底是用继承还是组合就看这两个类之间是否有明确的从属关系。如果没有，那最好用组合的思路。&lt;/p&gt;
&lt;h2&gt;定制类的行为&lt;/h2&gt;
&lt;p&gt;有的时候我们需要自定义类的行为，这时候我们可以利用魔术方法（绝大多数情况）或元类（极少数情况）来实现我们的想法。&lt;/p&gt;
&lt;h3&gt;利用魔术方法&lt;/h3&gt;
&lt;h4&gt;通过__new__自定义实例化流程&lt;/h4&gt;
&lt;p&gt;我们最常用的魔术方法是&lt;code&gt;__init__()&lt;/code&gt;。但&lt;code&gt;__init__()&lt;/code&gt;并不是第一个被调用的，在它前面还有&lt;code&gt;__new__()&lt;/code&gt;。默认情况下，&lt;code&gt;__new__()&lt;/code&gt;函数创建实例并将其作为第一个参数传递给&lt;code&gt;__init__()&lt;/code&gt;函数，&lt;code&gt;__init__()&lt;/code&gt;函数接收这个新实例并直接返回它。通过重写&lt;code&gt;__init__()&lt;/code&gt;我们可以实现自动为新创建的实例添加属性，而通过重写&lt;code&gt;__new__()&lt;/code&gt;我们可以自定义实例创建流程。比如我们需要实现单例模式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A:
    _instance = None
    _initialized = False
    
    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, test):
        if not self._initialized:
            self._initialized = True
            self.test = test

a1 = A(test=10)
a2 = A(test=20)

print(a1 is a2)  # Output: True
print(a2.test)  # Output: 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于我们重写了&lt;code&gt;__new__()&lt;/code&gt;，所以说我们必须显式调用父类（默认是&lt;code&gt;object&lt;/code&gt;，这是所有类的父类）的实例化逻辑（即&lt;code&gt;super().__new__(cls)&lt;/code&gt;）。而且我们必须返回一个实例，不然后继逻辑将不会执行，类的实例化将会被直接中断。如果我们在这里返回了另一个类的实例，那么后继流程使用的就是另一个类的实例。&lt;/p&gt;
&lt;p&gt;此外，由于每次实例化都会调用&lt;code&gt;__init__()&lt;/code&gt;，所以说我们需要限制&lt;code&gt;__init__()&lt;/code&gt;的执行次数，否则后续的实例化会覆盖已存在单例的状态——除非这是预期行为。&lt;/p&gt;
&lt;p&gt;还有一个和实例生命周期有关的魔术方法是&lt;code&gt;__del__()&lt;/code&gt;。这个我不推荐自定义，Python官方文档也不建议用，我也不会讲。与其自定义这个不如写一个&lt;code&gt;close()&lt;/code&gt;方法。&lt;/p&gt;
&lt;h4&gt;自定义对象的字符串表示&lt;/h4&gt;
&lt;p&gt;当我们&lt;code&gt;print&lt;/code&gt;我们定义的对象时，我们会发现显示的内容是类似于&lt;code&gt;&amp;lt;__main__.Book object at 0x00000230AA7EA060&amp;gt;&lt;/code&gt;的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Book:
    def __init__(self, title, author, isbn, stock):
        self.title = title
        self.author = author
        self.isbn = isbn
        self.stock = stock

fluent_python = Book(&quot;流畅的Python&quot;, &quot;卢西亚诺·拉马略&quot;, &quot;978-1492056355&quot;, 3)
print(fluent_python)  # Output: &amp;lt;__main__.Book object at 0x00000230AA7EA060&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想要返回一个易于阅读的对象字符串表示形式，似乎我们只能定义一个&lt;code&gt;get_book_info&lt;/code&gt;方法——真的是这样吗？&lt;/p&gt;
&lt;p&gt;我们可以通过魔术方法&lt;code&gt;__str__()&lt;/code&gt;来定义对象的字符串表示形式:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Book:
    def __init__(self, title, author, isbn, stock):
        self.title = title
        self.author = author
        self.isbn = isbn
        self.stock = stock
    
    def __str__(self):
        return f&quot;《{self.title}》 by {self.author} (ISBN: {self.isbn}) - 库存: {self.stock}&quot;

fluent_python = Book(&quot;流畅的Python&quot;, &quot;卢西亚诺·拉马略&quot;, &quot;978-1492056355&quot;, 3)
print(fluent_python)  # Output: 《流畅的Python》 by 卢西亚诺·拉马略 (ISBN: 978-1492056355) - 库存: 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在看起来是不是更简单易懂了？但是这样显示有一个问题，我们没办法通过这个结果知道类都有什么变量，都有什么值，简而言之，就是不够正式。这时候我们可以定义一个&lt;code&gt;__repr__()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Book:
    def __init__(self, title, author, isbn, stock):
        self.title = title
        self.author = author
        self.isbn = isbn
        self.stock = stock

    def __str__(self):
        return f&quot;《{self.title}》 by {self.author} (ISBN: {self.isbn}) - 库存: {self.stock}&quot;
    
    def __repr__(self):
        return f&quot;Book(title={self.title}, author={self.author}, isbn={self.isbn}, stock={self.stock})&quot;

fluent_python = Book(&quot;流畅的Python&quot;, &quot;卢西亚诺·拉马略&quot;, &quot;978-1492056355&quot;, 3)
print(fluent_python)  # 《流畅的Python》 by 卢西亚诺·拉马略 (ISBN: 978-1492056355) - 库存: 3
print(f&quot;{fluent_python!s}&quot;)  # 《流畅的Python》 by 卢西亚诺·拉马略 (ISBN: 978-1492056355) - 库存: 3
print(f&quot;{fluent_python!r}&quot;)  # Book(title=流畅的Python, author=卢西亚诺·拉马略, isbn=978-1492056355, stock=3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当没有指定的时候，打印出来的是&lt;code&gt;__str__&lt;/code&gt;所定义的内容。当我们显式要求正式输出时，打印出来的是&lt;code&gt;__repr__&lt;/code&gt;所定义的内容。&lt;/p&gt;
&lt;h4&gt;使用魔术方法实现比较&lt;/h4&gt;
&lt;p&gt;以下是用于实现比较的魔术方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;__eq__(self, other):&lt;/code&gt;：定义等号行为 (==)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__ne__(self, other):&lt;/code&gt;：定义不等号行为 (!=)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__lt__(self, other):&lt;/code&gt;：定义小于号行为 (&amp;lt;)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__le__(self, other):&lt;/code&gt;：定义小于等于号行为 (&amp;lt;=)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__gt__(self, other):&lt;/code&gt;：定义大于号行为 (&amp;gt;)。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__ge__(self, other):&lt;/code&gt;：定义大于等于号行为 (&amp;gt;=)。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举一个例子，创建一个类来表现一个词语。我们也许会想要比较单词的字典序(通过字母表)，通过默认的字符串比较的方法就可以实现，但是我们也想要通过一些其他的标准来实现，比如单词长度或者音节数量。在这个例子中，我们来比较长度实现。以下是实现代码:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Word(str):

    def __new__(cls, word):
        # 注意我们必须要用到__new__方法，因为str是不可变类型
        # 所以我们必须在创建的时候将它初始化
        if &apos; &apos; in word:
            print(&quot;Value contains spaces. Truncating to first space.&quot;)
            word = word[:word.index(&apos; &apos;)] #单词是第一个空格之前的所有字符
        return str.__new__(cls, word)

    def __gt__(self, other):
        return len(self) &amp;gt; len(other)
    def __lt__(self, other):
        return len(self) &amp;lt; len(other)
    def __ge__(self, other):
        return len(self) &amp;gt;= len(other)
    def __le__(self, other):
        return len(self) &amp;lt;= len(other)

word1 = Word(&apos;hello&apos;)
word2 = Word(&apos;world&apos;)
word3 = Word(&apos;python&apos;)

print(word1 &amp;gt; word2)  # Output: False
print(word1 &amp;lt; word3)  # Output: True
print(word2 &amp;gt;= word1) # Output: True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，我们没有定义&lt;code&gt;__eq__()&lt;/code&gt;和&lt;code&gt;__ne__()&lt;/code&gt;方法。这是因为将会产生一些怪异的结果（比如 Word(&apos;foo&apos;) == Word(&apos;bar&apos;) 将会返回true）。所以我们退回去，用&lt;code&gt;str&lt;/code&gt;内置来进行比较。&lt;/p&gt;
&lt;h4&gt;上下文管理器&lt;/h4&gt;
&lt;p&gt;上下文管理器可以确保资源在使用完毕后被正确清理。我们可以通过&lt;code&gt;__enter__()&lt;/code&gt;和&lt;code&gt;__exit__()&lt;/code&gt;这两个魔术方法来实现上下文管理器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class DatabaseConnection:
    def __init__(self, database_name):
        self.database_name = database_name
        self.connection = None
    
    def __enter__(self):
        print(f&quot;连接到数据库: {self.database_name}&quot;)
        self.connection = self.database_name
        return self.connection
    
    def __exit__(self, exc_type, exc_value, traceback):
        print(f&quot;关闭数据库连接: {self.database_name}&quot;)

with DatabaseConnection(&quot;用户数据库&quot;) as conn:
    print(f&quot;使用连接: {conn}&quot;)
    print(&quot;执行数据库操作...&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;with&lt;/code&gt;会在进入代码块时执行&lt;code&gt;__enter__()&lt;/code&gt;中定义的操作，并把&lt;code&gt;__enter__()&lt;/code&gt;的返回值绑定到&lt;code&gt;as&lt;/code&gt;子句中指定的目标（如果有的话）。在离开代码块时会执行&lt;code&gt;__exit__&lt;/code&gt;中定义的操作。&lt;/p&gt;
&lt;p&gt;如果是异步，换成&lt;code&gt;__aenter__()&lt;/code&gt;和&lt;code&gt;__aexit__()&lt;/code&gt;即可。&lt;/p&gt;
&lt;h4&gt;使用__slots__优化属性访问&lt;/h4&gt;
&lt;p&gt;默认情况下，Python对象使用字典来存储属性。优点是可以动态添加属性，缺点是占用内存可能较多并且查找操作相对较慢。这时候我们可以定义&lt;code&gt;__slots__&lt;/code&gt;，这将阻止类创建&lt;code&gt;__dict__&lt;/code&gt;和&lt;code&gt;__wekref__&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;要使用&lt;code&gt;__slots__&lt;/code&gt;，你需要在类中定义一个名为&lt;code&gt;__slots__&lt;/code&gt;的属性，并将允许的属性名提供给它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Book:
    __slots__ = (&quot;title&quot;, &quot;author&quot;, &quot;isbn&quot;, &quot;stock&quot;)

    def __init__(self, title, author, isbn, stock):
        self.title = title
        self.author = author
        self.isbn = isbn
        self.stock = stock
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，因为没有了&lt;code&gt;__wekref__&lt;/code&gt;，这个类将不支持弱引用。如果需要弱引用支持，需要在&lt;code&gt;__slots__&lt;/code&gt;中手动声明&lt;code&gt;__wekref__&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当涉及继承时，如果父类定义了&lt;code&gt;__slots__&lt;/code&gt;，子类也必须定义&lt;code&gt;__slots__&lt;/code&gt;，否则子类实例将会有&lt;code&gt;__dict__&lt;/code&gt;和&lt;code&gt;__wekref__&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;迭代器与生成器&lt;/h4&gt;
&lt;p&gt;各位可能注意到了，大多数容器对象都可以使用for语句:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for char in &quot;abc&quot;:
    print(char)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在幕后，for语句会在容器对象上调用&lt;code&gt;iter()&lt;/code&gt;。该函数返回一个定义了&lt;code&gt;__next__()&lt;/code&gt;方法的迭代器对象，此方法将逐一访问容器中的元素。当元素用尽时，&lt;code&gt;__next__()&lt;/code&gt;将引发&lt;code&gt;StopIteration&lt;/code&gt;来通知终止for循环。我们可以使用&lt;code&gt;next()&lt;/code&gt;来调用&lt;code&gt;__next__()&lt;/code&gt;方法。下面是&lt;code&gt;for char in &quot;abc&quot;&lt;/code&gt;的等价模拟：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; s = &apos;abc&apos;
&amp;gt;&amp;gt;&amp;gt; it = iter(s)
&amp;gt;&amp;gt;&amp;gt; next(it)
&apos;a&apos;
&amp;gt;&amp;gt;&amp;gt; next(it)
&apos;b&apos;
&amp;gt;&amp;gt;&amp;gt; next(it)
&apos;c&apos;
&amp;gt;&amp;gt;&amp;gt; next(it)
Traceback (most recent call last):
  File &quot;&amp;lt;stdin&amp;gt;&quot;, line 1, in &amp;lt;module&amp;gt;
StopIteration
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果想让我们自己的类能够被迭代，那我们实现上面的操作就行了。定义&lt;code&gt;__iter__()&lt;/code&gt;用于返回一个带有&lt;code&gt;__next__()&lt;/code&gt;的对象，如果类已经定义了&lt;code&gt;__next__()&lt;/code&gt;，那么直接返回self就行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Reverse:
    &quot;&quot;&quot;对一个序列执行反向循环的迭代器。&quot;&quot;&quot;
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来点题外话，有没有更简单的方式来实现迭代呢？我们可以用生成器。生成器的写法类似于标准的函数，但当它们要返回数据时会使用&lt;code&gt;yield&lt;/code&gt;。它会自动创建&lt;code&gt;__iter__()&lt;/code&gt;和&lt;code&gt;__next__()&lt;/code&gt;，每次在生成器上调用&lt;code&gt;next()&lt;/code&gt;，它就会从上次离开的位置恢复执行。当元素全部取出后，还会自动引发&lt;code&gt;StopIteration&lt;/code&gt;。这种情况下，利用生成器创建迭代器可以和编写常规函数一样容易：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;利用元类&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;元类是一种99%的用户都无需关心的深奥魔法。如果你还在犹豫自己是否需要它，答案就是不需要。真正需要元类的人会非常清楚自己非用不可，根本不需要解释原因。&lt;/p&gt;
&lt;p&gt;——Tim Peters&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;s&gt;所以我决定不讲了&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;开个玩笑，我们继续。&lt;/p&gt;
&lt;h4&gt;引入&lt;/h4&gt;
&lt;p&gt;让我们从&quot;Hello, World!&quot;开始：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(&quot;Hello, World!&quot;.__class__)  # Output: &amp;lt;class &apos;str&apos;&amp;gt;
print(str.__class__)  # Output: &amp;lt;class &apos;type&apos;&amp;gt;
print(str.__bases__)  # Output: (&amp;lt;class &apos;object&apos;&amp;gt;,)

print(&quot;Hello, World!&quot;.__bases__)  # Raise: AttributeError: &apos;str&apos; object has no attribute &apos;__bases__&apos;. Did you mean: &apos;__class__&apos;?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Python中，万物皆对象。即使是基本数据类型，也都是由内置类创建的对象（其实这也是你能直接在字符串上面用方法的原因）。如代码所示，字符串&quot;Hello, World!&quot;内置类&lt;code&gt;str&lt;/code&gt;的实例，而字符串实例本身没有基类。将视线移向内置类&lt;code&gt;str&lt;/code&gt;，它是&lt;code&gt;type&lt;/code&gt;类的实例，基类是&lt;code&gt;object&lt;/code&gt;。众所周知，实例是类实例化的结果，也就是说，内置类&lt;code&gt;str&lt;/code&gt;是由&lt;code&gt;type&lt;/code&gt;类实例化的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Book:
    pass

print(Book.__class__)  # &amp;lt;class &apos;type&apos;&amp;gt;
print(Book.__bases__)  # (&amp;lt;class &apos;object&apos;&amp;gt;,)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再来看看我们自定义的类。结果也是一样的，我们定义的类继承自&lt;code&gt;object&lt;/code&gt;，是&lt;code&gt;type&lt;/code&gt;类的实例。&lt;/p&gt;
&lt;h4&gt;Python对象系统——从根源开始&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本节内容主要参考Shalabh Chaturvedi所编写的《Python Types and Objects》，本人（Raven95676）出于教育目的进行整理与二次创作。由于原作者保留所有权利，本节内容不以CC BY-NC-SA 4.0协议发布。如原版权人认为本节内容涉及版权问题，请联系本人（Raven95676）移除相关内容。&lt;/p&gt;
&lt;p&gt;本节内容引用图片较为古老，请自行将图中所有的形如&lt;code&gt;&amp;lt;type &apos;object&apos;&amp;gt;&lt;/code&gt;的视为&lt;code&gt;&amp;lt;class &apos;object&apos;&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们现在从头开始构建Python对象系统。让我们从最基本的地方开始——一张白纸。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/3.png&quot; alt=&quot;python_classes_and_oop_3&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在画图之前，我们先来介绍下关系。我们使用两种关系来连接两个对象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;子类-父类关系（继承）：表示一个对象是另一个对象的特化版本，例如“猫娘是幻想生物的一种”。&lt;/li&gt;
&lt;li&gt;类型-实例关系（实例化）：表示一个对象是另一个对象的具体实例，例如“猫娘Nova是猫娘的实例”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;继承关系使用实线从子到父连接，实例化关系使用虚线从实例到类型连接，如下图示例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/4.png&quot; alt=&quot;python_classes_and_oop_4&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们从一切的根源开始探索：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(object.__class__)  # Output: &amp;lt;class &apos;type&apos;&amp;gt;
print(object.__bases__)  # Output: ()

print(type.__class__)  # Output: &amp;lt;class &apos;type&apos;&amp;gt;
print(type.__bases__)  # Output: (&amp;lt;class &apos;object&apos;&amp;gt;,)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;object&lt;/code&gt;类是由&lt;code&gt;type&lt;/code&gt;类实例化而来的，没有父类。这意味&lt;code&gt;object&lt;/code&gt;是继承的起点。&lt;code&gt;type&lt;/code&gt;类是由自身实例化的，父类是&lt;code&gt;object&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;让我们把这些发现画在图上：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/5.png&quot; alt=&quot;python_classes_and_oop_5&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们再引入&lt;code&gt;list&lt;/code&gt;,&lt;code&gt;dict&lt;/code&gt;,&lt;code&gt;tuple&lt;/code&gt;这些内置数据类型来看看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(list.__class__)  # Output: &amp;lt;class &apos;type&apos;&amp;gt;
print(list.__bases__)  # Output: (&amp;lt;class &apos;object&apos;&amp;gt;,)
print(tuple.__class__)  # Output: &amp;lt;class &apos;type&apos;&amp;gt;
print(tuple.__bases__)  # Output: (&amp;lt;class &apos;object&apos;&amp;gt;,)
print(dict.__class__)  # Output: &amp;lt;class &apos;type&apos;&amp;gt;
print(dict.__bases__)  # Output: (&amp;lt;class &apos;object&apos;&amp;gt;,)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它们的父类都是&lt;code&gt;object&lt;/code&gt;，都是由&lt;code&gt;type&lt;/code&gt;类实例化而来的。然后我们实例化一个list看看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mylist = [1,2,3]
print(mylist.__class__)  # Output: &amp;lt;class &apos;list&apos;&amp;gt;
print(mylist.__bases__)  # Raise: AttributeError: &apos;list&apos; object has no attribute &apos;__bases__&apos;. Did you mean: &apos;__class__&apos;?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;mylist&lt;/code&gt;由&lt;code&gt;list&lt;/code&gt;实例化而来，没有父类。&lt;/p&gt;
&lt;p&gt;让我们把这些发现画在图上：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/6.png&quot; alt=&quot;python_classes_and_oop_6&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在我们来根据现有的&lt;code&gt;object&lt;/code&gt;类来定义属于自己的类（不写从哪继承默认继承自&lt;code&gt;object&lt;/code&gt;），然后实例化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class C:
    pass

obj = object()
cobj = C()


print(C.__class__)  # Output: &amp;lt;class &apos;type&apos;&amp;gt;
print(C.__bases__)  # Output: (&amp;lt;class &apos;object&apos;&amp;gt;,)
print(obj.__class__)  # Output: &amp;lt;class &apos;object&apos;&amp;gt;
print(cobj.__class__)  # Output: &amp;lt;class &apos;__main__.C&apos;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让我们把这些发现画在图上：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/7.png&quot; alt=&quot;python_classes_and_oop_7&quot; /&gt;&lt;/p&gt;
&lt;p&gt;实际上这三个格子都代表着什么已经跃然纸上了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/8.png&quot; alt=&quot;python_classes_and_oop_8&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第一个格子，这是所有类的类，我们称之为元类。&lt;/p&gt;
&lt;p&gt;第二个格子既是第三个格子的类，又是第一个格子的实例，我们称之为类型对象。&lt;/p&gt;
&lt;p&gt;第三个格子是第二个格子的实例，我们就称之为实例。&lt;/p&gt;
&lt;p&gt;元类实例化了类型对象，类型对象实例化了类。所以说一切新对象的创建都可以归结为实例化。&lt;/p&gt;
&lt;p&gt;所以，下面的两段代码在本质上是等价的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyClass:
    pass

MyClass = type(&quot;MyClass&quot;, (), {})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;元类的用法&lt;/h4&gt;
&lt;p&gt;那么现在我们——&lt;/p&gt;
&lt;p&gt;已知：默认情况下，类是使用&lt;code&gt;type&lt;/code&gt;来构建的。&lt;/p&gt;
&lt;p&gt;已知：类是可以被继承的。&lt;/p&gt;
&lt;p&gt;那么我们可不可以继承&lt;code&gt;type&lt;/code&gt;，创建一个新的元类，用新的元类来创建类呢——答案是当然可以，这就是元类的核心。&lt;/p&gt;
&lt;p&gt;恭喜各位，我们终于要开始学习元类的用法了。&lt;/p&gt;
&lt;p&gt;一个基本元类通常是继承自type并重写它的&lt;code&gt;__new__()&lt;/code&gt;或者&lt;code&gt;__init__()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Meta(type):
    def __new__(cls, clsname: str, bases: tuple, clsdict: dict):
        return super().__new__(cls, clsname, bases, clsdict)

    def __init__(self, clsname: str, bases: tuple, clsdict: dict):
        super().__init__(clsname, bases, clsdict)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;clsname&lt;/code&gt;是类名，会成为新类的&lt;code&gt;__name__&lt;/code&gt;属性。&lt;code&gt;bases&lt;/code&gt;元组包含基类并会成为&lt;code&gt;__bases__&lt;/code&gt;属性，如果为空，则会添加&lt;code&gt;object&lt;/code&gt;,&lt;code&gt;clsdict&lt;/code&gt;字典包含类体的属性和方法定义；它在成为&lt;code&gt;__dict__&lt;/code&gt;属性之前可能会被拷贝或包装。&lt;/p&gt;
&lt;p&gt;现在我们来给这个元类加点功能，这个元类会拒绝任何有混合大小写名字作为方法的类定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class NoMixedCaseMeta(type):
    def __new__(cls, clsname, bases, clsdict):
        for name in clsdict:
            if name.lower() != name:
                raise TypeError(&apos;Bad attribute name: &apos; + name)
        return super().__new__(cls, clsname, bases, clsdict)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;怎么应用元类呢？我们可以通过指定&lt;code&gt;metaclass&lt;/code&gt;参数来实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A(metaclass=NoMixedCaseMeta):
    def foo_bar(self):
        pass

class B(metaclass=NoMixedCaseMeta):
    def fooBar(self):  # Raise: TypeError: Bad attribute name: fooBar
        pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上面的讨论我们知道，类是由元类实例化来的，默认情况下的元类是&lt;code&gt;type&lt;/code&gt;，所以说指定&lt;code&gt;metaclass&lt;/code&gt;相当于从：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A = type(&apos;A&apos;, (), {&apos;foo_bar&apos;: lambda self: None})
B = type(&apos;B&apos;, (), {&apos;fooBar&apos;: lambda self: None})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;变成了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A = NoMixedCaseMeta(&apos;A&apos;, (), {&apos;foo_bar&apos;: lambda self: None})
B = NoMixedCaseMeta(&apos;B&apos;, (), {&apos;fooBar&apos;: lambda self: None})  # Raise: TypeError: Bad attribute name: fooBar
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Python面向对象编程实用技巧&lt;/h2&gt;
&lt;h3&gt;“私有”属性&lt;/h3&gt;
&lt;p&gt;Python中并没有真正意义上的私有属性。不过大多数Python代码都遵循这样一个约定：带有一个下划线的名称应该被当作是API的非公有部分 (无论它是函数、方法或是数据成员)。哪天这个实现没了或者行为变了概不负责。&lt;/p&gt;
&lt;p&gt;当名称至少带有两个前缀下划线，至多一个后缀下划线时，名称将被替换为_classname__名称，其中classname为去除了前缀下划线的当前类名称。这种机制被称为名称改写。一般情况下，这种机制用于避免名称与子类所定义的名称相冲突。&lt;/p&gt;
&lt;p&gt;网上有些教程说这个机制可以用来实现私有属性，说这样做可以保证内部可以访问，外部不能访问，其实后半部分是错的，名字改了不代表访问不到。而且个人不推荐因为这个用名称改写这个机制——无论怎么改，从本质上都能访问到，那就没有必要费这个功夫。&lt;/p&gt;
&lt;h3&gt;通过抽象基类定义接口&lt;/h3&gt;
&lt;p&gt;在多人协作的大型项目中，怎么保证不同开发者实现的类都遵循统一的接口规范？这时候我们可以使用抽象基类。它可以定义一组必须实现的方法，任何继承自该抽象基类的子类都必须实现这些抽象方法，否则在实例化时会直接抛出异常：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from abc import ABC, abstractmethod


class CacheInterface(ABC):
    @abstractmethod
    def save_to_cache(self, key, value):
        pass

    @abstractmethod
    def load_from_cache(self, key):
        pass


class RedisCache(CacheInterface):
    def save_to_cache(self, key, value):
        print(f&quot;保存到Redis: {key} = {value}&quot;)

    def load_from_cache(self, key):
        print(f&quot;从Redis加载: {key}&quot;)
        return f&quot;cached_{key}&quot;


class FileCache(CacheInterface):
    def save_to_cache(self, key, value):
        print(f&quot;保存到文件: {key} = {value}&quot;)

    def load_from_cache(self, key):
        print(f&quot;从文件加载: {key}&quot;)
        return f&quot;file_{key}&quot;


class IncompleteCache(CacheInterface):
    def save_to_cache(self, key, value):
        print(f&quot;保存: {key} = {value}&quot;)


redis_cache = RedisCache()
file_cache = FileCache()
test = IncompleteCache()  # Raise: TypeError: Can&apos;t instantiate abstract class IncompleteCache without an implementation for abstract method &apos;load_from_cache&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用枚举类&lt;/h3&gt;
&lt;p&gt;写代码的时候我们经常需要表示一组固定的状态或类型，如订单状态（待支付、已支付、已发货）。这时候使用枚举类更加优雅：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from enum import Enum, auto

class OrderStatus(Enum):
    PENDING = &quot;pending&quot;
    PAID = &quot;paid&quot;
    SHIPPED = &quot;shipped&quot;
    DELIVERED = &quot;delivered&quot;
    CANCELLED = &quot;cancelled&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果具体的枚举值无所谓是什么，可以使用&lt;code&gt;auto&lt;/code&gt;自动生成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Priority(Enum):
    LOW = auto()
    MEDIUM = auto()
    HIGH = auto()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;通过装饰器消除重复代码&lt;/h3&gt;
&lt;h4&gt;开发者自定义装饰器&lt;/h4&gt;
&lt;p&gt;一般来说装饰器是一个函数，接收另一个函数作为参数，最后返回一个添加了额外行为的闭包（所以说叫“装饰器”）。假设我们现在需要编写生成邮件内容的函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def welcome_message():
    print(&quot;欢迎加入我们的团队。&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;众所周知，正式的电子邮件需要遵循一定的礼仪。但是添加的格式及内容是固定的，如果每个生成邮件内容的函数的函数都复制粘贴一份，如果将来决定更改邮件格式，那就必须修改每个函数中的相关内容。为了避免这种麻烦的情况，我们可以编写一个装饰器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def email_decorator(func):
    def wrapper():
        print(&quot;你好，&quot;)
        func()
        print(&quot;此致，\nRaven&quot;)
    return wrapper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让我们应用这个装饰器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@email_decorator
def welcome_message():
    print(&quot;欢迎加入我们的团队。&quot;)

welcome_message()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们在调用&lt;code&gt;welcome_message&lt;/code&gt;时，实际上执行的是闭包&lt;code&gt;wrapper&lt;/code&gt;。它先打印了问候语，然后调用了原始函数，最后打印了签名。&lt;/p&gt;
&lt;p&gt;实际上，上述使用了&lt;code&gt;@&lt;/code&gt;的代码等价于下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;welcome_message = email_decorator(welcome_message)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@&lt;/code&gt;是一个方便使用的语法糖，不用这个语法糖也可以使用装饰器。&lt;/p&gt;
&lt;p&gt;现在&lt;code&gt;welcome_message&lt;/code&gt;不接受任何参数。如果我们想拓展一下，比如允许指定部门，应该怎么做？你可能会这样做：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@email_decorator
def welcome_message(department):
    print(f&quot;欢迎加入我们的{department}团队。&quot;)

welcome_message()  # Raise: TypeError: email_decorator.&amp;lt;locals&amp;gt;.wrapper() takes 0 positional arguments but 1 was given
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这会抛出&lt;code&gt;TypeError&lt;/code&gt;。注意看，我们的装饰器中&lt;code&gt;func()&lt;/code&gt;并没有接收任何参数，这是问题的根源。为了让装饰器更通用，我们需要添加&lt;code&gt;*args&lt;/code&gt;和&lt;code&gt;**kwargs&lt;/code&gt;以接收任意数量的参数（当然，需要加入多少参数就加多少也可以）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def email_decorator(func):
    def wrapper(*args, **kwargs):
        print(&quot;你好，&quot;)
        func(*args, **kwargs)
        print(&quot;此致，\nRaven&quot;)

    return wrapper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;各位可能注意到了，装饰器没有返回任何的值。被装饰的函数可能需要返回一个值，这时候就需要装饰器“捕获”这个值然后返回。这次让我们用一个测量函数执行时间的装饰器作为例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time

def timer_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f&quot;Function {func.__name__} took {end_time - start_time:.4f} seconds&quot;)
        return result
    return wrapper

@timer_decorator
def calc(n):
    def fibonacci(x):
        if x &amp;lt;= 1:
            return x
        return fibonacci(x - 1) + fibonacci(x - 2)

    result = fibonacci(n)

    return result

calc(35)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个可能很少有人注意到的点是，当你试图获取原始函数的元数据时，比如&lt;code&gt;__name__&lt;/code&gt;，你会发现输出的是&lt;code&gt;wrapper&lt;/code&gt;而不是直觉上的&lt;code&gt;calc&lt;/code&gt;。这很正常，因为实际调用的就是&lt;code&gt;wrapper&lt;/code&gt;。但是有时我们就是需要原始函数的元数据，这时候就可以通过&lt;code&gt;functools&lt;/code&gt;模块提供的&lt;code&gt;wraps&lt;/code&gt;装饰器来实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time

from functools import wraps


def timer_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.perf_counter()
        result = func(*args, **kwargs)
        end_time = time.perf_counter()
        print(f&quot;Function {func.__name__} took {end_time - start_time:.4f} seconds&quot;)
        return result

    return wrapper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;回到我们的邮件装饰器。有的时候我们需要装饰器本身也能接收参数，比如我们需要装饰器可以可以自定义发件人。这时我们需要在现有装饰器结构之上再添加一层被称为装饰器工厂的函数，它接收参数并“生产”一个装饰器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from functools import wraps


def email_decorator(from_who):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(&quot;你好，&quot;)
            func(*args, **kwargs)
            print(f&quot;此致，\n{from_who}&quot;)

        return wrapper

    return decorator


@email_decorator(from_who=&quot;Raven&quot;)
def welcome_message(department):
    print(f&quot;欢迎加入我们的{department}团队。&quot;)


welcome_message(&quot;开发&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么需要装饰器工厂？直接传参不行吗？因为&lt;code&gt;@&lt;/code&gt;语法在应用装饰器时，只会将被装饰的函数传递给装饰器函数。如果我们希望装饰器本身也能接受参数，直接传参是不行的。这里使用的&lt;code&gt;@&lt;/code&gt;语法等价于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;decorated_welcome = email_decorator(from_who=&quot;Raven&quot;)(welcome_message)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里，&lt;code&gt;email_decorator(from_who=&quot;Raven&quot;)&lt;/code&gt;先执行，返回一个装饰器函数，然后这个装饰器再接收&lt;code&gt;welcome_message&lt;/code&gt;来生成最终的装饰函数。&lt;/p&gt;
&lt;p&gt;有的时候我们不止需要用一个装饰器来装饰函数，这种情况下我们直接堆叠装饰器就可以。需要注意的是，装饰器的应用顺序是从下到上的，顺序不要弄错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from functools import wraps


def email_decorator(from_who):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(&quot;你好，&quot;)
            func(*args, **kwargs)
            print(f&quot;此致，\n{from_who}&quot;)

        return wrapper

    return decorator

def add_studio_info(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(&quot;---&quot;)
        print(&quot;Raven-Studio&quot;)
        return result
    return wrapper

@add_studio_info
@email_decorator(from_who=&quot;Raven&quot;)
def welcome_message(department):
    print(f&quot;欢迎加入我们的{department}团队。&quot;)


welcome_message(&quot;开发&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际上这个顺序也很好理解。这里使用的&lt;code&gt;@&lt;/code&gt;等价于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;welcome_message = add_studio_info(email_decorator(from_who=&quot;Raven&quot;)(welcome_message))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了函数，其实类也可以用作装饰器。这在需要管理状态或者需要把复杂逻辑组织在一起时非常有用，这通常需要重写&lt;code&gt;__init__()&lt;/code&gt;、&lt;code&gt;__call__()&lt;/code&gt;这两个魔术方法:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from functools import wraps


class EmailDecorator:
    def __init__(self, from_who):
        self.from_who = from_who

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(&quot;你好，&quot;)
            func(*args, **kwargs)
            print(f&quot;此致，\n{self.from_who}&quot;)

        return wrapper


@EmailDecorator(from_who=&quot;Raven&quot;)
def welcome_message(department):
    print(f&quot;欢迎加入我们的{department}团队。&quot;)


welcome_message(&quot;开发&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;__init__()&lt;/code&gt;负责接收装饰器的参数，&lt;code&gt;__call__()&lt;/code&gt;负责接收被装饰的函数，并返回包装后的函数。&lt;/p&gt;
&lt;p&gt;当然，装饰器也可以用来装饰类，后面我们会在“元类的代替”这一节统一讲一下。&lt;/p&gt;
&lt;h4&gt;使用lru_cache装饰器&lt;/h4&gt;
&lt;p&gt;现在让我们把视线转向之前的斐波那契数列计算函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time

from functools import wraps, lru_cache


def timer_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.perf_counter()
        result = func(*args, **kwargs)
        end_time = time.perf_counter()
        print(f&quot;Function {func.__name__} took {end_time - start_time:.4f} seconds&quot;)
        return result
    return wrapper

@timer_decorator
def calc(n):
    @lru_cache(maxsize=4096)
    def fibonacci(x):
        if x &amp;lt;= 1:
            return x
        return fibonacci(x - 1) + fibonacci(x - 2)

    result = fibonacci(n)

    return result

calc(50)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@lru_cache&lt;/code&gt;会缓存函数调用的结果。当函数以相同的参数再次被调用时，它会立即返回缓存的结果，而无需重新计算。如果没有&lt;code&gt;@lru_cache&lt;/code&gt;，计算&lt;code&gt;calc(50)&lt;/code&gt;需要的时间比较长。加上&lt;code&gt;@lru_cache&lt;/code&gt;之后，它几乎是瞬间完成的。&lt;/p&gt;
&lt;p&gt;当然，也不是什么场景下都可以用&lt;code&gt;@lru_cache&lt;/code&gt;的。首先参数需要是可哈希的。因为&lt;code&gt;@lru_cache&lt;/code&gt;的内部实现依赖于一个字典，它使用函数的参数作为键。其次，函数需存在频繁使用相同参数调用的场景才有优化价值。并且，如果函数依赖未作为参数传递的会动态改变的数据（如当前时间或全局状态），使用缓存可能返回过时的计算结果。&lt;/p&gt;
&lt;h4&gt;使用unique保证枚举值唯一&lt;/h4&gt;
&lt;p&gt;默认情况下，枚举允许多个名称作为一个值的别名。如果需要保证枚举中的值只用一次，可以使用&lt;code&gt;@unique&lt;/code&gt;装饰器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from enum import Enum, unique

@unique
class Mistake(Enum):
    ONE = 1
    TWO = 2
    THREE = 3
    FOUR = 3  # Raise: ValueError: duplicate values found in &amp;lt;enum &apos;Mistake&apos;&amp;gt;: FOUR -&amp;gt; THREE
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;使用property装饰器&lt;/h4&gt;
&lt;p&gt;假设我们在开发某管理系统，需要一个&lt;code&gt;Person&lt;/code&gt;类来管理一个人的信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

x = Person(&quot;M&quot;, 30)
print(x.age)  # Output: 30
x.age = 360
print(x.age)  # Output: 360
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题来了，属性的设置是不受控的，这意味着无论设置多么不合理的值都可以。我们需要在设置值时进行校验。我们可以选择通过编写set_xxx、get_xxx等方法来实现，但是这样在Python中不够优雅。这时候我们可以使用&lt;code&gt;@property&lt;/code&gt;装饰器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Person:
    def __init__(self, name, age):
        self.name = name
        self._age = age

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if 0 &amp;lt;= value &amp;lt; 150:
            self._age = value
        else:
            raise ValueError(&quot;年龄必须在0到150之间&quot;)

x = Person(&quot;M&quot;, 30)
print(x.age)  # Output: 30
x.age = 45
print(x.age)  # Output: 45
x.age = 360  # Raise: ValueError: 年龄必须在0到150之间
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;顺带一提，如果不定义&lt;code&gt;setattr&lt;/code&gt;，我们可以创建一个只读属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Person:
    def __init__(self, name, age):
        self.name = name
        self._age = age

    @property
    def age(self):
        return self._age

x = Person(&quot;M&quot;, 30)
print(x.age)  # Output: 30
x.age = 360  # Raise: AttributeError: property &apos;age&apos; of &apos;Person&apos; object has no setter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你还可以通过&lt;code&gt;deleter&lt;/code&gt;定义当使用&lt;code&gt;del&lt;/code&gt;删除属性时应该执行什么操作。顺带一提，如果不定义就del，那就会抛出&lt;code&gt;AttributeError&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Person:
    def __init__(self, name, age):
        self.name = name
        self._age = age

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if 0 &amp;lt;= value &amp;lt; 150:
            self._age = value
        else:
            raise ValueError(&quot;年龄必须在0到150之间&quot;)

    @age.deleter
    def age(self):
        self._age = 0

x = Person(&quot;M&quot;, 30)
print(x.age)  # Output: 30
del x.age
print(x.age)  # Output: 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;使用dataclass装饰器&lt;/h4&gt;
&lt;p&gt;在Python里创建类来存储数据的时候，通常情况下我们需要编写很多样板代码。比如我们创建一个类来表示一个产品：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Product:
    def __init__(self, name: str, unit_price: float, quantity: int = 0):
        self.name = name
        self.unit_price = unit_price
        self.quantity = quantity

    def __repr__(self):
        return f&quot;Product(name=&apos;{self.name}&apos;, unit_price={self.unit_price}, quantity={self.quantity})&quot;

    def __eq__(self, other):
        if not isinstance(other, Product):
            return NotImplemented
        return (self.name, self.unit_price, self.quantity) == (
            other.name,
            other.unit_price,
            other.quantity,
        )
    
    def total_cost(self) -&amp;gt; float:
        return self.unit_price * self.quantity
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每定义一个数据类都要写很多重复的代码，这很不优雅。所幸，我们可以使用&lt;code&gt;@dataclass&lt;/code&gt;装饰器来自动生成样板代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from dataclasses import dataclass

@dataclass
class Product:
    name: str
    unit_price: float
    quantity: int = 0

    def total_cost(self) -&amp;gt; float:
        return self.unit_price * self.quantity
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在定义dataclass的时候，可以给字段指定默认值。需要注意的是所有没有默认值的字段必须在有默认值的字段上面，不然会抛出&lt;code&gt;TypeError&lt;/code&gt;。对于不可变对象来说，直接赋值就好。但对于可变对象，必须使用&lt;code&gt;field&lt;/code&gt;的&lt;code&gt;default_factory&lt;/code&gt;参数来避免所有实例共享同一个可变对象（毕竟默认值定义在类属性上）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from dataclasses import dataclass, field


@dataclass
class Product:
    name: str
    unit_price: float
    quantity: int = 0
    tags: list[str] = field(default_factory=list)

    def total_cost(self) -&amp;gt; float:
        return self.unit_price * self.quantity
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;default_factory&lt;/code&gt;需要提供一个无参数的可调用对象，dataclass会在需要默认值时调用它生成新的值。这里直接填&lt;code&gt;list&lt;/code&gt;是初始化为空列表。如果需要默认值是一个包含特定内容的列表，我们可以使用&lt;code&gt;lambda&lt;/code&gt;定义一个匿名函数，比如&lt;code&gt;lambda: [&quot;内容&quot;]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;顺带一提，如果需要指定默认值为不可变对象，可以直接用&lt;code&gt;default&lt;/code&gt;参数。需要注意的是&lt;code&gt;default&lt;/code&gt;和&lt;code&gt;default_factory&lt;/code&gt;是冲突的——这也很好理解，有两个初始化默认值的方法Python根本不知道调用哪一个。&lt;/p&gt;
&lt;p&gt;当然，&lt;code&gt;field&lt;/code&gt;不止这点作用。比如我们如果不想让字段出现在&lt;code&gt;__init__()&lt;/code&gt;当中，我们可以设定&lt;code&gt;init&lt;/code&gt;参数为&lt;code&gt;False&lt;/code&gt;。如果我们需要某个字段不参与比较，我们可以设定&lt;code&gt;compare&lt;/code&gt;为&lt;code&gt;False&lt;/code&gt;。如果我们需要设定某个字段为仅限关键字参数，我们可以把&lt;code&gt;kw_only&lt;/code&gt;设定为&lt;code&gt;True&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;dataclass装饰器本身也有一些非常有用的参数。比如说我们可以设定&lt;code&gt;frozen&lt;/code&gt;参数为&lt;code&gt;True&lt;/code&gt;来创建一个不可修改值的数据类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: int
    y: int

p = Point(1, 2)
p.x = 3  # Raise: dataclasses.FrozenInstanceError: cannot assign to field &apos;x&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们还可以设定&lt;code&gt;order&lt;/code&gt;参数为&lt;code&gt;True&lt;/code&gt;来让数据类实例可以进行大小比较，不仅限于相等判断。如果我们需要某些值是在初始化之后根据其他值动态计算出来的，我们可以定义&lt;code&gt;__post_init__&lt;/code&gt;方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import math

from dataclasses import dataclass, field

@dataclass
class Circle:
    radius: float
    area: float = field(init=False)

    def __post_init__(self):
        self.area = math.pi * (self.radius ** 2)

c = Circle(5.0)
print(c)  # Output: Circle(radius=5.0, area=78.53981633974483)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;元类的替代&lt;/h3&gt;
&lt;p&gt;还是那句话，99%的情况都没必要上元类。以防有人认为只要是定制类就一定需要元类，这里讲一下元类的替代方案。&lt;/p&gt;
&lt;h4&gt;类装饰器替代元类&lt;/h4&gt;
&lt;p&gt;对于对类的一次性修改/检查，可以用类装饰器。我们拿&lt;code&gt;NoMixedCaseMeta&lt;/code&gt;的类装饰器等价实现来举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def no_mixed_case(cls):
    for name in cls.__dict__:
        if name.lower() != name:
            raise TypeError(&apos;Bad attribute name: &apos; + name)

    return cls


@no_mixed_case
class A:
    def foo_bar(self):
        pass

@no_mixed_case
class B:
    def fooBar(self):  # Raise: TypeError: Bad attribute name: fooBar
        pass
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;魔术方法替代元类&lt;/h4&gt;
&lt;p&gt;那如果我们需要继承的时候就能检查呢？这时候我们可以用魔术方法&lt;code&gt;__init_subclass__&lt;/code&gt;。还是拿&lt;code&gt;NoMixedCaseMeta&lt;/code&gt;的等价实现来举例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class NoMixedCaseBase:
    def __init_subclass__(cls, *args, **kwargs):
        super().__init_subclass__(*args, **kwargs)

        for name in cls.__dict__:
            if name.lower() != name:
                raise TypeError(&quot;Bad attribute name: &quot; + name)


class A(NoMixedCaseBase):
    def foo_bar(self):
        pass


class B(NoMixedCaseBase):
    def fooBar(self):  # Raise: TypeError: Bad attribute name: fooBar
        pass
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;真正非元类不可的场景&lt;/h4&gt;
&lt;p&gt;那有没有什么真正意义上的非元类不可，只有元类才能实现的功能呢？有的，比如我们突发奇想，想让MRO倒过来，这时候就要用元类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class PluginA:
    def test(self):
        return &quot;Plugin A&quot;


class PluginB:
    def test(self):
        return &quot;Plugin B&quot;


class ReverseMeta(type):
    def mro(cls):
        default_mro = list(type.mro(cls))
        reversed_mro = list(reversed(default_mro[:-1])) + [object]
        return tuple(reversed_mro)


class A(PluginA, PluginB):
    pass

class B(PluginA, PluginB, metaclass=ReverseMeta):
    pass

print(A.__mro__)  # (&amp;lt;class &apos;__main__.A&apos;&amp;gt;, &amp;lt;class &apos;__main__.PluginA&apos;&amp;gt;, &amp;lt;class &apos;__main__.PluginB&apos;&amp;gt;, &amp;lt;class &apos;object&apos;&amp;gt;)
print(B.__mro__)  # (&amp;lt;class &apos;__main__.PluginB&apos;&amp;gt;, &amp;lt;class &apos;__main__.PluginA&apos;&amp;gt;, &amp;lt;class &apos;__main__.B&apos;&amp;gt;, &amp;lt;class &apos;object&apos;&amp;gt;)

a = A()
b = B()

print(a.test())  # Output: Plugin A
print(b.test())  # Output: Plugin B
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好吧我们讲个接地气的例子，比如我们要实现一个简易的ORM：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Field:
    def __init__(self, default=None):
        self.default = default

class CharField(Field):
    def __init__(self, default=&quot;&quot;):
        super().__init__(default)

class IntegerField(Field):
    def __init__(self, default=0):
        super().__init__(default)

class ModelMeta(type):
    def __new__(mcs, name, bases, clsdict):
        if name == &apos;Model&apos;:
            return super().__new__(mcs, name, bases, clsdict)
        fields = {}
        for k, v in clsdict.items():
            if isinstance(v, Field):
                fields[k] = v
        for k in fields:
            del clsdict[k]
        clsdict[&apos;__fields__&apos;] = fields
        return super().__new__(mcs, name, bases, clsdict)

class Model(metaclass=ModelMeta):
    def __init__(self, **kwargs):
        for field_name, field in self.__fields__.items():
            value = kwargs.get(field_name, field.default)
            setattr(self, field_name, value)
    
    def __repr__(self):
        field_values = []
        for field_name in self.__fields__:
            value = getattr(self, field_name)
            field_values.append(f&quot;{field_name}={value!r}&quot;)
        return f&quot;{self.__class__.__name__}({&apos;, &apos;.join(field_values)})&quot;
    
class Product(Model):
    title = CharField()
    price = IntegerField()
    description = CharField(default=&quot;无描述&quot;)

product1 = Product(title=&quot;笔记本电脑&quot;, price=7999, description=&quot;便携式电脑&quot;)
product2 = Product(title=&quot;手机&quot;, price=4999)

print(product1)  # Output: Product(title=&apos;笔记本电脑&apos;, price=7999, description=&apos;便携式电脑&apos;)
print(product2)  # Output: Product(title=&apos;手机&apos;, price=4999, description=&apos;无描述&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;面向对象最佳实践&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;代码是写给人看的。除非你有必须这样做的理由并留下注释，或者写的是一次性代码，否则别把未来的自己或者接手的同事逼成考古学家。&lt;/p&gt;
&lt;p&gt;——Raven&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;KISS&lt;/h3&gt;
&lt;p&gt;Keep it simple stupid——这是最佳实践的总纲。始终追求简单、清晰的设计与实现，避免不必要的复杂性。&lt;/p&gt;
&lt;p&gt;举一个最简单的例子。假设我们需要一个函数，它接受一个整数列表，然后返回这个列表中所有偶数的平方。一个较为复杂的写法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def process_data(data: list[int]) -&amp;gt; list[int]:
    result = []
    temp = []
    for item in data:
        if item % 2 == 0:
            temp.append(item)
    for num in temp:
        result.append(num**2)
    return result
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际上只需要用一个列表推导式即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def process_data(data: list[int]) -&amp;gt; list[int]:
    return [x**2 for x in data if x % 2 == 0]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;YAGNI&lt;/h3&gt;
&lt;p&gt;YAGNI是You aren&apos;t gonna need it的缩写，直译就是“你不会需要它”。写代码的时候别想那么多，先把眼前的事搞定，等真需要的时候再改。不要假设“万一以后要用呢？”，要知道只要不是有明确的需求，所谓的“万一以后要用”那就只是万一，没必要为了一个可能性花费大把精力。&lt;/p&gt;
&lt;h3&gt;SOLID&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本节内容参考了维基百科相关定义。由于维基百科相关内容遵循CC BY-SA 4.0协议，本节内容不以CC BY-NC-SA 4.0协议发布。&lt;/p&gt;
&lt;p&gt;本节内容主要参考梅雪松所著的《写了这么多年代码，你真的了解SOLID吗？》。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./images/9.png&quot; alt=&quot;python_classes_and_oop_9&quot; /&gt;
（图源 写了这么多年代码，你真的了解SOLID吗？By 梅雪松）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;总结来说，单一职责是所有设计原则的基础，开闭原则是设计的终极目标。里氏替换原则强调的是子类替换父类后程序运行时的正确性，它用来帮助实现开闭原则。而接口隔离原则用来帮助实现里氏替换原则，同时它也体现了单一职责。依赖倒置原则是过程式编程与面向对象编程的分水岭，同时它也被用来指导接口隔离原则。&lt;/p&gt;
&lt;p&gt;——梅雪松&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;S-单一职责原则&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;对象应该仅具有一种单一功能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;根据罗伯特·C·马丁的定义，单一职责原则的核心思想是将&quot;改变的原因&quot;作为界定功能职责的标准，认为每个类或模块应当仅存在一个导致其变更的动机。如果你能够想到多于一个的动机去改变一个类，那么这个类就具有多于一个的职责。&lt;/p&gt;
&lt;p&gt;以报表处理模块为例，想象有一个用于编辑和打印报表的模块。这样的一个模块存在两个改变的原因。第一，报表的内容可以改变（编辑）。第二，报表的格式可以改变（打印）。这两方面的改变会因为完全不同的起因而发生。单一功能原则认为这两方面的问题事实上是两个分离的功能，因此他们应该分离在不同的类或者模块里。&lt;/p&gt;
&lt;p&gt;把有不同的改变原因的事物耦合在一起的设计是糟糕的。如果有一个对于报表编辑流程的修改，那么将存在极大的危险性，因为假设这两个功能存在于同一个类中，修改报表的编辑流程可能会导致公共状态或者依赖关系的改变，打印功能的代码会因此不工作。&lt;/p&gt;
&lt;p&gt;变更频率是另一个值得考虑的变化原因。即使对同一类角色，需求变更的频率也会存在差异。最典型的例子是业务处理的需求比较稳定，而业务展示的需求更容易发生变更，毕竟人总是喜新厌旧的。因此这两类需求通常要在不同的类中实现。&lt;/p&gt;
&lt;p&gt;单一职责原则某种程度上说是在分离关注点。分离不同角色的关注点，分离不同时间的关注点。&lt;/p&gt;
&lt;p&gt;在实践中，怎么运用单一职责原则呢？什么时候要拆分，什么时候要合并？我们看看新厨师在学炒菜时，是如何掌握“盐少许”的。他会不断地品尝，直到味道刚好为止。写代码也一样，你需要识别需求变化的信号，不断“品尝”你的代码，当“味道”不够好时，持续重构，直到“味道”刚刚好。&lt;/p&gt;
&lt;h4&gt;O-开闭原则&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;软件应该是对于扩展开放的，但是对于修改封闭的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一个设计良好的实体应该能够在无需修改源代码的情况下改变其行为。&lt;/p&gt;
&lt;p&gt;为什么要这样呢？假设你是一名成功的开源类库作者，很多开发者使用你的类库。如果某天你要扩展功能，只能通过修改某些代码完成，结果导致类库的使用者都需要修改代码。更可怕的是，他们被迫修改了代码后，又可能造成别的依赖者也被迫修改代码。这种场景绝对是一场灾难。&lt;/p&gt;
&lt;p&gt;然而，我们不可能在初始设计阶段就预见所有可能的扩展点，也不可能在所有地方都预留出扩展点。正确的做法是让需求变化驱动设计决策（即YAGNI）。&lt;/p&gt;
&lt;p&gt;我们也许永远都无法完全做到开闭原则，但不妨碍它是设计的终极目标。SOLID的其它原则都直接或间接为开闭原则服务。&lt;/p&gt;
&lt;h4&gt;L-里氏替换原则&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;程序中任何使用父类的地方，都应该可以用其子类来代替，并且程序的行为不会发生任何改变。子类应该延续父类的行为，而不是改变它。一个最经典的例子是罗伯特·C·马丁所著的《敏捷软件开发：原则、模式与实践》中讲到的正方形继承矩形：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Rectangle:
    def __init__(self, width: int, height: int) -&amp;gt; None:
        self._width = width
        self._height = height

    @property
    def width(self) -&amp;gt; int:
        return self._width

    @width.setter
    def width(self, value: int):
        self._width = value

    @property
    def height(self) -&amp;gt; int:
        return self._height

    @height.setter
    def height(self, value: int):
        self._height = value

    @property
    def area(self) -&amp;gt; int:
        return self._width * self._height


class Square(Rectangle):
    def __init__(self, size: int):
        super().__init__(size, size)

    @property
    def width(self) -&amp;gt; int:
        return super().width

    @width.setter
    def width(self, value: int):
        self._width = value
        self._height = value

    @property
    def height(self) -&amp;gt; int:
        return super().height

    @height.setter
    def height(self, value: int):
        self._width = value
        self._height = value


def print_area(rect: Rectangle):
    rect.width = 5
    rect.height = 10
    print(f&quot;面积：{rect.area}&quot;)


rect = Rectangle(2, 3)
print_area(rect)  # Output: 面积：50

sq = Square(5)
print_area(sq)  # Output: 面积：100  &amp;lt;-- 行为发生了改变
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然从常理的角度出发，正方形是一种特殊的矩形，正方形继承矩形似乎没什么问题。但是函数&lt;code&gt;print_area&lt;/code&gt;假设传入的对象&lt;code&gt;Rectangle&lt;/code&gt;在设置宽度为5、高度为10后，面积为50。当传入对象&lt;code&gt;Square&lt;/code&gt;时，由于&lt;code&gt;Square&lt;/code&gt;强制宽度和高度相等，&lt;code&gt;set_height(10)&lt;/code&gt;将宽度和高度都设为10，导致面积为100。所以说对象&lt;code&gt;Square&lt;/code&gt;不能完全替代对象&lt;code&gt;Rectangle&lt;/code&gt;，因为它改变了函数&lt;code&gt;print_area&lt;/code&gt;的预期行为。&lt;/p&gt;
&lt;p&gt;总而言之，冲突源于父类&lt;code&gt;Rectangle&lt;/code&gt;方法所隐含的行为契约。&lt;code&gt;Rectangle&lt;/code&gt;的&lt;code&gt;setter&lt;/code&gt;方法暗示了“宽度和高度可以被独立修改”，而&lt;code&gt;Square&lt;/code&gt;的实现打破了这个契约，这也导致了其违背里氏替换原则。&lt;/p&gt;
&lt;p&gt;如果你的设计满足里氏替换原则，那么子类（或接口的实现类）就可以保证正确性的前提下替换父类（或接口），改变系统的行为，从而实现扩展。Branch By Abstraction（引入一个抽象层，旧实现和新实现均遵循该抽象，逐步替换功能）和绞杀者模式（在新旧系统共存的过渡期，通过抽象层将请求路由到新旧实现，逐步将功能迁移到新系统）都是基于里氏替换原则，实现系统扩展和演进。这也就是对修改封闭，对扩展开放，因此里氏替换原则是实现开闭原则的一种解决方案。&lt;/p&gt;
&lt;p&gt;而为了达成里氏替换原则，你需要接口隔离原则。&lt;/p&gt;
&lt;h4&gt;I-接口隔离原则&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;多个特定客户端接口要好于一个宽泛用途的接口。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;接口之所以存在，是为了解耦。开发者常常有一个错误的认知，以为是实现类需要接口。其实是消费者需要接口，实现类只是提供服务，因此应该由消费者（客户端）来定义接口。理解了这一点，才能正确地站在消费者的角度定义Role interface，而不是从实现类中提取Header Interface。&lt;/p&gt;
&lt;p&gt;举个例子，假设我们有一个包含打印、扫描、传真功能的现代打印机，如果直接提取以下接口，这就是Header Interface：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from abc import ABC, abstractmethod


class PrinterInterface(ABC):
    @abstractmethod
    def print_doc(self, doc: str):
        pass

    @abstractmethod
    def scan_doc(self):
        pass

    @abstractmethod
    def fax_doc(self, doc: str, num: str):
        pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但问题来了，假设现在有一台老式打印机，它只能打印，不支持扫描和传真。老式打印机被迫依赖了自己不需要的接口方法，这就违反了接口隔离原则：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class OldPrinter(PrinterInterface):
    def print_doc(self, doc: str):
        print(f&quot;打印文档: {doc}&quot;)

    def scan_doc(self):
        raise NotImplementedError(&quot;此打印机不支持扫描&quot;)

    def fax_doc(self, doc: str, num: str):
        raise NotImplementedError(&quot;此打印机不支持传真&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们不能保证依赖该接口的客户端绝对不调用不支持的方法，所以正确的做法是站在消费者的角度，抽象出Role interface:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from abc import ABC, abstractmethod


class Printable(ABC):
    @abstractmethod
    def print_doc(self, doc: str):
        pass


class Scannable(ABC):
    @abstractmethod
    def scan_doc(self):
        pass


class Faxable(ABC):
    @abstractmethod
    def fax_doc(self, doc: str, num: str):
        pass


class OldPrinter(Printable):
    def print_doc(self, doc: str):
        print(f&quot;打印文档: {doc}&quot;)


class ModernPrinter(Printable, Scannable, Faxable):
    def print_doc(self, doc: str):
        print(f&quot;打印文档: {doc}&quot;)

    def scan_doc(self):
        print(&quot;扫描文档&quot;)

    def fax_doc(self, doc: str, num: str):
        print(f&quot;发送传真: {doc} 到 {num}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有了Role interface，作为消费者的老式打印机和现代打印机就可以分别消费自己的接口。&lt;/p&gt;
&lt;p&gt;实际上，接口隔离原则本质上也是单一职责原则的体现，同时它也服务于里氏替换原则。&lt;/p&gt;
&lt;h4&gt;D-依赖倒置原则&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;依赖于抽象而不是一个实例。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个原则其实是在指导如何实现接口隔离原则，也就是前文提到的，高层的消费者不应该依赖于具体实现，应该由消费者定义并依赖于Role interface，底层的具体实现也依赖于Role interface，因为它要实现此接口。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/10.jpg&quot; alt=&quot;python_classes_and_oop_10&quot; /&gt;
（图源 A Simple DIP Example）&lt;/p&gt;
&lt;p&gt;上图的关系中，当Button直接调用灯的开和关时，Button就依赖于灯了。其代码完全相当于过程式编程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Lamp:
    def turn_on(self):
        print(&quot;Lamp is ON&quot;)

    def turn_off(self):
        print(&quot;Lamp is OFF&quot;)


class Button:
    def __init__(self, lamp: Lamp):
        self.lamp = lamp

    def press(self):
        self.lamp.turn_on()

    def release(self):
        self.lamp.turn_off()


lamp = Lamp()
button = Button(lamp)
button.press()  # Output: Lamp is ON
button.release()  # Output: Lamp is OFF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./images/11.jpg&quot; alt=&quot;python_classes_and_oop_11&quot; /&gt;
（图源 A Simple DIP Example）&lt;/p&gt;
&lt;p&gt;如果Button还想控制电视机，微波炉怎么办？应对这种变化的办法就是抽象，抽象出Role interface ButtonServer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from abc import ABC, abstractmethod


class ButtonServer(ABC):
    @abstractmethod
    def turn_on(self):
        pass

    @abstractmethod
    def turn_off(self):
        pass


class Lamp(ButtonServer):
    def turn_on(self):
        print(&quot;Lamp is ON&quot;)

    def turn_off(self):
        print(&quot;Lamp is OFF&quot;)


class TV(ButtonServer):
    def turn_on(self):
        print(&quot;TV is ON&quot;)

    def turn_off(self):
        print(&quot;TV is OFF&quot;)


class Button:
    def __init__(self, device: ButtonServer):
        self.device = device

    def press(self):
        self.device.turn_on()

    def release(self):
        self.device.turn_off()


lamp = Lamp()
tv = TV()

button1 = Button(lamp)
button2 = Button(tv)

button1.press()  # Output: Lamp is ON
button1.release()  # Output: Lamp is OFF
button2.press()  # Output: TV is ON
button2.release()  # Output: TV is OFF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不管是电灯，还是电视机，只要实现了ButtonServer，Button都可以控制。这是面向对象的编程方式。&lt;/p&gt;
&lt;h3&gt;DRY&lt;/h3&gt;
&lt;p&gt;DRY，即Don&apos;t repeat yourself。当代码存在高度重复时（比如通过大量复制粘贴实现功能），就应该好好想想是不是有更好的解决方案了。在Python当中我们通常可以使用装饰器来解决这类问题。&lt;/p&gt;
&lt;h3&gt;综述&lt;/h3&gt;
&lt;p&gt;上述所有的最佳实践都可以归结为两个操作——“解耦”和“复用”。&lt;/p&gt;
&lt;p&gt;KISS强调简单性，而实现简单性的主要手段之一就是通过合理的解耦消除不必要的复杂依赖，通过有效的复用避免重复实现。YAGNI提醒我们解耦和复用应该基于实际需求而非臆测，防止为了解耦而解耦、为了复用而复用。SOLID和DRY则是为解耦和复用提供了具体的指导方针。而解耦和复用又是相关的。为了实现复用我们需要解耦，模块间充分解耦才能实现有效复用。&lt;/p&gt;
&lt;p&gt;这些原则最终都指向同一个目标：构建可维护、可扩展、健壮且易理解的软件系统。&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/3.12/tutorial/&quot;&gt;【Latest】The Python Tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/3.12/reference&quot;&gt;【Latest】The Python Language Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nostarch.com/python-crash-course-3rd-edition&quot;&gt;【2015】Python Crash Course, 3rd Edition&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://oreilly.com/catalog/errata.csp?isbn=9781449340377&quot;&gt;【2011】Python Cookbook, 3rd Edition&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.runoob.com/python3/python3-tutorial.html&quot;&gt;【Unknown】菜鸟教程 Python 3 教程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pycoders-weekly-chinese.readthedocs.io/en/latest/issue6/a-guide-to-pythons-magic-methods.html&quot;&gt;【2012】Python 魔术方法指南&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://flylib.com/books/en/4.444.1.71/1/&quot;&gt;【2006】A Simple DIP Example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;【2018】写了这么多年代码，你真的了解SOLID吗？By 梅雪松(该地址已不可达)：hxxps://insights.thoughtworks.cn/what-is-solid-principle/&lt;/li&gt;
&lt;li&gt;【2009】Python Types and Objects By Shalabh Chaturvedi(该地址已不可达)：hxxp://www.cafepy.com/article/python_types_and_objects/python_types_and_objects.html&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../images/not-by-ai.png&quot; alt=&quot;Not By AI&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>好用的注册表项（持续更新）</title><link>https://blog.ravenote.me/posts/useful_registry_key/</link><guid isPermaLink="true">https://blog.ravenote.me/posts/useful_registry_key/</guid><description>一些很实用的注册表项</description><pubDate>Thu, 23 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
修改注册表存在风险，请谨慎操作。错误修改注册表可能导致系统严重故障，甚至需要重装系统。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
感谢来自Disa的补充。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;自启动&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Run：每一次随着开机而启动。&lt;/li&gt;
&lt;li&gt;RunOnce：只作用一次，执行完毕后自动删除&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;用户级&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce\Setup
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunServices
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunServicesOnce
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run
HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\Windows\load
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;系统级&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce\Setup
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnceEx
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunServices
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunServicesOnce
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Userinit
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\RunOnce
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Image File Execution Options&lt;/h2&gt;
&lt;h3&gt;映像劫持&lt;/h3&gt;
&lt;p&gt;启动目标程序时，启动的是劫持后的程序而不是原来的程序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Option
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加一个名为目标进程的项，这个项中添加&lt;code&gt;debugger&lt;/code&gt;键，键值为劫持程序。&lt;/p&gt;
&lt;h3&gt;SilentProcessExit&lt;/h3&gt;
&lt;p&gt;当&lt;code&gt;ReportingMode&lt;/code&gt;为1的情况下，被监控进程结束时系统会执行&lt;code&gt;MonitorProcess&lt;/code&gt;指定的程序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SilentProcessExit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加一个名为目标进程的项，这个项中添加&lt;code&gt;MonitorProcess&lt;/code&gt;键，键值为劫持程序;添加&lt;code&gt;ReportingMode&lt;/code&gt;键，键值为1。&lt;/p&gt;
&lt;h3&gt;MinimumStackCommitInBytes&lt;/h3&gt;
&lt;p&gt;将&lt;code&gt;MinimumStackCommitInBytes&lt;/code&gt;设定为足够大的值可以让指定程序因堆栈溢出而崩溃。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\【目标进程】\MinimumStackCommitInBytes
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;计划任务&lt;/h2&gt;
&lt;h3&gt;层级信息&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若index为0，则在&lt;code&gt;taskschd.msc&lt;/code&gt;中不可见。&lt;/p&gt;
&lt;h3&gt;任务详情&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tasks
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;重启后删除文件&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个操作由一对字符串组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一个字符串是源文件或目录的路径。&lt;/li&gt;
&lt;li&gt;第二个字符串是目标文件或目录的路径（如果是删除操作，则为空）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\??\C:\OldFile.txt
\??\C:\NewFile.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表示将 C:\OldFile.txt 重命名为 C:\NewFile.txt。&lt;/p&gt;
</content:encoded></item><item><title>使用命令行解锁Bitlocker</title><link>https://blog.ravenote.me/posts/unlock_bitlocker_with_command_line/</link><guid isPermaLink="true">https://blog.ravenote.me/posts/unlock_bitlocker_with_command_line/</guid><description>命令行解锁Bitlocker</description><pubDate>Fri, 20 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;以下命令假设待解锁盘符为 C，且使用 48 位恢复密钥解锁。&lt;/p&gt;
&lt;p&gt;CMD：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; manage-bde.exe -unlock C: -recoverypassword xxxxxx-xxxxxx-xxxxxx-xxxxxx-xxxxxx-xxxxxx-xxxxxx-xxxxxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PowerShell：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; Unlock-BitLocker -MountPoint C -RecoveryPassword xxxxxx-xxxxxx-xxxxxx-xxxxxx-xxxxxx-xxxxxx-xxxxxx-xxxxxx
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>VPS 基本安全措施</title><link>https://blog.ravenote.me/posts/basic_security_measures_for_vps/</link><guid isPermaLink="true">https://blog.ravenote.me/posts/basic_security_measures_for_vps/</guid><description>拿到 VPS 后的基本安全措施</description><pubDate>Fri, 22 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文由本人自行转载的副本在：&lt;a href=&quot;https://linux.do/t/topic/267502&quot;&gt;https://linux.do/t/topic/267502&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;当我们拿到 VPS 之后，我们都需要做些什么呢？&lt;/p&gt;
&lt;p&gt;如果选择将网站 / 服务放在知名厂商虚拟主机上，虚拟主机的厂商会负责基本的服务器安全措施。但如果放在 VPS 上，那么你就是服务器的安全负责人了。更多的权限代表着更多的义务，VPS 在具有更高的自由度的情况下自然有更高的风险。&lt;strong&gt;而我们要做到的不是绝对安全，而是比大多数人安全。&lt;/strong&gt; 只要做到没那么容易被攻破那就是胜利。&lt;/p&gt;
&lt;p&gt;本文使用的环境为 Ubuntu 24.04 LTS，其他发行版可大致参照思路。&lt;/p&gt;
&lt;p&gt;在继续阅读本文之前，建议提前阅读由 &lt;a href=&quot;https://lug.ustc.edu.cn/&quot;&gt;LUG@USTC&lt;/a&gt; 编写的：&lt;a href=&quot;https://101.lug.ustc.edu.cn/&quot;&gt;https://101.lug.ustc.edu.cn/&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文与 &lt;a href=&quot;https://github.com/ustclug/Linux101-docs&quot;&gt;https://github.com/ustclug/Linux101-docs&lt;/a&gt; 项目无关&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;安全管理系统&lt;/h2&gt;
&lt;p&gt;有些 VPS 厂商默认提供的是 root 账户。众所周知，root 账户拥有整个系统最高的权限，这么高的权限自然是不安全的。正确的做法是创建一个非 root 账户，在必须使用 root 权限时使用 sudo 提权。&lt;/p&gt;
&lt;h3&gt;创建非 root 账户&lt;/h3&gt;
&lt;p&gt;使用以下命令创建一个具有提权能力的账户：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;useradd -m -G sudo -s /bin/bash 用户名
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们给这个用户设置一个至少为 16 位的随机大小写字母 + 数字的密码（个人建议的最低安全性需求）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;passwd 用户名
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;建议把 root 用户的密码也改掉，云服务器的默认密码强度还是差点，而且有些服务商会通过邮件来发送默认密码，不太安全。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;禁用 root SSH 密码登陆&lt;/h3&gt;
&lt;p&gt;先不提 root 登录本身就是危险的行为，root 账户的用户名固定为”root”，如果允许其通过密码登录，攻击者只需进行密码穷举即可尝试攻破系统。之前我们已经创建了非 root 账户，在这里我们只需要禁用 root 账户的 SSH 登录即可。&lt;/p&gt;
&lt;p&gt;执行以下命令编辑 SSH 配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vim /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进行如下设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 禁止 Root 用户通过密码远程登录
PermitRootLogin prohibit-password
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后重启 SSH 服务生效：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么不设置成 no：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;另，直接禁掉 root 登录 &lt;code&gt;PermitRootLogin no&lt;/code&gt; 我也不常用，更习惯和其他 sudoer 一样配密钥然后禁止密码登录仅允许密钥登录。我是一般能操做线下集群的机子才会这么配，不然出个故障没 root 用不了了，比如存储满了远程 ssh session 都建立不了。&lt;/p&gt;
&lt;p&gt;理论上 PermitRootLogin 是好的，但在服务器寄掉需要救援时就会变麻烦，我其实更推荐只关闭远程密码登录，保留本地密码登录，出问题时可以通过 vnc 或者 ipmi 本地登录 root 进行救援。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;修改 SSH 端口号&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;首先就要把 22 端口改了。&lt;/p&gt;
&lt;p&gt;这是参照原帖里的基础上补充～&lt;/p&gt;
&lt;p&gt;实际端口一般登上去就会改掉&lt;/p&gt;
&lt;p&gt;登上去第一件事就是改 SSHD 的 端口并修改防火墙，22 全是猜密码的请求。&lt;/p&gt;
&lt;p&gt;有个小技巧，就是改端口并重启 sshd 后当前的连接并没有断，向新端口发请求，能连上就是修改正确，要是不能建立新连接，还可以改回来，或查下防火墙的配置。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;正常情况下，直接通过 &lt;code&gt;sudo vim /etc/ssh/sshd_config&lt;/code&gt; 修改 SSH 端口，然后再使用 &lt;code&gt;sudo systemctl restart ssh.service&lt;/code&gt; 重启 SSH 服务应用更改是可行的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 设置 SSH 端口
Port 自拟
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是在 &lt;strong&gt;Ubuntu 22.10 或更高版本&lt;/strong&gt; 中各位可能发现这是 &lt;strong&gt;无效&lt;/strong&gt; 的，各位会发现 SSH 服务在重启后依然监听原端口。&lt;/p&gt;
&lt;p&gt;因为在 Ubuntu 22.10 或更高版本中，ssh 默认通过套接字激活。&lt;/p&gt;
&lt;p&gt;在 Ubuntu 22.10、Ubuntu 23.04 和 Ubuntu 23.10 中进行修改的方法是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo mkdir -p /etc/systemd/system/ssh.socket.d
sudo vim /etc/systemd/system/ssh.socket.d/listen.conf
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
sudo systemctl restart ssh.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;listen.conf 的参考配置为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Socket]
ListenStream=
ListenStream=2233
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Ubuntu 24.04 中进行修改的方法是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vim /etc/ssh/sshd_config
sudo systemctl daemon-reload
sudo systemctl restart ssh.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不在乎通过套接字激活节省的内存，可以通过以下命令恢复到非套接字激活：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
务必确认配置文件正常。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl disable --now ssh.socket
sudo systemctl enable --now ssh.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如有配置迁移（Ubuntu 22.10 及以上，Ubuntu 24.04 以下）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl disable --now ssh.socket
rm -f /etc/systemd/system/ssh.service.d/00-socket.conf
rm -f /etc/systemd/system/ssh.socket.d/addresses.conf
sudo systemctl daemon-reload
sudo systemctl enable --now ssh.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;建议在 &lt;code&gt;/etc/ssh/sshd_config.d/&lt;/code&gt; 创建一个 conf 文件来自定义 sshd 配置，而不是直接编辑 &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;，防止 OpenSSH 更新后配置冲突。&lt;/p&gt;
&lt;p&gt;参考： &lt;a href=&quot;https://unix.stackexchange.com/questions/727492/passwordauthentication-no-but-i-can-still-login-by-password/727500#727500&quot;&gt;ssh - PasswordAuthentication no, but I can still login by password - Unix &amp;amp; Linux Stack Exchange&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;有些云服务商为了启用远程密码登录（sshd 默认禁用 ），会在 &lt;code&gt;/etc/ssh/sshd_config.d/&lt;/code&gt; 自定义一个 conf 文件，修改 sshd 配置前先要排除它们的干扰。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看 sshd_config.d 是否存在其他 conf 文件
sudo ls /etc/ssh/sshd_config.d/*.conf
# 如果存在，重命名，防止后续自定义配置被覆盖
sudo mv /etc/ssh/sshd_config.d/xxx.conf/etc/ssh/sshd_config.d/xxx.conf.bak
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如 CloudCone 就有一个 &lt;code&gt;/etc/ssh/sshd_config.d/50-cloud-init.conf&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;sshd 配置修改完，先用 &lt;code&gt;sudo sshd -T&lt;/code&gt; 看一下有效配置，免得被覆盖了都不知道:joy:。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Root 用户登录方式
sudo sshd -T | grep -i &quot;PermitRootLogin&quot;
# 密码认证
sudo sshd -T | grep -i &quot;PasswordAuthentication&quot;
# ssh 端口
sudo sshd -T | grep -i &quot;Port&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;值得一提的是，&lt;code&gt;prohibit-password&lt;/code&gt; 是 &lt;code&gt;without-password&lt;/code&gt; 的别名，所以看到下面的输出是正常现象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sudo sshd -T | grep -i &quot;PermitRootLogin&quot;
permitrootlogin without-password
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://unix.stackexchange.com/questions/99307/permit-root-to-login-via-ssh-only-with-key-based-authentication/99308#99308&quot;&gt;Permit root to login via ssh only with key-based authentication - Unix &amp;amp; Linux Stack Exchange&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.openssh.com/txt/release-7.0&quot;&gt;OpenSSH 7.0 更新日志&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fail2ban 防暴力破解 SSH&lt;/h3&gt;
&lt;p&gt;执行以下命令安装 Fail2ban：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install fail2ban
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;官方推荐的做法是利用 jail.local 来进行自定义设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vim /etc/fail2ban/jail.local
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以参照以下配置文件来进行自己的配置（记得删注释）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[sshd]
ignoreip = 127.0.0.1/8 # 白名单
enabled = true
filter = sshd
port = 22 # 端口，改了的话这里也要改
maxretry = 5 # 最大尝试次数
findtime = 300 # 多少秒以内最大尝试次数规则生效
bantime = 600 # 封禁多少秒，-1 是永久封禁（不建议永久封禁）
action = %(action_) s [port=&quot;%(port) s&quot;, protocol=&quot;%(protocol) s&quot;, logpath=&quot;%(logpath) s&quot;, chain=&quot;%(chain) s&quot;] # 不需要发邮件通知就这样设置
banaction = iptables-multiport # 禁用方式
logpath = /var/log/auth.log # SSH 登陆日志位置
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;通知服务器 SSH 登录&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;很有用，我还加了一个在登录 ssh 时候自动发通知到企业微信机器人，以防偷家不知道&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可以通过 PAM 模块在每次 ssh 登录时触发脚本来实现。&lt;/p&gt;
&lt;p&gt;编辑 &lt;code&gt;/etc/pam.d/sshd&lt;/code&gt;，在文件末尾添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;session    optional    pam_exec.so 脚本路径
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于提到的用例，脚本大致如下：
参照：&lt;a href=&quot;https://developer.work.weixin.qq.com/document/path/91770&quot;&gt;https://developer.work.weixin.qq.com/document/path/91770&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;2024 年 11 月 26 日注：修复了示例脚本的一些问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

if [ &quot;$PAM_TYPE&quot; != &quot;open_session&quot; ]; then
    exit 0
fi

ip=$PAM_RHOST
date=$(date +&quot;% e % b % Y, % a % r&quot;)
name=$PAM_USER

webhook_url=&quot;https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxxxxxxxxx&quot;

curl -s -X POST &quot;$webhook_url&quot; \
    -H &quot;Content-Type: application/json&quot; \
    -d &quot;{
    \&quot;msgtype\&quot;: \&quot;markdown\&quot;,
    \&quot;markdown\&quot;: {
        \&quot;content\&quot;: \&quot;** 登录提醒 **\\n&amp;gt; 登录用户: $name\\n&amp;gt; 客户端 IP: $ip\\n&amp;gt; 登录时间: $date\&quot;
    }
}&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请根据自己的用例替换 api 及调用方式。&lt;/p&gt;
&lt;p&gt;写完了脚本需要给脚本加上运行权限。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sudo chmod +x /usr/local/bin/notify_ssh_login.sh&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;【严格】使用绝对路径运行命令&lt;/h3&gt;
&lt;p&gt;使用绝对路径可以精准指定要运行的程序或文件，可避免因环境变量被篡改等原因误执行潜在的恶意程序。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;使用 su 的完整路径可以避免运行潜在的攻击者植入的 su。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;使用密钥登录&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
如果 VPS 厂商提供了 SSH 密钥绑定功能，可以忽略本节内容并按照 VPS 厂商提供的方法绑定。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 powershell 中运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t ed25519
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接使用默认的密钥路径即可。密码可以留空，也可以设置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Generating public/private ed25519 key pair.
Enter file in which to save the key (C:\Users\&amp;lt;user&amp;gt;/.ssh/id_ed25519): # 直接回车
Enter passphrase (empty for no passphrase): # 可以留空，也可以设置
Enter same passphrase again: # 和上一个一样
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们在 VPS 上编辑 SSH 授权密钥文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim ~/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后打开 C:\Users&amp;lt;user&amp;gt;/.ssh/id_ed25519.pub，复制其内容并粘贴过去。&lt;/p&gt;
&lt;p&gt;执行以下命令编辑 SSH 配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vim /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进行如下设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
PasswordAuthentication no
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后重启 SSH 服务生效：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;启用 UFW 防火墙&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
如果 VPS 厂商提供了防火墙功能，且没有复杂的需求，可以忽略本节内容并使用 VPS 厂商提供的防火墙。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在正式启用 UFW 之前，我们需要先设置规则。我们首先来设置 UFW 的默认行为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw default allow outgoing # 默认允许所有数据出站
sudo ufw default deny incoming # 默认禁止所有数据入站
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以通过以下命令查看 UFW 当前生效的规则：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw status
sudo ufw status numbered  # 加上数字编号
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以通过以下命令允许或拒绝某端口的传入 / 传出流量（部分以 22、80、443 端口为例）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 允许 22 端口的 proto 协议的流量入站
sudo ufw allow in 22/proto

# 允许 22 端口的 proto 协议的流量出站
sudo ufw allow out 22/proto

# 在未指定 in/out 的情况下，默认为 in
sudo ufw allow 22/proto

# 在未指定 proto 的情况下，默认为 tcp 和 udp
sudo ufw allow 22

# 拒绝的话就把 allow 改成 deny
sudo ufw deny 22

# 允许从 start_port 到 end_port 的端口
sudo ufw allow start_port:end_port

# 允许复数个端口，以英文逗号分隔
sudo ufw allow port1,port2

# 允许来自于特定 ip 或 cidr 段的流量
sudo ufw allow from ip/cidr

# 允许来自于特定 ip 或 cidr 段端口 22 的流量
sudo ufw allow from ip/cidr to any port 22

# 允许来自于特定 ip 或 cidr 段端口 22 的 tcp 协议的流量
sudo ufw allow from ip/cidr to any proto tcp port 22

# 如果指定复数个端口，则必须指定协议
sudo ufw allow from ip to any proto tcp port 80,443

# comment 用于注释
sudo ufw allow from ip to any proto tcp port 80,443 comment &quot;hello&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以通过以下命令删除生效的规则：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw delete allow 22 # 在规则前面加个 delete
sudo ufw delete 1 # 按照 numbered 的编号删除也行
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在确定所有规则均成功设置后，通过以下命令启动 \ 关闭 \ 重启 UFW&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
启动防火墙前务必保证 22 端口（或者其他 SSH 端口）被放行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw enable|disable|reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要重置规则，请使用：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
重置规则前务必保证 UFW 处于关闭状态。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw reset
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本人建议仅放行正在使用的端口，比如 22、80、443。&lt;/p&gt;
&lt;p&gt;默认情况下，UFW 仅记录不符合规则的被拒绝的数据包。如果需要记录与该服务相关的每个详细信息，可以在 allow 后加上 log 以进行记录。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 成功连接 ssh 的也记一下日志备查比较好
ufw allow log 22/tcp
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;禁止 ping 服务器&lt;/h3&gt;
&lt;h4&gt;使用 iptables 禁止 ping&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;查看当前 iptables 规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;iptables -L -n&lt;/li&gt;
&lt;li&gt;这条命令可以列出当前 iptables 的规则，-L 表示列出规则，-n 表示以数字形式显示 IP 地址和端口号，而不是解析为主机名和服务名。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;禁止 ping（入站 ICMP Echo Request）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;iptables -A INPUT -p icmp --icmp - type 8 -j DROP&lt;/li&gt;
&lt;li&gt;这里 - A INPUT 表示在 INPUT 链（用于处理进入本机的数据包）的末尾添加一条规则。-p icmp 指定协议为 ICMP，--icmp - type 8 表示 ICMP 类型为 8（即 Echo Request），-j DROP 表示将匹配的数据包丢弃。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;保存 iptables 规则（如果需要永久生效）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于不同的 Linux 发行版，保存 iptables 规则的方式不同。&lt;/li&gt;
&lt;li&gt;在 CentOS 等基于 RHEL 的系统中，可以使用 service iptables save 命令来保存规则。这条命令会将当前的 iptables 规则保存到 /etc/sysconfig/iptables 文件中，这样在系统重启后规则依然生效。&lt;/li&gt;
&lt;li&gt;在 Ubuntu 等 Debian 系系统中，需要安装 iptables - persistent 软件包。安装后可以使用 iptables - save &amp;gt; /etc/iptables/rules.v4（对于 IPv4 规则）来保存规则，这样在系统重启后也能恢复规则。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;恢复 ping 功能&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果要恢复 ping 功能，可以删除刚才添加的禁止 ping 的规则。使用 iptables -D INPUT -p icmp --icmp - type 8 -j DROP 命令。其中 - D INPUT 表示从 INPUT 链中删除规则，其他参数和添加规则时相同。&lt;/p&gt;
&lt;h4&gt;使用 ufw 禁止 ping&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
保守起见，与 destination-unreachable、time-exceeded、parameter-problem 相关的规则已移除。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;ufw 本身没有直接支持阻止 icmp 协议的命令。ufw 在 /etc/ufw/before.rules 中定义了针对 ping 的允许规则，我们可以修改 ACCEPT 为 DROP 来达成禁止 ping 的目的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-A ufw-before-input -p icmp --icmp-type echo-request -j DROP
-A ufw-before-forward -p icmp --icmp-type echo-request -j DROP
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;佬，这个好像只能禁 v4 的，发现 v6 还可以 ping&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果需要设置 ipv6 的禁 ping 规则，可以修改 &lt;code&gt;/etc/ufw/before6.rules&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-A ufw6-before-output -p icmpv6 --icmpv6-type echo-request -j DROP
-A ufw6-before-output -p icmpv6 --icmpv6-type echo-reply -j DROP
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!INFO]
其余 icmpv6 规则应保持不变&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;限定 SSH 登录 IP&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;我办公室的网络是固定 ip，VPS 运营商的防火墙开 22 端口然后指定 IP 地址，这样以后需要别的 IP 访问的话，直接添加 IP 即可&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果拥有动态公网 IP 且厂商支持通过接口修改防火墙规则，可以直接使用厂商的接口。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果是家里没有固定 ip 的可以使用类似腾讯云的 &lt;a href=&quot;https://orcaterm.cloud.tencent.com/&quot;&gt;orcaterm&lt;/a&gt; 终端来访问，基础功能是永久免费的，这是 &lt;a href=&quot;https://www.tencentcloud.com/zh/document/product/213/39708?lang=zh&quot;&gt;orcaterm 的 IP 段&lt;/a&gt;，然后用 ipset 把这些 IP 段添加进去，设置为仅这些 IP 可以访问 ssh 端口即可，记得设置之前先测试是否可以正常使用连接。如果是腾讯云用户，安全组里甚至可以把 ssh 端口直接关掉，之前用腾讯云的时候就只开了 80 和 443 端口&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也可以使用 ufw 进行设置。&lt;/p&gt;
&lt;p&gt;还可以使用 VPS 厂商提供的防火墙（如果支持），如果出现连接问题更方便更改配置。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://orcaterm.cloud.tencent.com/&quot;&gt;orcaterm&lt;/a&gt; 添加其他厂商云服务器步骤：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/2.webp&quot; alt=&quot;step1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/3.webp&quot; alt=&quot;step2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/4.webp&quot; alt=&quot;step3&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;保证软件更新&lt;/h2&gt;
&lt;h3&gt;日常更新系统&lt;/h3&gt;
&lt;p&gt;个人建议定期登录 VPS 运行 &lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade&lt;/code&gt; 来保证 VPS 内所有软件包均为最新。&lt;/p&gt;
&lt;p&gt;不过 Ubuntu 默认会每天自动安装系统的安全更新，所以说这个频率没必要太勤。&lt;/p&gt;
&lt;h3&gt;开启 Ubuntu Pro&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;同样出色的操作系统，更多的安全更新
将平均 CVE（通用漏洞披露）暴露时间从 98 天减少到 1 天
具备扩展的 CVE 补丁、十年的安全维护、可选的支持和对整个开源应用程序堆栈的维护。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上面是 Ubuntu 官方的广告词。到底多有用我不知道，有修复总比没修复好，而且个人免费 5 台机器，开了不亏。&lt;/p&gt;
&lt;p&gt;我们先来创建一个 Ubuntu One 帐户：&lt;a href=&quot;https://ubuntu.com/login&quot;&gt;https://ubuntu.com/login&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;注册结束之后，转到 &lt;a href=&quot;https://ubuntu.com/pro/dashboard&quot;&gt;https://ubuntu.com/pro/dashboard&lt;/a&gt; 查看 Token。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/1.jpg&quot; alt=&quot;Ubuntu Pro&quot; /&gt;&lt;/p&gt;
&lt;p&gt;得到了 Token 之后，前往我们的 VPS，运行 &lt;code&gt;sudo pro attach [YOUR_TOKEN]&lt;/code&gt;。等待一段时间，我们的 VPS 就成功开启 Ubuntu Pro 了。建议在开启之后再运行一次 &lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade&lt;/code&gt; 以确保系统安装了最新的安全更新。&lt;/p&gt;
&lt;h2&gt;隐藏公网 IP&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;隐藏公网 IP 并不是所有 VPS 使用者的共同安全需求，有一个胡诌的针对未来（指 ipv6 广泛使用）的方案就是只暴露源站 v6 地址给 CDN 用，这样 Censys 这样强扫的工具耗时会很长，不过也还是要配白名单。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;防止 SSL 证书泄露 IP&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;本节 “防止 SSL 证书泄露 IP” 引自 “如何避免证书泄露源站 IP”，作者为秋未萌，根据 CC BY-SA 4.0 授权协议发布。本文其他部分根据 CC BY-NC-SA 4.0 授权协议发布，但本节内容使用 CC BY-SA 4.0 授权协议。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;申请并下载证书&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;注册并且登录 &lt;a href=&quot;https://app.zerossl.com/login&quot;&gt;ZeroSSL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;到 &lt;a href=&quot;https://app.zerossl.com/dashboard&quot;&gt;Dashboard&lt;/a&gt; 找到 Create SSL Certificate，点击 New Certificate 蓝色按钮&lt;/li&gt;
&lt;li&gt;在 Enter Domains 处输入源站 IP&lt;/li&gt;
&lt;li&gt;其实过期也无妨，总之不让 censys 扫描到真正的域名证书就可以。因此选择 90 天证书&lt;/li&gt;
&lt;li&gt;CSR &amp;amp; Contat 保持不变&lt;/li&gt;
&lt;li&gt;验证域名的办法选择 HTTP File Upload
&lt;ul&gt;
&lt;li&gt;使用 NGINX 的话，如果你保持原先设置不变，即 &lt;code&gt;/etc/nginx/sites-available/default&lt;/code&gt; 不变就没问题。当然，如果你改变了，记得保留 &lt;code&gt;server {listen 80; root /var/www/html;}&lt;/code&gt; 就可以&lt;/li&gt;
&lt;li&gt;然后下载 Auth File，并且把它上传到 &lt;code&gt;/var/www/html/.well-known/pki-validation&lt;/code&gt;。如果没有文件夹，就新建，记得让 NGINX 有权访问这些文件，否则还是会失败&lt;/li&gt;
&lt;li&gt;按照提示，点击一下 &lt;code&gt;.txt&lt;/code&gt; 文件是不是可以访问。成功的话，就验证好了&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;配置证书并且设置禁止 IP 80/433 的 HTTP 访问&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;下载 &lt;code&gt;*.zip&lt;/code&gt; 文件，解压它。解压好的文件夹里面有好申请到的证书&lt;/li&gt;
&lt;li&gt;将 &lt;code&gt;ca_bundle.crt&lt;/code&gt; 和 &lt;code&gt;certificate.crt&lt;/code&gt; 合并，方法是用 notepad3 或者 vsode 等可靠的编辑器打开 &lt;code&gt;certificate.crt&lt;/code&gt;。然后把 &lt;code&gt;ca_bundle.crt&lt;/code&gt; 内容复制进去。格式是：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;-----BEGIN CERTIFICATE-----
certificate.crt 内容
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
ca_bundle.crt 内容
-----END CERTIFICATE-----
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;上传合并好的文件和 &lt;code&gt;private.key&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;上传证书到一个 Nginx 有权限的文件夹，咱放在 &lt;code&gt;/etc/nginx/ip-certificate/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;设置 &lt;code&gt;/etc/nginx/sites-available/default&lt;/code&gt; 文件。参考设置如下：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;server {    
#HTTP Server Default Set    
    listen 80;
    listen 443 ssl http2 default_server;
    server_name ip;     

    #HTTP_TO_HTTPS_END    
    ssl_certificate /etc/nginx/ip-certificate/certificate.crt;
    ssl_certificate_key /etc/nginx/ip-certificate/private.key;
    ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    
    #Server ROOT
    index index.html;
    root /var/www/html/;
    index index.html;

    return 444; #NGINX HTPP Code 444
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务器后台输入 &lt;code&gt;nginx -t&lt;/code&gt; 检查是否有误，正确即可 &lt;code&gt;sudo systemctl restart nginx&lt;/code&gt; 重启。&lt;/p&gt;
&lt;p&gt;直接输入 IP 作为网址检查是不是连接后立刻变成空白页&lt;/p&gt;
&lt;h3&gt;Nginx 1.19.4 之后的新方法&lt;/h3&gt;
&lt;p&gt;如果各位的 Nginx 版本大于等于 1.19.4，可以直接使用 ssl_reject_handshake 配置项，通过简单的配置就能拒绝所有未匹配到域名的 TLS 握手：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
    listen 443 ssl default_server;
    server_name _;
    ssl_reject_handshake on;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果出现如 &lt;code&gt;nginx: [emerg] unknown directive “ssl_reject_handshake”&lt;/code&gt; 的报错，首先请检查 nginx 版本是否大于等于 1.19.4。&lt;code&gt;ssl_reject_handshake on;&lt;/code&gt; 在 nginx 1.19.4 主线版加入。&lt;/p&gt;
&lt;p&gt;如果是通过源代码编译安装的，请确认 ngx_http_ssl_module 模块是否启用。 该模块不是默认构建的，需要通过 &lt;code&gt;--with-http_ssl_module&lt;/code&gt; 配置参数来启用。&lt;/p&gt;
&lt;h3&gt;真的安全了吗&lt;/h3&gt;
&lt;p&gt;前文中，我们只能确保攻击者无法通过直接访问 ip 获取默认证书来推断域名信息。然而又没有规定说攻击者只能用这种方式获取 IP 与域名的对应关系。可以看出，前文的规则依赖于 server_name 的匹配。攻击者完全可以携带正确的 server_name 握遍所有可能的非已知 CDN 的 IP 段，记录正确响应的目标。下面是判断（不包含遍历）的简单实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# https://gist.github.com/Raven95676/39ffdba22144e39d7155ad9dc1bcca55
import ssl
import socket
def check (ip, domain):
    try:
        context = ssl.create_default_context ()
         with socket.create_connection ((ip, 443)) as sock:
            with context.wrap_socket (sock, server_hostname=domain) as ssl_sock:
                request = f&quot;GET / HTTP/1.1\nHost: {domain}\nConnection: close\n\n&quot;
                ssl_sock.sendall (request.encode ())
                ssl_sock.recv (4096)
        return True
    except Exception:
        return False

print (check (&quot;192.168.0.256&quot;, &quot;example.com&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;于是我们打个补丁&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
如果 VPS 厂商提供了防火墙功能，可以直接使用 VPS 厂商提供的防火墙。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;s&gt;各位可以直接用这个方法&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;我们可以仅允许 CDN 的 CIDR 段访问服务器的 80/443 端口。先来添加允许的规则：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw allow from &quot;cidr 段&quot; to any proto tcp port 80,443 comment &quot;CDN 服务商&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;comment 不是必须的，只是为了方便日后能认出来这规则是干什么的才加上的。&lt;/p&gt;
&lt;p&gt;CDN 的 CIDR 段去找 cdn 服务商要，有的在官网也有公示。比如 Cloudflare 的 CIDR 段可以从这个网页获取：&lt;a href=&quot;https://www.cloudflare.com/ips-v4&quot;&gt;https://www.cloudflare.com/ips-v4&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;上面那个是 ipv4 的，ipv6 的可以从这个网页获取：&lt;a href=&quot;https://www.cloudflare.com/ips-v6&quot;&gt;https://www.cloudflare.com/ips-v6&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;对于 Cloudflare ，还有佬友编写的脚本可以在执行后更新 CF IP 到 ufw 443。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RULES=$(sudo ufw status numbered | grep &apos;Cloudflare IP&apos; | awk -F&quot;[][]&quot; &apos;{print $2}&apos; | sort -nr)
for RULE in $RULES; do
    echo &quot;Deleting rule $RULE&quot;
    echo &quot;y&quot; | sudo ufw delete $RULE
done

for cfip in `curl -sw &apos;\n&apos; https://www.cloudflare.com/ips-v {4,6}`; do ufw allow proto tcp from $cfip to any port 443 comment &apos;Cloudflare IP&apos;; done

ufw reload &amp;gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于特殊情况的存在，本人建议在部署该脚本之前先执行以下命令查看是否正确输出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for cfip in `curl -sw &apos;\n&apos; https://www.cloudflare.com/ips-v {4,6}`; do echo $cfip; done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果输出正常（如下），则可以部署：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;173.245.48.0/20
103.21.244.0/22
… 略
2400:cb00::/32
2606:4700::/32
… 略
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加完了之后我们使用以下命令查看防火墙现有规则列表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw status numbered
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用以下命令删除之前可能存在的针对 80 和 443 端口允许所有流量的规则：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw delete 序号
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么不直接使用 Nginx 的 deny 配置项呢？因为 deny 会返回 403 Forbidden 状态码，而在此之前必须完成 TLS 握手。只有在 TLS 握手成功后，客户端才能发送 HTTP 请求并接收到响应。如果直接使用 deny，我们相当于在做无用功。&lt;/p&gt;
&lt;h3&gt;真的安全了…… 吗？&lt;/h3&gt;
&lt;p&gt;这其实算是 Cloudflare 特辑，不过如果使用其他 CDN 提供商，为了增强安全性也可以参考。众所周知，Cloudflare 不仅提供 CDN 服务，还有一系列其他产品，比如 Workers 和 WARP。而这些服务有一些需要注意的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;能对外发出请求&lt;/li&gt;
&lt;li&gt;用的是 Cloudflare 的 IP 段&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然 Cloudflare 对于滥用肯定是限制的，但是为了以防万一，我们还可以再做点安全措施 —— 经过身份验证的源服务器拉取。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!INFO]
必须确保 SSL/TLS 加密模式为完全或者完全（严格）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下载&lt;a href=&quot;https://developers.cloudflare.com/ssl/static/authenticated_origin_pull_ca.pem&quot;&gt;Cloudflare证书&lt;/a&gt;并进行配置即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssl_client_certificate 证书地址；
ssl_verify_client on;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 SSL/TLS→源服务器这里开启经过身份验证的源服务器拉取。&lt;/p&gt;
&lt;p&gt;雷池 WAF 社区版目前暂时无法稳定使用此方法，除非乐意在雷池前面再叠一层反代。&lt;/p&gt;
&lt;h2&gt;迈向全面容器化&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;我的建议是善用容器化技术进行隔离&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;本节暂不涉及 1panel 等运维面板或 portainer 等 docker 面板。&lt;/p&gt;
&lt;h3&gt;Docker 基本使用&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
发现成熟的文档，不再重复造轮子。&lt;/p&gt;
&lt;p&gt;本文与 &lt;a href=&quot;https://github.com/yeasy/docker_practice&quot;&gt;https://github.com/yeasy/docker_practice&lt;/a&gt; 项目无关&lt;/p&gt;
&lt;p&gt;&lt;s&gt;小小的偷个懒&lt;/s&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;附注：如果 &lt;code&gt;docker-compose&lt;/code&gt; 提示找不到命令，试试 &lt;code&gt;docker compose&lt;/code&gt;。docker compose 现已弃用 &lt;code&gt;version&lt;/code&gt; 字段，现有的 compose 配置文件中 &lt;code&gt;version&lt;/code&gt; 字段将被忽略并显示一条警告。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://yeasy.gitbook.io/docker_practice&quot;&gt;https://yeasy.gitbook.io/docker_practice&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;更新：添加 Docker 官中文档。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.docker.net.cn/manuals/&quot;&gt;https://docs.docker.net.cn/manuals/&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;UFW 管不了 Docker 的解决方案（推荐）&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;再写一点关于 docker 的问题吧，其实没必要用 ufw 来管理 docker 的端口开放，docker 会自己写入 iptables 规则用以管控端口，以 docker compose 为例：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;1. 对于数据库或者 redis 之类仅在应用内使用的服务&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;仅在容器内部网络内开放，compose 拉起时会创建一个名为 &lt;code&gt;第一个服务名_default&lt;/code&gt; 的 bridge，可以用 &lt;code&gt;docker network ls&lt;/code&gt; 查看。&lt;/li&gt;
&lt;li&gt;容器默认对该内部网络开放所有端口。&lt;/li&gt;
&lt;li&gt;同一网络内的其他容器可以通过容器名和端口访问该服务。&lt;/li&gt;
&lt;li&gt;没有加入该网络的容器无法访问任何端口。&lt;/li&gt;
&lt;li&gt;对于仅在内部网络中暴露端口的服务，&lt;strong&gt;无需&lt;/strong&gt; 在 &lt;code&gt;ports&lt;/code&gt; 下指定任何映射&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 对于需要进行反代的服务&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;仅在 127.0.0.1 监听即可防止外部端口访问，在对应容器处使用&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;ports:
    - 127.0.0.1: 端口：端口
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 对于需要外部直接访问端口的服务&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;直接使用&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;ports:
      - 端口：端口
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的，贴一个示例 compose.yml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
    rsshub:
        image: diygod/rsshub:latest
        restart: always
        ports:
            - 127.0.0.1:1200:1200
        environment:
            NODE_ENV: production
            CACHE_TYPE: redis
            REDIS_URL: &quot;redis://redis:6379/&quot;
            PUPPETEER_WS_ENDPOINT: &quot;ws://browserless:3000&quot;
        env_file:
            - .env
        healthcheck:
            test: [&quot;CMD&quot;, &quot;curl&quot;, &quot;-f&quot;, &quot;http://localhost:1200/healthz&quot;]
            interval: 30s
            timeout: 10s
            retries: 3
        depends_on:
            - redis
            - browserless
 
    browserless:
        image: browserless/chrome
        restart: always
        ulimits:
            core:
                hard: 0
                soft: 0
        healthcheck:
            test: [&quot;CMD&quot;, &quot;curl&quot;, &quot;-f&quot;, &quot;http://localhost:3000/pressure&quot;]
            interval: 30s
            timeout: 10s
            retries: 3
 
    redis:
        image: redis:alpine
        restart: always
        volumes:
            - ./data:/data
        healthcheck:
            test: [&quot;CMD&quot;, &quot;redis-cli&quot;, &quot;ping&quot;]
            interval: 30s
            timeout: 10s
            retries: 5
            start_period: 5s
    
    sb:
        image: ghcr.io/sagernet/sing-box
        container_name: sb
        restart: always
        volumes:
            - ./sing-box:/etc/sing-box/
        command: -D /var/lib/sing-box -C /etc/sing-box/run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此外，如果需要阻止外部端口访问，除去仅在 127.0.0.1 监听外还可以：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;需要 Nginx/Caddy 等反代的服务，在 Compose 文件里可以配置一个 Docker 专用的 172 开头的内网 ip，且不配置端口。这样 Docker 就不会暴露公共端口，反代地址写 内网 ip: 默认端口即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;比如 MySQL 的 Compose 文件配置，指定了使用内网 IP 为 &lt;code&gt;172.20.0.15&lt;/code&gt;，注释了端口映射，在配置反向代理时 直接反代 &lt;code&gt;172.20.0.15:3306&lt;/code&gt; 即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;networks:
  default:
    external: true
    name: ${DOCKER_MY_NETWORK}
services:
  mysql:
    container_name: mysql8
    image: mysql:8
    # ports:
    #   - &quot;3306:3306&quot;
    environment:
      TZ: Asia/Shanghai
    networks:
      default:
        ipv4_address: 172.20.0.15
    restart: unless-stopped
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;UFW 管不了 Docker 的解决方案（不太推荐）&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;本节 “UFW 管不了 Docker 的解决方案（不太推荐）” 引自项目 &lt;strong&gt;&lt;a href=&quot;https://github.com/chaifeng/ufw-docker&quot;&gt;ufw-docker&lt;/a&gt;&lt;/strong&gt; 的 README，作者为 &lt;a href=&quot;https://github.com/chaifeng&quot;&gt;chaifeng&lt;/a&gt;，根据 GPL-3.0 license 授权协议发布。本文其他部分根据 CC BY-NC-SA 4.0 授权协议发布，但本节内容使用 GPL-3.0 license 授权协议。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;目前新的解决方案只需要修改一个 UFW 配置文件即可，Docker 的所有配置和选项都保持默认。&lt;/p&gt;
&lt;p&gt;修改 UFW 的配置文件 &lt;code&gt;/etc/ufw/after.rules&lt;/code&gt;，在最后添加上如下规则：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:ufw-docker-logging-deny - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -j ufw-user-forward

-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16

-A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN

-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 172.16.0.0/12

-A DOCKER-USER -j RETURN

-A ufw-docker-logging-deny -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix &quot;[UFW DOCKER BLOCK]&quot;
-A ufw-docker-logging-deny -j DROP

COMMIT
# END UFW AND DOCKER
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后重启 UFW，&lt;code&gt;sudo systemctl restart ufw&lt;/code&gt;。现在外部就已经无法访问 Docker 发布出来的任何端口了，但是容器内部以及私有网络地址上可以正常互相访问，而且容器也可以正常访问外部的网络。&lt;strong&gt;可能由于某些未知原因，重启 UFW 之后规则也无法生效，请重启服务器。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果希望允许外部网络访问 Docker 容器提供的服务，比如有一个容器的服务端口是 &lt;code&gt;80&lt;/code&gt;。那就可以用以下命令来允许外部网络访问这个服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ufw route allow proto tcp from any to any port 80
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个命令会允许外部网络访问所有用 Docker 发布出来的并且内部服务端口为 &lt;code&gt;80&lt;/code&gt; 的所有服务。&lt;/p&gt;
&lt;p&gt;请注意，这个端口 &lt;code&gt;80&lt;/code&gt; 是容器的端口，而非使用 &lt;code&gt;-p 0.0.0.0:8080:80&lt;/code&gt; 选项发布在服务器上的 &lt;code&gt;8080&lt;/code&gt; 端口。&lt;/p&gt;
&lt;p&gt;如果有多个容器的服务端口为 80，但只希望外部网络访问某个特定的容器。比如该容器的私有地址为 &lt;code&gt;172.17.0.2&lt;/code&gt;，就用类似下面的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ufw route allow proto tcp from any to 172.17.0.2 port 80
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果一个容器的服务是 UDP 协议，假如是 DNS 服务，可以用下面的命令来允许外部网络访问所有发布出来的 DNS 服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ufw route allow proto udp from any to any port 53
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的，如果只针对一个特定的容器，比如 IP 地址为 &lt;code&gt;172.17.0.2&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ufw route allow proto udp from any to 172.17.0.2 port 53
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;长亭雷池 WAF+Cloudflare 单节点部署&lt;/h2&gt;
&lt;p&gt;&lt;s&gt;思来想去没啥可写的&lt;/s&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对安全要求更高的话可以像我一样再套上 WAF + 蜜罐。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;部署&lt;/h3&gt;
&lt;p&gt;有一键部署命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bash -c &quot;$(curl -fsSLk https://waf-ce.chaitin.cn/release/latest/setup.sh)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或部署 LTS 版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RELEASE=lts bash -c &quot;$(curl -fsSLk https://waf-ce.chaitin.cn/release/latest/setup.sh)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于本用例在雷池 WAF 前套了一层 Cloudflare，所以说需要将防护站点→全局配置→源 IP 获取方式设置为 “取 X-Forwarded-For 中上一级代理的地址”&lt;/p&gt;
&lt;p&gt;如果出现其他问题，可前往官方文档的 &lt;a href=&quot;https://docs.waf-ce.chaitin.cn/zh/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5&quot;&gt;常见问题排查&lt;/a&gt; 查询。&lt;/p&gt;
&lt;h3&gt;使用域名访问雷池控制台的问题&lt;/h3&gt;
&lt;p&gt;&lt;s&gt;最佳解决方案：不用域名&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;如果我们需要通过域名访问雷池控制台，会发现启用 Cloudflare 小黄云后，域名访问无法实现。这是因为 Cloudflare 的端口转发仅支持其指定的几个端口：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Cloudflare 支持的 HTTP 端口： 80,8080,8880,2052,2082,2086,2095&lt;/p&gt;
&lt;p&gt;Cloudflare 支持的 HTTPS 端口： 443,2053,2083,2087,2096,8443&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;解决方案也很简单，打开 规则→Origin Rules，然后创建规则。传入请求匹配表达式大致如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(starts_with (http.request.full_uri, &quot;https:// 域名&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目标端口设置重写到 9443 即可&lt;/p&gt;
&lt;p&gt;为了隐藏公网 IP，请设定限定仅 cloudflare cdn cidr 段访问 9443 端口的防火墙规则。&lt;/p&gt;
&lt;h3&gt;重定向问题&lt;/h3&gt;
&lt;p&gt;如果重定向时 URL 中意外出现端口号，可通过防护站点→站点详情→自定义 NGINX 配置添加以下内容进行解决：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxy_redirect https://$host:[port] https://$host;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用 WireGuard 隐藏管理面板&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;对于 VPS 的管理面板，不要将管理面板（例如长亭 WAF、宝塔面板等）暴露到公网
使用 Wireguard 等 VPN 访问是更加安全的选择
Wireguard 可以在 &lt;a href=&quot;https://www.wireguard.com/install/&quot;&gt;Installation - WireGuard&lt;/a&gt; 查看到详细的安装方式&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;引用&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ubuntu.com/pro/tutorial&quot;&gt;开始使用 Ubuntu Pro 来自 Ubuntu 官网&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;如何避免证书泄露源站 IP 作者：秋未萌&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wiki.ubuntu.org.cn/Ufw%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97&quot;&gt;Ufw 使用指南 来自 Ubuntu 中文&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/windows-server/administration/openssh/openssh_keymanagement&quot;&gt;Key-based authentication in OpenSSH for Windows 来自 Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/engine/install/ubuntu&quot;&gt;Install Docker Engine on Ubuntu 来自 docker 官网&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/267502/2&quot;&gt;https://linux.do/t/topic/267502/2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/267502/3&quot;&gt;https://linux.do/t/topic/267502/3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/168843/11&quot;&gt;https://linux.do/t/topic/168843/11&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/267502/18&quot;&gt;https://linux.do/t/topic/267502/18&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/chaifeng/ufw-docker&quot;&gt;https://github.com/chaifeng/ufw-docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/267502/23&quot;&gt;https://linux.do/t/topic/267502/23&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/267502/42&quot;&gt;https://linux.do/t/topic/267502/42&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/267502/44&quot;&gt;https://linux.do/t/topic/267502/44&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/267502/20&quot;&gt;https://linux.do/t/topic/267502/20&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.work.weixin.qq.com/document/path/91770&quot;&gt;https://developer.work.weixin.qq.com/document/path/91770&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/267502/21&quot;&gt;https://linux.do/t/topic/267502/21&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/267502/22&quot;&gt;https://linux.do/t/topic/267502/22&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/267502/57&quot;&gt;https://linux.do/t/topic/267502/57&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bugs.launchpad.net/ubuntu/+source/openssh/+bug/2069041&quot;&gt;https://bugs.launchpad.net/ubuntu/+source/openssh/+bug/2069041&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/242440/29&quot;&gt;https://linux.do/t/topic/242440/29&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/267502/71&quot;&gt;https://linux.do/t/topic/267502/71&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/267502/87&quot;&gt;https://linux.do/t/topic/267502/87&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/267502/97&quot;&gt;https://linux.do/t/topic/267502/97&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/267502/99&quot;&gt;https://linux.do/t/topic/267502/99&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/267502/108&quot;&gt;https://linux.do/t/topic/267502/108&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/267502/121&quot;&gt;https://linux.do/t/topic/267502/121&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/267502/122&quot;&gt;https://linux.do/t/topic/267502/122&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/267502/135&quot;&gt;https://linux.do/t/topic/267502/135&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.do/t/topic/267502/175&quot;&gt;https://linux.do/t/topic/267502/175&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../images/not-by-ai.png&quot; alt=&quot;Not By AI&quot; /&gt;&lt;/p&gt;
</content:encoded></item></channel></rss>