IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Python知识库 -> Boost(9):使用Boost.Python在C++程序中嵌入Python编程 -> 正文阅读

[Python知识库]Boost(9):使用Boost.Python在C++程序中嵌入Python编程

目录

???????说明

嵌入 Python

编写嵌入 Python 程序

?C++ 调用 Python 代码

导入 Python 模块

执行 Python 文件

操作 Python 对象

运行传入的 Python 方法

处理异常

关于命名空间变量

handle 类详解

exec 详解

Python C API

完整示例

参考链接


说明

Boost.Python 不仅可以提供将 C++ 代码封装成 Python 接口供 Python 程序调用的功能,反过来,也可以使用 Boost.Python 在 C++ 程序中调用 Python 方法。但是需要注意的是,截至当前版本的 Boost(1.79) 的这一功能还不完善,需要与 Python/C API 结合使用。官方说法是后续可能会完善,但是当前还不能独立使用。

先总结一下个人想法吧,总体看下来,Boost.Python 的嵌入 Python 方案其实现完全依靠调用 Python/C API 的内容,只是在后者基础之上做了一层封装,使得看起来更加便利了而已。对于开发者来说,如果仅仅为了实现嵌入 Python 的功能,没有必要使用 Boost 库,但是对于依赖 Boost 的软件项目来说,使用 Boost.Python 确实能简化不少流程,且能降低这方面出错的概率。

嵌入 Python

Boost.Python 包含两种模型 extending 和 embedding。前者是将 C++ 编写的库封装成 Python 接口时使用的,后者则用于在 C++ 程序中调用 Python 解释器作为库子例程,一般为现有应用程序添加脚本功能。两者的主要区别在于 C++ main() 函数的位置,分别在 Python 解释器可执行文件或其他程序中。

我们将在 C/C++ 代码调用的 Python 语句称为 Embedding Python。

注:关于 extending 和 embedding,我们不必太在意细节,对于软件开发人员来说,他们在使用上没有任何区别。通常能使用 Boost.Python 库参与编译,这两者都是可用的。

编写嵌入 Python 程序

使用 Boost.Python 编写嵌入 Python 程序一般遵循以下4个步骤:

  1. 包含头文件 <boost/python.hpp>
  2. 调用 Py_Initialize() 来初始化 Python 解释器并创建 __main__ 模块
  3. 调用其他 Python C API 来使用 Python 解释器
  4. 编写自己的业务代码

注:在当前为止的版本(1.79),我们需要手动调用 Py_Finalize() 来停止 Python 解释器,在将来的版本中会解决这个问题,但是在当前还是需要。

先看一个简单的示例:

#include <boost/python.hpp>

using namespace boost::python;

int main( int argc, char ** argv ) {
  try {
    Py_Initialize();

    // 方法一: 使用 Python C API 打印
    /*
    object main_module((
      handle<>(borrowed(PyImport_AddModule("__main__")))));

    object main_namespace = main_module.attr("__dict__");
    handle<> ignored(( PyRun_String( "print(\"Hello, World\")",
                                     Py_file_input,
                                     main_namespace.ptr(),
                                     main_namespace.ptr() ) ));
    */

    // 方法二:使用 Boost.Python 接口打印
    object ignored = exec("print(\"Hello, World\")");
    
    Py_Finalize();
  } catch( error_already_set ) {
    PyErr_Print();
  }
}

CMakeLists.txt

set(MODULE_NAME  embedding)

add_executable(${MODULE_NAME} hello.cpp)

target_link_libraries(${MODULE_NAME}
  ${Boost_LIBRARIES}
  ${PYTHON_LIBRARIES}
)

在这个示例中实现了两种方法来打印 "Hello, World"。方法一主要使用 Python C API 的方法,二则使用 Boost.Python 提供的 exec 功能。两者实现上没什么区别,后者是也是用方法一实现的,是对 Python C API 的封装,但是提供了更完善的检查和引用计数的处理。所以,如果使用了 Boost.Python,建议使用方法二。一方面更简洁,另一方面也更安全。

我们知道 Python 中的对象是有引用计数的,虽然使用 Python C API 的 Python 对象也有引用计数,但两者还是有区别的,Python 的引用计数由 Python 自动管理,而 Python C API 的引用计数则要求用户手动管理。这就使得对引用计数的处理变得很麻烦,尤其当程序运行异常时尤其难以解决。针对这一问题,Boost.Python 提供了 handle 和 object 类模板来自动化管理引用计数。也就是上面的例子中的 handle<>, object 相关语句。

?C++ 调用 Python 代码

Boost.Python 提供了四个相关的函数来在 C++ 中调用 Python 代码:

// eval计算给定的表达式并返回结果值
object eval(str expression, object globals = object(), object locals = object())

// exec执行 code 指定的 Python 代码段(通常是一组语句)
object exec(str code, object globals = object(), object locals = object())

// exec_statement 执行 string 指定的 Python 语句
object exec_statement(str string, object global = object(), object local = object());

// exec_file执行给定文件中包含的代码
object exec_file(str filename, object globals = object(), object locals = object())

?注:上例中的 str 参数也可以使用 const char *代替。

eval(), exec()exec_statement() 三个接口的参数完全一致,应用场景也很接近。区别在于其内部实现里调用的 PyRun_String() 方法的 start 参数分别是 Py_eval_input, Py_file_input Py_single_input。详见:The Very High Level Layer

由于 exec_statement() 只能执行一行 Python 语句,而 exec() 可以执行一到多行,所以我建议尽量使用后者。

globals 和 locals 分别对应于 PyRun_String() 的同名参数, Boost.Python 在这里做成有默认值的可缺省的参数。

在 Python 中,全局和局部参数是以字典的形式保存的,即{arg_name:arg_value},在大多数情况下,我们可以使用 __main__ 模块来获取这些参数。比如:

object main_module = import("__main__");
object main_namespace = main_module.attr("__dict__");

object ignored = exec("hello = file('hello.txt', 'w')\n"
                      "hello.write('Hello world!')\n"
                      "hello.close()",
                      main_namespace);

这段代码会在当前目录创建一个名为 "hello.txt"的文件,其中包含 "Hello world!" 内容。

导入 Python 模块

通过 Boost.Python 导入 Python 模块有两种方式,先看一种简单的,直接将 Python 模块的操作直接作为 Python 语句的一部分,然后调用 exec() 来运行:

object ignored = exec("import sys\n"
                      "version = sys.version\n"
                      "print(version)\n", main_namespace);

ignored = exec("import sys\n"
                    "encode = sys.getdefaultencoding()\n"
                    "print(encode)\n", main_namespace);

这种方式简单,但是缺点也显而易见,对模块的操作全都集中在一段代码内还好,但如果操作需要分段进行,则每次都要重新导入模块,效率太低。

一种更灵活的方式是,导入一次模块后,作为一个 C++ 对象保留下来,在任意需要的地方随时调用。Boost.Python 提供的 import() 属性可以很好的用于这种场景,导入后的模块的方法和变量等都作为 C++ 对象的属性。该 import 语句用于导入 Python 模块的调用格式为:?

object import(str name)

name 表示要导入的 python 模块名称。

那么上面的示例就可以改写为以下内容:

object sys = import("sys");
object version_o = sys.attr("version");
std::string version_s = extract<std::string>(version_o);
std::cout << version_s << std::endl;

std::string encode = extract<std::string>(sys.attr("getdefaultencoding")());
std::cout << encode << std::endl;

注: import() 函数是 Boost.Python 对 PyImport_ImportModule() 函数的封装,其作用与 object main_module((

handle<>(borrowed(PyImport_AddModule("__main__"))))); 语句类似。建议直接使用 import()。

执行 Python 文件

在 C++ 中执行 Python 程序文件,直接使用 exec_file() 即可,本身没什么可说的。但是需要注意两点。

先看示例:

# embedding.py
number_x = 30

print ("This is embedded python file")

if __name__ == '__main__':
    number_y = 20
    print ("Py run itself.")
// embedding.cpp
int main( int argc, char ** argv ) 
{
  try {
    Py_Initialize();

    dict global;
    object result = exec_file("embedding.py", global, global);

    int num_x = extract<int>(global["number_x"]);
    // int num_y = extract<int>(global["number_y"]);    // KeyError: 'number_y'

    std::cout << num_x << std::endl;

    Py_Finalize();

  } catch( error_already_set ) {
    PyErr_Print();
  }
}

?编译后运行结果:

This is embedded python file
30

?需要注意的两点是:

  1. 虽然是执行 Python 文件,但仍然是以 module 的形式。.py 文件中, if __name__ == '__main__': 以下的内容都不会执行(当然,这是有前提的,后面会说明);
  2. .py 文件中设置的变量会被保存到 Boost.Python 字典类型的命名空间中。

操作 Python 对象

我在之前的文章提到过,Boost.Python 使用类来抽象 Python 对象,恰当地使用 object 类及其派生类可以完美地操作所有的 Python 对象。

以下示例用以说明这一事实:

object main_module = import("__main__");
object main_namespace = main_module.attr("__dict__");
object ignored = exec("result = 5 ** 2", main_namespace);
int five_squared = extract<int>(main_namespace["result"]);

在这里,我们为 __main__ 模块的命名空间创建一个字典对象。然后我们将 5 平方分配给结果变量并从字典中读取该变量。实现相同结果的另一种方法是使用 eval 代替,它直接返回结果:

object result = eval("5 ** 2");
int five_squared = extract<int>(result);

接下来我们回到上一个程序,对 C++ 代码稍作修改:

object main_module = import("__main__");
object main_namespace = main_module.attr("__dict__");

// dict global;
object result = exec_file("embedding.py", main_namespace, main_namespace);

int num_x = extract<int>(main_namespace["number_x"]);
int num_y = extract<int>(main_namespace["number_y"]);    // KeyError: 'number_y'

std::cout << num_x << num_y << std::endl;

再次编译运行,结果如下:

This is embedded python file
Py run itself.
3020

和之前的程序相比,我们这一次在代码里导入了 __main__ 模块,__main__ 是 Python 程序的入口,我们在这里导入了 "__main__" 之后,Python 魔法函数 __name__ 就被赋值了 __main__,那么 .py 文件里的语句也就会被执行了。

运行传入的 Python 方法

在上面这几种方式之外,C++ 程序还可以运行由 Python 作为参数传递进来的方法,此时 C++ 和 Python 之间的交互较为频繁,先由 Python 调用 C++ 接口,再在 C++ 内运行 Python 方法。

具体内容参见:boost.python:在c++程序中调用python的函数

处理异常

如果在执行 Python 代码的过程当中发生异常,Boost.Python 会抛出 error_already_set 类,该类包含用于管理和转换 Python 和 C++ 异常的类型和函数。

try
{
    object result = eval("5/0");
    // execution will never get here:
    int five_divided_by_zero = extract<int>(result);
}
catch(error_already_set const &)
{
    // handle the exception in some way
}

error_already_set 异常类本身不携带任何信息。要了解有关发生的 Python 异常的更多信息,您需要在 catch 语句中使用 Python C API 的异常处理函数。这可以像调用 PyErr_Print() 将异常的回溯打印到控制台一样简单,或者将异常的类型与标准异常的类型进行比较:

catch(error_already_set const &)
{
    if (PyErr_ExceptionMatches(PyExc_ZeroDivisionError))
    {
        // handle ZeroDivisionError specially
    }
    else
    {
        // print all other errors to stderr
        PyErr_Print();
    }
}

关于命名空间变量

嵌入 Python 是为了为我所用,而在前面的示例中,看似 C++ 调用了 Python 代码,但两者仍然是比较孤立的,C++ 内定义的一系列函数和变量等并不能为 Python 代码所使用,这样也起不到结合两者有点的作用。此时,命名空间就有文章可做了。

我们在调用 exec() 时传入了 main_space 作为全局变量,它是一个字典类型的变量,对应于 python 环境下的全局变量字典。格式为 ["name":object] ,也就是说几乎所有类型的 Python 对象都可以作为该变量的成员。当我们在 C++ 程序中初始化 Python 解释器后,main_space 下的成员可以作为 Python 目标的属性直接调用。

比如,我们在 C++ 下定义一个简单的类和函数:

class mainspace {
  public:
    int get_num() { return 10; }
};

void showtime()
{
  std::cout << "show time for namespace" << std::endl; 
}

然后在执行 Python 时将上述类和函数作为成员添加进 Python 全局变量:

object main_module = import("__main__");
object main_namespace = main_module.attr("__dict__");

main_namespace["mainspace"] = class_<mainspace>("mainspace")
                           .def("get_num",&mainspace::get_num);
main_namespace["show_time"] = showtime;

object ignored = exec("ms = mainspace()\n"
                    "print(ms.get_num())\n"
                    "show_time()\n", main_namespace);

此时 Python 全局变量下有两个成员: "mainspace" : class mainspace"show_time" : showtime()。 Python 可以直接使用这两个成员,运行结果如下:

$ ./embedding
10
show time for namespace

OK,前面我们是将类的定义传递到了 Python 空间,在 Python 运行时要先创建一个类实例,然后才能调用类的成员函数。如果在初始化 Python 解释器之前,我们就有一个类的实例,也可以直接将该实例引用到 Python 空间。

比如,先实例化类,再将该实例赋值给 main_space:

mainspace aaa;
...
main_namespace["mspace"] = ptr(&aaa);

然后在 Python 下就可以直接使用了:

object ignored = exec("print(mspace.get_num())\n", main_namespace);

于是,我们终于可以访问我们的 C++ 应用程序的数据了,但是所有的数据都在全局命名空间中,对于优秀的程序设计而言并不是一个好的做法。为了避免这种情况并提供适当的封装,我们应该将所有类和数据放入一个模块中。第一步是删除所有修改 main_namespace 对象的行,然后添加一个标准的 Boost 模块定义:

BOOST_PYTHON_MODULE(CppMod) {
  class_<mainspace>("mainspace")
    .def("get_num",&mainspace::get_num);
}

现在,在 main() 内部和调用 Py_Initialize() 之前,我们要调用 PyImport_AppendInittab( "CppMod", &initCppMod ); initCppMod 是由 BOOST_PYTHON_MODULE 宏创建的函数,用于初始化 Python 模块 CppMod。此时,我们的嵌入式 python 程序就可以调用 import CppMod,然后作为模块成员访问 mainspace。

handle 类详解

前面的内容提到,Boost.Python 使用 handle<> 来实现引用计数的自动管理。句柄本质上是一个指向 Python 对象类型的智能指针;它拥有一个 T* 类型的指针,其中 T 是它的模板参数。 T 必须是派生自 PyObject 的类型或初始 sizeof(PyObject) 字节与 PyObject 布局兼容的 POD 类型。在 Python/'C' API 和高级代码之间的边界处使用 handle<>;首选对象作为 Python 对象的通用接口。

handle 类定义在<boost/python/handle.hpp> 文件中,提供类模板句柄,一个用于管理引用计数的 Python 对象的智能指针。

namespace boost { namespace python
{
  template <class T>
  class handle
  {
      typedef T* (handle::* bool_type )() const;

   public: // types
      typedef T element_type;

   public: // member functions
      ~handle();

      template <class Y>
      explicit handle(detail::borrowed<null_ok<Y> >* p);

      template <class Y>
      explicit handle(null_ok<detail::borrowed<Y> >* p);

      template <class Y>
      explicit handle(detail::borrowed<Y>* p);

      template <class Y>
      explicit handle(null_ok<Y>* p);

      template <class Y>
      explicit handle(Y* p);

      handle();


      template <typename Y>
      handle(handle<Y> const& r);
      handle(handle const& r);

      T* operator-> () const;
      T& operator* () const;
      
      handle& operator=(handle const& r);
      template<typename Y>
      handle& operator=(handle<Y> const & r); // never throws
      
      T* get() const;          // 返回当前句柄
      void reset();            // 重置handle类对象,此时句柄指向的地址为NULL
      T* release();            // 返回当前句柄,然后将当前句柄清零

      operator bool_type() const; // never throws
   private:
      T* m_p;
  };

  template <class T> struct null_ok;
  namespace detail { template <class T> struct borrowed; }
}}

handle 的构造函数和析构函数实现上依赖于 Python/C API 的 Py_XINCREF() Py_XDECREF() 等操作,用于实现引用计数的加1减1操作。

构造函数的使用格式如下:

// 创建一个引用计数 +1 的 NULL 指针 x
handle<> x;
handle<> x = handle<> ();

// 创建一个基于对象 y 的带有引用计数的指针, null_ok 表示 y 可以为 NULL
handle<> x(y);
handle<> x(null_ok(y));
handle<> x = handle<> (y);
handle<> x = handle<> (null_ok(y));

// 创建一个与原有对象 y 的引用计数相同的的智能指针x,borrowed()表示对象是“借用”的,保持引用计数一致
handle<> x(borrowed(y));
handle<> x(null_ok(borrowed(y)));
handle<> x = handle<> (borrowed(y));
handle<> x = handle<> (null_ok(borrowed(y)));

null_ok 和 borrowed 的位置可以互换。

exec 详解

对于大部分的软件工程师来说,了解 exec() 等函数接口就足够了,这部分内容没必要了解。但是学习其具体实现有助于理解 Boost.Python 的实现逻辑,也有助于提升自己的编程能力,所以在这里分析一下。

object BOOST_PYTHON_DECL exec(str string, object global, object local)
{
    return exec(python::extract<char const *>(string), global, local);
}

object BOOST_PYTHON_DECL exec(char const *string, object global, object local)
{
  // Set suitable default values for global and local dicts.
  if (global.is_none())
  {
    if (PyObject *g = PyEval_GetGlobals())
      global = object(detail::borrowed_reference(g));
    else
      global = dict();
  }
  if (local.is_none()) local = global;
  // should be 'char const *' but older python versions don't use 'const' yet.
  char *s = const_cast<char *>(string);
  PyObject* result = PyRun_String(s, Py_file_input, global.ptr(), local.ptr());
  if (!result) throw_error_already_set();
  return object(detail::new_reference(result));
}

在没有指定全局和局部变量的情况下,Boost.Python 默认会借助 PyEval_GetGlobals() 来获取当前执行帧中全局变量的字典。如果失败则定义一个新的字典变量。但是头文件中将 global 和 local 设置了默认参数 dict(),所以一般情况下不会运行到 PyEval_GetGlobals()

调用 Python 的过程也是借助了 PyRun_String() 方法,只是在此基础上增加了异常处理。

eval() exec_statement() 的实现与 exec() 一样,只是在调用 PyRun_String() 的参数 Py_file_input 不同。

exec_file() 的实现由稍许不同,在此不做分析。

Python C API

Python/C API 是 Python 官方提供的使用 C 或 C++ 扩展 Python 的方法。这种方法需要以特定的方式来编写 C 代码以供 Python 去调用它。所有的 Python 对象都被表示为一种叫做 PyObject 的结构体,并且 Python.h 头文件中提供了各种操作它的函数。大部分对 Python 原生对象的基础函数和操作在 Python.h 头文件中都能找到。

详细教程见:Python/C API Reference Manualhttps://docs.python.org/3/c-api/index.html

完整示例

以下内容来自于 Boost 源代码,从文章完整性的角度供参考,其中有一些暂时还没有看到的内容,后面会补充文章进行说明。

// Copyright Stefan Seefeld 2005.
// Distributed under the Boost Software License, Version 1.0. (See
// accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)

#include <boost/python.hpp>

#include <boost/detail/lightweight_test.hpp>
#include <iostream>

namespace python = boost::python;

// An abstract base class
class Base : public boost::noncopyable
{
public:
  virtual ~Base() {};
  virtual std::string hello() = 0;
};

// C++ derived class
class CppDerived : public Base
{
public:
  virtual ~CppDerived() {}
  virtual std::string hello() { return "Hello from C++!";}
};

// Familiar Boost.Python wrapper class for Base
struct BaseWrap : Base, python::wrapper<Base>
{
  virtual std::string hello() 
  {
#if BOOST_WORKAROUND(BOOST_MSVC, <= 1300)
    // workaround for VC++ 6.x or 7.0, see
    // http://boost.org/libs/python/doc/tutorial/doc/html/python/exposing.html#python.class_virtual_functions
    return python::call<std::string>(this->get_override("hello").ptr());
#else
    return this->get_override("hello")();
#endif
  }
};

// Pack the Base class wrapper into a module
BOOST_PYTHON_MODULE(embedded_hello)
{
  python::class_<BaseWrap, boost::noncopyable> base("Base");
}


void exec_test()
{
    std::cout << "registering extension module embedded_hello..." << std::endl;
    
  // Register the module with the interpreter
  if (PyImport_AppendInittab("embedded_hello", initembedded_hello) == -1)
    throw std::runtime_error("Failed to add embedded_hello to the interpreter's "
                 "builtin modules");

  std::cout << "defining Python class derived from Base..." << std::endl;
  
  // Retrieve the main module
  python::object main = python::import("__main__");
  
  // Retrieve the main module's namespace
  python::object global(main.attr("__dict__"));

  // Define the derived class in Python.
  python::object result = python::exec(
    "from embedded_hello import *        \n"
    "class PythonDerived(Base):          \n"
    "    def hello(self):                \n"
    "        return 'Hello from Python!' \n",
    global, global);

  python::object PythonDerived = global["PythonDerived"];

  // Creating and using instances of the C++ class is as easy as always.
  CppDerived cpp;
  BOOST_TEST(cpp.hello() == "Hello from C++!");

  std::cout << "testing derived class from C++..." << std::endl;
  
  // But now creating and using instances of the Python class is almost
  // as easy!
  python::object py_base = PythonDerived();
  Base& py = python::extract<Base&>(py_base) BOOST_EXTRACT_WORKAROUND;

  // Make sure the right 'hello' method is called.
  BOOST_TEST(py.hello() == "Hello from Python!");

  std::cout << "success!" << std::endl;
}

void exec_file_test(std::string const &script)
{
    std::cout << "running file " << script << "..." << std::endl;
    
    // Run a python script in an empty environment.
    python::dict global;
    python::object result = python::exec_file(script.c_str(), global, global);

    // Extract an object the script stored in the global dictionary.
    BOOST_TEST(python::extract<int>(global["number"]) ==  42);
    
    std::cout << "success!" << std::endl;
}

void exec_test_error()
{
    std::cout << "intentionally causing a python exception..." << std::endl;
    
    // Execute a statement that raises a python exception.
    python::dict global;
    python::object result = python::exec("print unknown \n", global, global);

    std::cout << "Oops! This statement should be skipped due to an exception" << std::endl;
}

int main(int argc, char **argv)
{
  BOOST_TEST(argc == 2);
  std::string script = argv[1];
  // Initialize the interpreter
  Py_Initialize();

  bool error_expected = false;
  
  if (
      python::handle_exception(exec_test)
      || python::handle_exception(boost::bind(exec_file_test, script))
      || (
          (error_expected = true)
          && python::handle_exception(exec_test_error)
      )

  )
  {
    if (PyErr_Occurred())
    {
        if (!error_expected)
            BOOST_ERROR("Python Error detected");
        PyErr_Print();
    }
    else
    {
        BOOST_ERROR("A C++ exception was thrown  for which "
                    "there was no exception translator registered.");
    }
  }
  
  // Boost.Python doesn't support Py_Finalize yet, so don't call it!
  return boost::report_errors();
}

参考链接

boost.python/EmbeddingPython - Python Wiki

Embedding - 1.79.0

boost/python/errors.hpp - 1.79.0

boost/python/handle.hpp - 1.79.0

boost.python/handle - Python Wiki

  Python知识库 最新文章
Python中String模块
【Python】 14-CVS文件操作
python的panda库读写文件
使用Nordic的nrf52840实现蓝牙DFU过程
【Python学习记录】numpy数组用法整理
Python学习笔记
python字符串和列表
python如何从txt文件中解析出有效的数据
Python编程从入门到实践自学/3.1-3.2
python变量
上一篇文章      下一篇文章      查看所有文章
加:2022-05-18 17:35:34  更:2022-05-18 17:37:57 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/15 15:02:30-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码