프로그래밍

python + .net core integration, use pythonnet

itssue-host 2024. 7. 26. 15:04

파이썬가 .net core에 대한 통합 방법은 여러가지가 있다.

 

ironpython을 사용하는 방법.

process stdio를 사용하는 방법.

 

오늘은 .net에서 python 인터프린터 구현인 pythonnet을 소개한다.

 

먼저 pythonnet은 python의 .net 인터프린터 구현인 만큼 process stdio를 사용하는 방법보다 효율적이고 성능면에서 보다 나은 방법인다.

 

아래의 기본적인 사용코드를 보자.

 

    PythonEngine.Initialize();
    using (Py.GIL())
    {
        dynamic np = Py.Import("numpy");
        Console.WriteLine(np.cos(np.pi * 2));

        dynamic sin = np.sin;
        Console.WriteLine(sin(5));

        double c = (double)(np.cos(5) + sin(5));
        Console.WriteLine(c);

        dynamic a = np.array(new List<float> { 1, 2, 3 });
        Console.WriteLine(a.dtype);

        dynamic b = np.array(new List<float> { 6, 5, 4 }, dtype: np.int32);
        Console.WriteLine(b.dtype);

        Console.WriteLine(a * b);
        Console.ReadKey();
    }

 

위 코드는 기본적인 동작코드로 각각을 설명하면 아래와 같다.

 

PythonEngine.Initialize(); 는 Pythonnet의 내부 엔진 초기화이고 

using (Py.GIL())은 인터프린터 Lock으로 Py.GIL을 사용함으로 인해서 인터프린터 실행을 위한 잠금을 획득한다.

내부적으로 해당 잠금으로 인해서 인터프린터가 한번에 하나의 스크립트를 실행한다고 볼 수 있겠다.

(그럼 왜 process stdio보다 성능에서 좋다고 볼 수 있는가? Thread와 Process를 공부해 보자.)

 

물론 다중 Thread를 허용하는 옵션이 있지만, 기본적으로 위와 같이 동작함을 이해하자.

 

자세한 사항은 https://github.com/pythonnet/pythonnet/wiki/Threading 을 확인하자.

 

실제 구현한 코드는 아래의 코드이다.

 

using Python.Runtime;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PythonTestConsole
{
    public class PyExecutor
    {
        private static Lazy<PyExecutor> _instance = new Lazy<PyExecutor>(() => new PyExecutor());
        public static PyExecutor Instance => _instance.Value;

        nint threadState = 0;

        private PyExecutor()
        {
        }

        public void Initialize()
        {
            AppContext.SetSwitch("System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization", true);

            Runtime.PythonDLL = "/usr/lib/x86_64-linux-gnu/libpython3.10.so";
            
            if (!PythonEngine.IsInitialized)
            {
                PythonEngine.Initialize();
                threadState = PythonEngine.BeginAllowThreads();
            }
        }

        public string Execute(string fileName)
        {
            var path = string.Empty;
            using var gil = Py.GIL();
            using var scope = Py.CreateScope();

            dynamic sys = Py.Import("sys");            
            sys.path.append("[executable python path]");
            dynamic func = scope.Import("run");
            dynamic result = func.execute(fileName);
            path = result.ToString();

            return path;
        }

        public void Close()
        {
            PythonEngine.EndAllowThreads(threadState);
            PythonEngine.Shutdown();

            AppContext.SetSwitch("System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization", false);
        }
    }
}

 

맨 처음 확인한 코드와 상이하지만 위와 같이 만들어진 이유는 우리가 실제 프로그램에 적용할 경우 pythonnet인 프로그램에서 singleton instance로 동작해야 하기 때문이다.

 

추가된 사항은 Python Dll 위치를 설정하고 실제 동작할 python script가 있는 위치를 지정하는 것이다.

 

또한 linux를 사용할 경우에도 대응하기 위해 설정하고 있다.

 

여기서 확인해야 할 사항은 

threadState = PythonEngine.BeginAllowThreads();

되겠다.

 

위 설명에서 GIL 잠금은 내부적으로 동작하므로 사용할 수 있는 Thread가 있을 경우 다중 Thread로 동작할 수 있도록 설정하는 부분이다.

 

또한 .net core 8 부터는 BinaryFormatter를 허용하지 않고 있으므로 추가적으로 허용하도록 하는 옵션을 추가하였다.

 

마지막으로 안타깝게도 pythonnet은 asp.net core의 lifecycle을 적용할 수 없다.

 

따라서 라이브러리 모듈 형태로만 사용해야 하며 또한 singleton 형태로 사용해야만 한다.

 

테스트 코드의 경우 text를 이미지로 생성하는 python 코드를 수행하는 경우이며 테스트 결과 1000개의 이미지를 생성하는데 약 15초 정도의 시간이 소요되었다.

약 초당 66개의 이미지를 생성하므로 성능 측면에서 나쁘지 않은 것으로 보인다.

물론 테스트 환경에 대한 편차는 존재한다.

 

위와 같이 python과 .net core application에 대한 통합을 다루어 보았다.

 

도움이 되길 바라며.

 

PS : pythonnet을 사용할 경우 python 동작이 외부 model을 사용하여 구동할 경우 다운되는 현상이 있다.

단순한 스크립트나 소용시간이 많지 않은 스크립트 동작에 사용할 것을 추천한다.

만약, model을 통한 작업이 있는 경우에는 python으로 작성하는 것을 추천한다.