Cách Pythonic để Tiêm phụ thuộc là gì?


84

Giới thiệu

Đối với Java, Dependency Injection hoạt động như OOP thuần túy, tức là bạn cung cấp một giao diện được triển khai và trong mã khung của bạn chấp nhận một thể hiện của một lớp triển khai giao diện đã xác định.

Bây giờ đối với Python, bạn có thể làm theo cách tương tự, nhưng tôi nghĩ rằng phương pháp đó quá tốn kém ngay trong trường hợp của Python. Vậy thì bạn sẽ thực hiện nó theo cách Pythonic như thế nào?

Trường hợp sử dụng

Giả sử đây là mã khung:

class FrameworkClass():
    def __init__(self, ...):
        ...

    def do_the_job(self, ...):
        # some stuff
        # depending on some external function

Phương pháp tiếp cận cơ bản

Cách ngây thơ nhất (và có thể là tốt nhất?) Là yêu cầu hàm bên ngoài được cung cấp vào hàm FrameworkClasskhởi tạo, và sau đó được gọi từ do_the_jobphương thức.

Mã khung:

class FrameworkClass():
    def __init__(self, func):
        self.func = func

    def do_the_job(self, ...):
        # some stuff
        self.func(...)

Mã khách hàng:

def my_func():
    # my implementation

framework_instance = FrameworkClass(my_func)
framework_instance.do_the_job(...)

Câu hỏi

Câu hỏi ngắn gọn. Có cách nào tốt hơn Pythonic thường được sử dụng để làm điều này không? Hoặc có thể bất kỳ thư viện nào hỗ trợ chức năng như vậy?

CẬP NHẬT: Tình hình bê tông

Hãy tưởng tượng tôi phát triển một khuôn khổ web vi mô, xử lý xác thực bằng cách sử dụng mã thông báo. Khung này cần một chức năng để cung cấp một số IDthu được từ mã thông báo và nhận được người dùng tương ứng với nó ID.

Rõ ràng, khung công tác không biết bất kỳ điều gì về người dùng hoặc bất kỳ logic cụ thể nào khác của ứng dụng, vì vậy mã máy khách phải đưa chức năng nhận người dùng vào khung để xác thực hoạt động.


2
Tại sao bạn không "cung cấp một giao diện được triển khai và trong mã khung của bạn chấp nhận một thể hiện của một lớp triển khai giao diện đã xác định" ? Trong Python, bạn sẽ làm điều này theo kiểu EAFP (tức là giả sử nó đáp ứng giao diện đó và một AttributeErrorhoặc TypeErrorđược nâng lên theo cách khác), nhưng nếu không thì nó giống nhau.
jonrsharpe

Nó rất dễ dàng để làm điều đó bằng abs's ABCMetametaclass với @abstractmethodtrang trí, và không có xác nhận bằng tay. Chỉ muốn nhận được một số tùy chọn và đề xuất. Một trong những bạn đã trích dẫn là một trong sạch nhất, nhưng tôi nghĩ với nhiều chi phí hơn.
bagrat

Vậy thì tôi không biết bạn đang muốn hỏi câu hỏi nào.
jonrsharpe

Ok, tôi sẽ thử nói cách khác. Vấn đề là rõ ràng. Câu hỏi đặt ra là làm thế nào để thực hiện điều đó theo cách của Pythonic. Lựa chọn 1 : Cách bạn đã trích dẫn, Lựa chọn 2 : Cách tiếp cận cơ bản mà tôi đã mô tả trong câu hỏi. Vậy câu hỏi đặt ra là, có cách nào khác của Pythonic để làm điều đó không?
bagrat

Câu trả lời:


66

Xem Raymond Hettinger - Siêu được coi là siêu! - PyCon 2015 để tranh luận về cách sử dụng siêu kế thừa và đa kế thừa thay vì DI. Nếu bạn không có thời gian để xem toàn bộ video, hãy chuyển đến phút 15 (nhưng tôi khuyên bạn nên xem tất cả).

Dưới đây là một ví dụ về cách áp dụng những gì được mô tả trong video này vào ví dụ của bạn:

Mã khung:

class TokenInterface():
    def getUserFromToken(self, token):
        raise NotImplementedError

class FrameworkClass(TokenInterface):
    def do_the_job(self, ...):
        # some stuff
        self.user = super().getUserFromToken(...)

Mã khách hàng:

class SQLUserFromToken(TokenInterface):
    def getUserFromToken(self, token):      
        # load the user from the database
        return user

class ClientFrameworkClass(FrameworkClass, SQLUserFromToken):
    pass

framework_instance = ClientFrameworkClass()
framework_instance.do_the_job(...)

Điều này sẽ hoạt động vì Python MRO sẽ đảm bảo rằng phương thức máy khách getUserFromToken được gọi (nếu sử dụng super ()). Mã sẽ phải thay đổi nếu bạn đang sử dụng Python 2.x.

Một lợi ích bổ sung ở đây là điều này sẽ tạo ra một ngoại lệ nếu khách hàng không cung cấp triển khai.

Tất nhiên, đây không thực sự là phương pháp tiêm phụ thuộc, mà là đa kế thừa và các mixin, nhưng đó là một cách Pythonic để giải quyết vấn đề của bạn.


10
Câu trả lời này coi super():)
bagrat

2
Raymond gọi nó là CI trong khi tôi nghĩ rằng nó là một hỗn hợp thuần túy. Nhưng có thể là trong Python, mixin và CI gần như giống nhau? Sự khác biệt duy nhất là mức độ phát hiện. Mixin đưa sự phụ thuộc vào một cấp độ lớp trong khi CI đưa sự phụ thuộc vào một thể hiện.
nad2000,

1
Tôi nghĩ dù sao thì việc tiêm cấp phương thức khởi tạo cũng khá dễ thực hiện trong Python, giống như cách OP đã mô tả nó. Tuy nhiên, cách này có vẻ rất thú vị. nó chỉ yêu cầu nhiều dây hơn một chút so với IMO tiêm hàm tạo đơn giản.
stucash

6
Trong khi tôi thấy nó rất thanh lịch, tôi có hai vấn đề với cách tiếp cận này: 1. Điều gì xảy ra khi bạn cần các vật phẩm phục vụ được đưa vào lớp của bạn? 2. Thừa kế thường được sử dụng theo nghĩa "là một" / chuyên môn hóa. Sử dụng nó cho DI bất chấp ý tưởng đó (ví dụ: nếu tôi muốn đưa Dịch vụ vào Người trình bày).
AljoSt

18

Cách chúng tôi thực hiện tiêm phụ thuộc trong dự án của mình là sử dụng tiêm lib. Kiểm tra tài liệu . Tôi thực sự khuyên bạn nên sử dụng nó cho DI. Nó gần như vô nghĩa với chỉ một chức năng nhưng bắt đầu có ý nghĩa khi bạn phải quản lý nhiều nguồn dữ liệu, v.v.

Theo ví dụ của bạn, nó có thể tương tự như:

# framework.py
class FrameworkClass():
    def __init__(self, func):
        self.func = func

    def do_the_job(self):
        # some stuff
        self.func()

Chức năng tùy chỉnh của bạn:

# my_stuff.py
def my_func():
    print('aww yiss')

Ở đâu đó trong ứng dụng bạn muốn tạo tệp bootstrap theo dõi tất cả các phụ thuộc đã xác định:

# bootstrap.py
import inject
from .my_stuff import my_func

def configure_injection(binder):
    binder.bind(FrameworkClass, FrameworkClass(my_func))

inject.configure(configure_injection)

Và sau đó bạn có thể sử dụng mã theo cách này:

# some_module.py (has to be loaded with bootstrap.py already loaded somewhere in your app)
import inject
from .framework import FrameworkClass

framework_instance = inject.instance(FrameworkClass)
framework_instance.do_the_job()

Tôi e rằng điều này khó xảy ra nhất có thể (mô-đun có một số vị ngọt của python như trình trang trí để đưa vào theo tham số, v.v. - hãy kiểm tra tài liệu), vì python không có những thứ lạ mắt như giao diện hoặc gợi ý kiểu.

Vì vậy, để trả lời câu hỏi của bạn trực tiếp sẽ rất khó. Tôi nghĩ câu hỏi thực sự là: python có một số hỗ trợ riêng cho DI không? Và câu trả lời, đáng buồn là: không.


Cảm ơn câu trả lời của bạn, có vẻ khá thú vị. Tôi sẽ kiểm tra phần trang trí. Trong khi đó, chúng ta hãy chờ đợi thêm câu trả lời.
bagrat

Cảm ơn liên kết đến thư viện 'tiêm'. Đây là lần gần nhất tôi tìm thấy cho đến nay để lấp đầy khoảng trống mà tôi muốn được lấp đầy bởi DI - và tiền thưởng, nó thực sự đang được duy trì!
Andy Mortimer

13

Một thời gian trước, tôi đã viết microframework tiêm phụ thuộc với tham vọng biến nó thành Pythonic - Dependency Injector . Đó là cách mã của bạn có thể trông như thế nào trong trường hợp sử dụng nó:

"""Example of dependency injection in Python."""

import logging
import sqlite3

import boto.s3.connection

import example.main
import example.services

import dependency_injector.containers as containers
import dependency_injector.providers as providers


class Platform(containers.DeclarativeContainer):
    """IoC container of platform service providers."""

    logger = providers.Singleton(logging.Logger, name='example')

    database = providers.Singleton(sqlite3.connect, ':memory:')

    s3 = providers.Singleton(boto.s3.connection.S3Connection,
                             aws_access_key_id='KEY',
                             aws_secret_access_key='SECRET')


class Services(containers.DeclarativeContainer):
    """IoC container of business service providers."""

    users = providers.Factory(example.services.UsersService,
                              logger=Platform.logger,
                              db=Platform.database)

    auth = providers.Factory(example.services.AuthService,
                             logger=Platform.logger,
                             db=Platform.database,
                             token_ttl=3600)

    photos = providers.Factory(example.services.PhotosService,
                               logger=Platform.logger,
                               db=Platform.database,
                               s3=Platform.s3)


class Application(containers.DeclarativeContainer):
    """IoC container of application component providers."""

    main = providers.Callable(example.main.main,
                              users_service=Services.users,
                              auth_service=Services.auth,
                              photos_service=Services.photos)

Đây là liên kết đến mô tả chi tiết hơn về ví dụ này - http://python-dependency-injector.ets-labs.org/examples/services_miniapp.html

Hy vọng nó có thể giúp một chút. Để biết thêm thông tin, vui lòng truy cập:


Cảm ơn bạn @Roman Mogylatov. Tôi tò mò muốn biết cách bạn định cấu hình / điều chỉnh các vùng chứa này trong thời gian chạy, chẳng hạn như từ tệp cấu hình. Có vẻ như các phụ thuộc này được mã hóa cứng vào vùng chứa đã cho ( PlatformServices). Giải pháp để tạo một vùng chứa mới cho mọi sự kết hợp của các lớp thư viện có thể tiêm vào?
Bill DeRose

2
Xin chào @BillDeRose. Mặc dù câu trả lời của tôi được coi là quá dài để trở thành một nhận xét SO, nhưng tôi đã tạo ra một vấn đề trên github và đăng câu trả lời của mình lên đó - github.com/ets-labs/python-dependency-injector/issues/197 :) Hy vọng nó sẽ hữu ích, Cảm ơn, La Mã
La Mã Mogylatov

2

Tôi nghĩ rằng DI và có thể là AOP thường không được coi là Pythonic vì sở thích của các nhà phát triển Python điển hình, thay vì đó là các tính năng ngôn ngữ.

Trên thực tế, bạn có thể triển khai một khung DI cơ bản trong <100 dòng , bằng cách sử dụng kính đo và bộ trang trí lớp.

Đối với một giải pháp ít xâm lấn hơn, các cấu trúc này có thể được sử dụng để bổ sung các triển khai tùy chỉnh vào một khuôn khổ chung.


2

Ngoài ra còn có Pinject, một bộ tiêm phụ thuộc python mã nguồn mở của Google.

Đây là một ví dụ

>>> class OuterClass(object):
...     def __init__(self, inner_class):
...         self.inner_class = inner_class
...
>>> class InnerClass(object):
...     def __init__(self):
...         self.forty_two = 42
...
>>> obj_graph = pinject.new_object_graph()
>>> outer_class = obj_graph.provide(OuterClass)
>>> print outer_class.inner_class.forty_two
42

đây là mã nguồn


2

Chèn phụ thuộc là một kỹ thuật đơn giản mà Python hỗ trợ trực tiếp. Không có thư viện bổ sung được yêu cầu. Sử dụng gợi ý loại có thể cải thiện độ rõ ràng và dễ đọc.

Mã khung:

class UserStore():
    """
    The base class for accessing a user's information.
    The client must extend this class and implement its methods.
    """
    def get_name(self, token):
        raise NotImplementedError

class WebFramework():
    def __init__(self, user_store: UserStore):
        self.user_store = user_store

    def greet_user(self, token):
        user_name = self.user_store.get_name(token)
        print(f'Good day to you, {user_name}!')

Mã khách hàng:

class AlwaysMaryUser(UserStore):
    def get_name(self, token):      
        return 'Mary'

class SQLUserStore(UserStore):
    def __init__(self, db_params):
        self.db_params = db_params

    def get_name(self, token):
        # TODO: Implement the database lookup
        raise NotImplementedError

client = WebFramework(AlwaysMaryUser())
client.greet_user('user_token')

Các UserStorelớp và kiểu gián tiếp không cần thiết cho việc thực hiện dependency injection. Mục đích chính của họ là cung cấp hướng dẫn cho nhà phát triển khách hàng. Nếu bạn xóa UserStorelớp và tất cả các tham chiếu đến nó, mã vẫn hoạt động.


1

Một cách rất dễ dàng và Pythonic để tiêm phụ thuộc là importlib.

Bạn có thể xác định một chức năng tiện ích nhỏ

def inject_method_from_module(modulename, methodname):
    """
    injects dynamically a method in a module
    """
    mod = importlib.import_module(modulename)
    return getattr(mod, methodname, None)

Và sau đó bạn có thể sử dụng nó:

myfunction = inject_method_from_module("mypackage.mymodule", "myfunction")
myfunction("a")

Trong mypackage / mymodule.py bạn xác định chức năng của tôi

def myfunction(s):
    print("myfunction in mypackage.mymodule called with parameter:", s)

Tất nhiên bạn cũng có thể sử dụng iso MyClass lớp. chức năng chức năng. Nếu bạn xác định các giá trị của tên phương thức trong tệp settings.py, bạn có thể tải các phiên bản khác nhau của tên phương thức tùy thuộc vào giá trị của tệp cài đặt. Django đang sử dụng một lược đồ như vậy để xác định kết nối cơ sở dữ liệu của nó.


1

Do triển khai Python OOP, IoC và tiêm phụ thuộc không phải là các phương pháp tiêu chuẩn trong thế giới Python. Nhưng cách tiếp cận này có vẻ đầy hứa hẹn ngay cả đối với Python.

  • Sử dụng các phụ thuộc làm đối số là một cách tiếp cận không quan trọng. Python là một ngôn ngữ OOP với mô hình OOP đẹp và trang nhã, cung cấp những cách đơn giản hơn để duy trì các phụ thuộc.
  • Để định nghĩa các lớp với đầy đủ các phương thức trừu tượng chỉ để bắt chước kiểu giao diện cũng kỳ lạ.
  • Các giải pháp giải quyết trình bao bọc khổng lồ tạo ra chi phí mã.
  • Tôi cũng không thích sử dụng thư viện khi tất cả những gì tôi cần là một mẫu nhỏ.

Vì vậy, giải pháp của tôi là:

# Framework internal
def MetaIoC(name, bases, namespace):
    cls = type("IoC{}".format(name), tuple(), namespace)
    return type(name, bases + (cls,), {})


# Entities level                                        
class Entity:
    def _lower_level_meth(self):
        raise NotImplementedError

    @property
    def entity_prop(self):
        return super(Entity, self)._lower_level_meth()


# Adapters level
class ImplementedEntity(Entity, metaclass=MetaIoC):          
    __private = 'private attribute value'                    

    def __init__(self, pub_attr):                            
        self.pub_attr = pub_attr                             

    def _lower_level_meth(self):                             
        print('{}\n{}'.format(self.pub_attr, self.__private))


# Infrastructure level                                       
if __name__ == '__main__':                                   
    ENTITY = ImplementedEntity('public attribute value')     
    ENTITY.entity_prop         

BIÊN TẬP:

Hãy cẩn thận với các mẫu. Tôi đã sử dụng nó trong một dự án thực tế và nó cho thấy nó không phải là một cách tốt. Bài đăng của tôi trên Medium về trải nghiệm của tôi với mẫu này.


Tất nhiên IOC và DI thường được sử dụng, những gì không được sử dụng phổ biến là khung DI , tốt hơn hoặc xấu hơn.
juanpa.arrivillaga

0

Sau khi chơi xung quanh một số khung công tác DI trong python, tôi thấy rằng chúng cảm thấy hơi khó sử dụng khi so sánh mức độ đơn giản của nó trong các lĩnh vực khác chẳng hạn như với .NET Core. Điều này chủ yếu là do việc kết hợp thông qua những thứ như trình trang trí làm lộn xộn mã và khiến cho việc thêm nó vào hoặc xóa nó khỏi dự án hoặc kết hợp dựa trên tên biến.

Gần đây tôi đang làm việc trên một khung công tác tiêm phụ thuộc thay vào đó sử dụng chú thích nhập để thực hiện tiêm có tên là Simple-Injection. Dưới đây là một ví dụ đơn giản

from simple_injection import ServiceCollection


class Dependency:
    def hello(self):
        print("Hello from Dependency!")

class Service:
    def __init__(self, dependency: Dependency):
        self._dependency = dependency

    def hello(self):
        self._dependency.hello()

collection = ServiceCollection()
collection.add_transient(Dependency)
collection.add_transient(Service)

collection.resolve(Service).hello()
# Outputs: Hello from Dependency!

Thư viện này hỗ trợ vòng đời của dịch vụ và các dịch vụ ràng buộc với việc triển khai.

Một trong những mục tiêu của thư viện này là bạn cũng có thể dễ dàng thêm nó vào một ứng dụng hiện có và xem bạn thích nó như thế nào trước khi cam kết với nó vì tất cả những gì nó yêu cầu là ứng dụng của bạn phải có các kiểu đánh máy phù hợp và sau đó bạn xây dựng biểu đồ phụ thuộc tại điểm vào và chạy nó.

Hi vọng điêu nay co ich. Để biết thêm thông tin, vui lòng xem

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.