Development note/C#

[C#] Python을 사용하기 위한 방법(pythonnet)

v명월v 2020. 2. 7. 22:27

안녕하세요. 명월입니다.


이 글은 C#에서 Python을 사용하기 위한 방법(pythonnet)에 대한 설명입니다.


최근 Python에 대해 이것 저것 살펴보고 있는데, 예전에는 단순한 로컬 스크립트 언어라고만 인식이 있었는데 Python의 정말 많은 라이브러리를 보고 이런 기능을 C#에서도 사용하면 어떨까 생각이 들어 조사했습니다.

검색을 하면 꽤 많은 자료를 보면 대체적으로 ironpython에 대한 글이 많이 있네요. ironpython도 도큐멘트도 보고 했으나 버전이 2.7에서 멈추어져 있네요.. 참 아쉽습니다.


그러다가 발견한게 pythonnet인데, 초기 설정이 살짝 복잡한거 빼고는 오히려 ironpython보다 편한 느낌입니다.


먼저 pythonnet를 사용하기 위해서 라이브러리를 다운을 받아야 하는데 이게 Nuget에 없네요.. 아쉽습니다.

직접 컴파일해서 사용합시다.

링크 - https://github.com/pythonnet/pythonnet


위 깃을 가서 먼저 소스를 다운 받습니다.

참고로 MIT라이센스입니다. 복제, 사용은 가능하나 어떠한 책임을 지지 않는다.. 즉, 모르면 알아서 해결하라..입니다.(완전한 개인 의역입니다. 실제 라이센스의미가 아닙니다.)


압축을 풀고 프로젝트를 기동합니다.

아마 Visual studio 버전이 다르다고 경고 메시지가 나오는데 다 OK로 넘어갑니다.


그리고 먼저 설정하기 전에 현재 실행되고 있는 python의 버전을 알아야 합니다.

커맨더 창에서 나중에 python의 경로도 알아야 하기 때문에 미리 where python으로 경로도 조사해 놓습니다.


그리고 python 콘솔에 접속해서 version을 알아봅니다.

# cmd
where python

#python
import sys;
sys.version;

저는 32비트의 3.7.4입니다. 이걸 가지고 아까 Visual Studio의 pythonnet 프로젝트의 설정을 하러 갑니다.

디버그 프로퍼티를 클릭하여 들어가서 환경 설정을 Mono버젼이 아니기 때문에 WinPy3으로 설정합니다.

그리고 버젼을 3.7로 맞추었습니다. 플렛폼 타겟도 x86(32비트로 설정했습니다.)

그리고 빌드를 합니다.

pythonnet프로젝트의 bin 폴더에 가보면 dll파일이 있습니다.

이제 C# 프로젝트 하나 실행하고 참조에서 저 dll를 설정합니다.

여기까지 사용할 환경 설정은 끝났습니다. Python을 사용할 소스를 작성하겠습니다.

using System;
using System.Linq;
using Python.Runtime;
using System.IO;

namespace PythonExecutor
{
  class Program
  {
    // 환경설정 Path를 설정하는 함수이다. 실제 Path가 바뀌는 건 아니고 프로그램 세션 안에서만 path를 변경해서 사용한다.
    public static void AddEnvPath(params string[] paths)
    {
      // PC에 설정되어 있는 환경 변수를 가져온다.
      var envPaths = Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator).ToList();
      // 중복 환경 변수가 없으면 list에 넣는다.
      envPaths.InsertRange(0, paths.Where(x => x.Length > 0 && !envPaths.Contains(x)).ToArray());
      // 환경 변수를 다시 설정한다.
      Environment.SetEnvironmentVariable("PATH", string.Join(Path.PathSeparator.ToString(), envPaths), EnvironmentVariableTarget.Process);
    }
    // 시작 함수입니다.
    static void Main(string[] args)
    {
      // 아까 where python으로 나온 anaconda 설치 경로를 설정
      var PYTHON_HOME = Environment.ExpandEnvironmentVariables(@"D:\anaconda3-32\");
      // 환경 변수 설정
      AddEnvPath(PYTHON_HOME, Path.Combine(PYTHON_HOME, @"Library\bin"));
      // Python 홈 설정.
      PythonEngine.PythonHome = PYTHON_HOME;
      // 모듈 패키지 패스 설정.
      PythonEngine.PythonPath = string.Join(
        Path.PathSeparator.ToString(),
        new string[] {
          PythonEngine.PythonPath,
          // pip하면 설치되는 패키지 폴더.
          Path.Combine(PYTHON_HOME, @"Lib\site-packages"),
          // 개인 패키지 폴더
          "d:\\Python\\MyLib"
        }
      );
      // Python 엔진 초기화
      PythonEngine.Initialize();
      // Global Interpreter Lock을 취득
      using (Py.GIL())
      {
        // String 식으로 python 식을 작성, 실행
        PythonEngine.RunSimpleString(@"
import sys;

print('hello world');
print(sys.version);

");
        // 개인 패키지 폴더의 example/test.py를 읽어드린다.
        dynamic test = Py.Import("example.test");
        
        // example/test.py의 Calculator 클래스를 선언
        dynamic f = test.Calculator(1, 2);
        // Calculator의 add함수를 호출
        Console.WriteLine(f.add());
      }
      // python 환경을 종료한다.
      PythonEngine.Shutdown();

      Console.WriteLine("Press any key...");
      Console.ReadKey();

    }
  }
}

먼저 위에 개인 패키지 폴더를 설정했는데, 여기는 python 파일을 저장해 놓는 곳입니다. python를 파일로 작성한 것을 C#에서 읽어오기 위함입니다.

그럼 example.test의 python 파일을 작성하겠습니다.

# Calculator 클래스
class Calculator:
  def __init__(self, x, y):
    self.x = x;
    self.y = y;
  # 더하기 함수
  def add(self):
    return self.x + self.y;

# import될 때 호출된다.
print("hello world!!!");

첫번째 Hello world는 C# 내에서 print함수로 호출한 것입니다. 그 다음은 python 버젼이 출력되네요.

그 다음의 hello world는 test.py 파일에 있네요. Py.Import할 때 불렸습니다.

마지막으로 3은 C#에서 파이썬의 클래스를 호출했습니다. f.add로 함수까지 호출했네요. 그 결과가 나왔습니다.


C#에서 Python을 사용하는 것에 대한 성능은 어떨지 잘 모르겠네요.

그러나 C#에서 python 소스를 실행하는 것 이상으로 직접 클래스를 선언하고 함수 호출까지 가능하네요.

참조 - https://github.com/pythonnet/pythonnet


-- 추가 2020년 3월 12일 --

RunSimpleString 부분에서 한글을 사용하면 에러가 발생한다는 질문이 있어서 남깁니다.


인코딩 에러가 발생하네요..

기본적으로 윈도에서 사용하는 인코딩 타입은 chcp 949, 즉 ks_c_5601-1987 타입입니다.

python에서는 unicode를 사용해야 하네요.


방법은 두가지인데, 윈도우 인코딩 타입을 chcp 65001, utf8 타입으로 변경을 하던가, 프로그램에서 utf8타입을 허용하게 만들어야 합니다.

윈도우 인코딩 타입을 변경하는 것은 프로그램은 실행될 지 모르나, 혹시 모를 다른 에러가 발생할 수 있기 때문에 좀 위험요소가 있습니다.


그럼 프로그램을 변경하는 방법을 해야하는데 RunSimpleString의 파라미터가 string타입이라 인코딩 타입을 변경할 수가 없네요.


그럼 pythonnet의 소스를 직접 수정합시다.

RunSimpleString의 소스 추적을 하면 python의 PyRun_SimpleString 함수를 사용하네요.

링크 - https://docs.python.org/3/c-api/veryhigh.html

PyRun_SimpleString의 파라미터는 const char*입니다. (const char* 이면서 unicode를 취급하네요...)

그럼 String이 아니라 byte*(unsigned char*)를 넘겨도 된다는 뜻이 되네요..


runtime.cs의 소스를 수정합시다.

pythonengine.cs의 소스를 수정합시다.

다시 위처럼 빌드해서 참조합니다.

사용할 때는 String타입이 아니고 byte[]타입을 넘겨야 하기 때문에 Encoding클래스를 사용해서 utf8타입으로 변환합니다.

한글이 허용되네요... RunSimpleString을 통해 한글이 되네요...

Git 오픈 소스에도 언제 PR(Project Request)해야 겠네요...


-----추가 2020년 3월 21일-----

위 pythonnet이 WPF 환경에서는 작동이 되지 않는다는 질문이 있어서 테스트를 해보았습니다.


먼저 WPF프로젝트를 생성해서 버튼을 하나 추가하고 버튼의 이벤트를 작성했습니다.

using System;
using System.Linq;
using System.Windows;
using Python.Runtime;

namespace WpfApp1
{
  public partial class MainWindow : Window
  {
    // 생성자
    public MainWindow()
    {
      // 컨트롤 및 화면 초기화
      InitializeComponent();
      // pythonnet을 초기화
      // anaconda 설치 경로 설정
      var PYTHON_HOME = Environment.ExpandEnvironmentVariables(@"C:\Anaconda3\");
      // 환경 변수 설정
      AddEnvPath(PYTHON_HOME, System.IO.Path.Combine(PYTHON_HOME, @"Library\bin"));
      // Python 홈 설정
      PythonEngine.PythonHome = PYTHON_HOME;
      // 모듈 패키지 설정
      // 콘솔에서 개인 패키지 폴더 영역은 제외했다. 필요하면 추가하면 된다.
      PythonEngine.PythonPath = string.Join( 
        System.IO.Path.PathSeparator.ToString(),
        new string[] {
          PythonEngine.PythonPath
        }
      );
    }
    // 버튼 클릭 이벤트
    private void Button_Click(object sender, RoutedEventArgs e)
    {
      // Python 엔진 초기화
      PythonEngine.Initialize();
      // Global interpreter Lock을 취득
      using (Py.GIL())
      {
        // String 식으로 python 식을 작성
        PythonEngine.RunSimpleString(@"    
import sys;    
     
print('hello world');    
print(sys.version);    
     
");
      }
      // python 환경을 종료
      PythonEngine.Shutdown();
    }
    // 환경설정 path를 설정하는 함수이다.
    public void AddEnvPath(params string[] paths)
    {
      // PC에 설정되어 있는 환경 변수를 가져온다.
      var envPaths = Environment.GetEnvironmentVariable("PATH").Split(System.IO.Path.PathSeparator).ToList();
      // 중복 환경 변수가 없으면 list에 넣는다.
      envPaths.InsertRange(0, paths.Where(x => x.Length > 0 && !envPaths.Contains(x)).ToArray());
      // 환경 변수를 재설정
      Environment.SetEnvironmentVariable("PATH", string.Join(System.IO.Path.PathSeparator.ToString(), envPaths), EnvironmentVariableTarget.Process);
    }
  }
}

결과는 에러없이 실행이 됩니다.


제가 생각하는 Initialize에서 에러가 난다는 것은 아마도 64비트 32비트를 일치시키지 않아서 발생할 수도 있습니다.

C#에서 Any CPU를 설정하면 기본적으로 x86(32비트)로 빌드됩니다.

먼저 python의 버전을 확인합니다.

제가 PC가 여러대라서 이번에는 64비트로 설명하겠습니다...

pythonnet을 x64로 빌드합니다.(대신 일문판입니다.....)

WPF 프로젝트도 x64로 설정합니다...(요즘 분위기가 일본의 일자만 꺼내도 욕먹는 분위기라... 그냥 죄송해집니다.)


아마도 위의 문제일 가능성이 클 것 같습니다.


여기까지 C#에서 Python을 사용하기 위한 방법(pythonnet)에 대한 설명이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.