为什么IoC / DI在Python中不常见?


312

在Java中,IoC / DI是一种非常普遍的做法,广泛用于Web应用程序,几乎所有可用的框架和Java EE中。另一方面,也有很多大型的Python Web应用程序,但是除了Zope(我听说过应该非常可怕的编码)之外,IoC在Python世界中似乎并不普遍。(如果您认为我错了,请举一些例子)。

当然,有一些流行的Java IoC框架的克隆可用于Python,例如springpython。但是它们似乎都没有被实际使用。至少,我从来没有在一个stumpled Django的SQLAlchemy的 + <insert your favorite wsgi toolkit here>,它使用类似的东西,基于Web应用程序。

我认为IoC具有合理的优势,例如可以轻松替换django-default-user-model,但是在Python中广泛使用接口类和IoC看起来有些奇怪,而不是“ pythonic”。但是也许有人有一个更好的解释,为什么IoC在Python中没有得到广泛使用。


2
我的猜测是,同样的原因,它在Ruby,内置mixins和开放类中不那么受欢迎
Sam Saffron 2010年

3
你曾经尝试过springpython吗?它甚至不像广告中那样起作用。至少在aop部分。除非您来自Java并且在过渡期间需要一定程度的舒适度,否则其中的所有其他内容都不会很有用。
汤姆·威利斯

6
请注意区分使用DI和使用IOC框架。前者是一种设计模式,后者是一个有助于自动使用前者的框架。
Doug

道格,我相信您是说DI是通过使用Decorator模式获得的创新功能。
njappboy 2014年

4
我很乐意看到一个解决DI所解决的现实世界问题的答案:生命周期管理,测试存根的难易程度等。如果有更Python化的方法来解决这些问题,我将不胜枚举。
Josh Noe

Answers:


197

我实际上并不认为DI / IoC 在Python 并不罕见。什么不常见的,但是,是DI / IoC的框架/容器

想一想:DI容器做什么?它可以让你

  1. 将独立的组件连接成一个完整的应用程序...
  2. ...在运行时。

我们有“连接在一起”和“运行时”的名称:

  1. 脚本编写
  2. 动态

因此,DI容器不过是动态脚本语言的解释器。实际上,让我改写一下:一个典型的Java / .NET DI容器只不过是一个糟糕的解释器,它解释了一种非常糟糕的动态脚本语言,其使用的语法有些笨拙,有时甚至是基于XML的。

当您使用Python进行编程时,为什么要使用丑陋,糟糕的脚本语言,却要拥有漂亮,精妙的脚本语言呢?实际上,这是一个更笼统的问题:当您使用几乎任何一种语言进行编程时,为什么要使用Jython和IronPython来使用一种丑陋的,糟糕的脚本语言?

因此,回顾一下:出于完全相同的原因,DI / IoC 的实践在Python中与在Java中一样重要。但是,DI / IoC 的实现已内置于该语言中,并且通常如此轻巧,以至于它完全消失了。

(这里有一个简短的类比:在汇编中,子例程调用是一件很重要的事情-您必须将本地变量和寄存器保存到内存中,将返回地址保存在某个地方,将指令指针更改为要调用的子例程,安排它完成后以某种方式跳回到您的子例程中,将参数放在被调用者可以找到它们的地方,依此类推。IOW:在汇编中,“子例程调用”是一种设计模式,在出现诸如内置了子例程调用的Fortran,人们正在构建自己的“子例程框架”。您会说在Python中子例程调用是“罕见的”,仅仅是因为您不使用子例程框架吗?)

顺便说一句:让DI成为逻辑结论的示例,请看一下Gilad BrachaNewspeak编程语言及其在该主题上的著作:


58
虽然我同意。XML注释是错误的。许多(至少是现代)IOC容器在配置(XML)上使用约定(代码)。
Finglas

20
没有什么可以阻止您使用Java显式编写接线的,但是随着您拥有越来越多的服务,依赖关系变得越来越复杂。DI容器类似于Make:您声明依赖项,然后容器以正确的顺序对其进行初始化。Guice是一个Java DI框架,其中的所有内容均以Java代码编写。通过声明性地编写DI容器,还增加了对初始化之前对declecles进行后处理的支持(例如,用实际值替换属性占位符)
IttayD 2010年

133
“然而,DI / IoC的实现已内置于该语言中,并且通常如此轻巧,以至于它完全消失了。” 投反对票,因为这绝对是不正确的。DI是将接口传递给构造函数的模式。它不是python内置的。
道格

146
否决票,连接在一起与脚本无关,DI是一种模式,并不等同于脚本
Luxspes

38
我不同意这一点。DI不能解决静态语言中缺乏动态脚本的问题。它提供了用于配置和组成应用程序各部分的框架。我曾经听过一个Ruby开发人员说,在动态语言中DI是不必要的。但是他使用了Rails ... Rails只是一个大型的DI容器,它使用约定来确定何时配置哪些零件。他不需要DI,因为Rails解决了为他寻找零件的问题。
Brian Genisio 2013年

51

它的一部分是模块系统在Python中的工作方式。您只需从模块导入即可免费获得某种“单身”。在模块中定义对象的实际实例,然后任何客户端代码都可以导入该对象,并实际上获得一个可以正常工作的,完全构建的/填充的对象。

这与Java相反,在Java中,您不导入对象的实际实例。这意味着您始终必须自己实例化它们(或使用某种IoC / DI样式方法)。您可以通过使用静态工厂方法(或实际工厂类)来减轻必须实例化所有内容的麻烦,但是您仍然会每次实际创建新方法时会产生资源开销。


2
这就说得通了。如果要更改Python中的实现,只需使用相同的名称从其他位置导入即可。但是现在,我在想是否也可以通过在Java中MyClassInstances为每个类定义一个类来实现另一种方式,该类MyClass仅包含静态的,完全初始化的实例。那将被有线:D
tux21b 2010年

2
另一个想法是:提供一种在python中更改此类导入的方法,将使得无需触摸所有python文件即可轻松替换实现。from framework.auth.user import User 最好User = lookup('UserImplentation', 'framework.auth.user.User')在框架内编写(第二个参数可能是默认值),而不是这样做。这样,框架的用户就可以在User不接触框架的情况下替换/专业化实现。
tux21b

14
过于简化,回答,在现实生活中,您很少只需要“单例”,您需要控制范围(您可能需要线程本地单例或会话单例,等等),这让我认为这种问题用Python解决的问题不是企业环境中实际解决的那种现实问题
Luxspes 2012年

3
实际上,DI是关于能够测试和解耦代码依赖性的。另外,导入功能类似于Java中的静态导入,它使我可以导入对象的单个实例。
理查·沃伯顿

1
“只需从模块中导入即可免费获得某种“单身”。”在Java中,只需声明一个静态实例字段并将其设置为一个值即可轻松完成。这不是溶胶
-ggranum

45

IoC和DI在成熟的Python代码中非常常见。由于鸭子输入,您只需要一个框架来实现DI。

最好的示例是如何使用来设置Django应用程序settings.py

# settings.py
CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': REDIS_URL + '/1',
    },
    'local': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
        'LOCATION': 'snowflake',
    }
}

Django Rest Framework大量利用了DI:

class FooView(APIView):
    # The "injected" dependencies:
    permission_classes = (IsAuthenticated, )
    throttle_classes = (ScopedRateThrottle, )
    parser_classes = (parsers.FormParser, parsers.JSONParser, parsers.MultiPartParser)
    renderer_classes = (renderers.JSONRenderer,)

    def get(self, request, *args, **kwargs):
        pass

    def post(self, request, *args, **kwargs):
        pass

让我提醒一下(来源):

“依赖性注入”是5美分概念的25美元术语。依赖注入意味着给对象一个实例变量。[...]。


8
+1。说得好。作为一名Python程序员,我完全被C#中的DI框架的整个采访演示所困扰。花了我一段时间才意识到我已经在Flask应用程序中一直做到这一点,甚至没有考虑过,因为您不需要框架。对于一个除了C#/ Java之外一无所知的人,这个问题很有意义。对于鸭子式语言程序员来说,这很自然,就像您所说的,“ 5美分的概念用25美元的术语”。
塞缪尔·哈默

5
err ...这不是依赖项注入,因为实例(IsAuthenticatedScopedRateThrottle)由类实例化。它们不会传递到构造函数中。
dopatraman

5
IsAuthenticatedScopedRateThrottle不是情况下,这些都是类。它们在构造FooView时实例化(实际上是在FooView处理请求时)。无论如何,这仅仅是实现细节。IsAuthenticated并且ScopedRateThrottle是依赖项;他们被注入了FooView何时如何完成无关紧要。Python不是Java,因此有不同的实现方法。
马克斯·马里什

3
@MaxMalysh我同意dopatraman在这一点上。这甚至不是IoC,因为类本身具有对特定类的“硬编码”依赖项。在IoC中,应提供依赖性而不是硬编码。最重要的是,在依赖注入中,您将拥有一个实体,负责管理每个服务的生命周期,并在这种情况下注入它们。这些解决方案均未提供。
里卡多·阿尔维斯

3
@alex不,您无需更改代码即可使用其他渲染器。您甚至可以同时使用多个渲染器:renderer_classes = (JSONRenderer, BrowsableAPIRenderer, XMLRenderer)。模拟很简单@unittest.patch('myapp.views.FooView.permission_classes')。迫切需要“传递某些东西”是“ Java处事方式”的结果,因为Java是一种缺少强大的元编程功能的经过编译的静态类型语言。
Max Malysh

35

Django充分利用了控制反转。例如,数据库服务器由配置文件选择,然后框架向数据库客户端提供适当的数据库包装器实例。

区别在于Python具有一流的类型。数据类型(包括类)本身就是对象。如果您想要某些东西使用特定的类,只需为该类命名。例如:

if config_dbms_name == 'postgresql':
    import psycopg
    self.database_interface = psycopg
elif config_dbms_name == 'mysql':
    ...

随后的代码可以通过编写以下内容来创建数据库接口:

my_db_connection = self.database_interface()
# Do stuff with database.

Python用一两行普通代码来代替Java和C ++所需的样板工厂功能。这就是函数式编程与命令式编程的强项。


4
您所谓的代码实际上是接线部分。那将是您的ioc框架的XML。实际上,它可以简单地写为import psycopg2 as database_interfaceinjections.py把这条线放在etvoilà中。
频谱

29
恩 您在那儿所做的工作几乎是教科书中必不可少的丹尼尔。
Shayne 2015年

它绝对是命令性代码,但是它具有某种功能,因为它使用可调用的值。
杰里米

5
但是,这不只是一流的功能吗?en.wikipedia.org/wiki/First-class_function仅仅因为您拥有并使用它们并不能使您的代码正常运行。这里发生了很多副作用(例如change self.database_interface),这势在必行。
hjc1710 '16

15

它看到人们真的不再得到依赖注入和控制反转意味着什么了。

使用控制反转的做法是让类或函数依赖于另一个类或函数,但是与其在函数代码的类中创建实例相比,不如在函数代码的类中创建实例,则最好将其作为参数来接收,因此可以简化松耦合。这具有许多优点,因为它们具有更高的可测试性以及归档liskov替换原理。

您会发现,通过使用接口和注入,代码可以更容易维护,因为您可以轻松更改行为,因为您不必重写一行代码(在DI配置中为一两行)类更改其行为,因为实现您的类正在等待的接口的类可以独立变化,只要它们遵循该接口即可。保持代码分离和易于维护的最佳策略之一是至少遵循单一的责任,替换和依赖关系反转原则。

如果您可以自己在包中实例化对象并将其导入以自己注入,那么DI库有什么用?选择的答案是正确的,因为Java没有过程部分(类之外的代码),所有这些都进入了无聊的配置xml,因此需要类来实例化和注入依赖于惰性加载方式的依赖项,因此您不会感到厌烦您的性能,而在python上,您只需在代码的“过程”(类外部代码)部分上编码注入


您仍然会错过通过IoC / DI自动将对象连接在一起的想法。在运行时能够做到这一点并不多(Java仍然可以通过反射来做到这一点),而是框架可以处理它,而您无需显式地做到这一点。拥有过程部分也无关紧要,没有什么可以阻止一个人使用类作为静态子例程和函数的容器而完全不使用OOP功能来编写Java完整的过程应用程序。
zakmck

@zakmck:Python的“过程”部分与编写程序代码无关。Python的“过程”部分与静态语言不同的原因在于,它可以将过程代码放入类主体中,该主体在类定义时运行,并将import语句放入if语句中,并仅通过定义类来创建类工厂在工厂方法中。这些是您无法用静态语言真正完成的事情,它解决了IOC / DI试图解决的大多数问题。Python中的元编程通常看起来像常规的Python代码。
Lie Ryan

@LieRyan,您可以使用反射来实现,或者,如果您经常或在运行时需要它,则可以从其他语言(例如Groovy(旨在轻松与Java一起玩),甚至Python本身)中调用静态语言。但是,这与IoC / DI框架没有多大关系,因为它们的目的是自动为您自动进行大多数过程对象的连接,仅利用定义。可悲的是,大多数特此回答都错过了这一点。
zakmck

12

几年来没有使用过Python,但是我想说它与动态类型化语言的关系比其他任何事情都重要。举一个简单的例子,在Java中,如果我想测试是否适当地写了一些标准,我可以使用DI并传入任何PrintStream来捕获正在编写的文本并进行验证。但是,当我在Ruby中工作时,我可以动态替换STDOUT上的“ puts”方法来进行验证,而将DI完全排除在外。如果我创建抽象的唯一原因是测试使用抽象的类(例如文件系统操作或Java中的时钟),则DI / IoC会在解决方案中造成不必要的复杂性。


3
人们乐于改变系统的工作方式以测试其工作能力,这让我感到惊讶。现在,您需要测试您的测试不会引起副作用。
基本

2
他只谈论在测试范围内更改puts方法,就像注入对象的模拟方法一样。
dpa

2
@Basic在单元测试中很正常,实际上建议在这些测试中执行此操作,因为您不想用一个以上的代码块(正在测试的代码)来污染测试用例的覆盖范围。但是对于集成测试这样做是错误的,也许这就是您在评论中指的是什么?
samuelgrigolato

1
对我而言,可测试性是头等大事。如果某个设计不可测试,那么它就不是一个好的设计,而且我也可以毫无疑问地更改设计以使其更具可测试性。我必须重新验证它是否仍然有效,但是没关系。可测试性是更改代码IMO的完全有效的理由
Carlos Rodriguez

10

实际上,用DI编写足够干净和紧凑的代码是很容易的(我想知道那会是/保持pythonic,但无论如何:)),例如,我实际上更喜欢这种编码方式:

def polite(name_str):
    return "dear " + name_str

def rude(name_str):
    return name_str + ", you, moron"

def greet(name_str, call=polite):
    print "Hello, " + call(name_str) + "!"

_

>>greet("Peter")
Hello, dear Peter!
>>greet("Jack", rude)
Hello, Jack, you, moron!

是的,可以将其视为参数化函数/类的简单形式,但是它确实可以工作。因此,也许Python随附的默认电池在这里也足够了。

PS我还在动态评估Python中的简单布尔逻辑时还发布了这种天真方法的更大示例。


3
对于可能可行的简单情况,但请想象一个简单的Web博客控制器,该控制器使用各种模型(发布,评论,用户)。如果您希望用户注入自己的Post模型(带有附加的viewcount属性来跟踪该模型),以及自己的User模型以及更多的配置文件信息等等,那么所有参数可能会造成混淆。此外,用户可能也想更改Request对象,以支持文件系统会话,而不是基于简单cookie的会话或类似的东西。因此,您很快就会得到很多参数。
tux21b 2010年

1
@ tux21b嗯,用户想要应用程序实现“本质上​​的复杂性”,有一些体系结构的解决方案(在开发以及维护时间,执行速度等方面,某些解决方案并不比其他解决方案。 ),并具有理解API和软件架构的能力。如果根本没有人类可以理解的解决方案(不仅仅是在使用(任何形式的)DI的解决方案中)...那么,谁说所有问题都可以解决?实际上,拥有许多默认分配的参数(但可以由用户选择互换)通常就足够了。
mlvljr

9

IoC / DI是一个设计概念,但不幸的是,它通常被视为适用于某些语言(或键入系统)的概念。我希望看到依赖注入容器在Python中变得越来越流行。有Spring,但是那是一个超级框架,似乎是Java概念的直接移植,而无需过多考虑“ Python方式”。

给定Python 3中的注释,我决定对功能齐全但简单的依赖项注入容器进行破解:https : //github.com/zsims/dic。它基于.NET依赖项注入容器中的一些概念(如果您曾经在该领域中玩,那么IMO就是一个不错的选择),但是却被Python概念所突变。


6

我认为,由于python的动态性质,人们经常看不到需要另一个动态框架。当类从新样式的“对象”继承时,您可以动态创建一个新变量(https://wiki.python.org/moin/NewClassVsClassicClass)。

在纯python中:

#application.py
class Application(object):
    def __init__(self):
        pass

#main.py
Application.postgres_connection = PostgresConnection()

#other.py
postgres_connection = Application.postgres_connection
db_data = postgres_connection.fetchone()

但是,查看https://github.com/noodleflake/pyioc,这可能是您想要的。

在pyooc

from libs.service_locator import ServiceLocator

#main.py
ServiceLocator.register(PostgresConnection)

#other.py
postgres_connection = ServiceLocator.resolve(PostgresConnection)
db_data = postgres_connection.fetchone()

2
两个版本都使用相同数量的代码,这对于解释为什么使用框架不是很流行的做法大有帮助。
频谱

other.py第1行中,有一个自动的依赖项解析,但是不会将其视为依赖项注入。
andho

只是说,服务定位器通常是一种反模式。
PmanAce

6

我支持“JörgW Mittag”的回答:“ DI / IoC的Python实现非常轻巧,因此完全消失了”。

为了支持这一说法,请看一下著名的Martin Fowler从Java移植到Python的示例: Python:Design_Patterns:Inversion_of_Control

从上面的链接中可以看到,Python中的“容器”可以用8行代码编写:

class Container:
    def __init__(self, system_data):
        for component_name, component_class, component_args in system_data:
            if type(component_class) == types.ClassType:
                args = [self.__dict__[arg] for arg in component_args]
                self.__dict__[component_name] = component_class(*args)
            else:
                self.__dict__[component_name] = component_class

42
即使是最弱的DI容器也远远不够。生命周期管理,递归依赖性解析,模拟能力-或使所有配置失败-配置在哪里?这无非是一种查找和缓存是一样的东西为国际奥委会。
基本

2
多年前,我使用元类作为练习编写了一个小型DI框架。整个过程是一个零导入和doctest的文件,这使其易于说明。它表明基本功能并不是以“ pythonic”的方式实现就不那么难了,但是我真诚地感到遗憾的是,没有像Java的Spring那样完整的解决方案没有像Java那样引起人们的广泛关注,每个人都在做自定义插件体系结构。
安德烈·拉托

2

我的2cents是,在大多数Python应用程序中,您不需要它,即使您需要它,也有很多Java仇恨者(以及认为自己是开发人员的无能的提琴手)认为它不好,只是因为它在Java中很流行。

当您具有复杂的对象网络时,IoC系统实际上很有用,其中每个对象可能是其他几个对象的依赖项,而本身又是其他对象的依赖项。在这种情况下,您将希望一次定义所有这些对象,并具有一种机制,可以根据尽可能多的隐式规则将它们自动组合在一起。如果您还需要由应用程序用户/管理员以简单的方式定义配置,那么这就是希望IoC系统能够从简单的XML文件(即配置)中读取其组件的另一个原因。

没有这样复杂的体系结构,典型的Python应用程序要简单得多,只有一堆脚本。我个人知道IoC实际上是什么(与在此处写了某些答案的人相反),而我在有限的Python经验中从未感到过对IoC的需求(而且我并没有在所有地方都使用Spring,不是在优点时它给您带来了不合理的开发开销)。

也就是说,在某些Python情况下,IoC方法实际上是有用的,实际上,我在这里读到Django使用了它。

上面的相同推理可以应用于Java世界中的面向方面的编程,不同之处在于AOP真正值得的案例数量更加有限。


在django使用IoC的地方是否有指向信息源的参考URL?
Sajuuk

@Sajuuk,我已经在该问题的线程上了解到有关Django的信息,所以我不知道,您应该询问其他答案作者。
zakmck,

我认为这个答案的第一个alinea会增加0值...我认为我有能力决定我的python代码何时可以从IoC中受益,而且我也不在乎开发人员认为不好的东西。我重视实用主义而不是毫无根据的意见。
Mike de Klerk,

@MikedeKlerk我的建议是,无论像您这样的客观性和知识渊博的人,未知的事物(如许多答案在此证明)和预审的受害者都不太可能成为流行。当然,我不确定这是为什么您没有在Python中看到很多IoC用法的原因,我认为主要原因是低/中度兼容性应用程序不需要它们。
zakmck,

The typical Python application is much simpler, just a bunch of scripts, without such a complex architecture.-一个假设
hyankov


-1

我同意@Jorg的观点,那就是DI / IoC在Python中是可能的,更容易的,甚至更漂亮的。缺少的是支持它的框架,但是有一些例外。我想举几个例子:

  • Django注释使您可以使用自定义逻辑和表单来连接自己的Comment类。[更多信息]

  • Django允许您使用自定义Profile对象附加到您的User模型。这不是完全的IoC,而是一种很好的方法。我个人希望像注释框架那样替换空洞的User模型。[更多信息]


-3

在我看来,诸如依赖注入之类的东西就是僵化和过度复杂框架的症状。当代码主体变得过于繁重而无法轻松更改时,您会发现自己不得不选择其中的一小部分,为它们定义接口,然后允许人们通过插入这些接口的对象来更改行为。一切都很好,但是最好首先避免这种复杂性。

这也是静态类型语言的症状。当您唯一需要表达抽象的工具是继承时,那么几乎到处都可以使用它。话虽这么说,C ++非常相似,但从未像Java开发人员那样在任何地方都对Builders和Interfaces着迷。梦想拥有灵活性和可扩展性很容易变得过于狂妄,而这样做的代价是编写太多的通用代码,却没有什么实际的好处。我认为这是文化的事情。

通常,我认为Python人员习惯于为工作选择合适的工具,这是一个连贯且简单的整体,而不是一个可以做任何事情但提供令人困惑的可能配置排列的单一工具(带有千种插件) 。仍然有必要时可互换的部分,但是由于鸭子类型的灵活性和语言的相对简单性,因此不需要定义固定接口的庞大形式。


4
它与其说是语言本身,不如说是框架。为了创建鸭式语言所具有的灵活性,静态类型的语言需要非常复杂的框架和规则。DI是这些规则之一。Python人士不会三思而后行。Java人员必须真正地努力。
S.Lott

6
@ S.Lott-我完全同意你的看法,除了C ++的人们似乎在没有爆炸的设计和体系结构模式的情况下过得去,尽管与Java的约束类似。我认为这暗示着一种文化差异,在面对两种可能的方式来做某事时,Java人倾向于提取另一个接口来促进Strategy模式,而C ++人则直接加入并添加bool和if语句...
Kylotan 2010年

3
@Finglas,因此,如果我有十几个班级都在使用我的班级,EmailSender并决定将其替换为DesktopNotifier,则必须手动编辑12个班级。您认为仅编写INotifier接口并让容器确定详细信息就更简单,更简洁了吗?
2016年

1
不幸的是,专业软件开发人员必须面对一定程度的复杂性。我看到批评,但此答案没有解决方案。什么是解决此问题的“ pythonic”解决方案:我正在编写一个库,并且想提供一个用于日志记录的钩子(类似于PHP的PSR-3 LoggerInterface)。我知道如何使用日志级别,但是我不在乎程序如何实际报告它们。允许客户端应用程序注入该实现细节的干净方法是什么?注意:应用程序的其他部分可能具有此接口的不同实现。
罗布(Rob)

2
我的问题不是您如何使用标准的日志记录库,也不是关于创建记录器类的不同实例。我的问题是如何配置应用程序,以便应用程序的不同部分可以使用不同的实现,而不必担心那些细节(前提是他们知道如何使用该接口)。DI为我处理过的多个PHP应用程序解决了一个非常实际的问题。我正在寻找等效的python。提出“只是不要让您的应用程序变得如此复杂”并不是我要找的答案。
罗布(Rob)

-5

与Java中强类型化的特性不同。Python的鸭子输入行为使传递对象变得非常容易。

Java开发人员专注于构造对象之间的类结构和关系,同时保持事物的灵活性。IoC对于实现这一点极为重要。

Python开发人员专注于完成工作。他们只是在需要时上课。他们甚至不必担心类的类型。只要能发出嘎嘎声,它就是鸭子!这种性质没有留给IoC的空间。


4
您仍然需要找到可笑的东西。
andho
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.