python代码:开发指南

  • 为什么记录代码如此重要
  • 注释与文档代码
  • 注释代码基础
  • 通过类型提示(python 3.5+)注释代码
  • 使用docstrings记录python代码库
  • 文档字符串背景
  • 文档字符串类型
  • 文档字符串格式
  • 记录您的python项目
  • 私人项目
  • 共享项目
  • 公共和开源项目
  • 文档工具和资源
  • 我从哪里开始?

欢迎使用完整的指南来编辑你的python代码。无论你是在记录一个小脚本还是一个大项目,无论你是一个初学者还是经验丰富的Python编程老手,本指南将涵盖你需要知道的一切。

本教程分为四个部分:

  1. 为什么记录代码如此重要:文档简介及其重要性
  2. 注释与文档代码:概述注释与文档之间的主要区别,以及使用注释的适当时间和方法
  3. 使用docstrings记录您的python代码库:深入研究docstring中的类、类方法、函数、模块、包和脚本,以及每个类中应该找到的内容。
  4. 记录您的python项目:必要的元素以及它们应该为您的python项目包含什么

您可以从头到尾阅读本教程,或者跳到您感兴趣的部分。它被设计成双向工作。

为什么注释代码如此重要

当您编写代码时,您为两个主要的访问群体编写代码:您的用户和开发人员(包括您自己)。两个受众同样重要。如果你和我一样,你可能已经打开了旧的代码库,并且对自己想,“我到底在想什么?“如果您在阅读自己的代码时遇到问题,请想象一下您的用户或其他开发人员在尝试使用或贡献代码时所遇到的情况。

相反,我确信您遇到了这样一种情况:您希望在Python中做一些事情,并找到一个看起来像是可以完成工作的伟大库。然而,当您开始使用这个库时,您会寻找关于如何做一些特定的事情的示例、书面报告甚至官方文档,并且不能立即找到解决方案。

在搜索之后,您会发现文档是缺失的,甚至更糟的是,完全缺失了。这是一种令人沮丧的感觉,它会阻止您使用库,无论代码有多棒或有多高效。

在本指南中,您将学习如何从最小的脚本到最大的python项目正确地记录您的python代码,以帮助防止您的用户对您的项目的使用或贡献感到太沮丧。

注释与文档代码
代码告诉你怎么做;注释告诉你为什么。

简单的代码注释

def hello_world():
    # A simple comment preceding a simple print statement
    print("Hello World")

根据PEP 8,注释的最大长度应为72个字符。即使您的项目将最大行长度更改为大于建议的80个字符,这也是正确的。如果注释将大于注释字符限制,则可以对注释使用多行:

def hello_long_world():
    # A very long statement that just goes on and on and on and on and
    # never ends until after it's reached the 80 char limit
    print("Hellooooooooooooooooooooooooooooooooooooooooooooooooooooooo World")

注释您的代码有[多种用途,包括](https://en.wikipedia.org/wiki/comment UU(计算机编程)用途):

*规划和审阅:在开发代码的新部分时,可以首先使用注释作为规划或概述该部分代码的一种方式。请记住,在实际编码已经实施并经过评审/测试之后,删除这些注释:

# First step
# Second step
# Third step

代码描述:注释可用于解释代码特定部分的意图:

# Attempt a connection based on previous settings. If unsuccessful,
# prompt user for new settings.

算法描述:当使用算法,特别是复杂的算法时,解释算法如何工作或如何在代码中实现是很有用的。描述为什么选择了一个特定的算法而不是另一个算法也是合适的。

# Using quick sort for performance gains

标记:标记的使用可用于标记已知问题或改进区域所在的特定代码部分。例如:bug、fixme和todo。

# TODO: Add condition for when val is None

对代码的注释应该保持简短和集中。尽可能避免使用长注释。此外,您还应使用Jeff Atwood建议的以下四个基本规则:

1.使注释尽可能接近所描述的代码。不接近描述代码的注释会让读者感到沮丧,并且在进行更新时很容易被忽略。

2.不要使用复杂的格式(如表格或ascii数字)。复杂的格式会导致内容分散注意力,并且随着时间的推移很难维护。

3.不要包含多余的信息。假设代码的读者对编程原理和语言语法有基本的理解。

4.设计代码以注释自身。理解代码的最简单方法是阅读它。当您使用清晰易懂的概念设计代码时,读者将能够快速概念化您的意图。

记住,评论是为读者(包括你自己)设计的,目的是帮助他们理解软件的目的和设计。

通过类型提示注释代码(python 3.5+)

类型提示已添加到Python3.5中,是帮助代码读者的附加表单。事实上,杰夫的第四个建议是从上到下一个层次的。它允许开发人员设计和解释部分代码而无需注释。下面是一个简单的例子:

def hello_name(name: str) -> str:
    return(f"Hello {name}")

通过检查类型提示,您可以立即知道函数希望输入名称是str或string类型。您还可以知道函数的预期输出也将是str或string类型。虽然类型提示有助于减少注释,但要考虑到在创建或更新项目文档时,这样做也可能会带来额外的工作。

您可以从dan bader创建的视频中了解有关类型提示和类型检查的更多信息。

使用docstring记录python代码库

既然我们已经了解了注释,那么让我们深入了解一下如何记录python代码库。在本节中,您将学习docstring以及如何将它们用于文档。本节进一步分为以下小节:

1.docstrings背景:关于docstrings如何在python内部工作的背景

2.docstring类型:各种docstring“类型”(函数、类、类方法、模块、包和脚本)

3.docstring格式:不同的docstring“格式”(google、numpy/scipy、structured text和epytext)

文档字符串背景

编写python代码的文档都以docstring为中心。这些是内置字符串,如果配置正确,可以帮助用户和您自己处理项目文档。除了docstring,python还有一个内置的函数help(),它将对象docstring输出到控制台。下面是一个简单的例子:

>>> help(str)
Help on class str in module builtins:

class str(object)
 |  str(object='') -> str
 |  str(bytes_or_buffer[, encoding[, errors]]) -> str
 |
 |  Create a new string object from the given object. If encoding or
 |  errors are specified, then the object must expose a data buffer
 |  that will be decoded using the given encoding and error handler.
 |  Otherwise, returns the result of object.__str__() (if defined)
 |  or repr(object).
 |  encoding defaults to sys.getdefaultencoding().
 |  errors defaults to 'strict'.
 # Truncated for readability

如何生成此输出?由于python中的所有内容都是一个对象,因此可以使用dir()命令检查对象的目录。我们来看看有什么发现:

>>> dir(str)
['__add__', ..., '__doc__', ..., 'zfill'] # Truncated for readability

在这个目录输出中,有一个有趣的属性,即“doc”。如果您检查该属性,您将发现:

>>> print(str.__doc__)
str(object='') -> str
str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or
errors are specified, then the object must expose a data buffer
that will be decoded using the given encoding and error handler.
Otherwise, returns the result of object.__str__() (if defined)
or repr(object).
encoding defaults to sys.getdefaultencoding().
errors defaults to 'strict'.

喂!您已经找到docstring在对象中的存储位置。这意味着您可以直接操作该属性。但是,对于内置设备有一些限制:

>>> str.__doc__ = "I'm a little string doc! Short and stout; here is my input and print me for my out"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't set attributes of built-in/extension type 'str'

可以操纵任何其他自定义对象:

def say_hello(name):
    print(f"Hello {name}, is it me you're looking for?")

say_hello.__doc__ = "A simple function that says hello... Richie style"

>>> help(say_hello)
Help on function say_hello in module __main__:

say_hello(name)
    A simple function that says hello... Richie style

python还有一个特性可以简化docstring的创建。不是直接操作“文档”属性,而是将字符串文本放置在对象正下方的策略性位置自动设置“文档”值。下面是与上面相同的例子:

def say_hello(name):
    """A simple function that says hello... Richie style"""
    print(f"Hello {name}, is it me you're looking for?")

>>> help(say_hello)
Help on function say_hello in module __main__:

say_hello(name)
    A simple function that says hello... Richie style

给你!现在您了解了docstring的背景。现在是时候了解不同类型的docstring以及它们应该包含哪些信息了。

文档字符串类型

PEP 257中描述了docstring约定。它们的目的是为用户提供对象的简要概述。它们应该保持足够简洁,易于维护,但仍然要足够详细,以便新用户了解其目的和如何使用文档对象。

在所有情况下,docstrings都应该使用三重双引号(“”)字符串格式。无论docstring是否是多行的,都应该这样做。docstring至少应该是您描述的内容的快速摘要,并且应该包含在一行中:

"""This is a quick summary line used as a description of the object."""

多行docstring用于在摘要之外进一步详细说明对象。所有多行docstring都有以下部分:

  • 单行摘要行
  • 进行总结的空行
  • 对docstring的任何进一步细化
  • 另一个空行
"""This is the summary line

This is the further elaboration of the docstring. Within this section,
you can elaborate further on details as appropriate for the situation.
Notice that the summary and the elaboration is separated by a blank new
line.
"""

# Notice the blank line above. Code should continue on this line.

所有docstring的最大字符长度应与注释相同(72个字符)。docstring可以进一步分为三大类:

  • 类docStrings:类和类方法

  • 包和模块文档字符串:包、模块和函数

  • 脚本文档字符串:脚本和函数

类docstrings

类docStrings是为类本身以及任何类方法创建的。docStrings紧跟在类或类方法之后,该类或类方法缩进一个级别:

class SimpleClass:
    """Class docstrings go here."""

    def say_hello(self, name: str):
        """Class method docstrings go here."""

        print(f'Hello {name}')

类docStrings应包含以下信息:

  • 对其目的和行为的简要概述

  • 任何公开的方法,以及简短的描述

  • 任何类属性(属性)

  • 与子类的接口相关的任何内容,如果该类是子类的话

类构造函数参数应记录在类方法docstring中。应使用各自的docstring记录各个方法。类方法docStrings应包含以下内容:

  • 方法是什么及其用途的简要说明

  • 传递的任何参数(必需的和可选的),包括关键字参数

  • 标记任何被视为可选或具有默认值的参数

  • 执行方法时发生的任何副作用

  • 引发的任何异常

  • 对何时可以调用方法的任何限制

让我们举一个表示动物的数据类的简单例子。此类将包含几个类属性、实例属性、初始化和单个实例方法:

class Animal:
    """
    A class used to represent an Animal

    ...

    Attributes
    ----------
    says_str : str
        a formatted string to print out what the animal says
    name : str
        the name of the animal
    sound : str
        the sound that the animal makes
    num_legs : int
        the number of legs the animal has (default 4)

    Methods
    -------
    says(sound=None)
        Prints the animals name and what sound it makes
    """

    says_str = "A {name} says {sound}"

    def __init__(self, name, sound, num_legs=4):
        """
        Parameters
        ----------
        name : str
            The name of the animal
        sound : str
            The sound the animal makes
        num_legs : int, optional
            The number of legs the animal (default is 4)
        """

        self.name = name
        self.sound = sound
        self.num_legs = num_legs

    def says(self, sound=None):
        """Prints what the animals name is and what sound it makes.

        If the argument `sound` isn't passed in, the default Animal
        sound is used.

        Parameters
        ----------
        sound : str, optional
            The sound the animal makes (default is None)

        Raises
        ------
        NotImplementedError
            If no sound is set for the animal or passed in as a
            parameter.
        """

        if self.sound is None and sound is None:
            raise NotImplementedError("Silent Animals are not supported!")

        out_sound = self.sound if sound is None else sound
        print(self.says_str.format(name=self.name, sound=out_sound))

包和模块文档字符串

包docstrings应该放在包的初始文件的顶部。此docstring应列出由包导出的模块和子包。

模块docstrings类似于类docstrings。不再记录类和类方法,现在是模块和其中的任何函数。模块docstrings甚至在任何导入之前就放在文件的顶部。模块文档字符串应包括以下内容:

  • 模块及其用途的简要说明

  • 模块导出的任何类、异常、函数和任何其他对象的列表

  • 模块函数的docstring应包含与类方法相同的项:

  • 功能是什么及其用途的简要说明

传递的任何参数(必需的和可选的),包括关键字参数

  • 标记任何被视为可选的参数

  • 执行函数时发生的任何副作用

  • 引发的任何异常

  • 对何时可以调用函数的任何限制

脚本文档字符串

脚本被认为是从控制台运行的单个文件可执行文件。脚本的docstring放在文件的顶部,并且应该有足够的文档记录,以便用户能够充分理解如何使用脚本。当用户错误地传递参数或使用-h选项时,它应该可以用于其“usage”消息。

如果使用argparse,则可以忽略参数特定的文档,前提是argparser.parser.add_argument函数的帮助参数中正确地记录了该文档。建议对argparse.argumentparser的构造函数中的description参数使用'uuu doc'。有关如何使用argparse和其他常用命令行解析器的详细信息,请参阅我们的命令行解析库教程。

最后,任何自定义或第三方导入都应列在docstring中,以便用户知道运行脚本可能需要哪些包。下面是一个脚本示例,用于简单地打印电子表格的列标题:

"""Spreadsheet Column Printer

This script allows the user to print to the console all columns in the
spreadsheet. It is assumed that the first row of the spreadsheet is the
location of the columns.

This tool accepts comma separated value files (.csv) as well as excel
(.xls, .xlsx) files.

This script requires that `pandas` be installed within the Python
environment you are running this script in.

This file can also be imported as a module and contains the following
functions:

    * get_spreadsheet_cols - returns the column headers of the file
    * main - the main function of the script
"""

import argparse

import pandas as pd


def get_spreadsheet_cols(file_loc, print_cols=False):
    """Gets and prints the spreadsheet's header columns

    Parameters
    ----------
    file_loc : str
        The file location of the spreadsheet
    print_cols : bool, optional
        A flag used to print the columns to the console (default is
        False)

    Returns
    -------
    list
        a list of strings used that are the header columns
    """

    file_data = pd.read_excel(file_loc)
    col_headers = list(file_data.columns.values)

    if print_cols:
        print("\n".join(col_headers))

    return col_headers


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        'input_file',
        type=str,
        help="The spreadsheet file to pring the columns of"
    )
    args = parser.parse_args()
    get_spreadsheet_cols(args.input_file, print_cols=True)


if __name__ == "__main__":
    main()

文档字符串格式

您可能已经注意到,在本教程中给出的所有示例中,都有使用公共元素(参数、返回和属性)的特定格式。有一些特定的docstring格式可用于帮助docstring解析器和用户拥有熟悉和已知的格式。本教程示例中使用的格式是numpy/scipy样式的docstrings。一些最常见的格式如下:

Formatting Type Description Supported by Sphynx Formal Specification
Google docstrings Google’s recommended form of documentation Yes No
reStructured Text Official Python documentation standard; Not beginner friendly but feature rich Yes Yes
NumPy/SciPy docstrings NumPy’s combination of reStructured and Google Docstrings Yes Yes
Epytext A Python adaptation of Epydoc; Great for Java developers Not officially Yes

docstring格式的选择取决于您,但是您应该在整个文档/项目中使用相同的格式。下面是每种类型的示例,让您了解每种文档格式的外观。

Google Docstrings Example

"""Gets and prints the spreadsheet's header columns

Args:
    file_loc (str): The file location of the spreadsheet
    print_cols (bool): A flag used to print the columns to the console
        (default is False)

Returns:
    list: a list of strings representing the header columns
"""

reStructured Text Example

"""Gets and prints the spreadsheet's header columns

:param file_loc: The file location of the spreadsheet
:type file_loc: str
:param print_cols: A flag used to print the columns to the console
    (default is False)
:type print_cols: bool
:returns: a list of strings representing the header columns
:rtype: list
"""

NumPy/SciPy Docstrings Example

"""Gets and prints the spreadsheet's header columns

Parameters
----------
file_loc : str
    The file location of the spreadsheet
print_cols : bool, optional
    A flag used to print the columns to the console (default is False)

Returns
-------
list
    a list of strings representing the header columns
"""

Epytext Example

"""Gets and prints the spreadsheet's header columns

@type file_loc: str
@param file_loc: The file location of the spreadsheet
@type print_cols: bool
@param print_cols: A flag used to print the columns to the console
    (default is False)
@rtype: list
@returns: a list of strings representing the header columns
"""

记录python项目

python项目有各种形状、大小和用途。你记录项目的方式应该适合你的具体情况。记住你的项目的用户是谁,并适应他们的需求。根据项目类型,推荐文档的某些方面。项目的总体布局及其文件应如下:

project_root/
│
├── project/  # Project source code
├── docs/
├── README
├── HOW_TO_CONTRIBUTE
├── CODE_OF_CONDUCT
├── examples.py

项目通常可以分为三种主要类型:私有、共享和公共/开源。

私有项目
私有项目是仅供个人使用的项目,通常不会与其他用户或开发人员共享。对于这些类型的项目,文档可能非常简单。根据需要,可以添加一些推荐的部件:

  • Readme: 项目及其目的的简要概述。包括安装或操作项目的任何特殊要求。

  • examples.py:一个python脚本文件,提供了如何使用项目的简单示例。

请记住,即使私人项目是为您个人设计的,您也被视为用户。考虑任何可能会让您困惑的事情,并确保在评论、文档字符串或自述文件中捕获这些内容。

共享项目

共享项目是指在项目的开发和/或使用过程中与其他人协作的项目。项目的“客户”或用户仍然是您自己,以及使用项目的少数人。

文档应该比私有项目更严格一点,主要是帮助新成员加入项目或提醒贡献者/用户项目的新更改。建议添加到项目中的部分如下:

Readme:项目及其目的的简要概述。包括安装或操作项目的任何特殊要求。此外,添加自上一版本以来的任何主要更改。

examples.py:一个python脚本文件,提供了如何使用项目的简单示例。

How to Contribute:这应该包括项目的新贡献者如何开始贡献。

公共和开源项目

公共和开放源码项目是打算与大量用户共享的项目,可以涉及大型开发团队。这些项目应该像项目本身的实际开发一样,把项目文档放在高度优先的位置。建议添加到项目中的部分如下:

Readme:项目及其目的的简要概述。包括安装或操作项目的任何特殊要求。此外,添加自上一版本以来的任何主要更改。最后,添加到进一步文档、错误报告和项目的任何其他重要信息的链接。丹·贝德为你的自述文件提供了一个很好的教程。

How to Contribute:这应该包括项目的新贡献者如何提供帮助。这包括开发新的特性、修复已知的问题、添加文档、添加新的测试或报告问题。

Code of Conduct:定义其他贡献者在开发或使用您的软件时应如何对待对方。这也说明了如果这个代码被破坏将会发生什么。如果您使用的是github,那么可以使用推荐的措辞生成行为准则模板。特别是对于开源项目,考虑添加这个。

License:描述项目正在使用的许可证的纯文本文件。特别是对于开源项目,考虑添加这个。

docs:包含更多文档的文件夹。下一节将更全面地描述应该包括哪些内容以及如何组织此文件夹的内容。

文档文件夹的四个主要部分

Daniele Procida发表了一篇精彩的Pycon 2017演讲和随后关于记录Python项目的博客文章。他提到所有的项目都应该有以下四个主要部分来帮助你集中精力工作:

  • 教程:引导读者通过一系列步骤完成项目(或有意义的练习)的课程。面向用户学习。

  • 如何指导:指导读者完成解决常见问题所需的步骤(面向问题的食谱)。

  • 参考文献:阐明和阐明某一特定主题的解释。倾向于理解。

  • 说明:机器的技术说明和操作方法(关键类、功能、api等)。想想百科全书的文章。

下表显示了所有这些部分之间的关系以及它们的总体目的:

Most Useful When We’re Studying Most Useful When We’re Coding
Practical Step Tutorials How-To Guides
Theoretical Knowledge Explanation Reference

最后,您要确保您的用户能够访问他们可能有的任何问题的答案。通过以这种方式组织项目,您将能够轻松地回答这些问题,并以他们能够快速导航的格式回答这些问题。

文档工具和资源

编写代码文档,特别是大型项目,可能会让人望而生畏。谢天谢地,有一些工具和参考资料可以帮助您开始:

Tool Description
Sphinx A collection of tools to auto-generate documentation in multiple formats
Epydoc A tool for generating API documentation for Python modules based on their docstrings
Read The Docs Automatic building, versioning, and hosting of your docs for you
Doxygen A tool for generating documentation that supports Python as well as multiple other languages
MkDocs A static site generator to help build project documentation using the Markdown language
pycco A “quick and dirty” documentation generator that displays code and documentation side by side. Check out our tutorial on how to use it for more info.

除了这些工具之外,还有一些附加的教程、视频和文章,在编写项目文档时非常有用:
有时候,学习的最好方法就是模仿别人。以下是一些很好地使用文档的项目的示例:

我从哪里开始?

项目文档有一个简单的过程:

  1. 没有文档

  2. 一些文件

  3. 完整的文件

  4. 好的文件

  5. 伟大的文献

如果您不知道下一步该如何处理文档,请查看与上面的进度相关的项目现在的位置。你有什么文件吗?如果没有,那就从那里开始。如果您有一些文档,但缺少一些关键项目文件,请从添加这些文件开始。

最后,不要因为编写代码所需的工作量而气馁或不知所措。一旦你开始记录你的代码,继续下去就变得更容易了。如果您有任何问题,请随时发表评论,或者在社交媒体上联系真正的python团队,我们将提供帮助。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容